// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// Blurring, and the sharpening that is defined as its inverse.
//
// A blur is a convolution with a Gaussian, done as two one-dimensional passes
// because the kernel is separable. The radius is the standard deviation, in
// pixels, which is the number every other implementation means by "radius" and
// therefore the one that makes a comparison mean something: a kernel truncated
// at three sigma holds 99.7 percent of the weight, and what is left over is
// smaller than a step of an eight-bit sample at any radius above a third.
//
// The convolution runs on premultiplied samples. Blurring colour and alpha
// separately leaves the colour of a transparent pixel bleeding outward as the
// alpha falls away, which shows up as a grey halo around every sprite.

///|
/// How a blur is worked out.
#derive(Eq, Debug)
pub(all) enum Kernel {
  /// The kernel itself: every sample within three sigma, weighted by the
  /// Gaussian, convolved in two passes.
  Exact
  /// Three box passes per axis, whose sides are chosen so that the three
  /// together have the variance of the Gaussian asked for. This is what Pillow,
  /// Skia and most drawing systems do: a box pass is an addition and a
  /// subtraction per sample, and three of them are within a step of a sample of
  /// the kernel everywhere.
  Boxes
}

///|
pub extend Kernel with Eq::{equal, not_equal}

///|
pub extend Kernel with Debug::{to_repr}

///|
/// The image blurred with a Gaussian of standard deviation `radius`.
///
/// A radius of zero or less is the image itself, so a caller that computes the
/// radius from a size does not have to check for the case.
///
/// `kernel` chooses how the Gaussian is worked out. `Exact` convolves with the
/// kernel, which is what the number means; `Boxes` is the same blur by the
/// arithmetic the rest of the world uses, for a caller comparing against a
/// picture some other library made.
pub fn Image::blur(
  self : Image,
  radius : Double,
  kernel~ : Kernel = Exact,
) -> Image {
  if radius <= 0.0 || self.width == 0 || self.height == 0 {
    return self
  }
  match kernel {
    Boxes => self.blur_boxes(radius)
    Exact => {
      let weights = gaussian_weights(radius)
      let source = self.working_copy()
      let across = blur_axis(source, self.width, self.height, weights, true)
      let down = blur_axis(across, self.width, self.height, weights, false)
      self.collect(down, self.width, self.height)
    }
  }
}

///|
/// How many steps the picture's samples are stored in: two hundred and fifty-five
/// for eight bits, sixty-five thousand five hundred and thirty-five for sixteen,
/// and none at all for floats, which are the same before and after a pass.
fn Image::storage_levels(self : Image) -> Double {
  match self.format {
    Gray8 | GrayAlpha8 | Rgb8 | Rgba8 => 255.0
    Gray16 | GrayAlpha16 | Rgb16 | Rgba16 => 65535.0
    _ => 0.0
  }
}

///|
/// The three box passes each axis needs, in the widths that make their variance
/// the variance of the Gaussian asked for.
///
/// A box of side `2r + 1` has variance `((2r+1)^2 - 1) / 12`, so three of them
/// want a side of `sqrt(12 sigma^2 / 3 + 1)`; the fractional part is carried as
/// a weight rather than rounded away, which is what keeps the result within a
/// step of the kernel for radii between the integer widths.
fn Image::blur_boxes(self : Image, radius : Double) -> Image {
  let side = box_span(radius)
  let levels = self.storage_levels()
  let mut buffer = self.working_copy()
  for _ in 0..<3 {
    buffer = box_axis(buffer, self.width, self.height, side, true, levels)
  }
  for _ in 0..<3 {
    buffer = box_axis(buffer, self.width, self.height, side, false, levels)
  }
  self.collect(buffer, self.width, self.height)
}

///|
/// The width of one box, as a real number of samples: the integer part is how
/// many samples each side the pass reaches, and the fractional part is weight
/// given to the sample beyond it.
fn box_span(radius : Double) -> Double {
  let variance = radius * radius / 3.0
  let side = (12.0 * variance + 1.0).sqrt()
  let whole = ((side - 1.0) / 2.0).floor()
  let rest = (2.0 * whole + 1.0) *
    (whole * (whole + 1.0) - 3.0 * variance) /
    (6.0 * (variance - (whole + 1.0) * (whole + 1.0)))
  whole + rest
}

