///|
pub enum Storage {
  Compact(Bytes)
  Contiguous(Int32?, Int32)
  Chunked(Int32?, Array[Int32])
  FixedArray(Int32?, Array[Int32], Int32)
} derive(Debug, Eq)

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

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

///|
pub(all) struct DatasetInfo {
  datatype : DataType
  dataspace : Dataspace
  storage : Storage
  fill : Bytes
} derive(Debug)

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

///|
fn decode_layout(
  m : Message,
  sb : Superblock,
  limits : Limits,
) -> Storage raise H5Error {
  let c = cursor(m.bytes, 0, m.bytes.length())
  let version = c.u8()
  if version != 3 && version != 4 {
    raise Unsupported("data layout version", m.at)
  }
  match c.u8() {
    0 => {
      let n = c.index(2)
      Compact(c.bytes(n))
    }
    1 => {
      let address = c.address(sb.offset_width, sb.base)
      let size = c.index(sb.size_width)
      if size > limits.max_file_bytes {
        raise Limit("contiguous dataset size")
      }
      Contiguous(address, size)
    }
    2 if version == 3 => {
      let rank = c.u8()
      if rank < 2 || rank - 1 > limits.max_rank {
        raise Limit("chunk rank")
      }
      let root = c.address(sb.offset_width, sb.base)
      let dims : Array[Int32] = []
      for i = 0; i < rank; i = i + 1 {
        dims.push(c.index(4))
      }
      Chunked(root, dims)
    }
    2 => {
      let flags = c.u8()
      let rank = c.u8()
      let width = c.u8()
      if flags != 0 {
        raise Unsupported(
          "partial edge filtering or single-index chunk flags",
          m.at,
        )
      }
      if rank < 2 || rank - 1 > limits.max_rank {
        raise Limit("chunk rank")
      }
      let dims : Array[Int32] = []
      for i = 0; i < rank; i = i + 1 {
        dims.push(c.index(width))
      }
      let kind = c.u8()
      if kind != 3 {
        raise Unsupported("single/implicit/extensible/Btree2 chunk index", m.at)
      }
      let page_bits = c.u8()
      FixedArray(c.address(sb.offset_width, sb.base), dims, page_bits)
    }
    _ => raise Unsupported("virtual or unknown storage", m.at)
  }
}

///|
fn decode_fill(m : Message, width : Int32) -> Bytes raise H5Error {
  let c = cursor(m.bytes, 0, m.bytes.length())
  if m.kind == 4 {
    let size = c.index(4)
    if size == 0 {
      return Bytes::from_array(Array::make(width, 0))
    }
    if size != width {
      raise Invalid("legacy fill size differs from datatype", m.at)
    }
    return c.bytes(size)
  }
  let version = c.u8()
  let defined = match version {
    1 | 2 => {
      c.skip(2)
      c.u8() != 0
    }
    3 => {
      let flags = c.u8()
      if (flags & 0xc0) != 0 || (flags & 0x30) == 0x30 {
        raise Invalid("fill flags", m.at)
      }
      if (flags & 16) != 0 {
        raise Unsupported("undefined fill value", m.at)
      }
      if (flags & 32) == 0 {
        return Bytes::from_array(Array::make(width, 0))
      }
      (flags & 32) != 0
    }
    _ => raise Unsupported("fill message version", m.at)
  }
  if !defined {
    raise Unsupported("undefined fill value", m.at)
  }
  let size = c.index(4)
  if size == 0 {
    return Bytes::from_array(Array::make(width, 0))
  }
  if size != width {
    raise Invalid("fill size differs from datatype", m.at)
  }
  c.bytes(size)
}

///|
pub fn H5File::dataset_info(
  self : H5File,
  path : String,
) -> DatasetInfo raise H5Error {
  let messages = self.messages(self.resolve(path))
  let mut dtype : DataType? = None
  let mut space : Dataspace? = None
  let mut storage : Storage? = None
  let mut fill_message : Message? = None
  let mut old_fill_message : Message? = None
  for m in messages {
    let c = cursor(m.bytes, 0, m.bytes.length())
    match m.kind {
      4 => {
        if old_fill_message is Some(_) {
          raise Invalid("duplicate old fill value", m.at)
        }
        old_fill_message = Some(m)
      }
      1 => {
        if space != None {
          raise Invalid("duplicate dataspace", m.at)
        }
        space = Some(decode_space(c, self.sb.size_width, self.limits))
      }
      3 => {
        if dtype != None {
          raise Invalid("duplicate datatype", m.at)
        }
        dtype = Some(decode_type(c))
      }
      8 => {
        if storage != None {
          raise Invalid("duplicate data layout", m.at)
        }
        storage = Some(decode_layout(m, self.sb, self.limits))
      }
      5 => {
        if fill_message is Some(_) {
          raise Invalid("duplicate fill value", m.at)
        }
        fill_message = Some(m)
      }
      7 => raise Unsupported("external dataset files", m.at)
      _ =>
        if (m.flags & 128) != 0 &&
          m.kind != 12 &&
          m.kind != 11 &&
          m.kind != 21 &&
          m.kind != 22 {
          raise Unsupported("mandatory dataset message", m.at)
        }
    }
  }
  guard dtype is Some(datatype) else { raise Invalid("missing datatype", 0) }
  guard space is Some(dataspace) else { raise Invalid("missing dataspace", 0) }
  guard storage is Some(storage) else {
    raise Invalid("missing data layout", 0)
  }
  let width = datatype.width()
  let size = checked_mul(dataspace.elements(limits=self.limits), width)
  if size > self.limits.max_file_bytes {
    raise Limit("dataset bytes")
  }
  match storage {
    Compact(data) =>
      if data.length() != size {
        raise Invalid("compact size mismatch", 0)
      }
    Contiguous(_, n) =>
      if n != size {
        raise Invalid("contiguous size mismatch", 0)
      }
    Chunked(_, dims) | FixedArray(_, dims, _) => {
      guard dataspace is Simple(shape) else {
        raise Invalid("chunked non-simple dataspace", 0)
      }
      if dims.length() != shape.length() + 1 || dims.last() != Some(width) {
        raise Invalid("chunk dimensions", 0)
      }
      let mut total = 1
      for dim in dims {
        if dim <= 0 {
          raise Invalid("zero chunk dimension", 0)
        }
        total = checked_mul(total, dim)
      }
      if total > self.limits.max_file_bytes {
        raise Limit("chunk bytes")
      }
    }
  }
  let chosen_fill = match fill_message {
    Some(_) => fill_message
    None => old_fill_message
  }
  let fill = match chosen_fill {
    Some(m) => decode_fill(m, width)
    None => Bytes::from_array(Array::make(width, 0))
  }
  { datatype, dataspace, storage, fill, }
}