///|
/// RGBA color representation
pub(all) struct Color {
  r : Byte
  g : Byte
  b : Byte
  a : Byte
} derive(Eq, Debug)

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

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

///|
pub extend Color with Show::{to_string, output}

///|
pub impl Show for Color with fn output(self, logger) {
  logger.write_string("Color::new(r=")
  logger.write_object(self.r)
  logger.write_string(", g=")
  logger.write_object(self.g)
  logger.write_string(", b=")
  logger.write_object(self.b)
  logger.write_string(", a=")
  logger.write_object(self.a)
  logger.write_string(")")
}

///|
/// Create a color from RGBA values
pub fn Color::new(r~ : Byte, g~ : Byte, b~ : Byte, a? : Byte = 255) -> Color {
  { r, g, b, a, }
}

///|
/// Predefined colors
pub fn Color::white() -> Color {
  Color::new(r=255, g=255, b=255)
}

///|
pub fn Color::black() -> Color {
  Color::new(r=0, g=0, b=0)
}

///|
pub fn Color::red() -> Color {
  Color::new(r=255, g=0, b=0)
}

///|
pub fn Color::green() -> Color {
  Color::new(r=0, g=255, b=0)
}

///|
pub fn Color::blue() -> Color {
  Color::new(r=0, g=0, b=255)
}

///|
pub fn Color::transparent() -> Color {
  Color::new(r=0, g=0, b=0, a=0)
}

///|
/// Return this color with a replaced alpha channel
pub fn Color::with_alpha(self : Color, alpha : Byte) -> Color {
  { r: self.r, g: self.g, b: self.b, a: alpha, }
}

///|
/// Return this color with its alpha multiplied by opacity
pub fn Color::scale_alpha(self : Color, opacity : Double) -> Color {
  let clamped = normalize_opacity(opacity)
  let alpha = (self.a.to_int().to_double() * clamped + 0.5).to_int()
  self.with_alpha(clamp_byte(alpha))
}

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

///|
/// Composite this straight-alpha source color over a destination color
pub fn Color::blend_over(self : Color, dst : Color) -> Color {
  let src_a = self.a.to_int()
  let dst_a = dst.a.to_int()
  let inv_src_a = 255 - src_a
  let out_a = src_a + div_round(dst_a * inv_src_a, 255)

  guard out_a > 0 else { return Color::transparent() }

  let r_premul = self.r.to_int() * src_a +
    div_round(dst.r.to_int() * dst_a * inv_src_a, 255)
  let g_premul = self.g.to_int() * src_a +
    div_round(dst.g.to_int() * dst_a * inv_src_a, 255)
  let b_premul = self.b.to_int() * src_a +
    div_round(dst.b.to_int() * dst_a * inv_src_a, 255)

  Color::new(
    r=clamp_byte(div_round(r_premul, out_a)),
    g=clamp_byte(div_round(g_premul, out_a)),
    b=clamp_byte(div_round(b_premul, out_a)),
    a=clamp_byte(out_a),
  )
}

///|
/// Composite this straight-alpha source color over a destination color using
/// a configured blend mode.
pub fn Color::blend_with(
  self : Color,
  dst : Color,
  blend_mode : BlendMode,
) -> Color {
  match blend_mode {
    Clear => Color::transparent()
    Source => self
    Destination => dst
    SourceOver => self.blend_over(dst)
    DestinationOver => dst.blend_over(self)
    SourceIn => self.blend_alpha_mask(dst.a.to_int())
    DestinationIn => dst.blend_alpha_mask(self.a.to_int())
    SourceOut => self.blend_alpha_mask(255 - dst.a.to_int())
    DestinationOut => dst.blend_alpha_mask(255 - self.a.to_int())
    SourceAtop =>
      blend_porter_duff(
        self,
        dst,
        src_factor=dst.a.to_int(),
        dst_factor=255 - self.a.to_int(),
        out_alpha=dst.a.to_int(),
      )
    DestinationAtop =>
      blend_porter_duff(
        self,
        dst,
        src_factor=255 - dst.a.to_int(),
        dst_factor=self.a.to_int(),
        out_alpha=self.a.to_int(),
      )
    Xor =>
      blend_porter_duff(
        self,
        dst,
        src_factor=255 - dst.a.to_int(),
        dst_factor=255 - self.a.to_int(),
        out_alpha=div_round(self.a.to_int() * (255 - dst.a.to_int()), 255) +
          div_round(dst.a.to_int() * (255 - self.a.to_int()), 255),
      )
    Multiply
    | Screen
    | Add
    | Darken
    | Lighten
    | Difference
    | Exclusion
    | Overlay
    | HardLight
    | SoftLight
    | ColorDodge
    | ColorBurn
    | Hue
    | Saturation
    | Color
    | Luminosity
    | LinearBurn
    | Subtract => blend_non_source_over(self, dst, blend_mode)
  }
}

