///|
/// A compact HSV color value. Hue is measured in degrees in the range 0..360,
/// saturation and value are stored in the range 0..255.
pub(all) struct HsvColor {
  hue : Int
  saturation : Byte
  value : Byte
  alpha : Byte
} derive(Eq, Debug)

///|
pub fn HsvColor::hue(self : HsvColor) -> Int {
  self.hue
}

///|
pub fn HsvColor::saturation(self : HsvColor) -> Byte {
  self.saturation
}

///|
pub fn HsvColor::value(self : HsvColor) -> Byte {
  self.value
}

///|
pub fn HsvColor::alpha(self : HsvColor) -> Byte {
  self.alpha
}

///|
/// A compact HSL color value. Hue is measured in degrees in the range 0..360.
pub(all) struct HslColor {
  hue : Int
  saturation : Byte
  lightness : Byte
  alpha : Byte
} derive(Eq, Debug)

///|
pub fn HslColor::hue(self : HslColor) -> Int {
  self.hue
}

///|
pub fn HslColor::saturation(self : HslColor) -> Byte {
  self.saturation
}

///|
pub fn HslColor::lightness(self : HslColor) -> Byte {
  self.lightness
}

///|
pub fn HslColor::alpha(self : HslColor) -> Byte {
  self.alpha
}

///|
/// Blend operators used by compositing and layer pipelines.
pub(all) enum BlendMode {
  Normal
  Multiply
  Screen
  Overlay
  Darken
  Lighten
  Add
  Subtract
  Difference
  Exclude
} derive(Eq, Debug)

///|
fn clamp_color_byte(value : Int) -> Byte {
  if value < 0 {
    0
  } else if value > 255 {
    255
  } else {
    value.to_byte()
  }
}

///|
fn normalize_hue(value : Int) -> Int {
  let mut result = value % 360
  if result < 0 {
    result += 360
  }
  result
}

///|
/// Convert an RGBA color to HSV using integer arithmetic for deterministic
/// results across native, JavaScript, and WebAssembly targets.
pub fn Rgba::hsv(self : Rgba) -> HsvColor {
  let red = self.red().to_int()
  let green = self.green().to_int()
  let blue = self.blue().to_int()
  let maximum = if red > green {
    if red > blue {
      red
    } else {
      blue
    }
  } else if green > blue {
    green
  } else {
    blue
  }
  let minimum = if red < green {
    if red < blue {
      red
    } else {
      blue
    }
  } else if green < blue {
    green
  } else {
    blue
  }
  let delta = maximum - minimum
  let hue = if delta == 0 {
    0
  } else if maximum == red {
    normalize_hue(60 * (green - blue) / delta)
  } else if maximum == green {
    normalize_hue(60 * (blue - red) / delta + 120)
  } else {
    normalize_hue(60 * (red - green) / delta + 240)
  }
  let saturation = if maximum == 0 { 0 } else { delta * 255 / maximum }
  HsvColor::{
    hue,
    saturation: saturation.to_byte(),
    value: maximum.to_byte(),
    alpha: self.alpha(),
  }
}

///|
/// Convert an RGBA color to HSL.
pub fn Rgba::hsl(self : Rgba) -> HslColor {
  let red = self.red().to_int()
  let green = self.green().to_int()
  let blue = self.blue().to_int()
  let maximum = if red > green {
    if red > blue {
      red
    } else {
      blue
    }
  } else if green > blue {
    green
  } else {
    blue
  }
  let minimum = if red < green {
    if red < blue {
      red
    } else {
      blue
    }
  } else if green < blue {
    green
  } else {
    blue
  }
  let delta = maximum - minimum
  let sum = maximum + minimum
  let lightness = sum / 2
  let saturation = if delta == 0 {
    0
  } else if lightness < 128 {
    delta * 255 / sum
  } else {
    delta * 255 / (510 - sum)
  }
  let hue = if delta == 0 {
    0
  } else if maximum == red {
    normalize_hue(60 * (green - blue) / delta)
  } else if maximum == green {
    normalize_hue(60 * (blue - red) / delta + 120)
  } else {
    normalize_hue(60 * (red - green) / delta + 240)
  }
  HslColor::{
    hue,
    saturation: saturation.to_byte(),
    lightness: lightness.to_byte(),
    alpha: self.alpha(),
  }
}

