///|
/// Multi-stop gradients: linear, radial and conic axes, optional Oklab mixing.
///
/// The legacy `LinearGradient`/`RadialGradient` brushes stay two-color and
/// untouched. This richer spec is one additive `Brush::StopsGradient` variant so
/// a renderer implements one shape: axis + sorted stops + an interpolation
/// choice. Renderers without native multi-stop support can call
/// `srgb_stops()`, which expands Oklab segments into sRGB samples.

///|
#derive(Eq, Debug, ToJson)
pub(all) struct GradientStop {
  color : Color
  /// Position along the gradient axis, clamped into 0.0..1.0.
  at : Double
}

///|
pub fn GradientStop::new(color~ : Color, at~ : Double) -> GradientStop {
  { color, at: clamp_gradient_at(at), }
}

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

///|
/// Geometry of a stop gradient. `ConicAxis.angle_degrees` rotates the start of
/// the sweep clockwise from 12 o'clock, matching CSS `conic-gradient(from …)`.
#derive(Eq, Debug, ToJson)
pub(all) enum GradientAxis {
  LinearAxis(Point, Point)
  RadialAxis(Point, Double)
  ConicAxis(Point, Double)
}

///|
#derive(Eq, Debug, ToJson)
pub(all) struct StopsGradientSpec {
  axis : GradientAxis
  stops : Array[GradientStop]
  /// Mix adjacent stops in Oklab instead of sRGB.
  oklab : Bool
}

///|
pub fn StopsGradientSpec::new(
  axis~ : GradientAxis,
  stops~ : Array[GradientStop],
  oklab~ : Bool = false,
) -> StopsGradientSpec {
  { axis, stops: normalize_gradient_stops(stops), oklab, }
}

///|
fn normalize_gradient_stops(stops : Array[GradientStop]) -> Array[GradientStop] {
  let normalized = stops.copy()
  for index in 0.. a.at.compare(b.at))
  normalized
}

///|
/// The stops a renderer without Oklab support can consume. Oklab segments are
/// subdivided into `resolution` sRGB samples; already-sRGB specs return a copy.
pub fn StopsGradientSpec::srgb_stops(
  self : StopsGradientSpec,
  resolution~ : Int32 = 16,
) -> Array[GradientStop] {
  if !self.oklab || self.stops.length() < 2 {
    return self.stops.copy()
  }
  let steps = resolution.max(2)
  let output : Array[GradientStop] = []
  for index in 0..<(self.stops.length() - 1) {
    let from = self.stops[index]
    let to = self.stops[index + 1]
    for step in 0.. Color {
  if self.stops.is_empty() {
    return Color::transparent()
  }
  let position = if t <= 0.0 { 0.0 } else if t >= 1.0 { 1.0 } else { t }
  for index in 0..<(self.stops.length() - 1) {
    let from = self.stops[index]
    let to = self.stops[index + 1]
    if position <= to.at {
      let span = to.at - from.at
      let segment_t = if span <= 0.0 {
        0.0
      } else {
        (position - from.at) / span
      }
      return if self.oklab {
        Color::lerp_oklab(from.color, to.color, segment_t)
      } else {
        from.color.lerp(to.color, segment_t)
      }
    }
  }
  self.stops[self.stops.length() - 1].color
}

///|
/// The first and last stop colors, the two-color fallback used by renderers
/// without multi-stop support. Empty stop lists report transparent for both.
pub fn StopsGradientSpec::edge_colors(
  self : StopsGradientSpec,
) -> (Color, Color) {
  let first = match self.stops.get(0) {
    Some(stop) => stop.color
    None => Color::transparent()
  }
  let last = match self.stops.get(self.stops.length() - 1) {
    Some(stop) => stop.color
    None => first
  }
  (first, last)
}

///|
/// Interpolate two colors in Oklab. Alpha interpolates linearly; RGB is mixed
/// through linear-light Oklab so saturated pairs keep their lightness instead
/// of passing through a dull gray midpoint.
pub fn Color::lerp_oklab(from : Color, to : Color, t : Double) -> Color {
  let amount = if t <= 0.0 { 0.0 } else if t >= 1.0 { 1.0 } else { t }
  let from_lab = oklab_from_color(from)
  let to_lab = oklab_from_color(to)
  let lab = Oklab::new(
    l=from_lab.l + (to_lab.l - from_lab.l) * amount,
    a=from_lab.a + (to_lab.a - from_lab.a) * amount,
    b=from_lab.b + (to_lab.b - from_lab.b) * amount,
  )
  let mixed = color_from_oklab(lab)
  Color::rgba(
    r=mixed.r,
    g=mixed.g,
    b=mixed.b,
    a=from.a + (to.a - from.a) * amount,
  )
}

///|
priv struct Oklab {
  l : Double
  a : Double
  b : Double
}

///|
fn Oklab::new(l~ : Double, a~ : Double, b~ : Double) -> Oklab {
  { l, a, b, }
}

///|
fn srgb_channel_to_linear(value : Double) -> Double {
  if value <= 0.04045 {
    value / 12.92
  } else {
    @math.pow((value + 0.055) / 1.055, 2.4)
  }
}

///|
fn linear_channel_to_srgb(value : Double) -> Double {
  if value <= 0.0031308 {
    value * 12.92
  } else {
    1.055 * @math.pow(value, 1.0 / 2.4) - 0.055
  }
}

///|
fn oklab_from_color(color : Color) -> Oklab {
  let r = srgb_channel_to_linear(color.r)
  let g = srgb_channel_to_linear(color.g)
  let b = srgb_channel_to_linear(color.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)
  Oklab::new(
    l=0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
    a=1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
    b=0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
  )
}

///|
fn color_from_oklab(lab : Oklab) -> Color {
  let l_ = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b
  let m_ = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b
  let s_ = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b
  let l = l_ * l_ * l_
  let m = m_ * m_ * m_
  let s = s_ * s_ * s_
  let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
  let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
  let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
  Color::rgba(
    r=linear_channel_to_srgb(r).clamp(min=0.0, max=1.0),
    g=linear_channel_to_srgb(g).clamp(min=0.0, max=1.0),
    b=linear_channel_to_srgb(b).clamp(min=0.0, max=1.0),
    a=1.0,
  )
}

///|
/// Multi-stop linear gradient brush.
pub fn Brush::linear_stops(
  start~ : Point,
  end~ : Point,
  stops~ : Array[GradientStop],
  oklab~ : Bool = false,
) -> Brush {
  StopsGradient(
    StopsGradientSpec::new(axis=LinearAxis(start, end), stops~, oklab~),
  )
}

///|
/// Multi-stop radial gradient brush.
pub fn Brush::radial_stops(
  center~ : Point,
  radius~ : Double,
  stops~ : Array[GradientStop],
  oklab~ : Bool = false,
) -> Brush {
  StopsGradient(
    StopsGradientSpec::new(
      axis=RadialAxis(center, radius.max(0.0)),
      stops~,
      oklab~,
    ),
  )
}

///|
/// Multi-stop conic (sweep) gradient brush. `angle_degrees` rotates the start
/// of the sweep clockwise from 12 o'clock.
pub fn Brush::conic(
  center~ : Point,
  stops~ : Array[GradientStop],
  angle_degrees~ : Double = 0.0,
  oklab~ : Bool = false,
) -> Brush {
  StopsGradient(
    StopsGradientSpec::new(
      axis=ConicAxis(center, angle_degrees),
      stops~,
      oklab~,
    ),
  )
}