///|
/// Playback policy for a reusable motion clip.
pub(all) enum ClipLoopMode {
  Once
  Repeat
  PingPong
} derive(Eq, Debug)

///|
/// A named, timestamped scalar motion clip with optional marker events.
pub struct MotionClip {
  name : String
  track : ScalarTrack
  markers : MarkerTimeline
  loop_mode : ClipLoopMode
} derive(Debug)

///|
pub fn motion_clip(
  name : String,
  track : ScalarTrack,
  markers? : MarkerTimeline = MarkerTimeline::empty(),
  loop_mode? : ClipLoopMode = Once,
) -> MotionClip raise MotionError {
  if name.is_empty() {
    raise MotionError::InvalidStateName
  }
  { name, track, markers, loop_mode }
}

///|
pub fn MotionClip::name(self : MotionClip) -> String {
  self.name
}

///|
pub fn MotionClip::track(self : MotionClip) -> ScalarTrack {
  self.track
}

///|
pub fn MotionClip::markers(self : MotionClip) -> MarkerTimeline {
  self.markers
}

///|
pub fn MotionClip::loop_mode(self : MotionClip) -> ClipLoopMode {
  self.loop_mode
}

///|
pub fn MotionClip::start_time(self : MotionClip) -> Double {
  self.track.start_time()
}

///|
pub fn MotionClip::end_time(self : MotionClip) -> Double {
  self.track.end_time()
}

///|
pub fn MotionClip::duration(self : MotionClip) -> Double {
  self.track.duration()
}

///|
fn MotionClip::map_time(self : MotionClip, time : Double) -> Double {
  let start = self.start_time()
  let duration = self.duration()
  if duration <= 0.0 || self.loop_mode is Once {
    return time.max(start).min(self.end_time())
  }
  let offset = time - start
  let cycles = @math.floor(offset / duration).to_int()
  let phase = offset - cycles.to_double() * duration
  if self.loop_mode is Repeat {
    start + phase
  } else if cycles % 2 == 0 {
    start + phase
  } else {
    self.end_time() - phase
  }
}

///|
pub fn MotionClip::sample(self : MotionClip, time : Double) -> Double {
  self.track.sample(self.map_time(time))
}

///|
pub fn MotionClip::sample_motion(
  self : MotionClip,
  time : Double,
) -> MotionSample {
  let mapped = self.map_time(time)
  self.track.sample_motion(mapped)
}

///|
pub fn MotionClip::sample_many(
  self : MotionClip,
  count : Int,
) -> Array[SamplePoint] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[SamplePoint] = []
  for index in 0.. Array[MarkerHit] {
  self.markers.between(self.map_time(start), self.map_time(end))
}

///|
pub fn MotionClip::with_loop_mode(
  self : MotionClip,
  loop_mode : ClipLoopMode,
) -> MotionClip {
  { ..self, loop_mode, }
}

///|
pub fn MotionClip::with_markers(
  self : MotionClip,
  markers : MarkerTimeline,
) -> MotionClip {
  { ..self, markers, }
}

///|
pub fn MotionClip::shift(
  self : MotionClip,
  offset : Double,
) -> MotionClip raise MotionError {
  let frames : Array[Keyframe] = []
  for frame in self.track.keyframes() {
    frames.push(keyframe(frame.time + offset, frame.value, curve=frame.curve))
  }
  motion_clip(
    self.name,
    ScalarTrack::new(frames),
    markers=shift_markers(self.markers, offset),
    loop_mode=self.loop_mode,
  )
}

///|
pub fn MotionClip::scale_time(
  self : MotionClip,
  factor : Double,
) -> MotionClip raise MotionError {
  ensure_finite(factor)
  if factor <= 0.0 {
    raise MotionError::InvalidDuration(factor)
  }
  let frames : Array[Keyframe] = []
  for frame in self.track.keyframes() {
    frames.push(keyframe(frame.time * factor, frame.value, curve=frame.curve))
  }
  motion_clip(
    self.name,
    ScalarTrack::new(frames),
    markers=scale_markers(self.markers, factor),
    loop_mode=self.loop_mode,
  )
}

///|
/// A clip placed at a start offset in a larger composition.
pub struct ClipSequenceItem {
  clip : MotionClip
  start : Double
  weight : Double
} derive(Debug)

///|
pub fn sequence_item(
  clip : MotionClip,
  start : Double,
  weight? : Double = 1.0,
) -> ClipSequenceItem raise MotionError {
  ensure_finite(start)
  ensure_finite(weight)
  if weight < 0.0 {
    raise MotionError::InvalidThreshold(weight)
  }
  { clip, start, weight }
}

