///|
pub struct Header {
  priv data_window : Window
  priv display_window : Window
  priv channels : Array[Channel]
  priv compression : Compression
  priv attributes : Array[Attribute]
  priv line_order : Int32
  priv end : Int32
}

///|
pub fn Header::data_window(self : Header) -> Window {
  self.data_window
}

///|
pub fn Header::display_window(self : Header) -> Window {
  self.display_window
}

///|
pub fn Header::channels(self : Header) -> Array[Channel] {
  self.channels.copy()
}

///|
pub fn Header::compression(self : Header) -> Compression {
  self.compression
}

///|
pub fn Header::line_order(self : Header) -> Int32 {
  self.line_order
}

///|
pub fn Header::metadata(self : Header) -> Array[Attribute] {
  self.attributes.filter(a => !reserved(a.name))
}

///|
pub fn Header::attribute(self : Header, name : String) -> Attribute? {
  for a in self.attributes {
    if a.name == name {
      return Some(a)
    }
  }
  None
}

///|
fn required(
  attrs : Map[String, Attribute],
  name : String,
  kind : String,
) -> Bytes raise ExrError {
  match attrs.get(name) {
    Some(a) if a.kind == kind => a.value
    _ => raise Invalid("missing/wrong attribute: " + name, 8)
  }
}

///|
fn read_channels(
  bytes : Bytes,
  maximum_name : Int32,
  limits : Limits,
) -> Array[Channel] raise ExrError {
  let r = reader(bytes)
  let channels : Array[Channel] = []
  while true {
    let name = r.cstring(maximum_name)
    if name == "" {
      break
    }
    if channels.length() >= limits.max_channels {
      raise Limit("channel count")
    }
    let kind = pixel_type(r.i32())
    let linear = r.u8()
    if linear > 1 || r.uint(3) != 0UL {
      raise Invalid("channel flags/reserved bytes", r.pos)
    }
    if r.i32() != 1 || r.i32() != 1 {
      raise Unsupported("subsampled channels")
    }
    if channels.length() > 0 &&
      name_compare(channels[channels.length() - 1].name, name) >= 0 {
      raise Invalid("channel names must be unique and sorted", r.pos)
    }
    channels.push({ name, kind, linear: linear == 1, })
  }
  if channels.is_empty() || r.pos != r.end {
    raise Invalid("empty/trailing channel list", r.pos)
  }
  channels
}

///|
fn parse_header(bytes : Bytes, limits : Limits) -> Header raise ExrError {
  limits.validate()
  if bytes.length() > limits.max_file_bytes {
    raise Limit("input file bytes")
  }
  let r = reader(bytes)
  if r.uint(4) != 20000630UL {
    raise Invalid("OpenEXR magic", 0)
  }
  let version = r.uint(4).to_uint()
  if (version & 255U) != 2U {
    raise Unsupported("OpenEXR version")
  }
  if (version & 0xfffffb00U) != 0U {
    raise Unsupported("tiled, deep, multipart or unknown file flags")
  }
  let maximum_name = if (version & 1024U) != 0U { 255 } else { 31 }
  let attrs : Map[String, Attribute] = Map([])
  let ordered : Array[Attribute] = []
  while true {
    if r.pos >= limits.max_header_bytes {
      raise Limit("header byte budget")
    }
    let name = r.cstring(maximum_name)
    if name == "" {
      break
    }
    if ordered.length() >= limits.max_attributes {
      raise Limit("attribute count")
    }
    let kind = r.cstring(maximum_name)
    valid_name(kind)
    let size = r.i32()
    if size < 0 || size > limits.max_header_bytes - r.pos {
      raise Limit("attribute/header bytes")
    }
    let attr = { name, kind, value: r.take(size), }
    if attrs.contains(name) {
      raise Invalid("duplicate header attribute: " + name, r.pos)
    }
    attrs[name] = attr
    ordered.push(attr)
  }
  if r.pos > limits.max_header_bytes {
    raise Limit("header byte budget")
  }
  let data_window = read_window(required(attrs, "dataWindow", "box2i"))
  let display_window = read_window(required(attrs, "displayWindow", "box2i"))
  if data_window.pixels() > limits.max_pixels {
    raise Limit("data window pixels")
  }
  let channels = read_channels(
    required(attrs, "channels", "chlist"),
    maximum_name,
    limits,
  )
  let encoded_compression = required(attrs, "compression", "compression")
  if encoded_compression.length() != 1 {
    raise Invalid("compression length", 0)
  }
  let codec = compression(encoded_compression[0].to_int())
  let order = required(attrs, "lineOrder", "lineOrder")
  if order.length() != 1 || order[0].to_int() > 1 {
    raise Unsupported("non-monotonic scanline order")
  }
  let aspect = required(attrs, "pixelAspectRatio", "float")
  let center = required(attrs, "screenWindowCenter", "v2f")
  let screen = required(attrs, "screenWindowWidth", "float")
  if aspect.length() != 4 || center.length() != 8 || screen.length() != 4 {
    raise Invalid("screen/aspect attribute size", 0)
  }
  let ratio = reader(aspect).f32()
  let width = reader(screen).f32()
  let xy = reader(center)
  if !finite(ratio) ||
    ratio <= 0.0 ||
    !finite(width) ||
    width < 0.0 ||
    !finite(xy.f32()) ||
    !finite(xy.f32()) {
    raise Invalid("invalid screen/aspect values", 0)
  }
  let mut stride = 0
  for ch in channels {
    stride = add(stride, ch.kind.bytes())
  }
  if mul(data_window.pixels(), stride) > limits.max_decoded_bytes {
    raise Limit("logical decoded image bytes")
  }
  if attrs.contains("tiles") ||
    attrs.contains("chunkCount") ||
    attrs.contains("maxSamplesPerPixel") {
    raise Unsupported("non-flat header structure")
  }
  {
    data_window,
    display_window,
    channels,
    compression: codec,
    attributes: ordered,
    line_order: order[0].to_int(),
    end: r.pos,
  }
}

