///|
/// Non-destructive timeline clips for editors and reusable export scenes.
pub(all) struct AnimationClip {
  source : Animation
  start_cs : Int
  end_cs : Int
  speed_per_mille : Int
  loop_count : Int
} derive(Debug)

///|
pub fn AnimationClip::new(
  source : Animation,
  start_cs : Int,
  end_cs : Int,
) -> AnimationClip {
  let start = if start_cs < 0 { 0 } else { start_cs }
  let end = if end_cs < start { start } else { end_cs }
  { source, start_cs: start, end_cs: end, speed_per_mille: 1000, loop_count: 1 }
}

///|
pub fn AnimationClip::source(self : AnimationClip) -> Animation {
  self.source
}

///|
pub fn AnimationClip::range(self : AnimationClip) -> (Int, Int) {
  (self.start_cs, self.end_cs)
}

///|
pub fn AnimationClip::speed(
  self : AnimationClip,
  per_mille : Int,
) -> AnimationClip {
  { ..self, speed_per_mille: if per_mille < 1 { 1 } else { per_mille } }
}

///|
pub fn AnimationClip::loops(self : AnimationClip, count : Int) -> AnimationClip {
  { ..self, loop_count: if count < 1 { 1 } else { count } }
}

///|
pub fn AnimationClip::duration(self : AnimationClip) -> Int {
  (self.end_cs - self.start_cs) * 1000 / self.speed_per_mille
}

///|
/// Render a clip into a fresh animation while preserving sampled timing.
pub fn AnimationClip::render(self : AnimationClip) -> Animation {
  let duration = self.duration()
  let sampled = self.source.sample(
    if self.source.frame_count() < 1 {
      1
    } else {
      self.source.frame_count()
    },
  )
  let frames : Array[Frame] = []
  for frame in sampled.frames {
    if frames.length() == 0 || frames.length() < self.source.frame_count() {
      frames.push(
        frame.with_delay(
          delay_cs=if sampled.frame_count() == 0 {
            0
          } else {
            duration / sampled.frame_count()
          },
        ),
      )
    }
  }
  let one = Animation::{ ..self.source, frames, }
  one.repeat(self.loop_count)
}

///|
/// Render multiple clips back-to-back with no implicit timing gap.
pub fn render_clip_sequence(clips : ArrayView[AnimationClip]) -> Animation? {
  if clips.length() == 0 {
    None
  } else {
    let first = clips[0].render()
    let mut result = first
    for i in 1.. AnimationClip {
  { ..self, source: self.source.reverse() }
}

///|
/// Set an explicit frame rate for clip playback.
pub fn AnimationClip::fps(
  self : AnimationClip,
  frames_per_second : Int,
) -> AnimationClip {
  let safe = if frames_per_second < 1 { 1 } else { frames_per_second }
  let frame_count = self.source.frame_count()
  let duration = if frame_count == 0 { 0 } else { frame_count * 100 / safe }
  { ..self, end_cs: self.start_cs + duration }
}

///|
/// Extract a clip directly from a frame range and annotate its timing.
pub fn Animation::clip_frames(
  self : Animation,
  start : Int,
  end : Int,
) -> AnimationClip {
  let safe_start = if start < 0 {
    0
  } else if start >= self.frame_count() {
    self.frame_count()
  } else {
    start
  }
  let safe_end = if end < safe_start {
    safe_start
  } else if end > self.frame_count() {
    self.frame_count()
  } else {
    end
  }
  let first_time = if safe_start >= self.frame_count() {
    self.duration_cs()
  } else {
    self.timeline_starts()[safe_start]
  }
  let final_time = if safe_end >= self.frame_count() {
    self.duration_cs()
  } else {
    self.timeline_starts()[safe_end]
  }
  AnimationClip::new(self, first_time, final_time)
}

///|
/// Return a clip that includes a small pre-roll and post-roll around a marker.
pub fn Animation::clip_around(
  self : Animation,
  center_cs : Int,
  padding_cs : Int,
) -> AnimationClip {
  let safe_padding = if padding_cs < 0 { 0 } else { padding_cs }
  AnimationClip::new(self, center_cs - safe_padding, center_cs + safe_padding)
}

///|
/// Concatenate clips and normalize delays to a target frame duration.
pub fn render_clips_at_rate(
  clips : ArrayView[AnimationClip],
  delay_cs : Int,
) -> Animation? {
  match render_clip_sequence(clips) {
    None => None
    Some(animation) =>
      Some(animation.with_delay(if delay_cs < 0 { 0 } else { delay_cs }))
  }
}