///|
/// One axis of three box passes: the image is `length` wide, or `length` tall,
/// and the other dimension is untouched.
///
/// The window is the `2r + 1` samples around the output one, read from the edge
/// when it reaches past it, plus the two samples beyond that at the weight the
/// fractional part of the width leaves over. Each pass is written back rounded
/// the way the picture is stored, because a pass whose result is held exactly
/// and rounded once at the end is not the same picture: the difference shows on
/// every edge.
fn box_axis(
  source : FixedArray[Double],
  width : Int32,
  height : Int32,
  side : Double,
  horizontal : Bool,
  levels : Double,
) -> FixedArray[Double] {
  let reach = side.floor().to_int()
  let steps = if horizontal { width } else { height }
  let lines = if horizontal { height } else { width }
  let stride = if horizontal { 4 } else { width * 4 }
  let weight = 1.0 / (side * 2.0 + 1.0)
  let spare = (1.0 - (reach * 2 + 1).to_double() * weight) / 2.0
  let total = (reach * 2 + 1).to_double() * weight + 2.0 * spare
  let pixels : FixedArray[Double] = FixedArray::make(width * height * 4, 0.0)
  for line in 0.. Double {
  if levels == 0.0 {
    return value
  }
  let held = if value < 0.0 { 0.0 } else if value > 1.0 { 1.0 } else { value }
  (held * levels + 0.5).floor() / levels
}

///|
/// The weights of a one-dimensional Gaussian, symmetric, from `−n` to `n`.
///
/// The tail is cut at three sigma and the rest renormalised, so that the sum is
/// exactly one and a flat image stays exactly flat.
fn gaussian_weights(radius : Double) -> Array[Double] {
  let reach = (3.0 * radius).ceil().to_int()
  let weights : Array[Double] = []
  let mut total = 0.0
  for i in 0..<=(reach * 2) {
    let x = (i - reach).to_double()
    let weight = @math.exp(-(x * x) / (2.0 * radius * radius))
    weights.push(weight)
    total = total + weight
  }
  for i in 0.. FixedArray[Double] {
  let reach = (weights.length() - 1) / 2
  let pixels : FixedArray[Double] = FixedArray::make(width * height * 4, 0.0)
  let steps = if horizontal { width } else { height }
  let lines = if horizontal { height } else { width }
  let stride = if horizontal { 4 } else { width * 4 }
  for line in 0..= steps {
          steps - 1
        } else {
          index
        }
        let from = at + (clamped - step) * stride
        r = r + source[from] * weights[k]
        g = g + source[from + 1] * weights[k]
        b = b + source[from + 2] * weights[k]
        a = a + source[from + 3] * weights[k]
      }
      pixels[at] = clamp_unit(r)
      pixels[at + 1] = clamp_unit(g)
      pixels[at + 2] = clamp_unit(b)
      pixels[at + 3] = clamp_unit(a)
    }
  }
  pixels
}

///|
/// The image sharpened by subtracting a blurred copy of itself.
///
/// `amount` is a percentage, a hundred being the difference added once: an edge
/// that the blur softened is pushed back out by the same distance it lost.
/// `threshold` is how much of a difference, in eight-bit steps, is worth
/// sharpening at all, so that the grain of a photograph is not sharpened into
/// noise. Sharpen with radius 1, amount 150 and threshold 3 is the usual
/// starting point.
pub fn Image::unsharp_mask(
  self : Image,
  radius? : Double,
  amount? : Double,
  threshold? : Int32,
) -> Image {
  let radius = radius.unwrap_or(1.0)
  let amount = amount.unwrap_or(100.0)
  let threshold = threshold.unwrap_or(0)
  if radius <= 0.0 || amount == 0.0 {
    return self
  }
  let soft = self.blur(radius)
  if soft.format != self.format ||
    soft.width != self.width ||
    soft.height != self.height {
    return self
  }
  let bytes = self.format.bytes()
  let stride = self.width * bytes
  let buffer : FixedArray[Byte] = FixedArray::make(
    if self.height == 0 {
      0
    } else {
      stride * self.height
    },
    b'\x00',
  )
  let strength = amount / 100.0
  let gate = threshold.to_double() / 255.0
  for y in 0.. Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
fn peak(one : Double, other : Double) -> Double {
  if one > other {
    one
  } else {
    other
  }
}