///|
fn channel_bytes(channels : Array[Channel]) -> Bytes {
  let s = sink()
  for ch in channels {
    s.cstring(ch.name)
    s.i32(ch.kind.code())
    s.uint(if ch.linear { 1UL } else { 0UL }, 1)
    s.uint(0UL, 3)
    s.i32(1)
    s.i32(1)
  }
  s.uint(0UL, 1)
  s.finish()
}

///|
fn header_attributes(
  data : Window,
  display : Window,
  channels : Array[Channel],
  codec : Compression,
) -> Array[Attribute] {
  [
    { name: "channels", kind: "chlist", value: channel_bytes(channels), },
    {
      name: "compression",
      kind: "compression",
      value: Bytes::from_array([codec.code().to_byte()]),
    },
    { name: "dataWindow", kind: "box2i", value: window_bytes(data), },
    { name: "displayWindow", kind: "box2i", value: window_bytes(display), },
    { name: "lineOrder", kind: "lineOrder", value: b"\x00", },
    { name: "pixelAspectRatio", kind: "float", value: b"\x00\x00\x80\x3f", },
    {
      name: "screenWindowCenter",
      kind: "v2f",
      value: b"\x00\x00\x00\x00\x00\x00\x00\x00",
    },
    { name: "screenWindowWidth", kind: "float", value: b"\x00\x00\x80\x3f", },
  ]
}

///|
fn header_bytes(attrs : Array[Attribute]) -> Bytes {
  let out = sink()
  out.uint(20000630UL, 4)
  out.uint(1026UL, 4)
  for a in attrs {
    write_attribute(out, a)
  }
  out.uint(0UL, 1)
  out.finish()
}

///|
test "header preserves offset windows unknown attributes and flags" {
  let window = Window::new(-4, 7, 8, 10)
  let attrs = header_attributes(
    window,
    window,
    [{ name: "Z", kind: F32, linear: false, }],
    Zip,
  )
  attrs.push({ name: "custom", kind: "opaque", value: b"\xff\x00", })
  let h = parse_header(header_bytes(attrs), Limits::default())
  assert_eq(h.data_window(), window)
  assert_eq(h.channels()[0].name, "Z")
  assert_eq(h.compression(), Zip)
  assert_eq(h.metadata()[0].value, b"\xff\x00")
  attrs.push(attrs[0])
  assert_true(
    try {
      ignore(parse_header(header_bytes(attrs), Limits::default()))
      false
    } catch {
      _ => true
    },
  )
}