///|
/// Construct an RGBA value from HSV components.
pub fn rgba_from_hsv(
  hue : Int,
  saturation : Byte,
  value : Byte,
  alpha : Byte,
) -> Rgba {
  let maximum = value.to_int()
  if saturation == 0 {
    rgba(value, value, value, alpha)
  } else {
    let minimum = maximum * (255 - saturation.to_int()) / 255
    let delta = maximum - minimum
    let sector = normalize_hue(hue) / 60
    let offset = normalize_hue(hue) % 60
    let rising = minimum + delta * offset / 60
    let falling = maximum - delta * offset / 60
    let red = match sector {
      0 => maximum
      1 => falling
      2 => minimum
      3 => minimum
      4 => rising
      _ => maximum
    }
    let green = match sector {
      0 => rising
      1 => maximum
      2 => maximum
      3 => falling
      4 => minimum
      _ => minimum
    }
    let blue = match sector {
      0 => minimum
      1 => minimum
      2 => rising
      3 => maximum
      4 => maximum
      _ => falling
    }
    rgba(red.to_byte(), green.to_byte(), blue.to_byte(), alpha)
  }
}

///|
fn hsl_component(p : Int, q : Int, hue : Int) -> Int {
  let h = normalize_hue(hue)
  if h < 60 {
    p + (q - p) * h / 60
  } else if h < 180 {
    q
  } else if h < 240 {
    p + (q - p) * (240 - h) / 60
  } else {
    p
  }
}

///|
/// Construct an RGBA value from HSL components.
pub fn rgba_from_hsl(
  hue : Int,
  saturation : Byte,
  lightness : Byte,
  alpha : Byte,
) -> Rgba {
  let light = lightness.to_int()
  if saturation == 0 {
    rgba(lightness, lightness, lightness, alpha)
  } else {
    let saturation_value = saturation.to_int()
    let q = if light < 128 {
      light * (255 + saturation_value) / 255
    } else {
      light + saturation_value - light * saturation_value / 255
    }
    let p = 2 * light - q
    let red = hsl_component(p, q, hue + 120)
    let green = hsl_component(p, q, hue)
    let blue = hsl_component(p, q, hue - 120)
    rgba(
      clamp_color_byte(red),
      clamp_color_byte(green),
      clamp_color_byte(blue),
      alpha,
    )
  }
}

///|
pub fn Rgba::premultiply(self : Rgba) -> Rgba {
  let alpha = self.alpha().to_int()
  rgba(
    (self.red().to_int() * alpha / 255).to_byte(),
    (self.green().to_int() * alpha / 255).to_byte(),
    (self.blue().to_int() * alpha / 255).to_byte(),
    self.alpha(),
  )
}

///|
pub fn Rgba::unpremultiply(self : Rgba) -> Rgba {
  let alpha = self.alpha().to_int()
  if alpha == 0 {
    rgba(0, 0, 0, 0)
  } else {
    rgba(
      clamp_color_byte(self.red().to_int() * 255 / alpha),
      clamp_color_byte(self.green().to_int() * 255 / alpha),
      clamp_color_byte(self.blue().to_int() * 255 / alpha),
      self.alpha(),
    )
  }
}

///|
fn clamp_color_int(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 255 {
    255
  } else {
    value
  }
}

///|
pub fn Rgba::is_transparent(self : Rgba) -> Bool {
  self.alpha() == 0
}

///|
pub fn Rgba::is_opaque(self : Rgba) -> Bool {
  self.alpha() == 255
}

