///|
fn image_attributes(
  image : Image,
  codec : Compression,
) -> Array[Attribute] raise ExrError {
  let attrs = header_attributes(
    image.data_window,
    image.display_window,
    image.channels(),
    codec,
  )
  for i = 0; i < attrs.length(); i = i + 1 {
    if attrs[i].name == "pixelAspectRatio" {
      attrs[i] = Attribute::real("pixelAspectRatio", image.pixel_aspect)
    }
    if attrs[i].name == "screenWindowWidth" {
      attrs[i] = Attribute::real("screenWindowWidth", image.screen_width)
    }
    if attrs[i].name == "screenWindowCenter" {
      let s = sink()
      s.f32(image.screen_x)
      s.f32(image.screen_y)
      attrs[i] = { name: "screenWindowCenter", kind: "v2f", value: s.finish(), }
    }
  }
  let metadata = image.metadata.copy()
  metadata.sort_by((a, b) => name_compare(a.name, b.name))
  for attr in metadata {
    attrs.push(attr)
  }
  attrs
}

///|
/// Deterministic scanline output; compression is lossless on sample bits.
pub fn encode(
  image : Image,
  compression~ : Compression = Zip,
  limits~ : Limits = Limits::default(),
) -> Bytes raise ExrError {
  limits.validate()
  // Revalidate against this operation's budgets, which may differ from creation.
  ignore(
    Image::new(
      image.data_window,
      image.planes,
      display=image.display_window,
      metadata=image.metadata,
      pixel_aspect=image.pixel_aspect,
      screen_center=(image.screen_x, image.screen_y),
      screen_width=image.screen_width,
      limits~,
    ),
  )
  let header = header_bytes(image_attributes(image, compression))
  if header.length() > limits.max_header_bytes {
    raise Limit("encoded header bytes")
  }
  let height = image.data_window.height()
  let lines = compression.scanlines()
  let count = (height - 1) / lines + 1
  if count > limits.max_chunks {
    raise Limit("writer chunk directory entries")
  }
  let prefix_size = add(header.length(), mul(count, 8))
  if prefix_size > limits.max_file_bytes {
    raise Limit("offset table/file budget")
  }
  let body = sink()
  let offsets : Array[Int32] = []
  let width = image.data_window.width()
  for i = 0; i < count; i = i + 1 {
    offsets.push(add(prefix_size, body.size()))
    let row0 = i * lines
    let rows = imin(lines, height - row0)
    let raw = sink()
    for row = row0; row < row0 + rows; row = row + 1 {
      for p in image.planes {
        let row_bytes = mul(width, p.channel.kind.bytes())
        let begin = mul(row, row_bytes)
        raw.raw(p.data[begin:begin + row_bytes].to_owned())
      }
    }
    let packed = pack(raw.finish(), compression)
    if add(add(prefix_size, body.size()), add(8, packed.length())) >
      limits.max_file_bytes {
      raise Limit("encoded file bytes")
    }
    body.i32((image.data_window.y0.to_int64() + row0.to_int64()).to_int())
    body.i32(packed.length())
    body.raw(packed)
  }
  let out = sink()
  out.raw(header)
  for offset in offsets {
    out.uint(offset.to_uint64(), 8)
  }
  out.raw(body.finish())
  out.finish()
}

///|
test "EXR multichannel lossless codecs crop projection and metadata roundtrip" {
  let w = Window::new(-5, -3, 7, 15)
  let count = w.pixels()
  let image = Image::new(
    w,
    [
      Plane::from_values(
        { name: "R", kind: F16, linear: false, },
        w,
        Array::makei(count, i => (i % 31).to_double() / 8.0),
      ),
      Plane::from_values(
        { name: "Z", kind: F32, linear: true, },
        w,
        Array::makei(count, i => (i - 20).to_double()),
      ),
      Plane::from_values(
        { name: "id", kind: U32, linear: true, },
        w,
        Array::makei(count, i => 4294967295.0 - i.to_double()),
      ),
    ],
    metadata=[
      Attribute::text("comments", "synthetic"),
      { name: "opaque", kind: "bytes", value: b"\xff\x00", },
    ],
    pixel_aspect=2.0,
    screen_center=(0.25, -0.5),
  )
  for codec in [None, Rle, Zips, Zip] {
    let encoded = encode(image, compression=codec)
    let file = ExrFile::open(encoded)
    let restored = file.read()
    assert_eq(restored.channels(), image.channels())
    for ch in image.channels() {
      assert_eq(
        restored.plane(ch.name).sample_bits(),
        image.plane(ch.name).sample_bits(),
      )
    }
    assert_eq(restored.metadata(), image.metadata())
    assert_eq(restored.pixel_aspect(), 2.0)
    assert_eq(restored.screen(), (0.25, -0.5, 1.0))
    let region = Window::new(-2, 0, 3, 3)
    let result = file.read_with_stats(
      region=Some(region),
      channels=Some(["id"]),
    )
    assert_eq(result.image.window(), region)
    assert_eq(result.image.channels().length(), 1)
    assert_eq(
      result.image.plane("id").bits_at(0, 2),
      image.plane("id").bits_at(0, 2),
    )
    assert_eq(result.chunks_decoded, if codec == Zip { 1 } else { 4 })
    assert_eq(encode(restored, compression=codec), encoded)
  }
}