///|
/// A square or rectangular convolution kernel used by the image effects API.
/// Weights and divisor are integer-scaled so results are reproducible on all
/// MoonBit backends.
pub(all) struct ConvolutionKernel {
  width : Int
  height : Int
  weights : Array[Int]
  divisor : Int
  bias : Int
} derive(Debug)

///|
pub fn ConvolutionKernel::new(
  width~ : Int,
  height~ : Int,
  weights~ : ArrayView[Int],
  divisor~ : Int,
  bias~ : Int,
) -> ConvolutionKernel {
  if width <= 0 ||
    height <= 0 ||
    width % 2 == 0 ||
    height % 2 == 0 ||
    weights.length() != width * height ||
    divisor == 0 {
    panic()
  }
  let copied : Array[Int] = []
  for value in weights {
    copied.push(value)
  }
  { width, height, weights: copied, divisor, bias }
}

///|
pub fn ConvolutionKernel::width(self : ConvolutionKernel) -> Int {
  self.width
}

///|
pub fn ConvolutionKernel::height(self : ConvolutionKernel) -> Int {
  self.height
}

///|
pub fn ConvolutionKernel::weights(self : ConvolutionKernel) -> ArrayView[Int] {
  self.weights[:]
}

///|
pub fn ConvolutionKernel::divisor(self : ConvolutionKernel) -> Int {
  self.divisor
}

///|
pub fn ConvolutionKernel::bias(self : ConvolutionKernel) -> Int {
  self.bias
}

///|
pub fn ConvolutionKernel::box_blur(size? : Int = 3) -> ConvolutionKernel {
  let safe = if size < 1 { 1 } else if size % 2 == 0 { size + 1 } else { size }
  ConvolutionKernel::new(
    width=safe,
    height=safe,
    weights=Array::make(safe * safe, 1)[:],
    divisor=safe * safe,
    bias=0,
  )
}

///|
pub fn ConvolutionKernel::gaussian_3() -> ConvolutionKernel {
  ConvolutionKernel::new(
    width=3,
    height=3,
    weights=[1, 2, 1, 2, 4, 2, 1, 2, 1][:],
    divisor=16,
    bias=0,
  )
}

///|
pub fn ConvolutionKernel::sharpen() -> ConvolutionKernel {
  ConvolutionKernel::new(
    width=3,
    height=3,
    weights=[0, -1, 0, -1, 5, -1, 0, -1, 0][:],
    divisor=1,
    bias=0,
  )
}

///|
pub fn ConvolutionKernel::unsharp() -> ConvolutionKernel {
  ConvolutionKernel::new(
    width=3,
    height=3,
    weights=[-1, -1, -1, -1, 9, -1, -1, -1, -1][:],
    divisor=1,
    bias=0,
  )
}

///|
pub fn ConvolutionKernel::edge() -> ConvolutionKernel {
  ConvolutionKernel::new(
    width=3,
    height=3,
    weights=[-1, -1, -1, -1, 8, -1, -1, -1, -1][:],
    divisor=1,
    bias=128,
  )
}

///|
pub fn ConvolutionKernel::emboss() -> ConvolutionKernel {
  ConvolutionKernel::new(
    width=3,
    height=3,
    weights=[-2, -1, 0, -1, 1, 1, 0, 1, 2][:],
    divisor=1,
    bias=128,
  )
}

///|
pub fn ConvolutionKernel::laplacian() -> ConvolutionKernel {
  ConvolutionKernel::new(
    width=3,
    height=3,
    weights=[0, 1, 0, 1, -4, 1, 0, 1, 0][:],
    divisor=1,
    bias=128,
  )
}

///|
fn convolution_sample(canvas : Canvas, x : Int, y : Int) -> Rgba {
  let safe_x = if x < 0 {
    0
  } else if x >= canvas.width {
    canvas.width - 1
  } else {
    x
  }
  let safe_y = if y < 0 {
    0
  } else if y >= canvas.height {
    canvas.height - 1
  } else {
    y
  }
  canvas.pixels[safe_x + safe_y * canvas.width]
}

///|
fn convolution_channel(
  canvas : Canvas,
  x : Int,
  y : Int,
  kernel : ConvolutionKernel,
  channel : Int,
) -> Int {
  let half_x = kernel.width / 2
  let half_y = kernel.height / 2
  let mut total = kernel.bias
  for ky in 0.. color.red().to_int()
        1 => color.green().to_int()
        2 => color.blue().to_int()
        _ => color.alpha().to_int()
      }
      total += value * kernel.weights[kx + ky * kernel.width]
    }
  }
  total / kernel.divisor
}

