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

///|
/// The space a colour's three components are read in.
///
/// | `space` | `x` | `y` | `z` |
/// |:--|:--|:--|:--|
/// | `Srgb`, `SrgbLinear`, `DisplayP3`, `Rec2020`, `A98Rgb`, `ProPhoto` | red | green | blue |
/// | `Hsl` | hue, degrees | saturation | lightness |
/// | `Hsv` | hue, degrees | saturation | value |
/// | `Hwb` | hue, degrees | whiteness | blackness |
/// | `Lab` | lightness, 0–100 | a | b |
/// | `Lch` | lightness, 0–100 | chroma | hue, degrees |
/// | `Oklab` | lightness, 0–1 | a | b |
/// | `Oklch` | lightness, 0–1 | chroma | hue, degrees |
/// | `XyzD50`, `XyzD65` | X | Y | Z |
/// | `YCbCr` | luma | blue difference | red difference |
/// | `Current` | — | — | — |
///
/// Component ranges are the ones the format itself uses, not a common scale:
/// `Lab` lightness is 0–100 because that is what `lab()` writes, while `Oklab`
/// lightness is 0–1 because that is what `oklab()` writes. A colour out of its
/// space's range is still a valid value — CSS Color 4 lets a colour be outside
/// its gamut and leaves clipping to the moment it is painted.
pub(all) enum Space {
  /// Gamma-encoded sRGB, components 0–1. The one space every screen has.
  Srgb
  /// Linear-light sRGB, components 0–1: sRGB without the transfer curve.
  SrgbLinear
  /// Hue in degrees, saturation and lightness 0–1.
  Hsl
  /// Hue in degrees, saturation and value 0–1. No CSS syntax of its own.
  Hsv
  /// Hue in degrees, whiteness and blackness 0–1.
  Hwb
  /// CIE Lab, D50. Lightness 0–100, a and b roughly ±125.
  Lab
  /// CIE LCh, D50: Lab in polar form, hue in degrees.
  Lch
  /// Oklab. Lightness 0–1, a and b roughly ±0.4.
  Oklab
  /// OkLCh: Oklab in polar form, hue in degrees.
  Oklch
  /// CIE XYZ relative to D50, Y = 1 for white.
  XyzD50
  /// CIE XYZ relative to D65, Y = 1 for white. What `color(xyz ...)` writes.
  XyzD65
  /// Display P3, components 0–1, sRGB's transfer curve with wider primaries.
  DisplayP3
  /// Linear-light Display P3: P3 without the transfer curve.
  DisplayP3Linear
  /// Rec. 2020, components 0–1.
  Rec2020
  /// Adobe RGB (1998), components 0–1.
  A98Rgb
  /// ProPhoto RGB, components 0–1, D50 white.
  ProPhoto
  /// A luma/chroma pair as BT.601, BT.709 or BT.2020 define it. Luma 0–1,
  /// chroma −0.5–0.5. No CSS syntax.
  YCbCr(Luma)
  /// Not a space: the `currentcolor` keyword, which stands for whatever colour
  /// the element it is painted on already has. Carried in a `Color` so that a
  /// declaration can be read and written back unchanged; resolve it before
  /// converting, with `Color::resolve`.
  Current
} derive(Eq, Debug)

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

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

///|
/// Which luma coefficients a `YCbCr` colour uses.
///
/// The three differ only in how much of each primary counts as brightness, and
/// a value converted with the wrong one is visibly off, so the choice is part of
/// the value rather than a global setting.
pub(all) enum Luma {
  /// BT.601: the coefficients of SD television.
  Bt601
  /// BT.709: the coefficients of HD television, and of sRGB in practice.
  Bt709
  /// BT.2020: the coefficients of UHD television, on wider primaries.
  Bt2020
} derive(Eq, Debug)

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

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

///|
/// A colour: three components in a named space, and an alpha.
///
/// Alpha is kept apart from the components because it is not a colour: it says
/// how much of what is behind shows through, and it survives a change of space
/// untouched.
///
/// A component may be `none`, which CSS writes as the `none` keyword and which
/// this library carries as a NaN — "this space did not say", which interpolation
/// then fills in from the other side. Ordinary arithmetic on such a component
/// gives NaN back, which is the honest answer.
pub(all) struct Color {
  space : Space
  x : Double
  y : Double
  z : Double
  alpha : Double
} derive(Debug)

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

///|
/// A colour in any space, from components already in that space's own units.
///
/// The named constructors below cover the common spaces; this one is for the
/// rest, and for code that picks a space at run time.
pub fn Color::of(
  space : Space,
  x : Double,
  y : Double,
  z : Double,
  alpha? : Double,
) -> Color {
  { space, x, y, z, alpha: alpha.unwrap_or(1.0), }
}

