///|
/// A single color stop in a gradient. `offset` is in 0.0..=1.0.
pub struct ColorStop {
  offset : Double
  color : Color
} derive(Eq)

///|
pub fn ColorStop::new(offset : Double, color : Color) -> ColorStop {
  { offset, color }
}

///|
pub impl Show for ColorStop with fn output(self, logger) {
  logger.write_string("ColorStop { offset: ")
  logger.write_object(self.offset)
  logger.write_string(", color: ")
  self.color.output(logger)
  logger.write_string(" }")
}

///|
fn color_stops_to_string(stops : Array[ColorStop]) -> String {
  let buffer = StringBuilder::new()
  buffer.write_string("[")
  for index, stop in stops {
    if index > 0 {
      buffer.write_string(", ")
    }
    buffer.write_string(stop.to_string())
  }
  buffer.write_string("]")
  buffer.to_string()
}

///|
/// Linear gradient from (x0, y0) to (x1, y1) in user space.
/// Stops are sorted by `offset` at construction time; out-of-range t values
/// clamp to the first/last color.
pub struct LinearGradient {
  x0 : Double
  y0 : Double
  x1 : Double
  y1 : Double
  stops : Array[ColorStop]
} derive(Eq)

///|
pub impl Show for LinearGradient with fn output(self, logger) {
  logger.write_string("LinearGradient { x0: ")
  logger.write_object(self.x0)
  logger.write_string(", y0: ")
  logger.write_object(self.y0)
  logger.write_string(", x1: ")
  logger.write_object(self.x1)
  logger.write_string(", y1: ")
  logger.write_object(self.y1)
  logger.write_string(", stops: ")
  logger.write_string(color_stops_to_string(self.stops))
  logger.write_string(" }")
}

///|
/// Copies `stops` and sorts the copy ascending by `offset` via insertion
/// sort. Stops arrays are small (typically 2-5 entries) so O(n²) is fine.
fn sorted_stops(stops : Array[ColorStop]) -> Array[ColorStop] {
  let out : Array[ColorStop] = []
  for s in stops {
    out.push(s)
  }
  let n = out.length()
  for i in 1.. 0 && out[j - 1].offset > cur.offset {
      out[j] = out[j - 1]
      j = j - 1
    }
    out[j] = cur
  }
  out
}

///|
pub fn LinearGradient::new(
  x0 : Double,
  y0 : Double,
  x1 : Double,
  y1 : Double,
  stops~ : Array[ColorStop],
) -> LinearGradient {
  { x0, y0, x1, y1, stops: sorted_stops(stops) }
}

///|
/// Radial gradient from circle (x0, y0, r0) to circle (x1, y1, r1) in user
/// space. Matches HTML Canvas `createRadialGradient`.
pub struct RadialGradient {
  x0 : Double
  y0 : Double
  r0 : Double
  x1 : Double
  y1 : Double
  r1 : Double
  stops : Array[ColorStop]
} derive(Eq)

///|
pub impl Show for RadialGradient with fn output(self, logger) {
  logger.write_string("RadialGradient { x0: ")
  logger.write_object(self.x0)
  logger.write_string(", y0: ")
  logger.write_object(self.y0)
  logger.write_string(", r0: ")
  logger.write_object(self.r0)
  logger.write_string(", x1: ")
  logger.write_object(self.x1)
  logger.write_string(", y1: ")
  logger.write_object(self.y1)
  logger.write_string(", r1: ")
  logger.write_object(self.r1)
  logger.write_string(", stops: ")
  logger.write_string(color_stops_to_string(self.stops))
  logger.write_string(" }")
}

///|
pub fn RadialGradient::new(
  x0 : Double,
  y0 : Double,
  r0 : Double,
  x1 : Double,
  y1 : Double,
  r1 : Double,
  stops~ : Array[ColorStop],
) -> RadialGradient {
  { x0, y0, r0, x1, y1, r1, stops: sorted_stops(stops) }
}

///|
/// A draw-time fill source — solid color or gradient.
pub enum FillStyle {
  Solid(Color)
  Linear(LinearGradient)
  Radial(RadialGradient)
} derive(Eq)

///|
pub impl Show for FillStyle with fn output(self, logger) {
  match self {
    Solid(c) => {
      logger.write_string("Solid(")
      c.output(logger)
      logger.write_string(")")
    }
    Linear(g) => {
      logger.write_string("Linear(")
      g.output(logger)
      logger.write_string(")")
    }
    Radial(g) => {
      logger.write_string("Radial(")
      g.output(logger)
      logger.write_string(")")
    }
  }
}

///|
pub fn FillStyle::solid(c : Color) -> FillStyle {
  FillStyle::Solid(c)
}