///|
/// Apply a convolution kernel with edge-clamped sampling.
pub fn Canvas::convolve(self : Canvas, kernel : ConvolutionKernel) -> Canvas {
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for y in 0.. Canvas {
  let size = if radius <= 0 { 1 } else { radius * 2 + 1 }
  self.convolve(ConvolutionKernel::box_blur(size~))
}

///|
pub fn Canvas::gaussian_blur(self : Canvas) -> Canvas {
  self.convolve(ConvolutionKernel::gaussian_3())
}

///|
pub fn Canvas::sharpen(self : Canvas) -> Canvas {
  self.convolve(ConvolutionKernel::sharpen())
}

///|
pub fn Canvas::unsharp_mask(self : Canvas, amount? : Int = 1) -> Canvas {
  if amount <= 0 {
    self.copy()
  } else {
    let blurred = self.gaussian_blur()
    let result = self.copy()
    for i in 0.. Canvas {
  self.convolve(ConvolutionKernel::emboss())
}

///|
pub fn Canvas::edge_map(self : Canvas) -> Canvas {
  self.convolve(ConvolutionKernel::edge())
}

///|
pub fn Canvas::laplacian(self : Canvas) -> Canvas {
  self.convolve(ConvolutionKernel::laplacian())
}

///|
fn luminance_at(canvas : Canvas, x : Int, y : Int) -> Int {
  convolution_sample(canvas, x, y).luma().to_int()
}

///|
/// Return the Sobel gradient magnitude as an opaque grayscale canvas.
pub fn Canvas::sobel(self : Canvas, threshold? : Int = 0) -> Canvas {
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgb(0, 0, 0),
  )
  for y in 0.. 65025 {
        255
      } else {
        int_sqrt(gx * gx + gy * gy)
      }
      let value = if magnitude < threshold { 0 } else { magnitude }
      result.pixels[x + y * self.width] = rgb(
        value.to_byte(),
        value.to_byte(),
        value.to_byte(),
      )
    }
  }
  result
}

///|
fn int_sqrt(value : Int) -> Int {
  if value <= 0 {
    0
  } else {
    let mut guess = if value > 255 * 255 { 255 } else { value }
    let mut changed = true
    while changed {
      let next = (guess + value / guess) / 2
      if next < guess {
        guess = next
      } else {
        changed = false
      }
    }
    if guess > 255 {
      255
    } else {
      guess
    }
  }
}

///|
fn neighborhood_values(
  canvas : Canvas,
  x : Int,
  y : Int,
  radius : Int,
  channel : Int,
) -> Array[Int] {
  let values : Array[Int] = []
  for row in (y - radius)..<(y + radius + 1) {
    for column in (x - radius)..<(x + radius + 1) {
      let color = convolution_sample(canvas, column, row)
      values.push(
        match channel {
          0 => color.red().to_int()
          1 => color.green().to_int()
          2 => color.blue().to_int()
          _ => color.alpha().to_int()
        },
      )
    }
  }
  values
}

///|
fn median_value(values : Array[Int]) -> Int {
  for i in 1.. 0 && values[j - 1] > value {
      values[j] = values[j - 1]
      j -= 1
    }
    values[j] = value
  }
  if values.length() == 0 {
    0
  } else {
    values[values.length() / 2]
  }
}

///|
/// Remove isolated salt-and-pepper noise with a median neighborhood filter.
pub fn Canvas::median_filter(self : Canvas, radius? : Int = 1) -> Canvas {
  let safe_radius = if radius < 0 { 0 } else if radius > 4 { 4 } else { radius }
  if safe_radius == 0 {
    self.copy()
  } else {
    let result = Canvas::new(
      width=self.width,
      height=self.height,
      fill=rgba(0, 0, 0, 0),
    )
    for y in 0.. Canvas {
  if block_size <= 1 {
    self.copy()
  } else {
    let result = self.copy()
    for top in 0.. self.width {
            self.width
          } else {
            left + block_size
          }
          let bottom = if top + block_size > self.height {
            self.height
          } else {
            top + block_size
          }
          let mut red = 0
          let mut green = 0
          let mut blue = 0
          let mut alpha = 0
          let mut count = 0
          for y in top.. 0 {
            let average = rgba(
              (red / count).to_byte(),
              (green / count).to_byte(),
              (blue / count).to_byte(),
              (alpha / count).to_byte(),
            )
            for y in top..