///|
pub fn Rgba::opaque_copy(self : Rgba) -> Rgba {
  self.with_alpha(255)
}

///|
pub fn Rgba::blend_over(self : Rgba, destination : Rgba) -> Rgba {
  blend_normal(self, destination)
}

///|
fn blend_channel(mode : BlendMode, source : Int, destination : Int) -> Int {
  match mode {
    Normal => source
    Multiply => source * destination / 255
    Screen => 255 - (255 - source) * (255 - destination) / 255
    Overlay =>
      if destination < 128 {
        2 * source * destination / 255
      } else {
        255 - 2 * (255 - source) * (255 - destination) / 255
      }
    Darken => if source < destination { source } else { destination }
    Lighten => if source > destination { source } else { destination }
    Add => clamp_color_int(source + destination)
    Subtract => clamp_color_int(destination - source)
    Difference =>
      if source > destination {
        source - destination
      } else {
        destination - source
      }
    Exclude => destination + source - 2 * source * destination / 255
  }
}

///|
fn blend_normal(source : Rgba, destination : Rgba) -> Rgba {
  let source_alpha = source.alpha().to_int()
  let destination_alpha = destination.alpha().to_int()
  if source_alpha == 0 {
    destination
  } else if source_alpha == 255 {
    source
  } else {
    let inverse = 255 - source_alpha
    let output_alpha = source_alpha + destination_alpha * inverse / 255
    if output_alpha == 0 {
      rgba(0, 0, 0, 0)
    } else {
      rgba(
        clamp_color_int(
          (
            source.red().to_int() * source_alpha +
            destination.red().to_int() * destination_alpha * inverse / 255
          ) /
          output_alpha,
        ).to_byte(),
        clamp_color_int(
          (
            source.green().to_int() * source_alpha +
            destination.green().to_int() * destination_alpha * inverse / 255
          ) /
          output_alpha,
        ).to_byte(),
        clamp_color_int(
          (
            source.blue().to_int() * source_alpha +
            destination.blue().to_int() * destination_alpha * inverse / 255
          ) /
          output_alpha,
        ).to_byte(),
        output_alpha.to_byte(),
      )
    }
  }
}

///|
/// Blend two pixels while preserving source-over alpha semantics.
pub fn blend_rgba_mode(
  source : Rgba,
  destination : Rgba,
  mode : BlendMode,
) -> Rgba {
  if mode == Normal {
    blend_normal(source, destination)
  } else {
    let color = rgba(
      blend_channel(mode, source.red().to_int(), destination.red().to_int()).to_byte(),
      blend_channel(mode, source.green().to_int(), destination.green().to_int()).to_byte(),
      blend_channel(mode, source.blue().to_int(), destination.blue().to_int()).to_byte(),
      source.alpha(),
    )
    blend_normal(color, destination)
  }
}

