///|
/// The kind of object stored under an HDF5 link.
#derive(Eq, @debug.Debug)
pub(all) enum Hdf5ObjectKind {
  Group
  Dataset
  Datatype
  Unknown
}

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

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

///|
/// One named object in the file hierarchy.
#derive(Eq, @debug.Debug)
pub(all) struct Hdf5Node {
  path : String
  address : UInt64
  kind : Hdf5ObjectKind
}

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

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

///|
/// A parsed HDF5 file backed by bytes or a random-access source.
pub struct Hdf5File {
  priv bytes : Bytes
  priv superblock : Superblock
  priv source : &Hdf5Source?
  priv shared_messages : SharedMessageContext
}

///|
/// Parses an HDF5 file from in-memory bytes.
pub fn Hdf5File::parse(bytes : Bytes) -> Hdf5File raise Hdf5Error {
  let superblock = parse_superblock(bytes)
  {
    bytes,
    superblock,
    source: None,
    shared_messages: parse_shared_message_context(bytes, None, superblock),
  }
}

///|
/// Parses an HDF5 file from a random-access source.
///
pub fn Hdf5File::parse_source(source : &Hdf5Source) -> Hdf5File raise Hdf5Error {
  let superblock = parse_source_superblock(source)
  {
    bytes: b"",
    superblock,
    source: Some(source),
    shared_messages: parse_shared_message_context(b"", Some(source), superblock),
  }
}

///|
/// Returns the parsed HDF5 superblock version.
pub fn Hdf5File::superblock_version(self : Hdf5File) -> Int32 {
  self.superblock.version
}

///|
/// Returns the file address of the root group object header.
fn Hdf5File::root_address(self : Hdf5File) -> UInt64 {
  self.superblock.root_group_object_header
}

///|
/// Reads an object header at a file address.
fn Hdf5File::object_header_at(
  self : Hdf5File,
  address : UInt64,
) -> ObjectHeader raise Hdf5Error {
  let header = match self.source {
    None =>
      parse_object_header(
        self.bytes,
        address,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
      )
    Some(source) =>
      parse_source_object_header(
        source,
        address,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
      )
  }
  self.shared_messages.resolve_object_header(header, address)
}

///|
fn object_kind(header : ObjectHeader) -> Hdf5ObjectKind {
  if header.first(msg_dataspace) is Some(_) &&
    header.first(msg_datatype) is Some(_) &&
    header.first(msg_data_layout) is Some(_) {
    Dataset
  } else if header.first(msg_symbol_table) is Some(_) ||
    header.first(msg_link_info) is Some(_) ||
    header.first(msg_link) is Some(_) {
    Group
  } else if header.first(msg_datatype) is Some(_) {
    Datatype
  } else {
    Unknown
  }
}

///|
/// Reads the links stored in a group object header, supporting both the
/// symbol-table (v1) and link-message (v2) group encodings.
fn Hdf5File::links(
  self : Hdf5File,
  header : ObjectHeader,
) -> Array[GroupLink] raise Hdf5Error {
  match header.first(msg_symbol_table) {
    Some(message) => {
      let symbol_table = parse_symbol_table_message(
        message,
        self.superblock.offset_size,
        self.superblock.length_size,
      )
      match self.source {
        None =>
          read_group_links_v1(
            self.bytes,
            symbol_table.btree_address,
            symbol_table.heap_address,
            self.superblock.offset_size,
            self.superblock.length_size,
            self.superblock.base_address,
          )
        Some(source) =>
          read_source_group_links_v1(
            source,
            symbol_table.btree_address,
            symbol_table.heap_address,
            self.superblock.offset_size,
            self.superblock.length_size,
            self.superblock.base_address,
          )
      }
    }
    None =>
      match self.source {
        Some(source) =>
          read_source_group_links_v2(
            source,
            header,
            self.superblock.offset_size,
            self.superblock.length_size,
            self.superblock.base_address,
          )
        None =>
          read_group_links_v2(
            self.bytes,
            header,
            self.superblock.offset_size,
            self.superblock.length_size,
            self.superblock.base_address,
          )
      }
  }
}

///|
fn split_path(path : String) -> Array[String] {
  let segments : Array[String] = []
  for part in path.split("/") {
    let segment = part.to_owned()
    if segment == "" || segment == "." {
      continue
    }
    if segment == ".." {
      if segments.length() > 0 {
        segments.pop() |> ignore
      }
    } else {
      segments.push(segment)
    }
  }
  segments
}

///|
fn join_path(parent : String, name : String) -> String {
  if parent == "/" || parent == "" {
    "/" + name
  } else {
    parent + "/" + name
  }
}

///|
/// Resolves an absolute or root-relative object path.
pub fn Hdf5File::resolve(
  self : Hdf5File,
  path : String,
) -> Hdf5Node raise Hdf5Error {
  self.resolve_from("/", self.root_address(), path, 0)
}

