///|
/// Paint color filter applied after color sampling and before opacity.
struct PaintFilter {
  filters : Array[PaintFilterKind]
} derive(Eq, Debug)

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

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

///|
/// Public filter stage family used for inspection and paint pipeline planning.
pub(all) enum PaintFilterType {
  Modulate
  Grayscale
  Invert
  Brightness
  Contrast
  Saturate
  Sepia
  ColorMatrix
} derive(Eq, Debug)

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

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

///|
priv enum PaintFilterKind {
  Modulate(Color)
  Grayscale
  Invert
  Brightness(Double)
  Contrast(Double)
  Saturate(Double)
  Sepia
  ColorMatrix(Array[Double])
} derive(Eq, Debug)

///|
/// Create a filter chain with no stages. Applying it returns the source color.
pub fn PaintFilter::identity() -> PaintFilter {
  { filters: [], }
}

///|
/// Create a filter that multiplies each source channel by a color.
pub fn PaintFilter::modulate(color : Color) -> PaintFilter {
  make_paint_filter(Modulate(color))
}

///|
/// Create a filter that converts RGB channels to luminance and preserves alpha.
pub fn PaintFilter::grayscale() -> PaintFilter {
  make_paint_filter(Grayscale)
}

///|
/// Create a filter that inverts RGB channels and preserves alpha.
pub fn PaintFilter::invert() -> PaintFilter {
  make_paint_filter(Invert)
}

///|
/// Create a filter that scales RGB brightness and preserves alpha.
pub fn PaintFilter::brightness(factor : Double) -> PaintFilter {
  make_paint_filter(Brightness(normalize_filter_factor(factor)))
}

///|
/// Create a filter that scales RGB contrast around mid-gray and preserves alpha.
pub fn PaintFilter::contrast(factor : Double) -> PaintFilter {
  make_paint_filter(Contrast(normalize_filter_factor(factor)))
}

///|
/// Create a filter that scales RGB saturation around luminance and preserves alpha.
pub fn PaintFilter::saturate(factor : Double) -> PaintFilter {
  make_paint_filter(Saturate(normalize_filter_factor(factor)))
}

///|
/// Create a filter that maps RGB channels through a warm sepia tone matrix.
pub fn PaintFilter::sepia() -> PaintFilter {
  make_paint_filter(Sepia)
}

///|
/// Create a filter from a 4x5 RGBA color matrix.
pub fn PaintFilter::color_matrix(values : Array[Double]) -> PaintFilter {
  make_paint_filter(ColorMatrix(normalize_color_matrix(values)))
}

///|
/// Return a filter that applies this filter and then the next filter.
pub fn PaintFilter::then(self : PaintFilter, next : PaintFilter) -> PaintFilter {
  { filters: concat_filter_kinds(self.filters, next.filters), }
}

///|
/// Compose two filters in application order: first, then second.
pub fn PaintFilter::compose(
  first : PaintFilter,
  second : PaintFilter,
) -> PaintFilter {
  first.then(second)
}

///|
/// Return whether this filter chain has no stages.
pub fn PaintFilter::is_identity(self : PaintFilter) -> Bool {
  self.filters.length() is 0
}

///|
/// Return the number of filter stages in this chain.
pub fn PaintFilter::get_filter_count(self : PaintFilter) -> Int {
  self.filters.length()
}

///|
/// Return the filter stage family at an index.
pub fn PaintFilter::get_stage_type(
  self : PaintFilter,
  index : Int,
) -> PaintFilterType? {
  match self.get_stage_kind(index) {
    Some(kind) => Some(paint_filter_type_of(kind))
    None => None
  }
}

///|
/// Return the stage color for color-bearing filter stages.
pub fn PaintFilter::get_stage_color(self : PaintFilter, index : Int) -> Color? {
  match self.get_stage_kind(index) {
    Some(Modulate(color)) => Some(color)
    _ => None
  }
}

///|
/// Return the numeric factor for factor-bearing filter stages.
pub fn PaintFilter::get_stage_factor(
  self : PaintFilter,
  index : Int,
) -> Double? {
  match self.get_stage_kind(index) {
    Some(Brightness(factor))
    | Some(Contrast(factor))
    | Some(Saturate(factor)) => Some(factor)
    _ => None
  }
}

