///|
pub(all) struct AnimationFrameRef {
  name : String
  frame_index : Int
  duration_ms : Int
  rect : Rect
} derive(Eq, @debug.Debug)

///|
pub fn AnimationFrameRef::from_frame(frame : FrameInfo) -> AnimationFrameRef {
  {
    name: frame.name,
    frame_index: frame.index,
    duration_ms: frame.duration_ms,
    rect: frame.rect,
  }
}

///|
pub(all) struct AnimationClip {
  name : String
  direction : TagDirection
  loop_mode : LoopMode
  frames : Array[AnimationFrameRef]
  total_duration_ms : Int
} derive(Eq, @debug.Debug)

///|
pub fn AnimationClip::new(
  name : String,
  frames : Array[AnimationFrameRef],
  direction? : TagDirection = DirectionForward,
  loop_mode? : LoopMode = LoopRepeat,
) -> AnimationClip {
  let total = for frame in frames; sum = 0 {
    continue sum + frame.duration_ms
  } nobreak {
    sum
  }
  { name, direction, loop_mode, frames, total_duration_ms: total }
}

///|
pub fn AnimationClip::is_valid(self : AnimationClip) -> Bool {
  self.name != "" && self.frames.length() > 0 && self.total_duration_ms > 0
}

///|
pub fn AnimationClip::frame_count(self : AnimationClip) -> Int {
  self.frames.length()
}

///|
pub fn AnimationClip::frame_at(
  self : AnimationClip,
  index : Int,
) -> AnimationFrameRef? {
  if index >= 0 && index < self.frames.length() {
    Some(self.frames[index])
  } else {
    None
  }
}

///|
pub(all) struct AnimationLibrary {
  texture_image : String
  atlas_size : Size
  clips : Array[AnimationClip]
} derive(Eq, @debug.Debug)

///|
pub fn AnimationLibrary::empty() -> AnimationLibrary {
  { texture_image: "", atlas_size: Size::empty(), clips: [] }
}

///|
pub fn AnimationLibrary::find_clip(
  self : AnimationLibrary,
  name : String,
) -> AnimationClip? {
  for clip in self.clips {
    if clip.name == name {
      return Some(clip)
    }
  }
  None
}

///|
pub fn build_animation_library(sheet : SpriteSheet) -> AnimationLibrary {
  let clips : Array[AnimationClip] = []
  if sheet.meta.frame_tags.length() == 0 {
    clips.push(build_all_frames_clip(sheet))
  } else {
    for tag in sheet.meta.frame_tags {
      clips.push(build_clip_from_tag(sheet, tag))
    }
  }
  { texture_image: sheet.meta.image, atlas_size: sheet.meta.size, clips }
}

///|
fn build_all_frames_clip(sheet : SpriteSheet) -> AnimationClip {
  let refs : Array[AnimationFrameRef] = []
  for frame in sheet.frames {
    refs.push(AnimationFrameRef::from_frame(frame))
  }
  AnimationClip::new("all", refs)
}

///|
fn build_clip_from_tag(sheet : SpriteSheet, tag : FrameTag) -> AnimationClip {
  let refs : Array[AnimationFrameRef] = []
  if tag.direction == DirectionReverse ||
    tag.direction == DirectionPingPongReverse {
    for i in tag.to_index>..tag.from_index {
      match sheet.frame_at(i - 1) {
        Some(frame) => refs.push(AnimationFrameRef::from_frame(frame))
        None => ()
      }
    }
  } else {
    for i in tag.from_index..<=tag.to_index {
      match sheet.frame_at(i) {
        Some(frame) => refs.push(AnimationFrameRef::from_frame(frame))
        None => ()
      }
    }
  }
  let mode = if tag.direction.is_pingpong() { LoopPingPong } else { LoopRepeat }
  AnimationClip::new(tag.name, refs, direction=tag.direction, loop_mode=mode)
}

///|
pub(all) struct AnimationPlayer {
  library : AnimationLibrary
  clip_name : String
  frame_cursor : Int
  elapsed_in_frame_ms : Int
  playing : Bool
  completed : Bool
  forward : Bool
} derive(Eq, @debug.Debug)

///|
pub fn AnimationPlayer::new(
  library : AnimationLibrary,
  clip_name? : String = "",
) -> AnimationPlayer {
  let chosen = if clip_name != "" {
    clip_name
  } else if library.clips.length() > 0 {
    library.clips[0].name
  } else {
    ""
  }
  {
    library,
    clip_name: chosen,
    frame_cursor: 0,
    elapsed_in_frame_ms: 0,
    playing: true,
    completed: false,
    forward: true,
  }
}