///|
/// Apply a layer using a blend mode over every overlapping pixel.
pub fn Canvas::blend(self : Canvas, layer : Canvas, mode : BlendMode) -> Canvas {
  let result = self.copy()
  let width = if self.width < layer.width { self.width } else { layer.width }
  let height = if self.height < layer.height {
    self.height
  } else {
    layer.height
  }
  for y in 0.. ColorAdjustment {
  { brightness: 0, contrast: 0, saturation: 0, hue_degrees: 0, alpha: 0 }
}

///|
pub fn ColorAdjustment::brightness(
  self : ColorAdjustment,
  value : Int,
) -> ColorAdjustment {
  { ..self, brightness: value }
}

///|
pub fn ColorAdjustment::contrast(
  self : ColorAdjustment,
  value : Int,
) -> ColorAdjustment {
  { ..self, contrast: value }
}

///|
pub fn ColorAdjustment::saturation(
  self : ColorAdjustment,
  value : Int,
) -> ColorAdjustment {
  { ..self, saturation: value }
}

///|
pub fn ColorAdjustment::hue(
  self : ColorAdjustment,
  value : Int,
) -> ColorAdjustment {
  { ..self, hue_degrees: value }
}

///|
pub fn ColorAdjustment::alpha(
  self : ColorAdjustment,
  value : Int,
) -> ColorAdjustment {
  { ..self, alpha: value }
}

///|
fn adjust_color(color : Rgba, adjustment : ColorAdjustment) -> Rgba {
  let hsv = color.hsv()
  let value = clamp_color_int(hsv.value().to_int() + adjustment.brightness)
  let saturation = clamp_color_int(
    hsv.saturation().to_int() + adjustment.saturation,
  )
  let mut result = rgba_from_hsv(
    hsv.hue() + adjustment.hue_degrees,
    saturation.to_byte(),
    value.to_byte(),
    color.alpha(),
  )
  if adjustment.contrast != 0 {
    let factor = 255 + adjustment.contrast
    result = rgba(
      clamp_color_int((result.red().to_int() - 128) * factor / 255 + 128).to_byte(),
      clamp_color_int((result.green().to_int() - 128) * factor / 255 + 128).to_byte(),
      clamp_color_int((result.blue().to_int() - 128) * factor / 255 + 128).to_byte(),
      result.alpha(),
    )
  }
  result.with_alpha(
    clamp_color_int(result.alpha().to_int() + adjustment.alpha).to_byte(),
  )
}

///|
/// Apply brightness, contrast, saturation, hue, and alpha adjustments.
pub fn Canvas::adjust(self : Canvas, adjustment : ColorAdjustment) -> Canvas {
  let result = self.copy()
  for i in 0.. ColorMatrix {
  {
    values: [256, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 256, 0, 0, 0, 0, 0, 256, 0],
  }
}

///|
pub fn ColorMatrix::from_values(values : ArrayView[Int]) -> ColorMatrix {
  if values.length() != 20 {
    panic()
  }
  let copied : Array[Int] = []
  for value in values {
    copied.push(value)
  }
  { values: copied }
}

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

///|
pub fn ColorMatrix::brightness(value : Int) -> ColorMatrix {
  let result = ColorMatrix::identity()
  result.values[4] = value
  result.values[9] = value
  result.values[14] = value
  result
}

///|
pub fn ColorMatrix::grayscale() -> ColorMatrix {
  {
    values: [
      77, 150, 29, 0, 0, 77, 150, 29, 0, 0, 77, 150, 29, 0, 0, 0, 0, 0, 256, 0,
    ],
  }
}

///|
pub fn ColorMatrix::apply(self : ColorMatrix, color : Rgba) -> Rgba {
  if self.values.length() != 20 {
    color
  } else {
    let input = [
      color.red().to_int(),
      color.green().to_int(),
      color.blue().to_int(),
      color.alpha().to_int(),
    ]
    let red = (
        self.values[0] * input[0] +
        self.values[1] * input[1] +
        self.values[2] * input[2] +
        self.values[3] * input[3]
      ) /
      256 +
      self.values[4]
    let green = (
        self.values[5] * input[0] +
        self.values[6] * input[1] +
        self.values[7] * input[2] +
        self.values[8] * input[3]
      ) /
      256 +
      self.values[9]
    let blue = (
        self.values[10] * input[0] +
        self.values[11] * input[1] +
        self.values[12] * input[2] +
        self.values[13] * input[3]
      ) /
      256 +
      self.values[14]
    let alpha = (
        self.values[15] * input[0] +
        self.values[16] * input[1] +
        self.values[17] * input[2] +
        self.values[18] * input[3]
      ) /
      256 +
      self.values[19]
    rgba(
      clamp_color_int(red).to_byte(),
      clamp_color_int(green).to_byte(),
      clamp_color_int(blue).to_byte(),
      clamp_color_int(alpha).to_byte(),
    )
  }
}

///|
pub fn Canvas::apply_matrix(self : Canvas, matrix : ColorMatrix) -> Canvas {
  let result = self.copy()
  for i in 0..