///|
/// Return a copied 4x5 RGBA color matrix for matrix filter stages.
pub fn PaintFilter::get_stage_color_matrix(
  self : PaintFilter,
  index : Int,
) -> Array[Double]? {
  match self.get_stage_kind(index) {
    Some(ColorMatrix(matrix)) => Some(copy_filter_matrix(matrix))
    _ => None
  }
}

///|
/// Apply this filter to a straight-alpha color.
pub fn PaintFilter::apply(self : PaintFilter, color : Color) -> Color {
  let mut current = color
  for filter in self.filters {
    current = apply_filter_kind(filter, current)
  }
  current
}

///|
fn make_paint_filter(kind : PaintFilterKind) -> PaintFilter {
  { filters: [copy_filter_kind(kind)], }
}

///|
fn concat_filter_kinds(
  first : Array[PaintFilterKind],
  second : Array[PaintFilterKind],
) -> Array[PaintFilterKind] {
  let filters = []
  for kind in first {
    filters.push(copy_filter_kind(kind))
  }
  for kind in second {
    filters.push(copy_filter_kind(kind))
  }
  filters
}

///|
fn copy_filter_kind(kind : PaintFilterKind) -> PaintFilterKind {
  match kind {
    ColorMatrix(matrix) => ColorMatrix(copy_filter_matrix(matrix))
    _ => kind
  }
}

///|
fn copy_filter_matrix(matrix : Array[Double]) -> Array[Double] {
  let copied = []
  for value in matrix {
    copied.push(value)
  }
  copied
}

///|
fn normalize_filter_factor(factor : Double) -> Double {
  guard factor == factor else { return 1.0 }
  if factor < 0.0 {
    0.0
  } else {
    factor
  }
}

///|
fn PaintFilter::get_stage_kind(
  self : PaintFilter,
  index : Int,
) -> PaintFilterKind? {
  guard index >= 0 && index < self.filters.length() else { return None }
  Some(self.filters[index])
}

///|
fn paint_filter_type_of(kind : PaintFilterKind) -> PaintFilterType {
  match kind {
    Modulate(_) => Modulate
    Grayscale => Grayscale
    Invert => Invert
    Brightness(_) => Brightness
    Contrast(_) => Contrast
    Saturate(_) => Saturate
    Sepia => Sepia
    ColorMatrix(_) => ColorMatrix
  }
}

///|
fn apply_filter_kind(kind : PaintFilterKind, color : Color) -> Color {
  match kind {
    Modulate(factor) =>
      Color::new(
        r=multiply_byte(color.r, factor.r),
        g=multiply_byte(color.g, factor.g),
        b=multiply_byte(color.b, factor.b),
        a=multiply_byte(color.a, factor.a),
      )
    Grayscale => {
      let gray = luminance_byte(color)
      Color::new(r=gray, g=gray, b=gray, a=color.a)
    }
    Invert =>
      Color::new(
        r=invert_byte(color.r),
        g=invert_byte(color.g),
        b=invert_byte(color.b),
        a=color.a,
      )
    Brightness(factor) =>
      Color::new(
        r=scale_rgb_byte(color.r, factor),
        g=scale_rgb_byte(color.g, factor),
        b=scale_rgb_byte(color.b, factor),
        a=color.a,
      )
    Contrast(factor) =>
      Color::new(
        r=contrast_rgb_byte(color.r, factor),
        g=contrast_rgb_byte(color.g, factor),
        b=contrast_rgb_byte(color.b, factor),
        a=color.a,
      )
    Saturate(factor) => {
      let gray = luminance_byte(color)
      Color::new(
        r=saturate_rgb_byte(color.r, gray, factor),
        g=saturate_rgb_byte(color.g, gray, factor),
        b=saturate_rgb_byte(color.b, gray, factor),
        a=color.a,
      )
    }
    Sepia =>
      Color::new(
        r=sepia_rgb_byte(color, 393, 769, 189),
        g=sepia_rgb_byte(color, 349, 686, 168),
        b=sepia_rgb_byte(color, 272, 534, 131),
        a=color.a,
      )
    ColorMatrix(matrix) => apply_color_matrix(matrix, color)
  }
}

