///|
pub(all) struct ChunkInfo {
  y : Int32
  rows : Int32
  offset : Int32
  encoded_bytes : Int32
  decoded_bytes : Int32
} derive(Debug, Eq)

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

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

///|
pub struct ExrFile {
  priv bytes : Bytes
  priv header : Header
  priv chunks : Array[ChunkInfo]
  priv limits : Limits
}

///|
pub fn ExrFile::open(
  bytes : Bytes,
  limits~ : Limits = Limits::default(),
) -> ExrFile raise ExrError {
  let h = parse_header(bytes, limits)
  let lines = h.compression.scanlines()
  let height = h.data_window.height()
  let count = (height - 1) / lines + 1
  if count > limits.max_chunks {
    raise Limit("chunk directory entries")
  }
  let table_end = add(h.end, mul(count, 8))
  if table_end > bytes.length() {
    raise Invalid("truncated chunk offset table", h.end)
  }
  let table = reader(bytes[h.end:table_end].to_owned())
  let chunks : Array[ChunkInfo] = []
  let mut stride = 0
  for ch in h.channels {
    stride = add(stride, ch.kind.bytes())
  }
  let row_bytes = mul(h.data_window.width(), stride)
  for i = 0; i < count; i = i + 1 {
    let offset = index(table.uint(8))
    if offset < table_end || offset > bytes.length() - 8 {
      raise Invalid("chunk offset outside file body", offset)
    }
    let r = reader(bytes[offset:offset + 8].to_owned())
    let y = r.i32()
    let encoded_bytes = r.i32()
    let expected_y = (h.data_window.y0.to_int64() + mul(i, lines).to_int64()).to_int()
    if y != expected_y {
      raise Invalid("offset table row mismatch", offset)
    }
    let rows = imin(lines, height - i * lines)
    let decoded_bytes = mul(row_bytes, rows)
    if encoded_bytes <= 0 ||
      encoded_bytes > decoded_bytes ||
      encoded_bytes > bytes.length() - offset - 8 {
      raise Invalid("chunk payload size", offset + 4)
    }
    if h.compression == None && encoded_bytes != decoded_bytes {
      raise Invalid("uncompressed chunk size", offset + 4)
    }
    chunks.push({ y, rows, offset, encoded_bytes, decoded_bytes, })
  }
  let physical = chunks.copy()
  physical.sort_by((a, b) => a.offset.compare(b.offset))
  let mut previous_end = table_end
  for c in physical {
    if c.offset < previous_end {
      raise Invalid("duplicate/overlapping chunk range", c.offset)
    }
    previous_end = add(add(c.offset, 8), c.encoded_bytes)
  }
  { bytes, header: h, chunks, limits, }
}

///|
pub fn ExrFile::header(self : ExrFile) -> Header {
  self.header
}

///|
pub fn ExrFile::chunks(self : ExrFile) -> Array[ChunkInfo] {
  self.chunks.copy()
}

///|
/// Exact encoded chunks needed for a region; full input Bytes are still retained.
pub fn ExrFile::plan(self : ExrFile, region : Window) -> Array[ChunkInfo] {
  match self.header.data_window.intersection(region) {
    None => []
    Some(w) => {
      let first = ((w.y0.to_int64() - self.header.data_window.y0.to_int64()) /
      self.header.compression.scanlines().to_int64()).to_int()
      let last = ((w.y1.to_int64() - self.header.data_window.y0.to_int64()) /
      self.header.compression.scanlines().to_int64()).to_int()
      Array::makei(last - first + 1, i => self.chunks[first + i])
    }
  }
}

///|
fn ExrFile::unpack_chunk(
  self : ExrFile,
  info : ChunkInfo,
) -> Bytes raise ExrError {
  let begin = info.offset + 8
  unpack(
    self.bytes[begin:begin + info.encoded_bytes].to_owned(),
    self.header.compression,
    info.decoded_bytes,
  )
}