///|
pub fn ClipSequenceItem::clip(self : ClipSequenceItem) -> MotionClip {
  self.clip
}

///|
pub fn ClipSequenceItem::start(self : ClipSequenceItem) -> Double {
  self.start
}

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

///|
pub fn ClipSequenceItem::weight(self : ClipSequenceItem) -> Double {
  self.weight
}

///|
/// A sequence compositor that averages overlapping clip contributions.
pub struct ClipSequence {
  items : Array[ClipSequenceItem]
  duration : Double
} derive(Debug)

///|
pub fn ClipSequence::new(
  items : Array[ClipSequenceItem],
) -> ClipSequence raise MotionError {
  if items.length() == 0 {
    raise MotionError::EmptyTrack
  }
  let sorted = items.copy()
  sorted.sort_by(fn(left, right) { left.start.compare(right.start) })
  let mut duration = 0.0
  for item in sorted {
    duration = duration.max(item.end())
  }
  { items: sorted, duration }
}

///|
pub fn ClipSequence::items(self : ClipSequence) -> Array[ClipSequenceItem] {
  self.items.copy()
}

///|
pub fn ClipSequence::length(self : ClipSequence) -> Int {
  self.items.length()
}

///|
pub fn ClipSequence::duration(self : ClipSequence) -> Double {
  self.duration
}

///|
pub fn ClipSequence::sample(self : ClipSequence, time : Double) -> Double {
  let mut total = 0.0
  let mut weight = 0.0
  for item in self.items {
    if time >= item.start && time <= item.end() && item.weight > 0.0 {
      total = total + item.clip.sample(time - item.start) * item.weight
      weight = weight + item.weight
    }
  }
  if weight == 0.0 {
    if time < self.items[0].start {
      self.items[0].clip.sample(0.0)
    } else {
      self.items[self.items.length() - 1].clip.sample(time)
    }
  } else {
    total / weight
  }
}

///|
pub fn ClipSequence::sample_many(
  self : ClipSequence,
  count : Int,
) -> Array[SamplePoint] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[SamplePoint] = []
  for index in 0.. Array[MarkerHit] {
  let result : Array[MarkerHit] = []
  for item in self.items {
    let local_start = start - item.start
    let local_end = end - item.start
    for hit in item.clip.markers_between(local_start, local_end) {
      result.push(hit)
    }
  }
  result.sort_by(fn(left, right) { left.time().compare(right.time()) })
  result
}

///|
pub struct ClipSequenceBuilder {
  items : Array[ClipSequenceItem]
} derive(Debug)

///|
pub fn ClipSequenceBuilder::new() -> ClipSequenceBuilder {
  { items: [] }
}

///|
pub fn ClipSequenceBuilder::add(
  self : ClipSequenceBuilder,
  item : ClipSequenceItem,
) -> ClipSequenceBuilder {
  self.items.push(item)
  self
}

///|
pub fn ClipSequenceBuilder::add_at(
  self : ClipSequenceBuilder,
  clip : MotionClip,
  start : Double,
  weight? : Double = 1.0,
) -> ClipSequenceBuilder raise MotionError {
  self.items.push(sequence_item(clip, start, weight~))
  self
}

///|
pub fn ClipSequenceBuilder::length(self : ClipSequenceBuilder) -> Int {
  self.items.length()
}

///|
pub fn ClipSequenceBuilder::build(
  self : ClipSequenceBuilder,
) -> ClipSequence raise MotionError {
  ClipSequence::new(self.items)
}

///|
/// A small library for resolving named clips at runtime.
pub struct MotionClipLibrary {
  clips : Array[MotionClip]
} derive(Debug)

///|
pub fn MotionClipLibrary::new() -> MotionClipLibrary {
  { clips: [] }
}

///|
pub fn MotionClipLibrary::add(
  self : MotionClipLibrary,
  clip : MotionClip,
) -> Unit {
  for index in 0.. MotionClip? {
  for index in 0.. MotionClip? {
  for clip in self.clips {
    if clip.name == name {
      return Some(clip)
    }
  }
  None
}

///|
pub fn MotionClipLibrary::clips(self : MotionClipLibrary) -> Array[MotionClip] {
  self.clips.copy()
}

///|
pub fn MotionClipLibrary::length(self : MotionClipLibrary) -> Int {
  self.clips.length()
}

///|
pub fn MotionClipLibrary::total_duration(self : MotionClipLibrary) -> Double {
  let mut total = 0.0
  for clip in self.clips {
    total = total + clip.duration()
  }
  total
}