///|
/// Device-space shader used as a Paint color source.
struct PaintShader {
  kind : PaintShaderKind
  local_from_device : Transform?
  transform_valid : Bool
} derive(Eq, Debug)

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

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

///|
/// One color stop in a gradient shader.
struct PaintGradientStop {
  offset : Double
  color : Color
} derive(Eq, Debug)

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

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

///|
/// Spread mode applied to gradient coordinates outside the unit interval.
pub(all) enum PaintShaderSpread {
  Clamp
  Repeat
  Mirror
  Decal
} derive(Eq, Debug)

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

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

///|
/// Public shader family used for inspection and paint pipeline planning.
pub(all) enum PaintShaderType {
  LinearGradient
  RadialGradient
  SweepGradient
} derive(Eq, Debug)

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

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

///|
priv enum PaintShaderKind {
  LinearGradient(LinearGradientShader)
  LinearGradientStops(LinearGradientStopsShader)
  RadialGradient(RadialGradientShader)
  RadialGradientStops(RadialGradientStopsShader)
  SweepGradient(SweepGradientShader)
  SweepGradientStops(SweepGradientStopsShader)
} derive(Eq, Debug)

///|
priv struct LinearGradientShader {
  x0 : Double
  y0 : Double
  x1 : Double
  y1 : Double
  start_color : Color
  end_color : Color
  spread : PaintShaderSpread
} derive(Eq, Debug)

///|
priv struct LinearGradientStopsShader {
  x0 : Double
  y0 : Double
  x1 : Double
  y1 : Double
  stops : Array[PaintGradientStop]
  spread : PaintShaderSpread
} derive(Eq, Debug)

///|
priv struct RadialGradientShader {
  cx : Double
  cy : Double
  radius : Double
  start_color : Color
  end_color : Color
  spread : PaintShaderSpread
} derive(Eq, Debug)

///|
priv struct RadialGradientStopsShader {
  cx : Double
  cy : Double
  radius : Double
  stops : Array[PaintGradientStop]
  spread : PaintShaderSpread
} derive(Eq, Debug)

///|
priv struct SweepGradientShader {
  cx : Double
  cy : Double
  start_angle : Double
  sweep_angle : Double
  start_color : Color
  end_color : Color
  spread : PaintShaderSpread
} derive(Eq, Debug)

///|
priv struct SweepGradientStopsShader {
  cx : Double
  cy : Double
  start_angle : Double
  sweep_angle : Double
  stops : Array[PaintGradientStop]
  spread : PaintShaderSpread
} derive(Eq, Debug)

///|
/// Create a gradient color stop. Offsets are clamped into 0.0..1.0.
pub fn PaintGradientStop::new(
  offset~ : Double,
  color~ : Color,
) -> PaintGradientStop {
  { offset: normalize_gradient_stop_offset(offset), color, }
}

///|
/// Return this stop's normalized offset.
pub fn PaintGradientStop::get_offset(self : PaintGradientStop) -> Double {
  self.offset
}

///|
/// Return this stop's color.
pub fn PaintGradientStop::get_color(self : PaintGradientStop) -> Color {
  self.color
}

///|
/// Create a device-space linear gradient shader.
pub fn PaintShader::linear_gradient(
  x0~ : Double,
  y0~ : Double,
  x1~ : Double,
  y1~ : Double,
  start_color~ : Color,
  end_color~ : Color,
  spread? : PaintShaderSpread = Clamp,
) -> PaintShader {
  make_paint_shader(
    LinearGradient({ x0, y0, x1, y1, start_color, end_color, spread, }),
  )
}

///|
/// Create a device-space linear gradient shader with multiple color stops.
pub fn PaintShader::linear_gradient_stops(
  x0~ : Double,
  y0~ : Double,
  x1~ : Double,
  y1~ : Double,
  stops~ : Array[PaintGradientStop],
  spread? : PaintShaderSpread = Clamp,
) -> PaintShader {
  make_paint_shader(
    LinearGradientStops({
      x0,
      y0,
      x1,
      y1,
      stops: copy_gradient_stops(stops),
      spread,
    }),
  )
}

