///|
/// Summary statistics for a raster preview or regression fixture.
pub(all) struct RasterStats {
  width : Int
  height : Int
  pixel_count : Int
  mean_luminance : Double
  minimum_luminance : Double
  maximum_luminance : Double
  opaque_pixels : Int
  transparent_pixels : Int
} derive(Eq, Debug, ToJson)

///|
/// A small immutable convolution kernel.
pub(all) struct RasterKernel {
  width : Int
  height : Int
  values : Array[Double]
  divisor : Double
  bias : Double
} derive(Eq, Debug, ToJson)

///|
pub fn RasterKernel::new(
  width : Int,
  height : Int,
  values : Array[Double],
  divisor? : Double = 1.0,
  bias? : Double = 0.0,
) -> RasterKernel {
  {
    width: width.max(0),
    height: height.max(0),
    values,
    divisor: if divisor == 0.0 {
      1.0
    } else {
      divisor
    },
    bias,
  }
}

///|
/// Creates a normalized box-blur kernel.
pub fn box_kernel(radius : Int) -> RasterKernel {
  let safe_radius = radius.max(0)
  let size = safe_radius * 2 + 1
  let count = size * size
  let values = []
  for _ in 0.. RasterKernel {
  let safe_radius = radius.max(0)
  let size = safe_radius * 2 + 1
  let weights = []
  for index in 0.. Raster {
  Raster::new(width, height, fill=Color::rgba(0.0, 0.0, 0.0, a=0.0))
}

///|
/// Returns a copy of the raster without sharing its pixel storage.
pub fn Raster::copy(self : Raster) -> Raster {
  { ..self, pixels: self.pixels.copy() }
}

///|
/// Computes luminance and alpha statistics in one pass.
pub fn Raster::stats(self : Raster) -> RasterStats {
  if self.pixels.length() == 0 {
    {
      width: self.width,
      height: self.height,
      pixel_count: 0,
      mean_luminance: 0.0,
      minimum_luminance: 0.0,
      maximum_luminance: 0.0,
      opaque_pixels: 0,
      transparent_pixels: 0,
    }
  } else {
    let mut total = 0.0
    let mut minimum = self.pixels[0].luminance()
    let mut maximum = minimum
    let mut solid = 0
    for pixel in self.pixels {
      let luminance = pixel.luminance()
      total += luminance
      minimum = minimum.min(luminance)
      maximum = maximum.max(luminance)
      if pixel.a >= 0.999 {
        solid += 1
      }
    }
    {
      width: self.width,
      height: self.height,
      pixel_count: self.pixels.length(),
      mean_luminance: total / self.pixels.length().to_double(),
      minimum_luminance: minimum,
      maximum_luminance: maximum,
      opaque_pixels: solid,
      transparent_pixels: self.pixels.length() - solid,
    }
  }
}

///|
/// Applies a function to every pixel and returns a new raster.
pub fn Raster::map_pixels(
  self : Raster,
  transform : (Int, Int, Color) -> Color,
) -> Raster {
  let result = self.copy()
  if self.width > 0 && self.height > 0 {
    for y in 0.. Color,
) -> Raster {
  self.map_pixels(fn(_, _, color) { transform(color) })
}

///|
/// Crops a raster to the intersection with a requested rectangle.
pub fn Raster::crop(self : Raster, bounds : Rect) -> Raster {
  let x0 = bounds.x.to_int().max(0).min(self.width)
  let y0 = bounds.y.to_int().max(0).min(self.height)
  let x1 = bounds.right().to_int().max(x0).min(self.width)
  let y1 = bounds.bottom().to_int().max(y0).min(self.height)
  let result = Raster::new(
    x1 - x0,
    y1 - y0,
    fill=Color::rgba(0.0, 0.0, 0.0, a=0.0),
  )
  for y in y0.. Color {
  raster
  .get(x.to_int(), y.to_int())
  .unwrap_or(Color::rgba(0.0, 0.0, 0.0, a=0.0))
}

///|
/// Samples a raster with bilinear interpolation and transparent out-of-bounds.
pub fn Raster::sample_bilinear(self : Raster, x : Double, y : Double) -> Color {
  if self.width == 0 || self.height == 0 {
    Color::rgba(0.0, 0.0, 0.0, a=0.0)
  } else {
    let x0 = x.to_int()
    let y0 = y.to_int()
    let tx = x - x0.to_double()
    let ty = y - y0.to_double()
    let a = raster_sample_nearest(self, x0.to_double(), y0.to_double())
    let b = raster_sample_nearest(self, (x0 + 1).to_double(), y0.to_double())
    let c = raster_sample_nearest(self, x0.to_double(), (y0 + 1).to_double())
    let d = raster_sample_nearest(
      self,
      (x0 + 1).to_double(),
      (y0 + 1).to_double(),
    )
    a.mix(b.mix(c, ty).mix(d, ty), tx)
  }
}

///|
/// Resizes a raster using nearest-neighbor sampling.
pub fn Raster::resize_nearest(
  self : Raster,
  width : Int,
  height : Int,
) -> Raster {
  let result = Raster::new(width, height)
  if width > 0 && height > 0 && self.width > 0 && self.height > 0 {
    for y in 0.. Raster {
  let result = Raster::new(width, height)
  if width > 0 && height > 0 && self.width > 0 && self.height > 0 {
    for y in 0.. Raster {
  let result = Raster::new(self.width, self.height)
  for y in 0.. Raster {
  let result = Raster::new(self.width, self.height)
  for y in 0.. Raster {
  let result = Raster::new(self.height, self.width)
  for y in 0.. Raster {
  let result = self.copy()
  if self.width > 0 && self.height > 0 && kernel.width > 0 && kernel.height > 0 {
    let cx = kernel.width / 2
    let cy = kernel.height / 2
    for y in 0.. Raster {
  self.convolve(box_kernel(radius.min(32)))
}

///|
/// Applies a smooth Gaussian-like blur.
pub fn Raster::blur_gaussian(self : Raster, radius : Int) -> Raster {
  self.convolve(gaussian_kernel(radius.min(16)))
}

///|
/// Enhances local edges using an unsharp-mask style kernel.
pub fn Raster::sharpen(self : Raster, amount : Double) -> Raster {
  let strength = amount.max(0.0).min(4.0)
  self.convolve(
    RasterKernel::new(3, 3, [
      0.0,
      -strength,
      0.0,
      -strength,
      1.0 + 4.0 * strength,
      -strength,
      0.0,
      -strength,
      0.0,
    ]),
  )
}

///|
/// Converts a raster to luminance while preserving alpha.
pub fn Raster::grayscale(self : Raster) -> Raster {
  self.map_colors(fn(color) {
    let value = color.luminance()
    Color::rgba(value, value, value, a=color.a)
  })
}

///|
/// Applies a contrast curve around the midpoint.
pub fn Raster::contrast(self : Raster, amount : Double) -> Raster {
  let factor = (1.0 + amount.max(-1.0).min(4.0)).max(0.0)
  self.map_colors(fn(color) {
    Color::rgba(
      (color.r - 0.5) * factor + 0.5,
      (color.g - 0.5) * factor + 0.5,
      (color.b - 0.5) * factor + 0.5,
      a=color.a,
    )
  })
}

///|
/// Adjusts brightness by adding a normalized amount.
pub fn Raster::brightness(self : Raster, amount : Double) -> Raster {
  self.map_colors(fn(color) {
    Color::rgba(color.r + amount, color.g + amount, color.b + amount, a=color.a)
  })
}

///|
/// Posterizes each channel into a fixed number of levels.
pub fn Raster::posterize(self : Raster, levels : Int) -> Raster {
  let count = levels.max(2)
  self.map_colors(fn(color) {
    let quantize = fn(value) {
      (value * (count - 1).to_double()).to_int().to_double() /
      (count - 1).to_double()
    }
    Color::rgba(
      quantize(color.r),
      quantize(color.g),
      quantize(color.b),
      a=color.a,
    )
  })
}

///|
/// Applies a two-color luminance ramp.
pub fn Raster::colorize(self : Raster, low : Color, high : Color) -> Raster {
  self.map_colors(fn(color) { low.mix(high, color.luminance()) })
}

///|
/// Returns a raster where alpha is multiplied by a grayscale mask.
pub fn Raster::apply_mask(self : Raster, mask : Raster) -> Raster {
  self.map_pixels(fn(x, y, color) {
    let amount = mask
      .get(x, y)
      .map(fn(pixel) { pixel.luminance() })
      .unwrap_or(0.0)
    color.with_alpha(color.a * amount)
  })
}

///|
/// Thresholds a raster into an opaque binary mask.
pub fn Raster::threshold(self : Raster, cutoff : Double) -> Raster {
  self.map_colors(fn(color) {
    let value = if color.luminance() >= cutoff { 1.0 } else { 0.0 }
    Color::rgba(value, value, value, a=1.0)
  })
}

///|
/// Computes a Sobel edge magnitude map.
pub fn Raster::sobel_edges(self : Raster) -> Raster {
  let gx = RasterKernel::new(3, 3, [
    -1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0,
  ])
  let gy = RasterKernel::new(3, 3, [
    -1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0,
  ])
  let result = Raster::new(self.width, self.height, fill=Color::rgb8(0, 0, 0))
  for y in 0.. Array[Int] {
  let result = self.histogram(bins)
  for index in 1.. Raster {
  let result = self.copy()
  let safe_radius = radius.max(0)
  for y in 0.. Raster {
  let result = self.copy()
  let safe_radius = radius.max(0)
  for y in 0.. Int {
  match self.get(start_x, start_y) {
    None => 0
    Some(original) => {
      let queue : Array[(Int, Int)] = [(start_x, start_y)]
      let mut head = 0
      let mut filled = 0
      while head < queue.length() {
        let (x, y) = queue[head]
        head += 1
        match self.get(x, y) {
          Some(current) =>
            if raster_color_distance_squared(current, original) <=
              tolerance * tolerance {
              let _ = self.set(x, y, replacement)
              filled += 1
              for neighbor in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)] {
                match self.get(neighbor.0, neighbor.1) {
                  Some(candidate) =>
                    if raster_color_distance_squared(candidate, original) <=
                      tolerance * tolerance {
                      queue.push(neighbor)
                    }
                  None => ()
                }
              }
            }
          None => ()
        }
      }
      filled
    }
  }
}

///|
fn raster_color_distance_squared(left : Color, right : Color) -> Double {
  let r = left.r - right.r
  let g = left.g - right.g
  let b = left.b - right.b
  r * r + g * g + b * b
}

///|
/// Counts connected components in a binary luminance raster.
pub fn Raster::connected_components(
  self : Raster,
  cutoff : Double,
) -> Array[Array[Vec2]] {
  let visited = []
  for _ in 0..<(self.width * self.height) {
    visited.push(false)
  }
  let result : Array[Array[Vec2]] = []
  for y in 0..= cutoff {
        let component : Array[Vec2] = []
        let queue : Array[(Int, Int)] = [(x, y)]
        let mut head = 0
        visited[index] = true
        while head < queue.length() {
          let (qx, qy) = queue[head]
          head += 1
          component.push(Vec2::new(qx.to_double(), qy.to_double()))
          for
            neighbor in [(qx + 1, qy), (qx - 1, qy), (qx, qy + 1), (qx, qy - 1)] {
            if self.in_bounds(neighbor.0, neighbor.1) {
              let ni = self.index(neighbor.0, neighbor.1)
              if !visited[ni] && self.pixels[ni].luminance() >= cutoff {
                visited[ni] = true
                queue.push(neighbor)
              }
            }
          }
        }
        result.push(component)
      }
    }
  }
  result
}

///|
/// Renders a polygon into a mask raster.
pub fn rasterize_mask(
  width : Int,
  height : Int,
  polygons : Array[Array[Vec2]],
) -> Raster {
  let result = Raster::new(width, height, fill=Color::rgb8(0, 0, 0))
  for polygon in polygons {
    result.fill_polygon(polygon, Color::rgb8(255, 255, 255))
  }
  result
}

///|
/// Composites a foreground raster over a background raster.
pub fn composite_rasters(
  background : Raster,
  foreground : Raster,
  offset : Vec2,
) -> Raster {
  let result = background.copy()
  for y in 0.. {
          let _ = result.blend_pixel(dx, dy, color)
        }
        None => ()
      }
    }
  }
  result
}