///|
/// Playback behavior for a procedural animation timeline.
pub(all) enum TimelineMode {
  Clamp
  Loop
  PingPong
  Continue
} derive(Eq, Debug, ToJson)

///|
/// A normalized time range used by clips and tracks.
pub(all) struct TimelineRange {
  start : Double
  end : Double
} derive(Eq, Debug, ToJson)

///|
pub fn TimelineRange::new(start : Double, end : Double) -> TimelineRange {
  { start, end: end.max(start) }
}

///|
pub fn TimelineRange::duration(self : TimelineRange) -> Double {
  self.end - self.start
}

///|
pub fn TimelineRange::at(self : TimelineRange, progress : Double) -> Double {
  self.start + self.duration() * progress.max(0.0).min(1.0)
}

///|
/// A scalar animation track with sorted keyframes.
pub(all) struct ScalarTrack {
  name : String
  frames : Array[Keyframe[Double]]
  easing : Easing
} derive(Debug)

///|
pub fn ScalarTrack::new(
  name : String,
  easing? : Easing = Smooth,
) -> ScalarTrack {
  { name, frames: [], easing }
}

///|
pub fn ScalarTrack::add(
  self : ScalarTrack,
  time : Double,
  value : Double,
) -> ScalarTrack {
  self.frames.push(keyframe(time.max(0.0), value))
  sort_scalar_frames(self.frames)
  self
}

