///|
/// Color interpolation used by static CSS Color functions.
///
/// These helpers belong to computed-value evaluation rather than to an
/// animation runtime. Rectangular spaces interpolate channels linearly; polar
/// spaces interpolate hue along the shortest arc.
fn lerp(a : Double, b : Double, t : Double) -> Double {
  a + (b - a) * t
}

///|
/// Round to the nearest integer and clamp to the 0-255 color-channel range.
fn lerp_channel(a : Int, b : Int, t : Double) -> Int {
  let v = lerp(a.to_double(), b.to_double(), t)
  let r = (v + 0.5).to_int()
  if r < 0 {
    0
  } else if r > 255 {
    255
  } else {
    r
  }
}

///|
/// Interpolate two colors in sRGB. Alpha is interpolated linearly.
fn interpolate_color(
  a : @types.Color,
  b : @types.Color,
  t : Double,
) -> @types.Color {
  @types.Color::rgba(
    lerp_channel(a.r, b.r, t),
    lerp_channel(a.g, b.g, t),
    lerp_channel(a.b, b.b, t),
    lerp(a.a, b.a, t),
  )
}
// ---- Oklab color interpolation (CSS Color 4) ----

///|
/// sRGB 0-255 channel to linear-light [0, 1].
fn srgb_to_linear(c : Int) -> Double {
  let x = c.to_double() / 255.0
  if x <= 0.04045 {
    x / 12.92
  } else {
    @math.pow((x + 0.055) / 1.055, 2.4)
  }
}

///|
/// Linear-light [0, 1] to sRGB 0-255 channel (rounded, clamped).
fn linear_to_srgb(x : Double) -> Int {
  let c = if x <= 0.0031308 {
    12.92 * x
  } else {
    1.055 * @math.pow(x, 1.0 / 2.4) - 0.055
  }
  let v = (c * 255.0 + 0.5).to_int()
  if v < 0 {
    0
  } else if v > 255 {
    255
  } else {
    v
  }
}

///|
/// Interpolate two colors in the Oklab color space (CSS Color 4 default for
/// `` interpolation). Alpha is interpolated linearly in [0, 1].
fn interpolate_color_oklab(
  a : @types.Color,
  b : @types.Color,
  t : Double,
) -> @types.Color {
  let (l1, a1, b1) = srgb_to_oklab(a)
  let (l2, a2, b2) = srgb_to_oklab(b)
  let (r, g, bl) = oklab_to_srgb(
    lerp(l1, l2, t),
    lerp(a1, a2, t),
    lerp(b1, b2, t),
  )
  @types.Color::rgba(r, g, bl, lerp(a.a, b.a, t))
}

///|
fn srgb_to_oklab(c : @types.Color) -> (Double, Double, Double) {
  let r = srgb_to_linear(c.r)
  let g = srgb_to_linear(c.g)
  let b = srgb_to_linear(c.b)
  let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
  let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
  let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
  let l_ = @math.cbrt(l)
  let m_ = @math.cbrt(m)
  let s_ = @math.cbrt(s)
  (
    0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
    1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
    0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
  )
}

///|
fn oklab_to_srgb(l : Double, a : Double, b : Double) -> (Int, Int, Int) {
  let l_ = l + 0.3963377774 * a + 0.2158037573 * b
  let m_ = l - 0.1055613458 * a - 0.0638541728 * b
  let s_ = l - 0.0894841775 * a - 1.2914855480 * b
  let l3 = l_ * l_ * l_
  let m3 = m_ * m_ * m_
  let s3 = s_ * s_ * s_
  let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
  let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
  let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3
  (linear_to_srgb(r), linear_to_srgb(g), linear_to_srgb(bl))
}

// ---- Additional color spaces (CSS Color 4): oklch, lab, lch ----

///|
/// Color space used for `` interpolation.
priv enum ColorInterpolationSpace {
  Srgb
  Oklab
  Oklch
  Lab
  Lch
}

///|
/// Interpolate a hue (radians) along the shortest arc.
fn lerp_hue(h1 : Double, h2 : Double, t : Double) -> Double {
  let two_pi = 2.0 * @math.PI
  let mut dh = h2 - h1
  while dh > @math.PI {
    dh = dh - two_pi
  }
  while dh < -@math.PI {
    dh = dh + two_pi
  }
  h1 + dh * t
}