///|
pub fn FillStyle::linear(g : LinearGradient) -> FillStyle {
  FillStyle::Linear(g)
}

///|
pub fn FillStyle::radial(g : RadialGradient) -> FillStyle {
  FillStyle::Radial(g)
}

///|
/// Interpolate a color from a sorted, non-empty `stops` array at position
/// `t`. Values of `t` outside `[stops[0].offset, stops[last].offset]` clamp
/// to the first/last color. Interpolation is sRGB component-wise.
pub fn interp_stops(stops : Array[ColorStop], t : Double) -> Color {
  if stops.length() == 1 {
    return stops[0].color
  }
  let first = stops[0]
  let last = stops[stops.length() - 1]
  if t <= first.offset {
    return first.color
  }
  if t >= last.offset {
    return last.color
  }
  for i in 0..<(stops.length() - 1) {
    let lo = stops[i]
    let hi = stops[i + 1]
    if t >= lo.offset && t <= hi.offset {
      let span = hi.offset - lo.offset
      let u = if span == 0.0 { 0.0 } else { (t - lo.offset) / span }
      return {
        r: lerp_u8(lo.color.r, hi.color.r, u),
        g: lerp_u8(lo.color.g, hi.color.g, u),
        b: lerp_u8(lo.color.b, hi.color.b, u),
        a: lo.color.a + (hi.color.a - lo.color.a) * u,
      }
    }
  }
  last.color
}

///|
/// sRGB-space lerp between two channel values (0..=255) rounded to nearest.
fn lerp_u8(a : Int, b : Int, u : Double) -> Int {
  let af = a.to_double()
  let bf = b.to_double()
  (af * (1.0 - u) + bf * u + 0.5).to_int()
}

///|
/// Sample a linear gradient at user-space `(u, v)`.
/// The gradient parameter `t` is the projection of `(u - x0, v - y0)` onto
/// the axis `(x1 - x0, y1 - y0)`, normalized by the axis squared length.
/// Values of `t` outside `[0, 1]` clamp to the endpoint colors.
pub fn sample_linear(g : LinearGradient, u : Double, v : Double) -> Color {
  let dx = g.x1 - g.x0
  let dy = g.y1 - g.y0
  let len_sq = dx * dx + dy * dy
  if len_sq == 0.0 {
    return g.stops[0].color
  }
  let t_raw = ((u - g.x0) * dx + (v - g.y0) * dy) / len_sq
  let t = if t_raw < 0.0 { 0.0 } else if t_raw > 1.0 { 1.0 } else { t_raw }
  interp_stops(g.stops, t)
}

///|
/// Sample a 2-circle radial gradient at user-space `(u, v)`. Solves the
/// quadratic for the parameter `t` such that `(u, v)` lies on the circle
/// `P(t) = (1-t) * C0 + t * C1` with radius `r0 + t * dr`. Picks the larger
/// valid root (outer circle contribution). Out-of-range `t` clamps to the
/// endpoint colors. Returns `Color::transparent()` when no valid root exists
/// (the pixel is outside the gradient's drawable region).
pub fn sample_radial(g : RadialGradient, u : Double, v : Double) -> Color {
  let dx = g.x1 - g.x0
  let dy = g.y1 - g.y0
  let dr = g.r1 - g.r0
  let fx = u - g.x0
  let fy = v - g.y0
  let a = dx * dx + dy * dy - dr * dr
  let b = -2.0 * (fx * dx + fy * dy + g.r0 * dr)
  let c = fx * fx + fy * fy - g.r0 * g.r0
  // Degenerate: the two circles coincide in position (a == 0) -> linear in t.
  let abs_a = if a < 0.0 { -a } else { a }
  if abs_a < 1.0e-12 {
    let abs_b = if b < 0.0 { -b } else { b }
    if abs_b < 1.0e-12 {
      return Color::transparent()
    }
    let t = -c / b
    if t < 0.0 || g.r0 + t * dr < 0.0 {
      return Color::transparent()
    }
    let t_clamped = if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
    return interp_stops(g.stops, t_clamped)
  }
  let discr = b * b - 4.0 * a * c
  if discr < 0.0 {
    return Color::transparent()
  }
  let sq = discr.sqrt()
  let t_plus = (-b + sq) / (2.0 * a)
  let t_minus = (-b - sq) / (2.0 * a)
  // Prefer the larger root whose circle radius is non-negative.
  let t_final = if g.r0 + t_plus * dr >= 0.0 {
    t_plus
  } else if g.r0 + t_minus * dr >= 0.0 {
    t_minus
  } else {
    return Color::transparent()
  }
  let t_clamped = if t_final < 0.0 {
    0.0
  } else if t_final > 1.0 {
    1.0
  } else {
    t_final
  }
  interp_stops(g.stops, t_clamped)
}