///|
/// Otsu's method: computes a global luma threshold automatically by choosing
/// the value that maximizes the between-class variance of the two pixel
/// groups it splits the histogram into. The whole search is integer/rational
/// (no floating point), so the chosen threshold is exactly reproducible.

///|
/// Builds the 256-bin luma histogram (BT.601 integer weights).
fn Image::luma_histogram(self : Image) -> FixedArray[Int] {
  let hist = FixedArray::make(256, 0)
  let n = self.pixel_count()
  for p in 0.. Int {
  let hist = self.luma_histogram()
  let total = self.pixel_count()
  if total == 0 {
    return 0
  }
  // Total weighted intensity sum (Double to avoid overflow on big images).
  let mut sum_all = 0.0
  for i in 0..<256 {
    sum_all = sum_all + (i * hist[i]).to_double()
  }
  let mut w_b = 0 // count of the background class (luma <= t)
  let mut sum_b = 0.0 // weighted intensity sum of the background class
  let mut best_t = 0
  let mut best_var = -1.0
  for t in 0..<256 {
    w_b = w_b + hist[t]
    if w_b == 0 {
      continue
    }
    let w_f = total - w_b
    if w_f == 0 {
      break
    }
    sum_b = sum_b + (t * hist[t]).to_double()
    let mean_b = sum_b / w_b.to_double()
    let mean_f = (sum_all - sum_b) / w_f.to_double()
    let diff = mean_b - mean_f
    let between = w_b.to_double() * w_f.to_double() * diff * diff
    if between > best_var {
      best_var = between
      best_t = t
    }
  }
  best_t
}

///|
/// Applies Otsu's automatic threshold, returning a binary (black/white)
/// grayscale image. Pixels with luma greater than the computed threshold
/// become white.
pub fn Image::otsu(self : Image) -> Image {
  let t = self.otsu_threshold()
  self.map_rgb(fn(r, g, b) {
    let y = (299 * r + 587 * g + 114 * b) / 1000
    let v = if y > t { 255 } else { 0 }
    (v, v, v)
  })
}