///|
pub(all) struct Frame {
  time_ms : Double
  delta_ms : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) enum AnimationStatus {
  Idle
  Running
  Completed
} derive(Eq, Debug, ToJson)

///|
pub(all) enum Easing {
  Linear
  EaseIn
  EaseOut
  EaseInOut
} derive(Eq, Debug, ToJson)

///|
priv struct Tween {
  from : Double
  to : Double
  easing : Easing
}

///|
pub struct TransitionStyle {
  opacity : Double
  offset : Point
  scale : Point
  foreground : Color?
} derive(Eq, Debug, ToJson)

///|
pub struct TransitionSpec {
  from : TransitionStyle
  to : TransitionStyle
  duration_ms : Double
  easing : Easing
} derive(Eq, Debug, ToJson)

///|
pub struct TransitionSample {
  progress : Double
  style : TransitionStyle
  present : Bool
} derive(Eq, Debug, ToJson)

///|
pub fn Frame::new(time_ms~ : Double, delta_ms~ : Double) -> Frame {
  { time_ms, delta_ms }
}

///|
pub fn Easing::sample(self : Easing, progress : Double) -> Double {
  let t = clamp_double(progress, 0.0, 1.0)
  match self {
    Linear => t
    EaseIn => t * t
    EaseOut => 1.0 - (1.0 - t) * (1.0 - t)
    EaseInOut =>
      if t < 0.5 {
        2.0 * t * t
      } else {
        1.0 - (-2.0 * t + 2.0) * (-2.0 * t + 2.0) / 2.0
      }
  }
}

///|
fn Tween::new(
  from~ : Double,
  to~ : Double,
  easing? : Easing = Easing::Linear,
) -> Tween {
  { from, to, easing }
}

///|
fn Tween::value(self : Tween, progress : Double) -> Double {
  self.from + (self.to - self.from) * self.easing.sample(progress)
}

///|
pub fn animated_value(
  from~ : Double,
  to~ : Double,
  progress~ : Double,
  easing? : Easing = Easing::EaseInOut,
  reduced_motion? : Bool = false,
) -> Double {
  Tween::new(from~, to~, easing~).value(
    if reduced_motion {
      1.0
    } else {
      progress
    },
  )
}

///|
pub fn animated_point(
  from~ : Point,
  to~ : Point,
  progress~ : Double,
  easing? : Easing = Easing::EaseInOut,
  reduced_motion? : Bool = false,
) -> Point {
  let sampled = easing.sample(if reduced_motion { 1.0 } else { progress })
  Point::new(
    x=from.x + (to.x - from.x) * sampled,
    y=from.y + (to.y - from.y) * sampled,
  )
}

///|
pub fn animated_color(
  from~ : Color,
  to~ : Color,
  progress~ : Double,
  easing? : Easing = Easing::EaseInOut,
  reduced_motion? : Bool = false,
) -> Color {
  let sampled = easing.sample(if reduced_motion { 1.0 } else { progress })
  // Interpolate in premultiplied-alpha space. Straight RGBA lerp between a
  // transparent color (e.g. `rgba(0,0,0,0)`) and an opaque light tint (e.g.
  // `surface_variant`) walks the RGB toward the tint at half alpha, producing
  // a mid-transition dark gray that flashes against the surface — "light →
  // dark → light". Premultiplying keeps the visible hue at the destination
  // throughout and only ramps coverage, so a transparent→opaque-tint fade
  // stays the tint's color the whole way. When both endpoints are opaque this
  // reduces to the same lerp as before, so opaque→opaque transitions are
  // unchanged.
  let from_pa = from.r * from.a
  let from_pg = from.g * from.a
  let from_pb = from.b * from.a
  let to_pa = to.r * to.a
  let to_pg = to.g * to.a
  let to_pb = to.b * to.a
  let pa = from_pa + (to_pa - from_pa) * sampled
  let pg = from_pg + (to_pg - from_pg) * sampled
  let pb = from_pb + (to_pb - from_pb) * sampled
  let a = from.a + (to.a - from.a) * sampled
  // Un-premultiply back to straight RGBA for the renderer. A fully transparent
  // result has no defined hue; carry the destination RGB so a later increase
  // in alpha doesn't resurrect a stray color.
  if a <= 0.0 {
    Color::rgba(r=to.r, g=to.g, b=to.b, a=0.0)
  } else {
    Color::rgba(r=pa / a, g=pg / a, b=pb / a, a~)
  }
}

///|
pub fn TransitionStyle::identity() -> TransitionStyle {
  {
    opacity: 1.0,
    offset: Point::new(x=0.0, y=0.0),
    scale: Point::new(x=1.0, y=1.0),
    foreground: None,
  }
}

///|
pub fn TransitionStyle::new(
  opacity? : Double = 1.0,
  offset? : Point = Point::new(x=0.0, y=0.0),
  scale? : Point = Point::new(x=1.0, y=1.0),
  foreground? : Color? = None,
) -> TransitionStyle {
  { opacity, offset, scale, foreground }
}

///|
pub fn TransitionStyle::with_opacity(
  self : TransitionStyle,
  opacity : Double,
) -> TransitionStyle {
  { ..self, opacity, }
}

///|
pub fn TransitionStyle::with_offset(
  self : TransitionStyle,
  dx? : Double = self.offset.x,
  dy? : Double = self.offset.y,
) -> TransitionStyle {
  { ..self, offset: Point::new(x=dx, y=dy) }
}

