///|
pub enum LoopMode {
  Once
  Loop
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AnimationClip {
  name : String
  frames : Array[String]
  durations : Array[Int]
  direction : Direction
  loop_mode : LoopMode
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct StateTransition {
  from : String
  event : String
  to : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AnimationMachine {
  initial : String
  clips : Array[AnimationClip]
  transitions : Array[StateTransition]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub suberror AnimationError {
  TagOutOfRange(String)
  EmptyTagName
} derive(Debug, Eq, ToJson, FromJson)

///|
fn frame_indices(tag : FrameTag) -> Array[Int] {
  let values : Array[Int] = []
  match tag.direction {
    Reverse =>
      for i = tag.to; i >= tag.from; i = i - 1 {
        values.push(i)
      }
    PingPong => {
      for i in tag.from..<=tag.to {
        values.push(i)
      }
      if tag.to > tag.from {
        for i = tag.to - 1; i > tag.from; i = i - 1 {
          values.push(i)
        }
      }
    }
    Forward =>
      for i in tag.from..<=tag.to {
        values.push(i)
      }
  }
  values
}

///|
fn clip_from_tag(sheet : SpriteSheet, tag : FrameTag) -> AnimationClip {
  let names : Array[String] = []
  let durations : Array[Int] = []
  for i in frame_indices(tag) {
    let frame = sheet.frames[i]
    names.push(frame.filename)
    durations.push(frame.duration)
  }
  {
    name: tag.name,
    frames: names,
    durations,
    direction: tag.direction,
    loop_mode: Loop,
  }
}

///|
pub fn build_machine(
  sheet : SpriteSheet,
  initial? : String = "",
) -> AnimationMachine {
  let clips = sheet.tags.filter_map(fn(tag) {
    if tag.from < 0 || tag.to < tag.from || tag.to >= sheet.frames.length() {
      None
    } else {
      Some(clip_from_tag(sheet, tag))
    }
  })
  let start = if initial == "" && clips.length() > 0 {
    clips[0].name
  } else {
    initial
  }
  { initial: start, clips, transitions: [] }
}

///|
pub fn build_machine_checked(
  sheet : SpriteSheet,
  initial? : String = "",
) -> AnimationMachine raise {
  for tag in sheet.tags {
    if tag.name == "" {
      raise EmptyTagName
    }
    if tag.from < 0 || tag.to < tag.from || tag.to >= sheet.frames.length() {
      raise TagOutOfRange(tag.name)
    }
  }
  build_machine(sheet, initial~)
}

///|
pub fn AnimationClip::total_duration(self : AnimationClip) -> Int {
  self.durations.fold(init=0, (total, duration) => total + duration)
}

///|
pub fn AnimationMachine::clip_named(
  self : AnimationMachine,
  name : String,
) -> AnimationClip? {
  match self.clips.search_by(fn(clip) { clip.name == name }) {
    Some(index) => Some(self.clips[index])
    None => None
  }
}

///|
pub fn AnimationMachine::transition_target(
  self : AnimationMachine,
  from~ : String,
  event~ : String,
) -> String? {
  match
    self.transitions.search_by(fn(transition) {
      transition.from == from && transition.event == event
    }) {
    Some(index) => Some(self.transitions[index].to)
    None => None
  }
}

///|
pub fn AnimationMachine::with_transition(
  self : AnimationMachine,
  from~ : String,
  event~ : String,
  to~ : String,
) -> AnimationMachine {
  let transitions = self.transitions.copy()
  transitions.push({ from, event, to })
  { ..self, transitions, }
}

///|
pub fn AnimationMachine::to_json_string(
  self : AnimationMachine,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}