///|
/// Playback state for embedders that need deterministic frame stepping.
pub(all) enum PlaybackMode {
  Once
  Loop
  PingPong
  Clamp
} derive(Eq, Debug)

///|
pub(all) struct PlaybackState {
  animation : Animation
  mode : PlaybackMode
  time_cs : Int
  direction : Int
  playing : Bool
} derive(Debug)

///|
pub fn PlaybackState::new(
  animation : Animation,
  mode : PlaybackMode,
) -> PlaybackState {
  { animation, mode, time_cs: 0, direction: 1, playing: true }
}

///|
pub fn PlaybackState::animation(self : PlaybackState) -> Animation {
  self.animation
}

///|
pub fn PlaybackState::mode(self : PlaybackState) -> PlaybackMode {
  self.mode
}

///|
pub fn PlaybackState::time(self : PlaybackState) -> Int {
  self.time_cs
}

///|
pub fn PlaybackState::playing(self : PlaybackState) -> Bool {
  self.playing
}

///|
pub fn PlaybackState::pause(self : PlaybackState) -> PlaybackState {
  { ..self, playing: false }
}

///|
pub fn PlaybackState::play(self : PlaybackState) -> PlaybackState {
  { ..self, playing: true }
}

///|
pub fn PlaybackState::seek(
  self : PlaybackState,
  time_cs : Int,
) -> PlaybackState {
  { ..self, time_cs: if time_cs < 0 { 0 } else { time_cs } }
}

///|
pub fn PlaybackState::reset(self : PlaybackState) -> PlaybackState {
  { ..self, time_cs: 0, direction: 1, playing: true }
}

///|
pub fn PlaybackState::current_frame(self : PlaybackState) -> Frame? {
  self.animation.frame_at(self.time_cs)
}

///|
fn clamp_playback_time(animation : Animation, time_cs : Int) -> Int {
  let duration = animation.duration_cs()
  if duration <= 0 {
    0
  } else if time_cs < 0 {
    0
  } else if time_cs >= duration {
    duration - 1
  } else {
    time_cs
  }
}

///|
/// Advance playback by centiseconds, applying the selected mode at boundaries.
pub fn PlaybackState::advance(
  self : PlaybackState,
  elapsed_cs : Int,
) -> PlaybackState {
  if !self.playing || self.animation.frame_count() == 0 {
    self
  } else {
    let duration = self.animation.duration_cs()
    let step = if elapsed_cs < 0 { 0 } else { elapsed_cs }
    if duration <= 0 {
      { ..self, playing: false }
    } else {
      match self.mode {
        Once => {
          let next = self.time_cs + step
          if next >= duration {
            { ..self, time_cs: duration - 1, playing: false }
          } else {
            { ..self, time_cs: next }
          }
        }
        Clamp =>
          {
            ..self,
            time_cs: clamp_playback_time(self.animation, self.time_cs + step),
          }
        Loop => { ..self, time_cs: (self.time_cs + step) % duration }
        PingPong => {
          let period = duration * 2
          let phase = (self.time_cs + step) % period
          let time = if phase < duration { phase } else { period - phase - 1 }
          let direction = if phase < duration { 1 } else { -1 }
          { ..self, time_cs: time, direction }
        }
      }
    }
  }
}

///|
/// Advance by a frame count using the animation's own delays.
pub fn PlaybackState::step_frames(
  self : PlaybackState,
  count : Int,
) -> PlaybackState {
  let mut result = self
  let safe_count = if count < 0 { 0 } else { count }
  for _ in 0.. result = result.advance(1)
      Some(frame) => result = result.advance(frame.delay_cs())
    }
  }
  result
}

///|
/// Return normalized playback progress in per mille.
pub fn PlaybackState::progress(self : PlaybackState) -> Int {
  let duration = self.animation.duration_cs()
  if duration <= 0 {
    0
  } else {
    self.time_cs * 1000 / duration
  }
}

///|
/// Build a list of visible frame indices at regular playback intervals.
pub fn sample_playback(state : PlaybackState, samples : Int) -> Array[Int] {
  let result : Array[Int] = []
  let count = if samples < 1 { 1 } else { samples }
  let duration = state.animation.duration_cs()
  for i in 0.. ()
      Some(position) => result.push(position.index())
    }
  }
  result
}

///|
/// Produce a playback snapshot suitable for a UI overlay.
pub(all) struct PlaybackSnapshot {
  time_cs : Int
  frame_index : Int
  progress_per_mille : Int
  playing : Bool
} derive(Debug)

///|
pub fn PlaybackSnapshot::time(self : PlaybackSnapshot) -> Int {
  self.time_cs
}

///|
pub fn PlaybackSnapshot::frame_index(self : PlaybackSnapshot) -> Int {
  self.frame_index
}

///|
pub fn PlaybackSnapshot::progress(self : PlaybackSnapshot) -> Int {
  self.progress_per_mille
}

///|
pub fn PlaybackSnapshot::playing(self : PlaybackSnapshot) -> Bool {
  self.playing
}

///|
pub fn PlaybackState::snapshot(self : PlaybackState) -> PlaybackSnapshot {
  let frame_index = match self.animation.position_at(self.time_cs) {
    None => -1
    Some(position) => position.index()
  }
  PlaybackSnapshot::{
    time_cs: self.time_cs,
    frame_index,
    progress_per_mille: self.progress(),
    playing: self.playing,
  }
}