///|
pub enum LinkTarget {
  Hard(Int32)
  Soft(String)
} derive(Debug, Eq)

///|
pub extend LinkTarget with @debug.Debug::{to_repr}

///|
pub extend LinkTarget with Eq::{equal, not_equal}

///|
pub(all) struct Link {
  name : String
  target : LinkTarget
} derive(Debug, Eq)

///|
pub extend Link with @debug.Debug::{to_repr}

///|
pub extend Link with Eq::{equal, not_equal}

///|
fn text(bytes : Bytes, at : Int32) -> String raise H5Error {
  @utf8.decode(bytes, ignore_bom=false) catch {
    _ => raise Invalid("invalid UTF8 name", at)
  }
}

///|
fn valid_name(name : String) -> Unit raise H5Error {
  if name == "" ||
    name == "." ||
    name == ".." ||
    name.contains("/") ||
    name.contains("\u0000") {
    raise Invalid("invalid link/attribute name", 0)
  }
}

///|
fn decode_link(m : Message, sb : Superblock) -> Link raise H5Error {
  let c = cursor(m.bytes, 0, m.bytes.length())
  if c.u8() != 1 {
    raise Unsupported("link version", m.at)
  }
  let flags = c.u8()
  if (flags & 0xe0) != 0 {
    raise Unsupported("link flags", m.at)
  }
  let kind = if (flags & 8) != 0 { c.u8() } else { 0 }
  if (flags & 4) != 0 {
    c.skip(8)
  }
  let charset = if (flags & 16) != 0 { c.u8() } else { 0 }
  if charset > 1 {
    raise Unsupported("name charset", m.at)
  }
  let n = c.index(1 << (flags & 3))
  let raw = c.bytes(n)
  if charset == 0 {
    for x in raw {
      if x > 127 {
        raise Invalid("nonASCII link name", m.at)
      }
    }
  }
  let name = text(raw, m.at)
  valid_name(name)
  let target = match kind {
    0 => Hard(c.address(sb.offset_width, sb.base).unwrap_or(-1))
    1 => {
      let n = c.index(2)
      Soft(text(c.bytes(n), m.at))
    }
    _ => raise Unsupported("external or user-defined link", m.at)
  }
  if c.pos != c.end {
    raise Invalid("extra link data", m.at)
  }
  { name, target, }
}

///|
fn encode_link(name : String, target : Int32) -> Message raise H5Error {
  valid_name(name)
  let raw = @utf8.encode(name)
  if raw.length() > 65500 {
    raise Limit("link name length")
  }
  let b = buffer()
  b.u8(1)
  b.u8(17)
  b.u8(1)
  b.uint(raw.length().to_uint64(), 2)
  b.bytes(raw)
  b.uint(target.to_uint64(), 8)
  message(6, b.finish())
}

///|
fn encode_soft_link(name : String, target : String) -> Message raise H5Error {
  valid_name(name)
  let rawname = @utf8.encode(name)
  let rawtarget = @utf8.encode(target)
  if checked_add(rawname.length(), rawtarget.length()) > 65000 {
    raise Limit("soft link length")
  }
  let b = buffer()
  b.u8(1)
  b.u8(25)
  b.u8(1)
  b.u8(1)
  b.uint(rawname.length().to_uint64(), 2)
  b.bytes(rawname)
  b.uint(rawtarget.length().to_uint64(), 2)
  b.bytes(rawtarget)
  message(6, b.finish())
}

///|
fn cstring(data : Bytes, at : Int32, end : Int32) -> String raise H5Error {
  let c = cursor(data, at, end - at)
  while c.pos < c.end {
    if c.u8() == 0 {
      return text(bytes_at(data, at, c.pos - at - 1), at)
    }
  }
  raise Invalid("unterminated heap string", at)
}