///|
fn sample_source_color_with_filter(
  color : Color,
  filter : PaintFilter?,
  opacity : Double,
) -> Color {
  apply_paint_filter(filter, color).scale_alpha(opacity)
}

///|
fn paint_filter_outputs_alpha_from_transparent(
  filter : PaintFilter?,
  opacity : Double,
) -> Bool {
  sample_source_color_with_filter(Color::transparent(), filter, opacity).a != 0
}

///|
fn paint_filter_options_are_visually_equivalent(
  left : PaintFilter?,
  right : PaintFilter?,
) -> Bool {
  if paint_filter_option_is_none_or_identity(left) &&
    paint_filter_option_is_none_or_identity(right) {
    true
  } else {
    left == right
  }
}

///|
fn paint_filter_option_is_none_or_identity(filter : PaintFilter?) -> Bool {
  match filter {
    Some(value) => value.is_identity()
    None => true
  }
}

///|
fn apply_paint_filter(filter : PaintFilter?, color : Color) -> Color {
  match filter {
    Some(value) => value.apply(color)
    None => color
  }
}

///|
fn multiply_byte(a : Byte, b : Byte) -> Byte {
  clamp_byte((a.to_int() * b.to_int() + 127) / 255)
}

///|
fn invert_byte(value : Byte) -> Byte {
  clamp_byte(255 - value.to_int())
}

///|
fn scale_rgb_byte(value : Byte, factor : Double) -> Byte {
  guard factor == factor else { return value }
  guard factor > 0.0 else { return 0 }
  clamp_byte((value.to_int().to_double() * factor + 0.5).to_int())
}

///|
fn contrast_rgb_byte(value : Byte, factor : Double) -> Byte {
  guard factor == factor else { return value }
  guard factor > 0.0 else { return 128 }
  let centered = value.to_int().to_double() - 128.0
  clamp_byte((centered * factor + 128.0 + 0.5).to_int())
}

///|
fn saturate_rgb_byte(value : Byte, gray : Byte, factor : Double) -> Byte {
  guard factor == factor else { return value }
  guard factor > 0.0 else { return gray }
  let base = gray.to_int().to_double()
  let delta = value.to_int().to_double() - base
  clamp_byte((base + delta * factor + 0.5).to_int())
}

///|
fn sepia_rgb_byte(
  color : Color,
  red_weight : Int,
  green_weight : Int,
  blue_weight : Int,
) -> Byte {
  clamp_byte(
    (
      color.r.to_int() * red_weight +
      color.g.to_int() * green_weight +
      color.b.to_int() * blue_weight +
      500
    ) /
    1000,
  )
}

///|
fn normalize_color_matrix(values : Array[Double]) -> Array[Double] {
  let matrix = identity_color_matrix()
  let count = values.length().min(20)
  for index in 0.. Array[Double] {
  [
    1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
  ]
}

///|
fn apply_color_matrix(matrix : Array[Double], color : Color) -> Color {
  let r = color.r.to_int().to_double()
  let g = color.g.to_int().to_double()
  let b = color.b.to_int().to_double()
  let a = color.a.to_int().to_double()
  Color::new(
    r=color_matrix_byte(matrix, 0, r, g, b, a),
    g=color_matrix_byte(matrix, 1, r, g, b, a),
    b=color_matrix_byte(matrix, 2, r, g, b, a),
    a=color_matrix_byte(matrix, 3, r, g, b, a),
  )
}

///|
fn color_matrix_byte(
  matrix : Array[Double],
  row : Int,
  r : Double,
  g : Double,
  b : Double,
  a : Double,
) -> Byte {
  let offset = row * 5
  clamp_filter_byte(
    matrix[offset] * r +
    matrix[offset + 1] * g +
    matrix[offset + 2] * b +
    matrix[offset + 3] * a +
    matrix[offset + 4],
  )
}

///|
fn clamp_filter_byte(value : Double) -> Byte {
  guard value == value else { return 0 }
  clamp_byte((value + 0.5).to_int())
}

///|
fn luminance_byte(color : Color) -> Byte {
  clamp_byte(
    (
      color.r.to_int() * 77 +
      color.g.to_int() * 150 +
      color.b.to_int() * 29 +
      128
    ) /
    256,
  )
}