///|
fn should_blend_source_color(color : Color, blend_mode : BlendMode) -> Bool {
  match blend_mode {
    Destination => false
    _ =>
      color.a.to_int() > 0 || blend_mode_blends_transparent_source(blend_mode)
  }
}

///|
fn blend_mode_blends_transparent_source(blend_mode : BlendMode) -> Bool {
  match blend_mode {
    Clear | Source | SourceIn | SourceOut | DestinationIn | DestinationAtop =>
      true
    _ => false
  }
}

///|
fn blend_porter_duff(
  src : Color,
  dst : Color,
  src_factor~ : Int,
  dst_factor~ : Int,
  out_alpha~ : Int,
) -> Color {
  guard out_alpha > 0 else { return Color::transparent() }
  let src_a = src.a.to_int()
  let dst_a = dst.a.to_int()
  Color::new(
    r=blend_porter_duff_channel(
      src.r,
      src_a,
      src_factor,
      dst.r,
      dst_a,
      dst_factor,
      out_alpha,
    ),
    g=blend_porter_duff_channel(
      src.g,
      src_a,
      src_factor,
      dst.g,
      dst_a,
      dst_factor,
      out_alpha,
    ),
    b=blend_porter_duff_channel(
      src.b,
      src_a,
      src_factor,
      dst.b,
      dst_a,
      dst_factor,
      out_alpha,
    ),
    a=clamp_byte(out_alpha),
  )
}

///|
fn blend_porter_duff_channel(
  src_channel : Byte,
  src_a : Int,
  src_factor : Int,
  dst_channel : Byte,
  dst_a : Int,
  dst_factor : Int,
  out_alpha : Int,
) -> Byte {
  let premul = div_round(
      src_channel.to_int() * src_a * src_factor.max(0).min(255),
      255,
    ) +
    div_round(dst_channel.to_int() * dst_a * dst_factor.max(0).min(255), 255)
  clamp_byte(div_round(premul, out_alpha))
}

///|
fn blend_non_source_over(
  src : Color,
  dst : Color,
  blend_mode : BlendMode,
) -> Color {
  let src_a = src.a.to_int()
  let dst_a = dst.a.to_int()
  let inv_src_a = 255 - src_a
  let inv_dst_a = 255 - dst_a
  let out_a = src_a + div_round(dst_a * inv_src_a, 255)
  guard out_a > 0 else { return Color::transparent() }
  let (blended_r, blended_g, blended_b) = blend_unpremul_rgb(
    src, dst, blend_mode,
  )
  Color::new(
    r=blend_non_source_over_channel(
      src.r.to_int(),
      src_a,
      dst.r.to_int(),
      dst_a,
      inv_src_a,
      inv_dst_a,
      out_a,
      blended_r,
    ),
    g=blend_non_source_over_channel(
      src.g.to_int(),
      src_a,
      dst.g.to_int(),
      dst_a,
      inv_src_a,
      inv_dst_a,
      out_a,
      blended_g,
    ),
    b=blend_non_source_over_channel(
      src.b.to_int(),
      src_a,
      dst.b.to_int(),
      dst_a,
      inv_src_a,
      inv_dst_a,
      out_a,
      blended_b,
    ),
    a=clamp_byte(out_a),
  )
}

///|
fn blend_non_source_over_channel(
  src_c : Int,
  src_a : Int,
  dst_c : Int,
  dst_a : Int,
  inv_src_a : Int,
  inv_dst_a : Int,
  out_a : Int,
  blended : Int,
) -> Byte {
  let premul = blended * src_a * dst_a +
    src_c * src_a * inv_dst_a +
    dst_c * dst_a * inv_src_a
  clamp_byte(div_round(premul, out_a * 255))
}

