///|
/// Crop by absolute coordinates; reject disjoint requests, clip partial overlap.
pub fn Image::crop(
  self : Image,
  region : Window,
  limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
  limits.validate()
  let window = match self.data_window.intersection(region) {
    Some(w) => w
    None => raise Invalid("crop does not intersect image", 0)
  }
  let mut total = 0
  for p in self.planes {
    total = add(total, mul(window.pixels(), p.channel.kind.bytes()))
  }
  if window.pixels() > limits.max_pixels || total > limits.max_decoded_bytes {
    raise Limit("crop output budget")
  }
  let planes = self.planes.map(p => {
    let b = sink()
    let row_bytes = mul(window.width(), p.channel.kind.bytes())
    for row = 0; row < window.height(); row = row + 1 {
      let start = mul(
        self.data_window.offset(window.x0, window.y0 + row),
        p.channel.kind.bytes(),
      )
      b.raw(p.data[start:start + row_bytes].to_owned())
    }
    Plane::from_bits(p.channel, window, b.finish(), limits~)
  })
  self.with_planes(window, planes, limits)
}

///|
pub fn Image::select(
  self : Image,
  names : Array[String],
  limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
  let channels = selected_channels(self.channels(), Some(names))
  self.with_planes(
    self.data_window,
    channels.map(c => self.plane(c.name)),
    limits,
  )
}

///|
pub fn Image::rename(
  self : Image,
  mapping : Map[String, String],
  limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
  for name, _ in mapping {
    ignore(self.plane(name))
  }
  let planes = self.planes.map(p => {
    let name = mapping.get(p.channel.name).unwrap_or(p.channel.name)
    Plane::from_bits({ ..p.channel, name, }, p.window, p.data, limits~)
  })
  self.with_planes(self.data_window, planes, limits)
}

///|
fn translated(v : Int32, delta : Int32) -> Int32 raise ExrError {
  let result = v.to_int64() + delta.to_int64()
  if result < -2147483648L || result > 2147483647L {
    raise Limit("translated coordinate")
  }
  result.to_int()
}

///|
/// Translate both windows without resampling or changing any sample bits.
pub fn Image::translate(
  self : Image,
  dx : Int32,
  dy : Int32,
  limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
  let moved = fn(w : Window) -> Window raise ExrError {
    Window::new(
      translated(w.x0, dx),
      translated(w.y0, dy),
      translated(w.x1, dx),
      translated(w.y1, dy),
    )
  }
  let window = moved(self.data_window)
  Image::new(
    window,
    self.planes.map(p => Plane::from_bits(p.channel, window, p.data, limits~)),
    display=moved(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 "crop selection rename and translation preserve image coordinate semantics" {
  let w = Window::new(-2, -1, 1, 1)
  let p = Plane::from_values(
    { name: "beauty.R", kind: F32, linear: false, },
    w,
    Array::makei(12, i => i.to_double()),
  )
  let image = Image::new(w, [p])
  let crop = image.crop(Window::new(-1, 0, 8, 8))
  assert_eq(crop.window(), Window::new(-1, 0, 1, 1))
  assert_eq(crop.display_window(), w)
  assert_eq(crop.plane("beauty.R").value_at(-1, 0), 5.0)
  let renamed = crop.rename({ "beauty.R": "R" }).select(["R"])
  assert_eq(
    renamed.plane("R").sample_bits(),
    crop.plane("beauty.R").sample_bits(),
  )
  let moved = renamed.translate(10, -5)
  assert_eq(moved.plane("R").value_at(9, -5), 5.0)
  assert_eq(moved.display_window(), Window::new(8, -6, 11, -4))
  assert_true(
    try {
      ignore(image.select(["absent"]))
      false
    } catch {
      _ => true
    },
  )
  assert_true(
    try {
      ignore(image.rename({ "absent": "R" }))
      false
    } catch {
      _ => true
    },
  )
}