///|
/// Interpolate two colors in the given color space. Rectangular spaces (srgb,
/// oklab, lab) interpolate channels linearly; polar spaces (oklch, lch)
/// interpolate lightness/chroma linearly and hue along the shortest arc. Alpha
/// is always interpolated linearly.
fn interpolate_color_in(
  space : ColorInterpolationSpace,
  a : @types.Color,
  b : @types.Color,
  t : Double,
) -> @types.Color {
  let alpha = lerp(a.a, b.a, t)
  match space {
    Srgb => interpolate_color(a, b, t)
    Oklab => interpolate_color_oklab(a, b, t)
    Lab => {
      let (l1, a1, b1) = srgb_to_lab(a)
      let (l2, a2, b2) = srgb_to_lab(b)
      let (r, g, bl) = lab_to_srgb(
        lerp(l1, l2, t),
        lerp(a1, a2, t),
        lerp(b1, b2, t),
      )
      @types.Color::rgba(r, g, bl, alpha)
    }
    Oklch => {
      let (l1, c1, h1) = srgb_to_oklch(a)
      let (l2, c2, h2) = srgb_to_oklch(b)
      let (oa, ob) = lch_to_ab(lerp(c1, c2, t), lerp_hue(h1, h2, t))
      let (r, g, bl) = oklab_to_srgb(lerp(l1, l2, t), oa, ob)
      @types.Color::rgba(r, g, bl, alpha)
    }
    Lch => {
      let (l1, c1, h1) = srgb_to_lch(a)
      let (l2, c2, h2) = srgb_to_lch(b)
      let (la, lb) = lch_to_ab(lerp(c1, c2, t), lerp_hue(h1, h2, t))
      let (r, g, bl) = lab_to_srgb(lerp(l1, l2, t), la, lb)
      @types.Color::rgba(r, g, bl, alpha)
    }
  }
}

///|
/// Convert a rectangular (a, b) pair from a chroma/hue (hue in radians).
fn lch_to_ab(c : Double, h : Double) -> (Double, Double) {
  (c * @math.cos(h), c * @math.sin(h))
}

///|
fn srgb_to_oklch(col : @types.Color) -> (Double, Double, Double) {
  let (l, a, b) = srgb_to_oklab(col)
  (l, (a * a + b * b).sqrt(), @math.atan2(b, a))
}

///|
/// sRGB color to CIE Lab (D65).
fn srgb_to_lab(col : @types.Color) -> (Double, Double, Double) {
  let r = srgb_to_linear(col.r)
  let g = srgb_to_linear(col.g)
  let b = srgb_to_linear(col.b)
  let x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b
  let y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b
  let z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b
  let xn = 0.95047
  let yn = 1.0
  let zn = 1.08883
  let fx = lab_f(x / xn)
  let fy = lab_f(y / yn)
  let fz = lab_f(z / zn)
  (116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
}

///|
fn srgb_to_lch(col : @types.Color) -> (Double, Double, Double) {
  let (l, a, b) = srgb_to_lab(col)
  (l, (a * a + b * b).sqrt(), @math.atan2(b, a))
}

///|
fn lab_f(t : Double) -> Double {
  let eps = 216.0 / 24389.0
  if t > eps {
    @math.cbrt(t)
  } else {
    (24389.0 / 27.0 * t + 16.0) / 116.0
  }
}

///|
/// CIE Lab (D65) to sRGB 0-255.
fn lab_to_srgb(l : Double, a : Double, b : Double) -> (Int, Int, Int) {
  let fy = (l + 16.0) / 116.0
  let fx = fy + a / 500.0
  let fz = fy - b / 200.0
  let eps = 216.0 / 24389.0
  let kappa = 24389.0 / 27.0
  let fx3 = fx * fx * fx
  let fz3 = fz * fz * fz
  let xr = if fx3 > eps { fx3 } else { (116.0 * fx - 16.0) / kappa }
  let yr = if l > kappa * eps { fy * fy * fy } else { l / kappa }
  let zr = if fz3 > eps { fz3 } else { (116.0 * fz - 16.0) / kappa }
  let x = xr * 0.95047
  let y = yr * 1.0
  let z = zr * 1.08883
  let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
  let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z
  let bl = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
  (linear_to_srgb(r), linear_to_srgb(g), linear_to_srgb(bl))
}