///|
/// Opening validates the superblock; objects are validated when accessed.
pub struct H5File {
  data : Bytes
  sb : Superblock
  limits : Limits
}

///|
pub fn H5File::open(
  data : Bytes,
  limits~ : Limits = Limits::default(),
) -> H5File raise H5Error {
  let sb = read_superblock(data, limits)
  let bounded = data[:sb.eof].to_owned()
  ignore(read_messages(bounded, sb, sb.root, limits))
  { data: bounded, sb, limits, }
}

///|
pub fn H5File::superblock_version(self : H5File) -> Int32 {
  self.sb.version
}

///|
fn path_parts(path : String) -> Array[String] raise H5Error {
  if path.contains("\u0000") {
    raise Invalid("NUL in object path", 0)
  }
  let parts : Array[String] = []
  for part in path.split("/") {
    if part == "" || part == "." {
      continue
    }
    if part == ".." {
      raise Invalid("parent traversal is not supported", 0)
    }
    parts.push(part.to_owned())
  }
  parts
}

///|
fn H5File::messages(
  self : H5File,
  address : Int32,
) -> Array[Message] raise H5Error {
  if address < self.sb.base || address >= self.sb.eof {
    raise Invalid("object address outside file", address)
  }
  read_messages(self.data, self.sb, address, self.limits)
}

///|
/// Soft links are resolved relative to their containing group. Every hop is bounded.
fn H5File::resolve(self : H5File, path : String) -> Int32 raise H5Error {
  let mut parts = path_parts(path)
  if parts.length() > self.limits.max_depth {
    raise Limit("path depth")
  }
  let mut current = self.sb.root
  let parents : Array[String] = []
  let mut i = 0
  let mut hops = 0
  while i < parts.length() {
    hops += 1
    if hops > self.limits.max_depth {
      raise Limit("link resolution depth")
    }
    let links = group_links(
      self.data,
      self.sb,
      self.messages(current),
      self.limits,
    )
    let mut found : LinkTarget? = None
    for link in links {
      if link.name == parts[i] {
        found = Some(link.target)
        break
      }
    }
    match found {
      Some(Hard(target)) => {
        current = target
        parents.push(parts[i])
        i += 1
      }
      Some(Soft(target)) => {
        let expanded = if target.has_prefix("/") { [] } else { parents.copy() }
        expanded.append(path_parts(target))
        for j = i + 1; j < parts.length(); j = j + 1 {
          expanded.push(parts[j])
        }
        parts = expanded
        parents.clear()
        current = self.sb.root
        i = 0
      }
      None => raise NotFound(path)
    }
  }
  ignore(self.messages(current))
  current
}

///|
pub fn H5File::links(
  self : H5File,
  path~ : String = "/",
) -> Array[Link] raise H5Error {
  group_links(
    self.data,
    self.sb,
    self.messages(self.resolve(path)),
    self.limits,
  )
}

///|
pub fn H5File::object_address(
  self : H5File,
  path : String,
) -> Int32 raise H5Error {
  self.resolve(path)
}

///|
pub(all) struct ObjectEntry {
  path : String
  address : Int32
  dataset : Bool
} derive(Debug, Eq)

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

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

///|
/// A hard-linked object is listed once, preventing exponential alias walks.
pub fn H5File::walk(self : H5File) -> Array[ObjectEntry] raise H5Error {
  let queue = [("/", self.sb.root, 0)]
  let seen : Map[Int32, Bool] = Map([])
  let out : Array[ObjectEntry] = []
  let mut next = 0
  while next < queue.length() {
    let (path, address, depth) = queue[next]
    next += 1
    if seen.contains(address) {
      continue
    }
    if depth > self.limits.max_depth || out.length() >= self.limits.max_objects {
      raise Limit("object traversal")
    }
    seen[address] = true
    let messages = self.messages(address)
    let dataset = messages.any(m => m.kind == 1)
    out.push({ path, address, dataset, })
    if !dataset {
      for link in group_links(self.data, self.sb, messages, self.limits) {
        if link.target is Hard(target) {
          if queue.length() >= self.limits.max_objects {
            raise Limit("object link traversal")
          }
          let child = (if path == "/" { "" } else { path }) + "/" + link.name
          queue.push((child, target, depth + 1))
        }
      }
    }
  }
  out
}

///|
test "path normalization rejects parent traversal and NUL" {
  assert_eq(path_parts("//a/./b/"), ["a", "b"])
  assert_true(
    try {
      ignore(path_parts("/a/../b"))
      false
    } catch {
      _ => true
    },
  )
  assert_true(
    try {
      ignore(path_parts("a\u0000b"))
      false
    } catch {
      _ => true
    },
  )
}