///|
/// Per-channel 8-bit histogram collected in one pass over a canvas.
pub(all) struct PixelHistogram {
  red : Array[Int]
  green : Array[Int]
  blue : Array[Int]
  alpha : Array[Int]
  luma : Array[Int]
  total : Int
} derive(Debug)

///|
fn empty_bins() -> Array[Int] {
  Array::make(256, 0)
}

///|
pub fn PixelHistogram::red_bins(self : PixelHistogram) -> ArrayView[Int] {
  self.red[:]
}

///|
pub fn PixelHistogram::green_bins(self : PixelHistogram) -> ArrayView[Int] {
  self.green[:]
}

///|
pub fn PixelHistogram::blue_bins(self : PixelHistogram) -> ArrayView[Int] {
  self.blue[:]
}

///|
pub fn PixelHistogram::alpha_bins(self : PixelHistogram) -> ArrayView[Int] {
  self.alpha[:]
}

///|
pub fn PixelHistogram::luma_bins(self : PixelHistogram) -> ArrayView[Int] {
  self.luma[:]
}

///|
pub fn PixelHistogram::total(self : PixelHistogram) -> Int {
  self.total
}

///|
/// Count all pixel channels, including transparent colors, for diagnostics.
pub fn Canvas::pixel_histogram(self : Canvas) -> PixelHistogram {
  let red = empty_bins()
  let green = empty_bins()
  let blue = empty_bins()
  let alpha = empty_bins()
  let luma = empty_bins()
  for color in self.pixels {
    red[color.red().to_int()] += 1
    green[color.green().to_int()] += 1
    blue[color.blue().to_int()] += 1
    alpha[color.alpha().to_int()] += 1
    luma[color.luma().to_int()] += 1
  }
  PixelHistogram::{ red, green, blue, alpha, luma, total: self.pixels.length() }
}

///|
fn histogram_peak(bins : ArrayView[Int]) -> Byte {
  let mut best_index = 0
  let mut best_count = -1
  for i, count in bins {
    if count > best_count {
      best_index = i
      best_count = count
    }
  }
  best_index.to_byte()
}

///|
pub fn PixelHistogram::red_peak(self : PixelHistogram) -> Byte {
  histogram_peak(self.red[:])
}

///|
pub fn PixelHistogram::green_peak(self : PixelHistogram) -> Byte {
  histogram_peak(self.green[:])
}

///|
pub fn PixelHistogram::blue_peak(self : PixelHistogram) -> Byte {
  histogram_peak(self.blue[:])
}

///|
pub fn PixelHistogram::luma_peak(self : PixelHistogram) -> Byte {
  histogram_peak(self.luma[:])
}

///|
fn histogram_mean(bins : ArrayView[Int], total : Int) -> Int {
  if total <= 0 {
    0
  } else {
    let mut sum = 0
    for i, count in bins {
      sum += i * count
    }
    sum / total
  }
}

///|
pub fn PixelHistogram::mean_luma(self : PixelHistogram) -> Int {
  histogram_mean(self.luma[:], self.total)
}

///|
pub fn PixelHistogram::percentile_luma(
  self : PixelHistogram,
  percentile : Int,
) -> Byte {
  if self.total == 0 {
    0
  } else {
    let target = self.total *
      (if percentile < 0 {
        0
      } else if percentile > 100 {
        100
      } else {
        percentile
      }) /
      100
    let mut seen = 0
    let mut result = 255
    for i, count in self.luma {
      seen += count
      if seen > target {
        result = i
        break
      }
    }
    result.to_byte()
  }
}

///|
/// Region-level statistics used for crop selection and scene analysis.
pub(all) struct RegionSummary {
  rect : GifRect
  pixels : Int
  opaque_pixels : Int
  transparent_pixels : Int
  average : Rgba
  minimum_luma : Byte
  maximum_luma : Byte
} derive(Debug)

///|
pub fn RegionSummary::rect(self : RegionSummary) -> GifRect {
  self.rect
}

///|
pub fn RegionSummary::pixels(self : RegionSummary) -> Int {
  self.pixels
}

