///|
/// Paste absolute-coordinate channel samples without converting their bits.
/// The patch must fit completely; only matching channels are replaced.
pub fn Image::paste_region(
  self : Image,
  patch : Image,
  limits~ : Limits = Limits::default(),
) -> Image raise ExrError {
  ignore(self.with_planes(self.data_window, self.planes, limits))
  let w = patch.data_window
  if !self.data_window.contains(w.x0, w.y0) ||
    !self.data_window.contains(w.x1, w.y1) {
    raise Invalid("patch outside destination data window", 0)
  }
  for p in patch.planes {
    if self.plane(p.channel.name).channel != p.channel {
      raise Invalid("patch channel type or linearity differs", 0)
    }
  }
  let planes : Array[Plane] = []
  for base in self.planes {
    let mut replacement : Plane? = None
    for p in patch.planes {
      if p.channel.name == base.channel.name {
        replacement = Some(p)
      }
    }
    match replacement {
      None => planes.push(base)
      Some(p) => {
        let data = Array::makei(base.data.length(), i => base.data[i])
        let stride = base.channel.kind.bytes()
        let row_bytes = mul(w.width(), stride)
        for row = 0; row < w.height(); row = row + 1 {
          let start = mul(self.data_window.offset(w.x0, w.y0 + row), stride)
          for b = 0; b < row_bytes; b = b + 1 {
            data[start + b] = p.data[row * row_bytes + b]
          }
        }
        planes.push(
          Plane::from_bits(
            base.channel,
            base.window,
            Bytes::from_array(data),
            limits~,
          ),
        )
      }
    }
  }
  self.with_planes(self.data_window, planes, limits)
}

///|
pub(all) struct Quantiles {
  finite_samples : Int32
  nonfinite_samples : Int32
  values : Array[Double?]
}

///|
/// Finite-only nearest-rank empirical quantiles, in caller probability order.
/// p=0 selects the minimum. Empty finite populations produce None.
pub fn Plane::quantiles(
  self : Plane,
  probabilities : Array[Double],
  limits~ : Limits = Limits::default(),
) -> Quantiles raise ExrError {
  limits.validate()
  let count = self.window.pixels()
  if count > limits.max_pixels ||
    probabilities.length() > limits.max_attributes ||
    mul(add(count, probabilities.length()), 8) > limits.max_decoded_bytes {
    raise Limit("quantile working set")
  }
  for p in probabilities {
    if !finite(p) || p < 0.0 || p > 1.0 {
      raise Invalid("quantile probability must be finite in 0..1", 0)
    }
  }
  let values : Array[Double] = []
  let r = reader(self.data)
  while r.pos < r.end {
    let bits = r.uint(self.channel.kind.bytes()).to_uint()
    let v = match self.channel.kind {
      U32 => bits.to_double()
      F16 => half_value(bits.reinterpret_as_int())
      F32 => Float::reinterpret_from_uint(bits).to_double()
    }
    if finite(v) {
      values.push(v)
    }
  }
  values.sort_by((a, b) => if a < b { -1 } else if a > b { 1 } else { 0 })
  let output : Array[Double?] = []
  for p in probabilities {
    if values.is_empty() {
      output.push(None)
    } else {
      let rank = p * values.length().to_double()
      let floor = rank.to_int()
      let ceiling = if rank > floor.to_double() { floor + 1 } else { floor }
      output.push(Some(values[imax(0, imin(values.length() - 1, ceiling - 1))]))
    }
  }
  {
    finite_samples: values.length(),
    nonfinite_samples: count - values.length(),
    values: output,
  }
}