///|
/// Follow only child pointers; sibling pointers are not needed to enumerate.
fn legacy_links(
  data : Bytes,
  sb : Superblock,
  m : Message,
  limits : Limits,
) -> Array[Link] raise H5Error {
  let p = cursor(m.bytes, 0, m.bytes.length())
  let tree = p.address(sb.offset_width, sb.base).unwrap_or(-1)
  let heap = p.address(sb.offset_width, sb.base).unwrap_or(-1)
  let h = cursor(data, heap, sb.eof - heap)
  h.signature(b"HEAP")
  if h.u8() != 0 {
    raise Unsupported("local heap version", heap)
  }
  h.skip(3)
  let heap_size = h.index(sb.size_width)
  ignore(h.uint(sb.size_width))
  let heap_data = h.address(sb.offset_width, sb.base).unwrap_or(-1)
  if heap_size > limits.max_metadata_bytes {
    raise Limit("local heap size")
  }
  ignore(cursor(data, heap_data, heap_size))
  let heap_end = checked_add(heap_data, heap_size)
  let queue = [(tree, 0)]
  let seen : Map[Int32, Bool] = Map([])
  let out : Array[Link] = []
  let mut next = 0
  while next < queue.length() {
    let (address, depth) = queue[next]
    next += 1
    if depth > limits.max_depth || queue.length() > limits.max_objects {
      raise Limit("group Btree traversal")
    }
    if seen.contains(address) {
      raise Invalid("cyclic group Btree", address)
    }
    seen[address] = true
    let c = cursor(data, address, sb.eof - address)
    c.signature(b"TREE")
    if c.u8() != 0 {
      raise Invalid("not a group Btree", address)
    }
    let level = c.u8()
    let count = c.index(2)
    c.skip(2 * sb.offset_width)
    for i = 0; i < count; i = i + 1 {
      ignore(c.uint(sb.size_width))
      let child = c.address(sb.offset_width, sb.base).unwrap_or(-1)
      if level > 0 {
        queue.push((child, depth + 1))
        continue
      }
      let leaf = cursor(data, child, sb.eof - child)
      leaf.signature(b"SNOD")
      if leaf.u8() != 1 || leaf.u8() != 0 {
        raise Invalid("symbol node version", child)
      }
      let entries = leaf.index(2)
      for j = 0; j < entries; j = j + 1 {
        if out.length() >= limits.max_objects {
          raise Limit("group links")
        }
        let offset = leaf.index(sb.offset_width)
        let obj = leaf.address(sb.offset_width, sb.base).unwrap_or(-1)
        let cache = leaf.index(4)
        leaf.skip(4)
        let scratch = leaf.bytes(16)
        if offset >= heap_size {
          raise Invalid("heap name offset", child)
        }
        let name = cstring(data, checked_add(heap_data, offset), heap_end)
        valid_name(name)
        let target = match cache {
          0 | 1 => Hard(obj)
          2 => {
            let soft = cursor(scratch, 0, scratch.length()).index(4)
            if soft >= heap_size {
              raise Invalid("heap soft link offset", child)
            }
            Soft(cstring(data, checked_add(heap_data, soft), heap_end))
          }
          _ => raise Unsupported("symbol cache type", child)
        }
        out.push({ name, target, })
      }
    }
    ignore(c.uint(sb.size_width))
  }
  out
}

///|
fn group_links(
  data : Bytes,
  sb : Superblock,
  messages : Array[Message],
  limits : Limits,
) -> Array[Link] raise H5Error {
  let out : Array[Link] = []
  let names : Map[String, Bool] = Map([])
  for m in messages {
    match m.kind {
      6 => out.push(decode_link(m, sb))
      17 => out.append(legacy_links(data, sb, m, limits))
      2 => {
        let c = cursor(m.bytes, 0, m.bytes.length())
        if c.u8() != 0 {
          raise Unsupported("link info version", m.at)
        }
        let flags = c.u8()
        if flags > 3 {
          raise Unsupported("link info flags", m.at)
        }
        if (flags & 1) != 0 {
          c.skip(8)
        }
        if c.address(sb.offset_width, sb.base) != None ||
          c.address(sb.offset_width, sb.base) != None {
          raise Unsupported("dense group fractal heap/Btree", m.at)
        }
      }
      1 | 3 | 8 => raise Invalid("dataset is not a group", m.at)
      _ =>
        if (m.flags & 128) != 0 &&
          m.kind != 10 &&
          m.kind != 12 &&
          m.kind != 21 &&
          m.kind != 22 {
          raise Unsupported("mandatory group message", m.at)
        }
    }
    if out.length() > limits.max_objects {
      raise Limit("group links")
    }
  }
  for link in out {
    if names.contains(link.name) {
      raise Invalid("duplicate group link", 0)
    }
    names[link.name] = true
  }
  out
}

///|
test "compact links preserve UTF8 names and exact addresses" {
  let sb : Superblock = {
    version: 2,
    offset_width: 8,
    size_width: 8,
    base: 0,
    root: 0,
    eof: 1000,
  }
  let m = encode_link("实验", 512)
  assert_eq(decode_link(m, sb), { name: "实验", target: Hard(512), })
  assert_true(
    try {
      ignore(encode_link("a/b", 0))
      false
    } catch {
      _ => true
    },
  )
}