///|
fn sort_scalar_frames(frames : Array[Keyframe[Double]]) -> Unit {
  for i in 0.. Double {
  if self.frames.length() == 0 {
    0.0
  } else {
    self.frames[self.frames.length() - 1].time
  }
}

///|
pub fn ScalarTrack::sample(self : ScalarTrack, time : Double) -> Double {
  frames_for_double(self.frames, time).unwrap_or(0.0)
}

///|
pub fn ScalarTrack::sample_eased(self : ScalarTrack, time : Double) -> Double {
  if self.frames.length() < 2 {
    self.sample(time)
  } else if time <= self.frames[0].time {
    self.frames[0].value
  } else if time >= self.duration() {
    self.frames[self.frames.length() - 1].value
  } else {
    let mut result = self.frames[0].value
    for index in 0..<(self.frames.length() - 1) {
      let left = self.frames[index]
      let right = self.frames[index + 1]
      if time >= left.time && time <= right.time {
        let amount = (time - left.time) / (right.time - left.time).max(0.000001)
        result = animate_double(
          left.value,
          right.value,
          ease(self.easing, amount),
        )
      }
    }
    result
  }
}

///|
pub fn ScalarTrack::map(
  self : ScalarTrack,
  transform : (Double) -> Double,
) -> ScalarTrack {
  let result = ScalarTrack::new(self.name, easing=self.easing)
  for frame in self.frames {
    let _ = result.add(frame.time, transform(frame.value))
  }
  result
}

///|
pub fn ScalarTrack::scale(self : ScalarTrack, factor : Double) -> ScalarTrack {
  self.map(fn(value) { value * factor })
}

///|
pub fn ScalarTrack::offset(self : ScalarTrack, amount : Double) -> ScalarTrack {
  self.map(fn(value) { value + amount })
}

///|
/// A vector animation track for moving and deforming motifs.
pub(all) struct Vec2Track {
  name : String
  frames : Array[Keyframe[Vec2]]
  easing : Easing
} derive(Debug)

///|
pub fn Vec2Track::new(name : String, easing? : Easing = Smooth) -> Vec2Track {
  { name, frames: [], easing }
}

///|
pub fn Vec2Track::add(
  self : Vec2Track,
  time : Double,
  value : Vec2,
) -> Vec2Track {
  self.frames.push(keyframe(time.max(0.0), value))
  sort_vec2_frames(self.frames)
  self
}

///|
fn sort_vec2_frames(frames : Array[Keyframe[Vec2]]) -> Unit {
  for i in 0.. Double {
  if self.frames.length() == 0 {
    0.0
  } else {
    self.frames[self.frames.length() - 1].time
  }
}

///|
pub fn Vec2Track::sample(self : Vec2Track, time : Double) -> Vec2 {
  if self.frames.length() == 0 {
    Vec2::zero()
  } else if time <= self.frames[0].time {
    self.frames[0].value
  } else if time >= self.duration() {
    self.frames[self.frames.length() - 1].value
  } else {
    let mut result = self.frames[0].value
    for index in 0..<(self.frames.length() - 1) {
      let left = self.frames[index]
      let right = self.frames[index + 1]
      if time >= left.time && time <= right.time {
        let amount = (time - left.time) / (right.time - left.time).max(0.000001)
        result = animate_vec2(
          left.value,
          right.value,
          ease(self.easing, amount),
        )
      }
    }
    result
  }
}

///|
pub fn Vec2Track::map(
  self : Vec2Track,
  transform : (Vec2) -> Vec2,
) -> Vec2Track {
  let result = Vec2Track::new(self.name, easing=self.easing)
  for frame in self.frames {
    let _ = result.add(frame.time, transform(frame.value))
  }
  result
}

///|
pub fn Vec2Track::translate(self : Vec2Track, offset : Vec2) -> Vec2Track {
  self.map(fn(value) { value.add(offset) })
}

///|
/// A color animation track for palette and lighting changes.
pub(all) struct ColorTrack {
  name : String
  frames : Array[Keyframe[Color]]
  easing : Easing
} derive(Debug)

///|
pub fn ColorTrack::new(name : String, easing? : Easing = Smooth) -> ColorTrack {
  { name, frames: [], easing }
}

///|
pub fn ColorTrack::add(
  self : ColorTrack,
  time : Double,
  value : Color,
) -> ColorTrack {
  self.frames.push(keyframe(time.max(0.0), value))
  sort_color_frames(self.frames)
  self
}

///|
fn sort_color_frames(frames : Array[Keyframe[Color]]) -> Unit {
  for i in 0.. Double {
  if self.frames.length() == 0 {
    0.0
  } else {
    self.frames[self.frames.length() - 1].time
  }
}

///|
pub fn ColorTrack::sample(self : ColorTrack, time : Double) -> Color {
  if self.frames.length() == 0 {
    Color::rgba(0.0, 0.0, 0.0, a=0.0)
  } else if time <= self.frames[0].time {
    self.frames[0].value
  } else if time >= self.duration() {
    self.frames[self.frames.length() - 1].value
  } else {
    let mut result = self.frames[0].value
    for index in 0..<(self.frames.length() - 1) {
      let left = self.frames[index]
      let right = self.frames[index + 1]
      if time >= left.time && time <= right.time {
        let amount = (time - left.time) / (right.time - left.time).max(0.000001)
        result = animate_color(
          left.value,
          right.value,
          ease(self.easing, amount),
        )
      }
    }
    result
  }
}

///|
pub fn ColorTrack::map(
  self : ColorTrack,
  transform : (Color) -> Color,
) -> ColorTrack {
  let result = ColorTrack::new(self.name, easing=self.easing)
  for frame in self.frames {
    let _ = result.add(frame.time, transform(frame.value))
  }
  result
}

///|
pub fn ColorTrack::with_alpha(self : ColorTrack, alpha : Double) -> ColorTrack {
  self.map(fn(color) { color.with_alpha(alpha) })
}

///|
/// A named event marker in a timeline.
pub(all) struct TimelineMarker {
  time : Double
  name : String
  payload : String
} derive(Eq, Debug, ToJson)

///|
/// A complete set of parameter tracks with playback settings.
pub(all) struct Timeline {
  range : TimelineRange
  mode : TimelineMode
  scalar_tracks : Array[ScalarTrack]
  vector_tracks : Array[Vec2Track]
  color_tracks : Array[ColorTrack]
  markers : Array[TimelineMarker]
} derive(Debug)

///|
pub fn Timeline::new(duration : Double) -> Timeline {
  {
    range: TimelineRange::new(0.0, duration.max(0.0)),
    mode: Clamp,
    scalar_tracks: [],
    vector_tracks: [],
    color_tracks: [],
    markers: [],
  }
}

///|
pub fn Timeline::with_range(self : Timeline, range : TimelineRange) -> Timeline {
  { ..self, range, }
}

///|
pub fn Timeline::with_mode(self : Timeline, mode : TimelineMode) -> Timeline {
  { ..self, mode, }
}

///|
pub fn Timeline::add_scalar(self : Timeline, track : ScalarTrack) -> Timeline {
  self.scalar_tracks.push(track)
  self
}

///|
pub fn Timeline::add_vector(self : Timeline, track : Vec2Track) -> Timeline {
  self.vector_tracks.push(track)
  self
}

///|
pub fn Timeline::add_color(self : Timeline, track : ColorTrack) -> Timeline {
  self.color_tracks.push(track)
  self
}

///|
pub fn Timeline::marker(
  self : Timeline,
  time : Double,
  name : String,
  payload : String,
) -> Timeline {
  self.markers.push({ time: time.max(0.0), name, payload })
  self
}

///|
pub fn Timeline::duration(self : Timeline) -> Double {
  let mut duration = self.range.duration()
  for track in self.scalar_tracks {
    duration = duration.max(track.duration())
  }
  for track in self.vector_tracks {
    duration = duration.max(track.duration())
  }
  for track in self.color_tracks {
    duration = duration.max(track.duration())
  }
  duration
}

///|
fn timeline_time(timeline : Timeline, time : Double) -> Double {
  let duration = timeline.duration()
  if duration <= 0.0 {
    0.0
  } else {
    let relative = time - timeline.range.start
    match timeline.mode {
      Clamp => relative.max(0.0).min(duration)
      Continue => relative.max(0.0)
      Loop => {
        let wrapped = relative % duration
        if wrapped < 0.0 {
          wrapped + duration
        } else {
          wrapped
        }
      }
      PingPong => {
        let cycle = duration * 2.0
        let wrapped = relative % cycle
        let positive = if wrapped < 0.0 { wrapped + cycle } else { wrapped }
        if positive <= duration {
          positive
        } else {
          cycle - positive
        }
      }
    }
  }
}

///|
pub fn Timeline::sample_scalar(
  self : Timeline,
  name : String,
  time : Double,
) -> Double? {
  let local_time = timeline_time(self, time)
  for track in self.scalar_tracks {
    if track.name == name {
      return Some(track.sample_eased(local_time))
    }
  }
  None
}

///|
pub fn Timeline::sample_vector(
  self : Timeline,
  name : String,
  time : Double,
) -> Vec2? {
  let local_time = timeline_time(self, time)
  for track in self.vector_tracks {
    if track.name == name {
      return Some(track.sample(local_time))
    }
  }
  None
}

///|
pub fn Timeline::sample_color(
  self : Timeline,
  name : String,
  time : Double,
) -> Color? {
  let local_time = timeline_time(self, time)
  for track in self.color_tracks {
    if track.name == name {
      return Some(track.sample(local_time))
    }
  }
  None
}

///|
pub fn Timeline::markers_at(
  self : Timeline,
  time : Double,
  tolerance : Double,
) -> Array[TimelineMarker] {
  let result = []
  for marker in self.markers {
    if (marker.time - timeline_time(self, time)).abs() <= tolerance.max(0.0) {
      result.push(marker)
    }
  }
  result
}

///|
pub fn Timeline::sample_times(
  self : Timeline,
  frames : Int,
  include_end : Bool,
) -> Array[Double] {
  let result = []
  if frames > 0 {
    let count = if include_end { frames } else { frames.max(1) }
    for index in 0.. TimelineFrame {
  let scalars = []
  let vectors = []
  let colors = []
  for track in self.scalar_tracks {
    scalars.push((track.name, track.sample_eased(timeline_time(self, time))))
  }
  for track in self.vector_tracks {
    vectors.push((track.name, track.sample(timeline_time(self, time))))
  }
  for track in self.color_tracks {
    colors.push((track.name, track.sample(timeline_time(self, time))))
  }
  { time, scalars, vectors, colors }
}

///|
pub fn Timeline::frames(self : Timeline, count : Int) -> Array[TimelineFrame] {
  let result = []
  for time in self.sample_times(count, true) {
    result.push(self.frame(time))
  }
  result
}

///|
/// Interpolates a motif's placement from timeline tracks.
pub fn Motif::animate_motif(
  self : Motif,
  timeline : Timeline,
  time : Double,
) -> Motif {
  let position = timeline.sample_vector("position", time).unwrap_or(self.anchor)
  let scale = timeline.sample_scalar("scale", time).unwrap_or(self.scale)
  let rotation = timeline
    .sample_scalar("rotation", time)
    .unwrap_or(self.rotation)
  let color = timeline
    .sample_color("color", time)
    .unwrap_or(Color::rgba(0.0, 0.0, 0.0, a=0.0))
  let style = if color.a == 0.0 {
    self.style
  } else {
    { ..self.style, fill: color.to_hex() }
  }
  { ..self, style, anchor: position, scale, rotation }
}

///|
/// Generates an array of animated motif states at sampled times.
pub fn animate_motif_frames(
  motif : Motif,
  timeline : Timeline,
  frame_count : Int,
) -> Array[Motif] {
  let result = []
  for time in timeline.sample_times(frame_count, true) {
    result.push(motif.animate_motif(timeline, time))
  }
  result
}

///|
/// Returns an SVG document for one animated motif state.
pub fn animated_motif_document(
  motif : Motif,
  timeline : Timeline,
  time : Double,
  width : Double,
  height : Double,
  background : Color,
) -> SvgDocument {
  let document = SvgDocument::new(width, height)
  document.add(
    SvgElement::SvgPath(
      Path::rect(width, height),
      SvgStyle::new(fill=background.to_hex(), stroke="none"),
    ),
  )
  let state = motif.animate_motif(timeline, time)
  document.add(SvgElement::SvgPath(state.transformed_path(), state.style))
  document
}

///|
/// Builds a timeline from a position and scale animation recipe.
pub fn motif_timeline(
  duration : Double,
  start : Vec2,
  end : Vec2,
  start_scale : Double,
  end_scale : Double,
) -> Timeline {
  Timeline::new(duration)
  .add_vector(Vec2Track::new("position").add(0.0, start).add(duration, end))
  .add_scalar(
    ScalarTrack::new("scale").add(0.0, start_scale).add(duration, end_scale),
  )
}

///|
/// Serializes timeline markers for debugging and build reports.
pub fn timeline_markers_csv(timeline : Timeline) -> String {
  let builder = StringBuilder()
  builder.write_string("time,name,payload\n")
  for marker in timeline.markers {
    builder.write_string("\{marker.time},\{marker.name},\{marker.payload}\n")
  }
  builder.to_string()
}

///|
/// Returns a deterministic summary of tracks and playback duration.
pub fn timeline_report(timeline : Timeline) -> String {
  "duration=\{timeline.duration()},scalars=\{timeline.scalar_tracks.length()},vectors=\{timeline.vector_tracks.length()},colors=\{timeline.color_tracks.length()},markers=\{timeline.markers.length()}"
}