///|
pub fn AnimationPlayer::current_clip(self : AnimationPlayer) -> AnimationClip? {
  self.library.find_clip(self.clip_name)
}

///|
pub fn AnimationPlayer::current_frame(
  self : AnimationPlayer,
) -> AnimationFrameRef? {
  match self.current_clip() {
    Some(clip) => clip.frame_at(self.frame_cursor)
    None => None
  }
}

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

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

///|
pub fn AnimationPlayer::reset(self : AnimationPlayer) -> AnimationPlayer {
  {
    ..self,
    frame_cursor: 0,
    elapsed_in_frame_ms: 0,
    playing: true,
    completed: false,
    forward: true,
  }
}

///|
pub fn AnimationPlayer::set_clip(
  self : AnimationPlayer,
  clip_name : String,
) -> AnimationPlayer {
  if self.clip_name == clip_name {
    self
  } else {
    {
      ..self,
      clip_name,
      frame_cursor: 0,
      elapsed_in_frame_ms: 0,
      playing: true,
      completed: false,
      forward: true,
    }
  }
}

///|
pub fn AnimationPlayer::step(
  self : AnimationPlayer,
  delta_ms : Int,
) -> AnimationPlayer {
  if delta_ms <= 0 || !self.playing || self.completed {
    self
  } else {
    match self.current_clip() {
      None => self
      Some(clip) => step_with_clip(self, clip, delta_ms)
    }
  }
}

///|
fn step_with_clip(
  player : AnimationPlayer,
  clip : AnimationClip,
  delta_ms : Int,
) -> AnimationPlayer {
  if clip.frames.length() == 0 {
    { ..player, playing: false, completed: true }
  } else {
    let current = clip.frames[player.frame_cursor]
    let elapsed = player.elapsed_in_frame_ms + delta_ms
    if elapsed < current.duration_ms {
      { ..player, elapsed_in_frame_ms: elapsed }
    } else {
      advance_player_frame(
        { ..player, elapsed_in_frame_ms: elapsed - current.duration_ms },
        clip,
      )
    }
  }
}

///|
fn advance_player_frame(
  player : AnimationPlayer,
  clip : AnimationClip,
) -> AnimationPlayer {
  match clip.loop_mode {
    LoopOnce => advance_once(player, clip)
    LoopRepeat => advance_repeat(player, clip)
    LoopPingPong => advance_pingpong(player, clip)
  }
}

///|
fn advance_once(
  player : AnimationPlayer,
  clip : AnimationClip,
) -> AnimationPlayer {
  if player.frame_cursor + 1 >= clip.frames.length() {
    {
      ..player,
      frame_cursor: clip.frames.length() - 1,
      playing: false,
      completed: true,
    }
  } else {
    { ..player, frame_cursor: player.frame_cursor + 1 }
  }
}

///|
fn advance_repeat(
  player : AnimationPlayer,
  clip : AnimationClip,
) -> AnimationPlayer {
  let next = if player.frame_cursor + 1 >= clip.frames.length() {
    0
  } else {
    player.frame_cursor + 1
  }
  { ..player, frame_cursor: next }
}

///|
fn advance_pingpong(
  player : AnimationPlayer,
  clip : AnimationClip,
) -> AnimationPlayer {
  if clip.frames.length() <= 1 {
    player
  } else if player.forward && player.frame_cursor + 1 >= clip.frames.length() {
    { ..player, frame_cursor: player.frame_cursor - 1, forward: false }
  } else if !player.forward && player.frame_cursor <= 0 {
    { ..player, frame_cursor: 1, forward: true }
  } else if player.forward {
    { ..player, frame_cursor: player.frame_cursor + 1 }
  } else {
    { ..player, frame_cursor: player.frame_cursor - 1 }
  }
}

///|
pub fn sample_clip_at(
  clip : AnimationClip,
  elapsed_ms : Int,
) -> AnimationFrameRef? {
  if !clip.is_valid() || elapsed_ms < 0 {
    None
  } else {
    let wrapped = if clip.total_duration_ms <= 0 {
      0
    } else {
      elapsed_ms % clip.total_duration_ms
    }
    let mut acc = 0
    for frame in clip.frames {
      acc = acc + frame.duration_ms
      if wrapped < acc {
        return Some(frame)
      }
    }
    clip.frame_at(clip.frames.length() - 1)
  }
}

///|
pub fn estimate_clip_fps(clip : AnimationClip) -> Int {
  if clip.total_duration_ms <= 0 {
    0
  } else {
    clip.frames.length() * 1000 / clip.total_duration_ms
  }
}