///|
/// sRGB from components in 0–1.
pub fn Color::srgb(
  r : Double,
  g : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: Srgb, x: r, y: g, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// sRGB from bytes, 0–255, which is how a colour usually arrives from a file,
/// a theme or a terminal.
pub fn Color::rgb(
  red : Int32,
  green : Int32,
  blue : Int32,
  alpha? : Double,
) -> Color {
  {
    space: Srgb,
    x: red.to_double() / 255.0,
    y: green.to_double() / 255.0,
    z: blue.to_double() / 255.0,
    alpha: alpha.unwrap_or(1.0),
  }
}

///|
/// Linear-light sRGB from components in 0–1.
pub fn Color::srgb_linear(
  r : Double,
  g : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: SrgbLinear, x: r, y: g, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// HSL: hue in degrees, saturation and lightness in 0–1.
pub fn Color::hsl(h : Double, s : Double, l : Double, alpha? : Double) -> Color {
  { space: Hsl, x: h, y: s, z: l, alpha: alpha.unwrap_or(1.0), }
}

///|
/// HSV: hue in degrees, saturation and value in 0–1.
pub fn Color::hsv(h : Double, s : Double, v : Double, alpha? : Double) -> Color {
  { space: Hsv, x: h, y: s, z: v, alpha: alpha.unwrap_or(1.0), }
}

///|
/// HWB: hue in degrees, whiteness and blackness in 0–1.
pub fn Color::hwb(h : Double, w : Double, b : Double, alpha? : Double) -> Color {
  { space: Hwb, x: h, y: w, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// CIE Lab with a D50 white: lightness 0–100.
pub fn Color::lab(l : Double, a : Double, b : Double, alpha? : Double) -> Color {
  { space: Lab, x: l, y: a, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// CIE LCh with a D50 white: lightness 0–100, hue in degrees.
pub fn Color::lch(l : Double, c : Double, h : Double, alpha? : Double) -> Color {
  { space: Lch, x: l, y: c, z: h, alpha: alpha.unwrap_or(1.0), }
}

///|
/// Oklab: lightness 0–1.
pub fn Color::oklab(
  l : Double,
  a : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: Oklab, x: l, y: a, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// OkLCh: lightness 0–1, hue in degrees.
pub fn Color::oklch(
  l : Double,
  c : Double,
  h : Double,
  alpha? : Double,
) -> Color {
  { space: Oklch, x: l, y: c, z: h, alpha: alpha.unwrap_or(1.0), }
}

///|
/// CIE XYZ relative to D65, Y = 1 for white.
pub fn Color::xyz_d65(
  x : Double,
  y : Double,
  z : Double,
  alpha? : Double,
) -> Color {
  { space: XyzD65, x, y, z, alpha: alpha.unwrap_or(1.0), }
}

///|
/// CIE XYZ relative to D50, Y = 1 for white.
pub fn Color::xyz_d50(
  x : Double,
  y : Double,
  z : Double,
  alpha? : Double,
) -> Color {
  { space: XyzD50, x, y, z, alpha: alpha.unwrap_or(1.0), }
}

///|
/// Display P3, components in 0–1.
pub fn Color::display_p3(
  r : Double,
  g : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: DisplayP3, x: r, y: g, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// Rec. 2020, components in 0–1.
pub fn Color::rec2020(
  r : Double,
  g : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: Rec2020, x: r, y: g, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// Adobe RGB (1998), components in 0–1.
pub fn Color::a98_rgb(
  r : Double,
  g : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: A98Rgb, x: r, y: g, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// ProPhoto RGB, components in 0–1, D50 white.
pub fn Color::prophoto(
  r : Double,
  g : Double,
  b : Double,
  alpha? : Double,
) -> Color {
  { space: ProPhoto, x: r, y: g, z: b, alpha: alpha.unwrap_or(1.0), }
}

///|
/// YCbCr: luma in 0–1, chroma in −0.5–0.5.
pub fn Color::ycbcr(
  y : Double,
  cb : Double,
  cr : Double,
  luma? : Luma,
  alpha? : Double,
) -> Color {
  {
    space: YCbCr(luma.unwrap_or(Luma::Bt709)),
    x: y,
    y: cb,
    z: cr,
    alpha: alpha.unwrap_or(1.0),
  }
}

///|
/// The `currentcolor` keyword.
pub fn Color::current() -> Color {
  { space: Current, x: 0.0, y: 0.0, z: 0.0, alpha: 1.0, }
}

///|
/// `transparent`: black with no alpha at all. Not a space of its own, because
/// what shows through is decided by the backdrop, not by the colour.
pub fn Color::transparent() -> Color {
  { space: Srgb, x: 0.0, y: 0.0, z: 0.0, alpha: 0.0, }
}

///|
/// Black in sRGB.
pub fn Color::black() -> Color {
  { space: Srgb, x: 0.0, y: 0.0, z: 0.0, alpha: 1.0, }
}

///|
/// White in sRGB.
pub fn Color::white() -> Color {
  { space: Srgb, x: 1.0, y: 1.0, z: 1.0, alpha: 1.0, }
}

///|
/// The three components, in the order the space writes them.
pub fn Color::components(self : Color) -> (Double, Double, Double) {
  (self.x, self.y, self.z)
}

///|
/// The same colour with different components, in the same space.
pub fn Color::with_components(
  self : Color,
  x : Double,
  y : Double,
  z : Double,
) -> Color {
  { ..self, x, y, z, }
}

///|
/// The same colour at a different alpha.
pub fn Color::with_alpha(self : Color, alpha : Double) -> Color {
  { ..self, alpha, }
}

///|
/// Whether this is the unresolved `currentcolor` keyword.
pub fn Color::is_current(self : Color) -> Bool {
  self.space is Current
}

///|
/// Substitute the colour that `currentcolor` stands for. Anything else is
/// returned unchanged, so this is safe to call on every colour.
pub fn Color::resolve(self : Color, current : Color) -> Color {
  if self.space is Current {
    current
  } else {
    self
  }
}

///|
/// Whether the space writes a hue in its third component (`Hsl`, `Hsv`, `Hwb`)
/// or in its third of a polar pair (`Lch`, `Oklch`).
pub fn Space::is_polar(self : Space) -> Bool {
  match self {
    Hsl | Hsv | Hwb | Lch | Oklch => true
    _ => false
  }
}

///|
/// The hue in degrees, or NaN where the space has none.
pub fn Color::hue(self : Color) -> Double {
  match self.space {
    // The hue is the first component in the HSL family and the last in the
    // polar Lab family; there is no single place to look.
    Hsl | Hsv | Hwb => self.x
    Lch | Oklch => self.z
    _ => @double.not_a_number
  }
}

///|
/// The chroma, or NaN where the space has none.
pub fn Color::chroma(self : Color) -> Double {
  match self.space {
    Lch | Oklch => self.y
    _ => @double.not_a_number
  }
}

///|
/// The lightness, or NaN where the space has none.
pub fn Color::lightness(self : Color) -> Double {
  match self.space {
    Lab | Lch => self.x
    Oklab | Oklch => self.x
    Hsl | Hwb => self.z
    Hsv => self.z
    _ => @double.not_a_number
  }
}

///|
/// The saturation, or NaN where the space has none.
pub fn Color::saturation(self : Color) -> Double {
  match self.space {
    Hsl | Hsv => self.y
    _ => @double.not_a_number
  }
}

///|
/// A space writes itself with the identifier CSS uses for it, or the one this
/// library uses where CSS has no name for it.
pub impl Show for Space with fn output(self, logger) {
  logger.write_string(self.code())
}

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

///|
/// The identifier this space is written with in CSS, or the one this library
/// uses where CSS has no syntax for it.
pub fn Space::code(self : Space) -> String {
  match self {
    Srgb => "srgb"
    SrgbLinear => "srgb-linear"
    Hsl => "hsl"
    Hsv => "hsv"
    Hwb => "hwb"
    Lab => "lab"
    Lch => "lch"
    Oklab => "oklab"
    Oklch => "oklch"
    XyzD50 => "xyz-d50"
    XyzD65 => "xyz-d65"
    DisplayP3 => "display-p3"
    DisplayP3Linear => "display-p3-linear"
    Rec2020 => "rec2020"
    A98Rgb => "a98-rgb"
    ProPhoto => "prophoto-rgb"
    YCbCr(luma) =>
      match luma {
        Bt601 => "ycbcr-601"
        Bt709 => "ycbcr-709"
        Bt2020 => "ycbcr-2020"
      }
    Current => "currentcolor"
  }
}

///|
/// The space an identifier names, if any.
pub fn Space::of(code : StringView) -> Space? {
  match code.to_lower() {
    "srgb" => Some(Srgb)
    "srgb-linear" => Some(SrgbLinear)
    "hsl" => Some(Hsl)
    "hsv" => Some(Hsv)
    "hwb" => Some(Hwb)
    "lab" => Some(Lab)
    "lch" => Some(Lch)
    "oklab" => Some(Oklab)
    "oklch" => Some(Oklch)
    "xyz" | "xyz-d65" => Some(XyzD65)
    "xyz-d50" => Some(XyzD50)
    "display-p3" => Some(DisplayP3)
    "display-p3-linear" => Some(DisplayP3Linear)
    "rec2020" => Some(Rec2020)
    "a98-rgb" => Some(A98Rgb)
    "prophoto-rgb" => Some(ProPhoto)
    "ycbcr-601" => Some(YCbCr(Bt601))
    "ycbcr-709" => Some(YCbCr(Bt709))
    "ycbcr-2020" => Some(YCbCr(Bt2020))
    "currentcolor" => Some(Current)
    _ => None
  }
}

///|
/// Whether this space can be written inside CSS's `color()`.
pub fn Space::is_predefined(self : Space) -> Bool {
  match self {
    Srgb
    | SrgbLinear
    | DisplayP3
    | DisplayP3Linear
    | Rec2020
    | A98Rgb
    | ProPhoto
    | XyzD50
    | XyzD65 => true
    _ => false
  }
}