///|
fn blend_unpremul_rgb(
  src : Color,
  dst : Color,
  blend_mode : BlendMode,
) -> (Int, Int, Int) {
  match blend_mode {
    Hue =>
      rounded_blend_rgb(
        blend_nonseparable_hue(color_to_blend_rgb(src), color_to_blend_rgb(dst)),
      )
    Saturation =>
      rounded_blend_rgb(
        blend_nonseparable_saturation(
          color_to_blend_rgb(src),
          color_to_blend_rgb(dst),
        ),
      )
    Color =>
      rounded_blend_rgb(
        blend_nonseparable_color(
          color_to_blend_rgb(src),
          color_to_blend_rgb(dst),
        ),
      )
    Luminosity =>
      rounded_blend_rgb(
        blend_nonseparable_luminosity(
          color_to_blend_rgb(src),
          color_to_blend_rgb(dst),
        ),
      )
    _ =>
      (
        blend_unpremul_channel(src.r.to_int(), dst.r.to_int(), blend_mode),
        blend_unpremul_channel(src.g.to_int(), dst.g.to_int(), blend_mode),
        blend_unpremul_channel(src.b.to_int(), dst.b.to_int(), blend_mode),
      )
  }
}

///|
fn blend_unpremul_channel(src : Int, dst : Int, blend_mode : BlendMode) -> Int {
  match blend_mode {
    Clear => 0
    Source => src
    Destination => dst
    SourceOver => src
    DestinationOver => dst
    SourceIn | SourceOut | SourceAtop => src
    DestinationIn | DestinationOut | DestinationAtop => dst
    Xor => src
    Multiply => div_round(src * dst, 255)
    Screen => 255 - div_round((255 - src) * (255 - dst), 255)
    Add => (src + dst).min(255)
    Darken => src.min(dst)
    Lighten => src.max(dst)
    Difference => (src - dst).abs()
    Exclusion => src + dst - div_round(2 * src * dst, 255)
    Overlay =>
      if dst < 128 {
        div_round(2 * src * dst, 255)
      } else {
        255 - div_round(2 * (255 - src) * (255 - dst), 255)
      }
    HardLight =>
      if src < 128 {
        div_round(2 * src * dst, 255)
      } else {
        255 - div_round(2 * (255 - src) * (255 - dst), 255)
      }
    SoftLight => blend_soft_light_channel(src, dst)
    ColorDodge => blend_color_dodge_channel(src, dst)
    ColorBurn => blend_color_burn_channel(src, dst)
    // Non-separable modes are handled by blend_unpremul_rgb before channel fallback.
    Hue | Saturation | Color | Luminosity => src
    LinearBurn => (src + dst - 255).max(0)
    Subtract => (dst - src).max(0)
  }
}

///|
fn color_to_blend_rgb(color : Color) -> (Double, Double, Double) {
  (
    color.r.to_int().to_double(),
    color.g.to_int().to_double(),
    color.b.to_int().to_double(),
  )
}

///|
fn blend_nonseparable_hue(
  src : (Double, Double, Double),
  dst : (Double, Double, Double),
) -> (Double, Double, Double) {
  set_lum(set_sat(src, sat(dst)), lum(dst))
}

///|
fn blend_nonseparable_saturation(
  src : (Double, Double, Double),
  dst : (Double, Double, Double),
) -> (Double, Double, Double) {
  set_lum(set_sat(dst, sat(src)), lum(dst))
}

///|
fn blend_nonseparable_color(
  src : (Double, Double, Double),
  dst : (Double, Double, Double),
) -> (Double, Double, Double) {
  set_lum(src, lum(dst))
}

///|
fn blend_nonseparable_luminosity(
  src : (Double, Double, Double),
  dst : (Double, Double, Double),
) -> (Double, Double, Double) {
  set_lum(dst, lum(src))
}

///|
fn lum(color : (Double, Double, Double)) -> Double {
  let (r, g, b) = color
  0.3 * r + 0.59 * g + 0.11 * b
}

///|
fn sat(color : (Double, Double, Double)) -> Double {
  max_rgb(color) - min_rgb(color)
}

///|
fn set_lum(
  color : (Double, Double, Double),
  target_lum : Double,
) -> (Double, Double, Double) {
  let (r, g, b) = color
  let delta = target_lum - lum(color)
  clip_color((r + delta, g + delta, b + delta))
}

