///|
/// Image statistics and tonal adjustments. `stats` summarizes the image
/// (per-channel range and mean plus a luma mean) with exact integer
/// accumulation; `auto_contrast` and `levels` are the two most common tonal
/// remaps built on top of it. All brightness math stays integer except the
/// optional gamma exponent, which uses `@math.pow`.

///|
/// Per-channel and luma summary of an image. Means are rounded to the
/// nearest integer. The fields are read-only (`pub`): the struct is a
/// result type, so keeping construction internal lets it gain fields later
/// without breaking downstream code.
pub struct Stats {
  min_r : Int
  max_r : Int
  mean_r : Int
  min_g : Int
  max_g : Int
  mean_g : Int
  min_b : Int
  max_b : Int
  mean_b : Int
  luma_min : Int
  luma_max : Int
  luma_mean : Int
}

///|
/// Computes per-channel min/max/mean and the luma min/max/mean over all
/// pixels. For the maximum 100-megapixel image the largest channel sum is
/// `255 * 1e8 ~= 2.55e10`, which exceeds 32-bit range, so the accumulators
/// are `Int64`.
pub fn Image::stats(self : Image) -> Stats {
  let n = self.pixel_count()
  // Degenerate (empty) image: return an all-zero, self-consistent summary
  // instead of the sentinel min=255 / max=0 the scan would otherwise leave.
  if n == 0 {
    return {
      min_r: 0,
      max_r: 0,
      mean_r: 0,
      min_g: 0,
      max_g: 0,
      mean_g: 0,
      min_b: 0,
      max_b: 0,
      mean_b: 0,
      luma_min: 0,
      luma_max: 0,
      luma_mean: 0,
    }
  }
  let mut mnr = 255
  let mut mxr = 0
  let mut mng = 255
  let mut mxg = 0
  let mut mnb = 255
  let mut mxb = 0
  let mut mnl = 255
  let mut mxl = 0
  let mut sr = 0L
  let mut sg = 0L
  let mut sb = 0L
  let mut sl = 0L
  for p in 0.. mxr {
      mxr = r
    }
    if g < mng {
      mng = g
    }
    if g > mxg {
      mxg = g
    }
    if b < mnb {
      mnb = b
    }
    if b > mxb {
      mxb = b
    }
    if l < mnl {
      mnl = l
    }
    if l > mxl {
      mxl = l
    }
    sr = sr + r.to_int64()
    sg = sg + g.to_int64()
    sb = sb + b.to_int64()
    sl = sl + l.to_int64()
  }
  let denom = if n == 0 { 1L } else { n.to_int64() }
  let mean = fn(s : Int64) -> Int { ((s + denom / 2L) / denom).to_int() }
  {
    min_r: mnr,
    max_r: mxr,
    mean_r: mean(sr),
    min_g: mng,
    max_g: mxg,
    mean_g: mean(sg),
    min_b: mnb,
    max_b: mxb,
    mean_b: mean(sb),
    luma_min: mnl,
    luma_max: mxl,
    luma_mean: mean(sl),
  }
}

///|
/// Auto-contrast: linearly stretches the luma range `[luma_min, luma_max]`
/// to the full `[0, 255]`, applying the same scale to every channel so hues
/// are preserved. A flat image (min == max) is returned unchanged.
pub fn Image::auto_contrast(self : Image) -> Image {
  let s = self.stats()
  let lo = s.luma_min
  let hi = s.luma_max
  if hi <= lo {
    return self.copy()
  }
  let span = hi - lo
  let half = span / 2
  self.map_rgb(fn(r, g, b) {
    (
      ((r - lo) * 255 + half) / span,
      ((g - lo) * 255 + half) / span,
      ((b - lo) * 255 + half) / span,
    )
  })
}

///|
/// Levels adjustment: maps the input window `[black, white]` onto `[0, 255]`
/// with an optional midtone `gamma` (1.0 = linear; > 1 brightens midtones).
/// Values are clamped, so inputs at or below `black` become 0 and inputs at
/// or above `white` become 255. `white` is clamped to stay above `black`.
pub fn Image::levels(
  self : Image,
  black : Int,
  white : Int,
  gamma : Double,
) -> Image {
  let b0 = if black < 0 { 0 } else if black > 254 { 254 } else { black }
  let w0 = if white <= b0 { b0 + 1 } else if white > 255 { 255 } else { white }
  let span = (w0 - b0).to_double()
  let inv_gamma = if gamma <= 0.0 { 1.0 } else { 1.0 / gamma }
  // Precompute the 256-entry lookup table once.
  let lut = FixedArray::make(256, 0)
  for i in 0..<256 {
    let t = if i <= b0 {
      0.0
    } else if i >= w0 {
      1.0
    } else {
      (i - b0).to_double() / span
    }
    let adjusted = @math.pow(t, inv_gamma)
    lut[i] = clamp_byte((adjusted * 255.0 + 0.5).to_int()).to_int()
  }
  self.map_rgb(fn(r, g, b) { (lut[r], lut[g], lut[b]) })
}