///|
pub fn RegionSummary::opaque_pixels(self : RegionSummary) -> Int {
  self.opaque_pixels
}

///|
pub fn RegionSummary::transparent_pixels(self : RegionSummary) -> Int {
  self.transparent_pixels
}

///|
pub fn RegionSummary::average(self : RegionSummary) -> Rgba {
  self.average
}

///|
pub fn RegionSummary::minimum_luma(self : RegionSummary) -> Byte {
  self.minimum_luma
}

///|
pub fn RegionSummary::maximum_luma(self : RegionSummary) -> Byte {
  self.maximum_luma
}

///|
/// Summarize a clipped logical rectangle. Empty rectangles return transparent
/// black and zero counts, which keeps analysis pipelines total.
pub fn Canvas::summarize(self : Canvas, rect : GifRect) -> RegionSummary {
  let left = if rect.left() < 0 {
    0
  } else if rect.left() > self.width {
    self.width
  } else {
    rect.left()
  }
  let top = if rect.top() < 0 {
    0
  } else if rect.top() > self.height {
    self.height
  } else {
    rect.top()
  }
  let right = if rect.right() > self.width {
    self.width
  } else if rect.right() < left {
    left
  } else {
    rect.right()
  }
  let bottom = if rect.bottom() > self.height {
    self.height
  } else if rect.bottom() < top {
    top
  } else {
    rect.bottom()
  }
  let mut pixels = 0
  let mut opaque_count = 0
  let mut transparent = 0
  let mut red = 0
  let mut green = 0
  let mut blue = 0
  let mut alpha = 0
  let mut minimum = 255
  let mut maximum = 0
  for y in top.. maximum {
        maximum = luma
      }
    }
  }
  let average = if pixels == 0 {
    rgba(0, 0, 0, 0)
  } else {
    rgba(
      (red / pixels).to_byte(),
      (green / pixels).to_byte(),
      (blue / pixels).to_byte(),
      (alpha / pixels).to_byte(),
    )
  }
  RegionSummary::{
    rect: GifRect::new(left~, top~, width=right - left, height=bottom - top),
    pixels,
    opaque_pixels: opaque_count,
    transparent_pixels: transparent,
    average,
    minimum_luma: if pixels == 0 {
      0
    } else {
      minimum.to_byte()
    },
    maximum_luma: if pixels == 0 {
      0
    } else {
      maximum.to_byte()
    },
  }
}

///|
/// Mean squared and perceptual error metrics for comparing encoded frames.
pub(all) struct QualityMetrics {
  pixels : Int
  differing_pixels : Int
  mean_squared_error : Int
  maximum_error : Int
  alpha_error : Int
  psnr_milli : Int
} derive(Debug)

///|
pub fn QualityMetrics::pixels(self : QualityMetrics) -> Int {
  self.pixels
}

///|
pub fn QualityMetrics::differing_pixels(self : QualityMetrics) -> Int {
  self.differing_pixels
}

///|
pub fn QualityMetrics::mean_squared_error(self : QualityMetrics) -> Int {
  self.mean_squared_error
}

///|
pub fn QualityMetrics::maximum_error(self : QualityMetrics) -> Int {
  self.maximum_error
}

///|
pub fn QualityMetrics::alpha_error(self : QualityMetrics) -> Int {
  self.alpha_error
}

///|
pub fn QualityMetrics::psnr_milli(self : QualityMetrics) -> Int {
  self.psnr_milli
}