///|
/// Create a device-space radial gradient shader.
pub fn PaintShader::radial_gradient(
  cx~ : Double,
  cy~ : Double,
  radius~ : Double,
  start_color~ : Color,
  end_color~ : Color,
  spread? : PaintShaderSpread = Clamp,
) -> PaintShader {
  make_paint_shader(
    RadialGradient({
      cx,
      cy,
      radius: normalize_radial_gradient_radius(radius),
      start_color,
      end_color,
      spread,
    }),
  )
}

///|
/// Create a device-space radial gradient shader with multiple color stops.
pub fn PaintShader::radial_gradient_stops(
  cx~ : Double,
  cy~ : Double,
  radius~ : Double,
  stops~ : Array[PaintGradientStop],
  spread? : PaintShaderSpread = Clamp,
) -> PaintShader {
  make_paint_shader(
    RadialGradientStops({
      cx,
      cy,
      radius: normalize_radial_gradient_radius(radius),
      stops: copy_gradient_stops(stops),
      spread,
    }),
  )
}

///|
/// Create a device-space sweep gradient shader around a center point.
pub fn PaintShader::sweep_gradient(
  cx~ : Double,
  cy~ : Double,
  start_angle~ : Double,
  sweep_angle~ : Double,
  start_color~ : Color,
  end_color~ : Color,
  spread? : PaintShaderSpread = Clamp,
) -> PaintShader {
  make_paint_shader(
    SweepGradient({
      cx,
      cy,
      start_angle: normalize_sweep_gradient_start_angle(start_angle),
      sweep_angle: normalize_sweep_gradient_sweep_angle(sweep_angle),
      start_color,
      end_color,
      spread,
    }),
  )
}

///|
/// Create a device-space sweep gradient shader with multiple color stops.
pub fn PaintShader::sweep_gradient_stops(
  cx~ : Double,
  cy~ : Double,
  start_angle~ : Double,
  sweep_angle~ : Double,
  stops~ : Array[PaintGradientStop],
  spread? : PaintShaderSpread = Clamp,
) -> PaintShader {
  make_paint_shader(
    SweepGradientStops({
      cx,
      cy,
      start_angle: normalize_sweep_gradient_start_angle(start_angle),
      sweep_angle: normalize_sweep_gradient_sweep_angle(sweep_angle),
      stops: copy_gradient_stops(stops),
      spread,
    }),
  )
}

///|
/// Return the shader family.
pub fn PaintShader::get_type(self : PaintShader) -> PaintShaderType {
  match self.kind {
    LinearGradient(_) | LinearGradientStops(_) => LinearGradient
    RadialGradient(_) | RadialGradientStops(_) => RadialGradient
    SweepGradient(_) | SweepGradientStops(_) => SweepGradient
  }
}

///|
/// Return the gradient spread mode.
pub fn PaintShader::get_spread(self : PaintShader) -> PaintShaderSpread {
  match self.kind {
    LinearGradient(shader) => shader.spread
    LinearGradientStops(shader) => shader.spread
    RadialGradient(shader) => shader.spread
    RadialGradientStops(shader) => shader.spread
    SweepGradient(shader) => shader.spread
    SweepGradientStops(shader) => shader.spread
  }
}

///|
/// Return the number of color stops used by this gradient. Two-color gradient
/// constructors report their implicit start/end stop pair.
pub fn PaintShader::get_gradient_stop_count(self : PaintShader) -> Int {
  match self.kind {
    LinearGradient(_) | RadialGradient(_) | SweepGradient(_) => 2
    LinearGradientStops(shader) => shader.stops.length()
    RadialGradientStops(shader) => shader.stops.length()
    SweepGradientStops(shader) => shader.stops.length()
  }
}

///|
/// Return a copied gradient stop list. Two-color gradient constructors expose
/// their implicit start/end pair as stops at 0.0 and 1.0.
pub fn PaintShader::get_gradient_stops(
  self : PaintShader,
) -> Array[PaintGradientStop] {
  match self.kind {
    LinearGradient(shader) =>
      two_gradient_stops(shader.start_color, shader.end_color)
    LinearGradientStops(shader) => copy_gradient_stops(shader.stops)
    RadialGradient(shader) =>
      two_gradient_stops(shader.start_color, shader.end_color)
    RadialGradientStops(shader) => copy_gradient_stops(shader.stops)
    SweepGradient(shader) =>
      two_gradient_stops(shader.start_color, shader.end_color)
    SweepGradientStops(shader) => copy_gradient_stops(shader.stops)
  }
}

