///|
/// A premultiplied-independent RGBA color used by pattern generators.
/// Components are stored in the closed interval [0, 1].
pub(all) struct Color {
  r : Double
  g : Double
  b : Double
  a : Double
} derive(Eq, Debug, ToJson)

///|
fn clamp01(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}

///|
/// Creates a color from normalized channels.
pub fn Color::rgba(
  r : Double,
  g : Double,
  b : Double,
  a? : Double = 1.0,
) -> Color {
  { r: clamp01(r), g: clamp01(g), b: clamp01(b), a: clamp01(a) }
}

///|
/// Creates an opaque color from 8-bit RGB channels.
pub fn Color::rgb8(r : Int, g : Int, b : Int) -> Color {
  Color::rgba(
    r.to_double() / 255.0,
    g.to_double() / 255.0,
    b.to_double() / 255.0,
  )
}

///|
pub fn Color::with_alpha(self : Color, alpha : Double) -> Color {
  Color::rgba(self.r, self.g, self.b, a=alpha)
}

///|
pub fn Color::mix(self : Color, other : Color, amount : Double) -> Color {
  let t = clamp01(amount)
  Color::rgba(
    self.r + (other.r - self.r) * t,
    self.g + (other.g - self.g) * t,
    self.b + (other.b - self.b) * t,
    a=self.a + (other.a - self.a) * t,
  )
}

///|
/// Alpha-composites `top` over this color.
pub fn Color::over(self : Color, top : Color) -> Color {
  let out_a = top.a + self.a * (1.0 - top.a)
  if out_a == 0.0 {
    Color::rgba(0.0, 0.0, 0.0, a=0.0)
  } else {
    Color::rgba(
      (top.r * top.a + self.r * self.a * (1.0 - top.a)) / out_a,
      (top.g * top.a + self.g * self.a * (1.0 - top.a)) / out_a,
      (top.b * top.a + self.b * self.a * (1.0 - top.a)) / out_a,
      a=out_a,
    )
  }
}

///|
/// Relative luminance according to the sRGB transfer curve.
pub fn Color::luminance(self : Color) -> Double {
  fn linear(channel : Double) -> Double {
    if channel <= 0.04045 {
      channel / 12.92
    } else {
      @math.pow((channel + 0.055) / 1.055, 2.4)
    }
  }
  0.2126 * linear(self.r) + 0.7152 * linear(self.g) + 0.0722 * linear(self.b)
}

///|
pub fn Color::contrast_ratio(self : Color, other : Color) -> Double {
  let a = self.luminance()
  let b = other.luminance()
  let hi = if a > b { a } else { b }
  let lo = if a > b { b } else { a }
  (hi + 0.05) / (lo + 0.05)
}

///|
/// Returns a CSS-compatible hexadecimal color. Alpha is included when needed.
pub fn Color::to_hex(self : Color) -> String {
  fn byte(value : Double) -> String {
    let n = (clamp01(value) * 255.0 + 0.5).to_int()
    let high = n / 16
    let low = n % 16
    fn digit(value : Int) -> String {
      match value {
        0 => "0"
        1 => "1"
        2 => "2"
        3 => "3"
        4 => "4"
        5 => "5"
        6 => "6"
        7 => "7"
        8 => "8"
        9 => "9"
        10 => "a"
        11 => "b"
        12 => "c"
        13 => "d"
        14 => "e"
        _ => "f"
      }
    }
    digit(high) + digit(low)
  }
  let base = "#\{byte(self.r)}\{byte(self.g)}\{byte(self.b)}"
  if self.a >= 0.999 {
    base
  } else {
    base + byte(self.a)
  }
}

///|
/// Converts HSL values (hue in radians, saturation and lightness in [0, 1]).
pub fn Color::hsl(
  hue : Double,
  saturation : Double,
  lightness : Double,
  alpha? : Double = 1.0,
) -> Color {
  let s = clamp01(saturation)
  let l = clamp01(lightness)
  if s == 0.0 {
    Color::rgba(l, l, l, a=alpha)
  } else {
    let h = hue / (2.0 * @math.PI) -
      (hue / (2.0 * @math.PI)).to_int().to_double()
    let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }
    let p = 2.0 * l - q
    fn hue_to_rgb(p : Double, q : Double, t0 : Double) -> Double {
      let t = if t0 < 0.0 { t0 + 1.0 } else if t0 > 1.0 { t0 - 1.0 } else { t0 }
      if t < 1.0 / 6.0 {
        p + (q - p) * 6.0 * t
      } else if t < 1.0 / 2.0 {
        q
      } else if t < 2.0 / 3.0 {
        p + (q - p) * (2.0 / 3.0 - t) * 6.0
      } else {
        p
      }
    }
    Color::rgba(
      hue_to_rgb(p, q, h + 1.0 / 3.0),
      hue_to_rgb(p, q, h),
      hue_to_rgb(p, q, h - 1.0 / 3.0),
      a=alpha,
    )
  }
}

///|
pub fn palette_complementary(base : Color) -> Array[Color] {
  [base, Color::rgba(1.0 - base.r, 1.0 - base.g, 1.0 - base.b)]
}

///|
pub fn palette_analogous(
  base : Color,
  count : Int,
  spread? : Double = @math.PI / 6.0,
) -> Array[Color] {
  let colors = []
  if count <= 0 {
    colors
  } else {
    let center = (count - 1).to_double() / 2.0
    for i in 0.. Array[Color] {
  let result = []
  if samples <= 0 || stops.length() == 0 {
    result
  } else if stops.length() == 1 {
    for _ in 0..= stops.length() { left } else { left + 1 }
      let amount = scaled - left.to_double()
      result.push(stops[left].mix(stops[right], amount))
    }
    result
  }
}