///|
pub fn TransitionStyle::with_scale(
  self : TransitionStyle,
  sx? : Double = self.scale.x,
  sy? : Double = self.scale.y,
) -> TransitionStyle {
  { ..self, scale: Point::new(x=sx, y=sy) }
}

///|
pub fn TransitionStyle::with_foreground(
  self : TransitionStyle,
  color : Color,
) -> TransitionStyle {
  { ..self, foreground: Some(color) }
}

///|
pub fn TransitionStyle::interpolate(
  self : TransitionStyle,
  to : TransitionStyle,
  progress~ : Double,
  easing? : Easing = Easing::EaseInOut,
  reduced_motion? : Bool = false,
) -> TransitionStyle {
  {
    opacity: animated_value(
      from=self.opacity,
      to=to.opacity,
      progress~,
      easing~,
      reduced_motion~,
    ),
    offset: animated_point(
      from=self.offset,
      to=to.offset,
      progress~,
      easing~,
      reduced_motion~,
    ),
    scale: animated_point(
      from=self.scale,
      to=to.scale,
      progress~,
      easing~,
      reduced_motion~,
    ),
    foreground: option_interpolate_color(
      self.foreground,
      to.foreground,
      progress~,
      easing~,
      reduced_motion~,
    ),
  }
}

///|
pub fn TransitionSpec::new(
  from? : TransitionStyle = TransitionStyle::identity(),
  to? : TransitionStyle = TransitionStyle::identity(),
  duration_ms? : Double = 180.0,
  easing? : Easing = Easing::EaseInOut,
) -> TransitionSpec {
  { from, to, duration_ms: max_double(0.0, duration_ms), easing }
}

///|
pub fn TransitionSpec::fade(
  from_opacity? : Double = 0.0,
  to_opacity? : Double = 1.0,
  duration_ms? : Double = 180.0,
  easing? : Easing = Easing::EaseInOut,
) -> TransitionSpec {
  TransitionSpec::new(
    from=TransitionStyle::identity().with_opacity(from_opacity),
    to=TransitionStyle::identity().with_opacity(to_opacity),
    duration_ms~,
    easing~,
  )
}

///|
pub fn TransitionSpec::slide(
  from_offset~ : Point,
  to_offset? : Point = Point::new(x=0.0, y=0.0),
  duration_ms? : Double = 180.0,
  easing? : Easing = Easing::EaseInOut,
) -> TransitionSpec {
  TransitionSpec::new(
    from=TransitionStyle::identity().with_offset(
      dx=from_offset.x,
      dy=from_offset.y,
    ),
    to=TransitionStyle::identity().with_offset(dx=to_offset.x, dy=to_offset.y),
    duration_ms~,
    easing~,
  )
}

///|
pub fn TransitionSpec::scale(
  from_scale? : Double = 0.96,
  to_scale? : Double = 1.0,
  duration_ms? : Double = 180.0,
  easing? : Easing = Easing::EaseInOut,
) -> TransitionSpec {
  TransitionSpec::new(
    from=TransitionStyle::identity().with_scale(sx=from_scale, sy=from_scale),
    to=TransitionStyle::identity().with_scale(sx=to_scale, sy=to_scale),
    duration_ms~,
    easing~,
  )
}

///|
pub fn TransitionSpec::sample(
  self : TransitionSpec,
  progress : Double,
  reduced_motion? : Bool = false,
) -> TransitionSample {
  let resolved = transition_resolved_progress(progress, reduced_motion~)
  {
    progress: resolved,
    style: self.from.interpolate(
      self.to,
      progress=resolved,
      easing=self.easing,
      reduced_motion=false,
    ),
    present: true,
  }
}

///|
pub fn TransitionSpec::sample_elapsed(
  self : TransitionSpec,
  elapsed_ms : Double,
  reduced_motion? : Bool = false,
) -> TransitionSample {
  self.sample(animation_progress(elapsed_ms, self.duration_ms), reduced_motion~)
}

///|
pub fn TransitionSpec::presence(
  self : TransitionSpec,
  present~ : Bool,
  progress~ : Double,
  reduced_motion? : Bool = false,
) -> TransitionSample {
  let resolved = transition_resolved_progress(progress, reduced_motion~)
  let style = if present {
    self.from.interpolate(
      self.to,
      progress=resolved,
      easing=self.easing,
      reduced_motion=false,
    )
  } else {
    self.to.interpolate(
      self.from,
      progress=resolved,
      easing=self.easing,
      reduced_motion=false,
    )
  }
  { progress: resolved, style, present: present || resolved < 1.0 }
}

///|
fn option_interpolate_color(
  from : Color?,
  to : Color?,
  progress~ : Double,
  easing~ : Easing,
  reduced_motion~ : Bool,
) -> Color? {
  match (from, to) {
    (Some(start), Some(end)) =>
      Some(
        animated_color(from=start, to=end, progress~, easing~, reduced_motion~),
      )
    (None, Some(end)) => Some(end)
    (Some(start), None) => Some(start)
    (None, None) => None
  }
}

///|
fn transition_resolved_progress(
  progress : Double,
  reduced_motion~ : Bool,
) -> Double {
  if reduced_motion {
    1.0
  } else {
    clamp_double(progress, 0.0, 1.0)
  }
}

///|
fn animation_progress(elapsed_ms : Double, duration_ms : Double) -> Double {
  if duration_ms <= 0.0 {
    1.0
  } else {
    clamp_double(elapsed_ms / duration_ms, 0.0, 1.0)
  }
}