///|
/// Return a copied gradient stop at an index.
pub fn PaintShader::get_gradient_stop(
  self : PaintShader,
  index : Int,
) -> PaintGradientStop? {
  let stops = self.get_gradient_stops()
  guard index >= 0 && index < stops.length() else { return None }
  Some(stops[index])
}

///|
/// Return local linear-gradient endpoints, if this is a linear gradient.
pub fn PaintShader::get_linear_gradient_points(
  self : PaintShader,
) -> (Point, Point)? {
  match self.kind {
    LinearGradient(shader) =>
      Some(
        (
          Point::new(x=shader.x0, y=shader.y0),
          Point::new(x=shader.x1, y=shader.y1),
        ),
      )
    LinearGradientStops(shader) =>
      Some(
        (
          Point::new(x=shader.x0, y=shader.y0),
          Point::new(x=shader.x1, y=shader.y1),
        ),
      )
    _ => None
  }
}

///|
/// Return local radial-gradient center and radius, if this is a radial gradient.
pub fn PaintShader::get_radial_gradient_circle(
  self : PaintShader,
) -> (Point, Double)? {
  match self.kind {
    RadialGradient(shader) =>
      Some((Point::new(x=shader.cx, y=shader.cy), shader.radius))
    RadialGradientStops(shader) =>
      Some((Point::new(x=shader.cx, y=shader.cy), shader.radius))
    _ => None
  }
}

///|
/// Return local sweep-gradient center and angles, if this is a sweep gradient.
pub fn PaintShader::get_sweep_gradient_geometry(
  self : PaintShader,
) -> (Point, Double, Double)? {
  match self.kind {
    SweepGradient(shader) =>
      Some(
        (
          Point::new(x=shader.cx, y=shader.cy),
          shader.start_angle,
          shader.sweep_angle,
        ),
      )
    SweepGradientStops(shader) =>
      Some(
        (
          Point::new(x=shader.cx, y=shader.cy),
          shader.start_angle,
          shader.sweep_angle,
        ),
      )
    _ => None
  }
}

///|
/// Return whether this shader has a local-to-device transform attached.
pub fn PaintShader::has_local_transform(self : PaintShader) -> Bool {
  self.local_from_device is Some(_)
}

///|
/// Return the local-from-device transform used before sampling, if any.
pub fn PaintShader::get_local_from_device_transform(
  self : PaintShader,
) -> Transform? {
  self.local_from_device
}

///|
/// Return whether this shader can produce non-transparent samples. Singular
/// transforms mark a shader as unsampleable.
pub fn PaintShader::is_sampleable(self : PaintShader) -> Bool {
  self.transform_valid
}

///|
/// Return a shader sampled in local coordinates mapped to device space by the
/// given transform. Singular transforms make the shader sample transparent.
pub fn PaintShader::transformed(
  self : PaintShader,
  transform : Transform,
) -> PaintShader {
  guard self.transform_valid else {
    return { kind: self.kind, local_from_device: None, transform_valid: false, }
  }
  match transform.inverse() {
    Some(inverse) => {
      let local_from_device = non_identity_shader_transform(
        match self.local_from_device {
          Some(existing) => existing.multiply(inverse)
          None => inverse
        },
      )
      { kind: self.kind, local_from_device, transform_valid: true, }
    }
    None =>
      { kind: self.kind, local_from_device: None, transform_valid: false, }
  }
}

///|
fn non_identity_shader_transform(transform : Transform) -> Transform? {
  if transform == Transform::identity() {
    None
  } else {
    Some(transform)
  }
}

///|
/// Sample this shader at a device-space point.
pub fn PaintShader::sample(self : PaintShader, x : Double, y : Double) -> Color {
  guard self.transform_valid else { return Color::transparent() }
  let (local_x, local_y) = match self.local_from_device {
    Some(transform) => transform.apply(x, y)
    None => (x, y)
  }
  self.sample_local(local_x, local_y)
}

///|
fn make_paint_shader(kind : PaintShaderKind) -> PaintShader {
  { kind, local_from_device: None, transform_valid: true, }
}