///|
fn Hdf5File::resolve_from(
  self : Hdf5File,
  base_path : String,
  base_address : UInt64,
  path : String,
  depth : Int32,
) -> Hdf5Node raise Hdf5Error {
  guard depth < 16 else {
    raise InvalidField("too many soft-link indirections")
  }
  let segments = split_path(path)
  if segments.length() == 0 {
    let header = self.object_header_at(base_address)
    return {
      path: base_path,
      address: base_address,
      kind: object_kind(header),
    }
  }
  let mut current_path = base_path
  let mut current_address = base_address
  let mut index = 0
  while index < segments.length() {
    let header = self.object_header_at(current_address)
    let links = self.links(header)
    let segment = segments[index]
    let link = match links.search_by(link => link.name == segment) {
      Some(found) => links[found]
      None => raise InvalidField("path component not found: " + segment)
    }
    match link.link_target {
      Some(target) => {
        if link.cache_type == 64U {
          let target_path = match link.external_path {
            Some(value) => value
            None => ""
          }
          raise UnsupportedFeature(
            "external links are not resolved: " + target + ":" + target_path,
          )
        }
        let rebased = if target.has_prefix("/") {
          target
        } else {
          join_path(current_path, target)
        }
        let mut remaining = ""
        let mut next = index + 1
        while next < segments.length() {
          remaining = join_path(remaining, segments[next])
          next = next + 1
        }
        let full_path = if remaining == "" {
          rebased
        } else {
          join_path(rebased, remaining)
        }
        return self.resolve_from("/", self.root_address(), full_path, depth + 1)
      }
      None => {
        current_path = join_path(current_path, segment)
        current_address = link.object_header_address
        index = index + 1
      }
    }
  }
  let header = self.object_header_at(current_address)
  { path: current_path, address: current_address, kind: object_kind(header), }
}

///|
/// Reads a dataset and decodes all of its values.
pub fn Hdf5File::read_dataset(
  self : Hdf5File,
  path : String,
) -> Dataset raise Hdf5Error {
  let node = self.resolve(path)
  guard node.kind == Dataset else {
    raise InvalidField("object is not a dataset: " + path)
  }
  let header = self.object_header_at(node.address)
  let metadata = read_dataset_metadata(
    header,
    self.superblock.offset_size,
    self.superblock.length_size,
  )
  match self.source {
    Some(source) => {
      let rank = metadata.dataspace.dimensions.length()
      let start = Array::make(rank, 0UL)
      let stride = Array::make(rank, 1UL)
      let selected = read_source_dataset_selection(
        source,
        header,
        metadata,
        Hyperslab(start, metadata.dataspace.dimensions.copy(), stride),
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
      )
      {
        dataspace: metadata.dataspace,
        datatype: metadata.datatype,
        layout: metadata.layout,
        values: selected.values,
      }
    }
    None =>
      read_dataset(
        self.bytes,
        header,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
      )
  }
}

///|
/// Reads a dataset's shape, datatype and storage layout without loading its
/// values.
pub fn Hdf5File::read_dataset_metadata(
  self : Hdf5File,
  path : String,
) -> DatasetMetadata raise Hdf5Error {
  let node = self.resolve(path)
  guard node.kind == Dataset else {
    raise InvalidField("object is not a dataset: " + path)
  }
  let header = self.object_header_at(node.address)
  read_dataset_metadata(
    header,
    self.superblock.offset_size,
    self.superblock.length_size,
  )
}

///|
/// Reads a hyperslab or point selection from a dataset.
pub fn Hdf5File::read_dataset_selection(
  self : Hdf5File,
  path : String,
  selection : DatasetSelection,
) -> DatasetSelectionResult raise Hdf5Error {
  let node = self.resolve(path)
  guard node.kind == Dataset else {
    raise InvalidField("object is not a dataset: " + path)
  }
  let header = self.object_header_at(node.address)
  let metadata = read_dataset_metadata(
    header,
    self.superblock.offset_size,
    self.superblock.length_size,
  )
  match self.source {
    Some(source) =>
      read_source_dataset_selection(
        source,
        header,
        metadata,
        selection,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
      )
    None =>
      read_dataset_selection(
        self.bytes,
        header,
        metadata,
        selection,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
      )
  }
}

///|
/// Reads the attributes attached to an object path.
pub fn Hdf5File::attributes(
  self : Hdf5File,
  path : String,
) -> Array[Attribute] raise Hdf5Error {
  let node = self.resolve(path)
  let header = self.object_header_at(node.address)
  match self.source {
    Some(source) =>
      read_source_attributes(
        source,
        header,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
        shared_messages=Some(self.shared_messages),
      )
    None =>
      read_attributes(
        self.bytes,
        header,
        self.superblock.offset_size,
        self.superblock.length_size,
        self.superblock.base_address,
        shared_messages=Some(self.shared_messages),
      )
  }
}

///|
fn Hdf5File::walk_group(
  self : Hdf5File,
  path : String,
  address : UInt64,
  ancestors : Array[UInt64],
  nodes : Array[Hdf5Node],
  depth : Int32,
) -> Unit raise Hdf5Error {
  guard depth < 128 else { raise InvalidField("group hierarchy is too deep") }
  if ancestors.contains(address) {
    return
  }
  ancestors.push(address)
  let header = self.object_header_at(address)
  for link in self.links(header) {
    let child_path = join_path(path, link.name)
    match link.link_target {
      Some(_) =>
        if link.cache_type == 64U {
          nodes.push({ path: child_path, address: 0UL, kind: Unknown, })
        } else {
          let child = self.resolve(child_path) catch {
            _ => { path: child_path, address: 0UL, kind: Unknown, }
          }
          nodes.push({
            path: child_path,
            address: child.address,
            kind: child.kind,
          })
        }
      None => {
        let child_header = self.object_header_at(link.object_header_address)
        let kind = object_kind(child_header)
        nodes.push({
          path: child_path,
          address: link.object_header_address,
          kind,
        })
        if kind == Group {
          self.walk_group(
            child_path,
            link.object_header_address,
            ancestors,
            nodes,
            depth + 1,
          )
        }
      }
    }
  }
  ancestors.pop() |> ignore
}

///|
/// Lists the full object hierarchy below the root group.
pub fn Hdf5File::walk(self : Hdf5File) -> Array[Hdf5Node] raise Hdf5Error {
  let nodes : Array[Hdf5Node] = []
  self.walk_group("/", self.root_address(), [], nodes, 0)
  nodes
}