///|
/// Compare two canvases using all four channels. `psnr_milli` is a fixed-point
/// approximation that avoids floating-point differences in test snapshots.
pub fn Canvas::quality_against(
  self : Canvas,
  reference : Canvas,
) -> QualityMetrics {
  if self.width != reference.width || self.height != reference.height {
    QualityMetrics::{
      pixels: self.pixels.length() + reference.pixels.length(),
      differing_pixels: self.pixels.length() + reference.pixels.length(),
      mean_squared_error: 65025,
      maximum_error: 255,
      alpha_error: 255,
      psnr_milli: 0,
    }
  } else {
    let mut sum = 0
    let mut different = 0
    let mut maximum = 0
    let mut alpha_sum = 0
    for i in 0.. maximum {
        maximum = error
      }
      if squared != 0 || da != 0 {
        different += 1
      }
    }
    let pixels = self.pixels.length()
    let mse = if pixels == 0 { 0 } else { sum / pixels }
    let psnr = if mse == 0 { 99000 } else { 48000 - int_log2(mse) * 3010 }
    QualityMetrics::{
      pixels,
      differing_pixels: different,
      mean_squared_error: mse,
      maximum_error: maximum,
      alpha_error: if pixels == 0 {
        0
      } else {
        alpha_sum / pixels
      },
      psnr_milli: if psnr < 0 {
        0
      } else {
        psnr
      },
    }
  }
}

///|
fn int_log2(value : Int) -> Int {
  let mut current = if value < 1 { 1 } else { value }
  let mut result = 0
  while current > 1 {
    current /= 2
    result += 1
  }
  result
}

///|
/// A hard threshold with optional soft transition around the cutoff.
pub fn Canvas::threshold(
  self : Canvas,
  cutoff : Int,
  softness? : Int = 0,
) -> Canvas {
  let safe_softness = if softness < 0 { 0 } else { softness }
  let result = self.copy()
  for i in 0..= cutoff {
        255
      } else {
        0
      }
    } else {
      let low = cutoff - safe_softness
      let high = cutoff + safe_softness
      if value <= low {
        0
      } else if value >= high {
        255
      } else {
        (value - low) * 255 / (high - low)
      }
    }
    result.pixels[i] = rgba(
      output.to_byte(),
      output.to_byte(),
      output.to_byte(),
      source.alpha(),
    )
  }
  result
}

///|
/// Stretch the luma range between the requested black and white points.
pub fn Canvas::normalize_luma(
  self : Canvas,
  black? : Int = 0,
  white? : Int = 255,
) -> Canvas {
  let low = if black < 0 { 0 } else if black > 255 { 255 } else { black }
  let high = if white <= low {
    low + 1
  } else if white > 255 {
    255
  } else {
    white
  }
  let result = self.copy()
  for i in 0.. Canvas {
  if levels <= 1 {
    self.threshold(128)
  } else {
    let safe_levels = if levels > 256 { 256 } else { levels }
    let result = self.copy()
    for i in 0.. Byte {
  let index = value.to_int() * (levels - 1) / 255
  (index * 255 / (levels - 1)).to_byte()
}

///|
/// Find connected opaque components using four-neighbor flood fill.
pub fn Canvas::opaque_components(
  self : Canvas,
  threshold? : Byte = 1,
) -> Array[GifRect] {
  let visited = Array::make(self.width * self.height, false)
  let components : Array[GifRect] = []
  for y in 0..= threshold {
        let queue : Array[(Int, Int)] = [(x, y)]
        visited[start] = true
        let mut head = 0
        let mut min_x = x
        let mut max_x = x
        let mut min_y = y
        let mut max_y = y
        while head < queue.length() {
          let (current_x, current_y) = queue[head]
          head += 1
          if current_x < min_x {
            min_x = current_x
          }
          if current_x > max_x {
            max_x = current_x
          }
          if current_y < min_y {
            min_y = current_y
          }
          if current_y > max_y {
            max_y = current_y
          }
          let neighbors = [
            (current_x - 1, current_y),
            (current_x + 1, current_y),
            (current_x, current_y - 1),
            (current_x, current_y + 1),
          ]
          for neighbor in neighbors {
            let (next_x, next_y) = neighbor
            if next_x >= 0 &&
              next_y >= 0 &&
              next_x < self.width &&
              next_y < self.height {
              let next = next_x + next_y * self.width
              if !visited[next] && self.pixels[next].alpha() >= threshold {
                visited[next] = true
                queue.push((next_x, next_y))
              }
            }
          }
        }
        components.push(
          GifRect::new(
            left=min_x,
            top=min_y,
            width=max_x - min_x + 1,
            height=max_y - min_y + 1,
          ),
        )
      }
    }
  }
  components
}