///|
fn PaintShader::sample_local(
  self : PaintShader,
  x : Double,
  y : Double,
) -> Color {
  match self.kind {
    LinearGradient(shader) => shader.sample(x, y)
    LinearGradientStops(shader) => shader.sample(x, y)
    RadialGradient(shader) => shader.sample(x, y)
    RadialGradientStops(shader) => shader.sample(x, y)
    SweepGradient(shader) => shader.sample(x, y)
    SweepGradientStops(shader) => shader.sample(x, y)
  }
}

///|
fn LinearGradientShader::sample(
  self : LinearGradientShader,
  x : Double,
  y : Double,
) -> Color {
  lerp_gradient_color(
    self.start_color,
    self.end_color,
    linear_gradient_t(self.x0, self.y0, self.x1, self.y1, x, y, self.spread),
  )
}

///|
fn LinearGradientStopsShader::sample(
  self : LinearGradientStopsShader,
  x : Double,
  y : Double,
) -> Color {
  sample_gradient_stops(
    self.stops,
    linear_gradient_t(self.x0, self.y0, self.x1, self.y1, x, y, self.spread),
  )
}

///|
fn RadialGradientShader::sample(
  self : RadialGradientShader,
  x : Double,
  y : Double,
) -> Color {
  let t = radial_gradient_t(self.cx, self.cy, self.radius, x, y, self.spread)
  lerp_gradient_color(self.start_color, self.end_color, t)
}

///|
fn RadialGradientStopsShader::sample(
  self : RadialGradientStopsShader,
  x : Double,
  y : Double,
) -> Color {
  sample_gradient_stops(
    self.stops,
    radial_gradient_t(self.cx, self.cy, self.radius, x, y, self.spread),
  )
}

///|
fn SweepGradientShader::sample(
  self : SweepGradientShader,
  x : Double,
  y : Double,
) -> Color {
  let t = sweep_gradient_t(
    self.cx,
    self.cy,
    self.start_angle,
    self.sweep_angle,
    x,
    y,
    self.spread,
  )
  lerp_gradient_color(self.start_color, self.end_color, t)
}

///|
fn SweepGradientStopsShader::sample(
  self : SweepGradientStopsShader,
  x : Double,
  y : Double,
) -> Color {
  sample_gradient_stops(
    self.stops,
    sweep_gradient_t(
      self.cx,
      self.cy,
      self.start_angle,
      self.sweep_angle,
      x,
      y,
      self.spread,
    ),
  )
}

///|
fn clamp_unit(value : Double) -> Double {
  guard value == value else { return 0.0 }
  guard value - value == value - value else {
    if value < 0.0 {
      0.0
    } else {
      1.0
    }
  }
  value.max(0.0).min(1.0)
}

///|
fn sweep_relative_angle(
  angle : Double,
  start_angle : Double,
  sweep_angle : Double,
) -> Double {
  if sweep_angle >= 0.0 {
    normalize_positive_angle(angle - start_angle)
  } else {
    -normalize_positive_angle(start_angle - angle)
  }
}

///|
fn normalize_positive_angle(value : Double) -> Double {
  let two_pi = @math.PI * 2.0
  guard two_pi > 0.0 else { return 0.0 }
  let turns = value / two_pi
  (turns - @math.floor(turns)) * two_pi
}

///|
fn normalize_radial_gradient_radius(radius : Double) -> Double {
  if radius > 0.000000001 {
    radius
  } else {
    0.0
  }
}

///|
fn normalize_sweep_gradient_start_angle(angle : Double) -> Double {
  guard angle == angle else { return 0.0 }
  angle
}

///|
fn normalize_sweep_gradient_sweep_angle(angle : Double) -> Double {
  guard angle == angle else { return 0.0 }
  if angle.abs() > 0.000000001 {
    angle
  } else {
    0.0
  }
}

///|
fn apply_shader_spread(value : Double, spread : PaintShaderSpread) -> Double {
  guard value == value else { return 0.0 }
  guard value - value == value - value else {
    match spread {
      Clamp => if value < 0.0 { 0.0 } else { 1.0 }
      Repeat | Mirror => 0.0
      Decal => -1.0
    }
  }
  match spread {
    Clamp => clamp_unit(value)
    Repeat => repeat_unit(value)
    Mirror => mirror_unit(value)
    Decal => if value >= 0.0 && value <= 1.0 { value } else { -1.0 }
  }
}