///|
fn set_sat(
  color : (Double, Double, Double),
  target_sat : Double,
) -> (Double, Double, Double) {
  let (r, g, b) = color
  let min_value = min_rgb(color)
  let max_value = max_rgb(color)
  guard max_value > min_value else { return (0.0, 0.0, 0.0) }
  let scale = target_sat / (max_value - min_value)
  (
    set_sat_component(r, min_value, max_value, target_sat, scale),
    set_sat_component(g, min_value, max_value, target_sat, scale),
    set_sat_component(b, min_value, max_value, target_sat, scale),
  )
}

///|
fn set_sat_component(
  value : Double,
  min_value : Double,
  max_value : Double,
  target_sat : Double,
  scale : Double,
) -> Double {
  if value == min_value {
    0.0
  } else if value == max_value {
    target_sat
  } else {
    (value - min_value) * scale
  }
}

///|
fn clip_color(color : (Double, Double, Double)) -> (Double, Double, Double) {
  let (r, g, b) = color
  let target_lum = 0.3 * r + 0.59 * g + 0.11 * b
  let min_value = r.min(g).min(b)
  let clipped = if min_value < 0.0 {
    let scale = target_lum / (target_lum - min_value)
    (
      target_lum + (r - target_lum) * scale,
      target_lum + (g - target_lum) * scale,
      target_lum + (b - target_lum) * scale,
    )
  } else {
    color
  }
  let (clipped_r, clipped_g, clipped_b) = clipped
  let clipped_lum = 0.3 * clipped_r + 0.59 * clipped_g + 0.11 * clipped_b
  let clipped_max = clipped_r.max(clipped_g).max(clipped_b)
  if clipped_max > 255.0 {
    let scale = (255.0 - clipped_lum) / (clipped_max - clipped_lum)
    (
      clipped_lum + (clipped_r - clipped_lum) * scale,
      clipped_lum + (clipped_g - clipped_lum) * scale,
      clipped_lum + (clipped_b - clipped_lum) * scale,
    )
  } else {
    (clipped_r, clipped_g, clipped_b)
  }
}

///|
fn min_rgb(color : (Double, Double, Double)) -> Double {
  let (r, g, b) = color
  r.min(g).min(b)
}

///|
fn max_rgb(color : (Double, Double, Double)) -> Double {
  let (r, g, b) = color
  r.max(g).max(b)
}

///|
fn rounded_blend_rgb(color : (Double, Double, Double)) -> (Int, Int, Int) {
  let (r, g, b) = color
  (rounded_blend_byte(r), rounded_blend_byte(g), rounded_blend_byte(b))
}

///|
fn rounded_blend_byte(value : Double) -> Int {
  clamp_byte((value + 0.5).to_int()).to_int()
}

///|
fn Color::blend_alpha_mask(self : Color, alpha_factor : Int) -> Color {
  let out_a = div_round(self.a.to_int() * alpha_factor.max(0).min(255), 255)
  guard out_a > 0 else { return Color::transparent() }
  self.with_alpha(clamp_byte(out_a))
}

///|
fn blend_color_dodge_channel(src : Int, dst : Int) -> Int {
  if dst <= 0 {
    0
  } else if src >= 255 {
    255
  } else {
    div_round(dst * 255, 255 - src).min(255)
  }
}

///|
fn blend_color_burn_channel(src : Int, dst : Int) -> Int {
  if dst >= 255 {
    255
  } else if src <= 0 {
    0
  } else {
    255 - div_round((255 - dst) * 255, src).min(255)
  }
}

///|
fn blend_soft_light_channel(src : Int, dst : Int) -> Int {
  let cs = src.to_double() / 255.0
  let cb = dst.to_double() / 255.0
  let blended = if cs <= 0.5 {
    cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb)
  } else {
    let d = if cb <= 0.25 {
      ((16.0 * cb - 12.0) * cb + 4.0) * cb
    } else {
      cb.sqrt()
    }
    cb + (2.0 * cs - 1.0) * (d - cb)
  }
  clamp_byte((blended * 255.0 + 0.5).to_int()).to_int()
}

///|
fn div_round(numerator : Int, denominator : Int) -> Int {
  guard denominator > 0 else { return 0 }
  (numerator + denominator / 2) / denominator
}

///|
fn clamp_byte(value : Int) -> Byte {
  value.max(0).min(255).to_byte()
}