///|
pub struct Image {
  priv data_window : Window
  priv display_window : Window
  priv planes : Array[Plane]
  priv metadata : Array[Attribute]
  priv pixel_aspect : Double
  priv screen_x : Double
  priv screen_y : Double
  priv screen_width : Double
}

///|
pub fn Image::new(
  window : Window,
  planes : Array[Plane],
  display~ : Window = window,
  metadata~ : Array[Attribute] = [],
  pixel_aspect~ : Double = 1.0,
  screen_center~ : (Double, Double) = (0.0, 0.0),
  screen_width~ : Double = 1.0,
  limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
  limits.validate()
  if planes.is_empty() || planes.length() > limits.max_channels {
    raise Limit("image channel count")
  }
  if window.pixels() > limits.max_pixels {
    raise Limit("image pixel count")
  }
  let ordered = planes.copy()
  ordered.sort_by((a, b) => name_compare(a.channel.name, b.channel.name))
  let mut previous = ""
  let mut bytes = 0
  for p in ordered {
    valid_name(p.channel.name)
    if p.window != window || p.channel.name == previous {
      raise Invalid("plane window or duplicate channel", 0)
    }
    previous = p.channel.name
    bytes = add(bytes, p.data.length())
  }
  if bytes > limits.max_decoded_bytes {
    raise Limit("image decoded bytes")
  }
  if metadata.length() > limits.max_attributes - 8 {
    raise Limit("image metadata count")
  }
  let names : Map[String, Bool] = Map([])
  let mut header_size = 0
  for a in metadata {
    a.validate(limits.max_header_bytes)
    if reserved(a.name) || names.contains(a.name) {
      raise Invalid("reserved/duplicate metadata", 0)
    }
    names[a.name] = true
    header_size = add(
      header_size,
      add(
        a.value.length(),
        @utf8.encode(a.name).length() + @utf8.encode(a.kind).length() + 6,
      ),
    )
  }
  if header_size > limits.max_header_bytes {
    raise Limit("metadata byte budget")
  }
  let (screen_x, screen_y) = screen_center
  for v in [pixel_aspect, screen_x, screen_y, screen_width] {
    if !finite(v) || Float::from_double(v).is_inf() {
      raise Invalid("finite screen/aspect value required", 0)
    }
  }
  if Float::from_double(pixel_aspect).to_double() <= 0.0 || screen_width < 0.0 {
    raise Invalid("invalid aspect/width", 0)
  }
  {
    data_window: window,
    display_window: display,
    planes: ordered,
    metadata: metadata.copy(),
    pixel_aspect,
    screen_x,
    screen_y,
    screen_width,
  }
}

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

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

///|
pub fn Image::channels(self : Image) -> Array[Channel] {
  self.planes.map(p => p.channel)
}

///|
pub fn Image::metadata(self : Image) -> Array[Attribute] {
  self.metadata.copy()
}

///|
pub fn Image::pixel_aspect(self : Image) -> Double {
  self.pixel_aspect
}

///|
pub fn Image::screen(self : Image) -> (Double, Double, Double) {
  (self.screen_x, self.screen_y, self.screen_width)
}

///|
pub fn Image::plane(self : Image, name : String) -> Plane raise ExrError {
  for p in self.planes {
    if p.channel.name == name {
      return p
    }
  }
  raise Invalid("missing channel: " + name, 0)
}

///|
fn Image::with_planes(
  self : Image,
  window : Window,
  planes : Array[Plane],
  limits : Limits,
) -> Image raise ExrError {
  Image::new(
    window,
    planes,
    display=self.display_window,
    metadata=self.metadata,
    pixel_aspect=self.pixel_aspect,
    screen_center=(self.screen_x, self.screen_y),
    screen_width=self.screen_width,
    limits~,
  )
}

///|
test "image owns channel and metadata arrays and enforces common windows" {
  let w = Window::new(0, 0, 0, 0)
  let p = Plane::from_values({ name: "R", kind: F32, linear: false, }, w, [2.0])
  let planes = [p]
  let attrs = [Attribute::text("comments", "a")]
  let img = Image::new(w, planes, metadata=attrs)
  planes.clear()
  attrs.clear()
  assert_eq(img.plane("R").value_at(0, 0), 2.0)
  assert_eq(img.metadata().length(), 1)
  assert_true(
    try {
      ignore(Image::new(w, [p, p]))
      false
    } catch {
      _ => true
    },
  )
  assert_true(
    try {
      ignore(Image::new(w, [p], metadata=[Attribute::text("channels", "bad")]))
      false
    } catch {
      _ => true
    },
  )
}