///|
fn linear_gradient_t(
  x0 : Double,
  y0 : Double,
  x1 : Double,
  y1 : Double,
  x : Double,
  y : Double,
  spread : PaintShaderSpread,
) -> Double {
  let dx = x1 - x0
  let dy = y1 - y0
  let length_sq = dx * dx + dy * dy
  if length_sq > 0.000000001 {
    apply_shader_spread(((x - x0) * dx + (y - y0) * dy) / length_sq, spread)
  } else {
    0.0
  }
}

///|
fn radial_gradient_t(
  cx : Double,
  cy : Double,
  radius : Double,
  x : Double,
  y : Double,
  spread : PaintShaderSpread,
) -> Double {
  let dx = x - cx
  let dy = y - cy
  if radius > 0.000000001 {
    apply_shader_spread((dx * dx + dy * dy).sqrt() / radius, spread)
  } else {
    0.0
  }
}

///|
fn sweep_gradient_t(
  cx : Double,
  cy : Double,
  start_angle : Double,
  sweep_angle : Double,
  x : Double,
  y : Double,
  spread : PaintShaderSpread,
) -> Double {
  let start = normalize_sweep_gradient_start_angle(start_angle)
  let sweep = normalize_sweep_gradient_sweep_angle(sweep_angle)
  let angle = @math.atan2(y - cy, x - cx)
  if sweep != 0.0 {
    apply_shader_spread(
      sweep_relative_angle(angle, start, sweep) / sweep,
      spread,
    )
  } else {
    0.0
  }
}

///|
fn copy_gradient_stops(
  stops : Array[PaintGradientStop],
) -> Array[PaintGradientStop] {
  let copied = []
  for stop in stops {
    copied.push(PaintGradientStop::new(offset=stop.offset, color=stop.color))
  }
  copied
}

///|
fn two_gradient_stops(
  start_color : Color,
  end_color : Color,
) -> Array[PaintGradientStop] {
  [
    PaintGradientStop::new(offset=0.0, color=start_color),
    PaintGradientStop::new(offset=1.0, color=end_color),
  ]
}

///|
fn normalize_gradient_stop_offset(offset : Double) -> Double {
  guard offset == offset else { return 0.0 }
  clamp_unit(offset)
}

///|
fn sample_gradient_stops(stops : Array[PaintGradientStop], t : Double) -> Color {
  guard stops.length() > 0 else { return Color::transparent() }
  guard t >= 0.0 else { return Color::transparent() }
  let value = clamp_unit(t)
  let mut lower : PaintGradientStop? = None
  let mut upper : PaintGradientStop? = None
  for stop in stops {
    let offset = stop.offset
    if offset <= value {
      lower = match lower {
        Some(existing) if existing.offset > offset => Some(existing)
        _ => Some(stop)
      }
    }
    if offset >= value {
      upper = match upper {
        Some(existing) if existing.offset <= offset => Some(existing)
        _ => Some(stop)
      }
    }
  }
  match (lower, upper) {
    (Some(start), Some(end)) =>
      if start.offset == end.offset {
        end.color
      } else {
        lerp_color(
          start.color,
          end.color,
          (value - start.offset) / (end.offset - start.offset),
        )
      }
    (Some(start), None) => start.color
    (None, Some(end)) => end.color
    (None, None) => Color::transparent()
  }
}

///|
fn lerp_gradient_color(start : Color, end : Color, t : Double) -> Color {
  guard t >= 0.0 else { return Color::transparent() }
  lerp_color(start, end, t)
}

///|
fn lerp_color(start : Color, end : Color, t : Double) -> Color {
  Color::new(
    r=lerp_byte(start.r, end.r, t),
    g=lerp_byte(start.g, end.g, t),
    b=lerp_byte(start.b, end.b, t),
    a=lerp_byte(start.a, end.a, t),
  )
}

///|
fn repeat_unit(value : Double) -> Double {
  value - @math.floor(value)
}

///|
fn mirror_unit(value : Double) -> Double {
  let repeated = value - @math.floor(value / 2.0) * 2.0
  if repeated <= 1.0 {
    repeated
  } else {
    2.0 - repeated
  }
}

///|
fn lerp_byte(start : Byte, end : Byte, t : Double) -> Byte {
  let start_value = start.to_int().to_double()
  let end_value = end.to_int().to_double()
  clamp_byte((start_value + (end_value - start_value) * t + 0.5).to_int())
}