///|
/// A point keyframe for UI, camera, and sprite motion tracks.
pub struct Point2Keyframe {
  time : Double
  value : Point2
  curve : Curve
} derive(Debug)

///|
pub fn point2_keyframe(
  time : Double,
  value~ : Point2,
  curve? : Curve = Curve::builtin(Linear),
) -> Point2Keyframe raise MotionError {
  ensure_finite(time)
  { time, value, curve }
}

///|
pub fn Point2Keyframe::time(self : Point2Keyframe) -> Double {
  self.time
}

///|
pub fn Point2Keyframe::value(self : Point2Keyframe) -> Point2 {
  self.value
}

///|
pub fn Point2Keyframe::curve(self : Point2Keyframe) -> Curve {
  self.curve
}

///|
/// A validated 2D point track with the same boundary policies as ScalarTrack.
pub struct Point2Track {
  frames : Array[Point2Keyframe]
} derive(Debug)

///|
pub fn Point2Track::new(
  frames : Array[Point2Keyframe],
) -> Point2Track raise MotionError {
  if frames.length() == 0 {
    raise MotionError::EmptyTrack
  }
  for i in 1.. Array[Point2Keyframe] {
  self.frames.copy()
}

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

///|
pub fn Point2Track::start_time(self : Point2Track) -> Double {
  self.frames[0].time
}

///|
pub fn Point2Track::end_time(self : Point2Track) -> Double {
  self.frames[self.frames.length() - 1].time
}

///|
pub fn Point2Track::duration(self : Point2Track) -> Double {
  self.end_time() - self.start_time()
}

///|
fn Point2Track::inside(self : Point2Track, time : Double) -> Point2 {
  let last = self.frames.length() - 1
  if time <= self.frames[0].time {
    return self.frames[0].value
  }
  if time >= self.frames[last].time {
    return self.frames[last].value
  }
  for index in 0.. Point2 {
  let last = self.frames.length() - 1
  if time < self.start_time() && last > 0 {
    let first = self.frames[0]
    let next = self.frames[1]
    let ratio = (time - first.time) / (next.time - first.time)
    return first.value.lerp(next.value, ratio)
  }
  if time > self.end_time() && last > 0 {
    let before = self.frames[last - 1]
    let end = self.frames[last]
    let ratio = (time - before.time) / (end.time - before.time)
    return before.value.lerp(end.value, ratio)
  }
  self.inside(time)
}

///|
pub fn Point2Track::sample(
  self : Point2Track,
  time : Double,
  mode? : Extrapolation = Clamp,
) -> Point2 {
  let mapped = map_track_time(time, self.start_time(), self.end_time(), mode)
  if mode is Continue {
    self.continue_value(mapped)
  } else {
    self.inside(mapped)
  }
}

///|
pub fn Point2Track::sample_many(
  self : Point2Track,
  count : Int,
  mode? : Extrapolation = Clamp,
) -> Array[Point2] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[Point2] = []
  for index in 0.. Point3Keyframe raise MotionError {
  ensure_finite(time)
  { time, value, curve }
}

///|
pub fn Point3Keyframe::time(self : Point3Keyframe) -> Double {
  self.time
}

///|
pub fn Point3Keyframe::value(self : Point3Keyframe) -> Point3 {
  self.value
}

///|
pub fn Point3Keyframe::curve(self : Point3Keyframe) -> Curve {
  self.curve
}

///|
pub struct Point3Track {
  frames : Array[Point3Keyframe]
} derive(Debug)

///|
pub fn Point3Track::new(
  frames : Array[Point3Keyframe],
) -> Point3Track raise MotionError {
  if frames.length() == 0 {
    raise MotionError::EmptyTrack
  }
  for i in 1.. Array[Point3Keyframe] {
  self.frames.copy()
}

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

///|
pub fn Point3Track::start_time(self : Point3Track) -> Double {
  self.frames[0].time
}

///|
pub fn Point3Track::end_time(self : Point3Track) -> Double {
  self.frames[self.frames.length() - 1].time
}

///|
pub fn Point3Track::duration(self : Point3Track) -> Double {
  self.end_time() - self.start_time()
}

///|
fn Point3Track::inside(self : Point3Track, time : Double) -> Point3 {
  let last = self.frames.length() - 1
  if time <= self.frames[0].time {
    return self.frames[0].value
  }
  if time >= self.frames[last].time {
    return self.frames[last].value
  }
  for index in 0.. Point3 {
  let last = self.frames.length() - 1
  if time < self.start_time() && last > 0 {
    let first = self.frames[0]
    let next = self.frames[1]
    let ratio = (time - first.time) / (next.time - first.time)
    return first.value.lerp(next.value, ratio)
  }
  if time > self.end_time() && last > 0 {
    let before = self.frames[last - 1]
    let end = self.frames[last]
    let ratio = (time - before.time) / (end.time - before.time)
    return before.value.lerp(end.value, ratio)
  }
  self.inside(time)
}

///|
pub fn Point3Track::sample(
  self : Point3Track,
  time : Double,
  mode? : Extrapolation = Clamp,
) -> Point3 {
  let mapped = map_track_time(time, self.start_time(), self.end_time(), mode)
  if mode is Continue {
    self.continue_value(mapped)
  } else {
    self.inside(mapped)
  }
}

///|
pub fn Point3Track::sample_many(
  self : Point3Track,
  count : Int,
  mode? : Extrapolation = Clamp,
) -> Array[Point3] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[Point3] = []
  for index in 0.. RgbaKeyframe raise MotionError {
  ensure_finite(time)
  { time, value, curve }
}

///|
pub struct RgbaTrack {
  frames : Array[RgbaKeyframe]
  premultiplied : Bool
} derive(Debug)

///|
pub fn RgbaTrack::new(
  frames : Array[RgbaKeyframe],
  premultiplied? : Bool = false,
) -> RgbaTrack raise MotionError {
  if frames.length() == 0 {
    raise MotionError::EmptyTrack
  }
  for i in 1.. Array[RgbaKeyframe] {
  self.frames.copy()
}

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

///|
pub fn RgbaTrack::start_time(self : RgbaTrack) -> Double {
  self.frames[0].time
}

///|
pub fn RgbaTrack::end_time(self : RgbaTrack) -> Double {
  self.frames[self.frames.length() - 1].time
}

///|
pub fn RgbaTrack::duration(self : RgbaTrack) -> Double {
  self.end_time() - self.start_time()
}

///|
fn RgbaTrack::inside(self : RgbaTrack, time : Double) -> Rgba {
  let last = self.frames.length() - 1
  if time <= self.frames[0].time {
    return self.frames[0].value
  }
  if time >= self.frames[last].time {
    return self.frames[last].value
  }
  for index in 0.. Rgba {
  let last = self.frames.length() - 1
  if time < self.start_time() && last > 0 {
    let first = self.frames[0]
    let next = self.frames[1]
    let ratio = (time - first.time) / (next.time - first.time)
    return interpolate_rgba(
      first.value,
      next.value,
      ratio,
      premultiplied=self.premultiplied,
    )
  }
  if time > self.end_time() && last > 0 {
    let before = self.frames[last - 1]
    let end = self.frames[last]
    let ratio = (time - before.time) / (end.time - before.time)
    return interpolate_rgba(
      before.value,
      end.value,
      ratio,
      premultiplied=self.premultiplied,
    )
  }
  self.inside(time)
}

///|
pub fn RgbaTrack::sample(
  self : RgbaTrack,
  time : Double,
  mode? : Extrapolation = Clamp,
) -> Rgba {
  let mapped = map_track_time(time, self.start_time(), self.end_time(), mode)
  if mode is Continue {
    self.continue_value(mapped)
  } else {
    self.inside(mapped)
  }
}

///|
pub fn RgbaTrack::sample_many(
  self : RgbaTrack,
  count : Int,
  mode? : Extrapolation = Clamp,
) -> Array[Rgba] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[Rgba] = []
  for index in 0.. TransformKeyframe raise MotionError {
  ensure_finite(time)
  { time, value, curve }
}

///|
pub struct TransformTrack {
  frames : Array[TransformKeyframe]
  shortest_rotation : Bool
} derive(Debug)

///|
pub fn TransformTrack::new(
  frames : Array[TransformKeyframe],
  shortest_rotation? : Bool = true,
) -> TransformTrack raise MotionError {
  if frames.length() == 0 {
    raise MotionError::EmptyTrack
  }
  for i in 1.. Array[TransformKeyframe] {
  self.frames.copy()
}

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

///|
pub fn TransformTrack::start_time(self : TransformTrack) -> Double {
  self.frames[0].time
}

///|
pub fn TransformTrack::end_time(self : TransformTrack) -> Double {
  self.frames[self.frames.length() - 1].time
}

///|
pub fn TransformTrack::duration(self : TransformTrack) -> Double {
  self.end_time() - self.start_time()
}

///|
fn TransformTrack::inside(self : TransformTrack, time : Double) -> Transform2D {
  let last = self.frames.length() - 1
  if time <= self.frames[0].time {
    return self.frames[0].value
  }
  if time >= self.frames[last].time {
    return self.frames[last].value
  }
  for index in 0.. Transform2D {
  let last = self.frames.length() - 1
  if time < self.start_time() && last > 0 {
    let first = self.frames[0]
    let next = self.frames[1]
    let ratio = (time - first.time) / (next.time - first.time)
    return interpolate_transform(
      first.value,
      next.value,
      ratio,
      shortest_rotation=self.shortest_rotation,
    )
  }
  if time > self.end_time() && last > 0 {
    let before = self.frames[last - 1]
    let end = self.frames[last]
    let ratio = (time - before.time) / (end.time - before.time)
    return interpolate_transform(
      before.value,
      end.value,
      ratio,
      shortest_rotation=self.shortest_rotation,
    )
  }
  self.inside(time)
}

///|
pub fn TransformTrack::sample(
  self : TransformTrack,
  time : Double,
  mode? : Extrapolation = Clamp,
) -> Transform2D {
  let mapped = map_track_time(time, self.start_time(), self.end_time(), mode)
  if mode is Continue {
    self.continue_value(mapped)
  } else {
    self.inside(mapped)
  }
}

///|
pub fn TransformTrack::sample_many(
  self : TransformTrack,
  count : Int,
  mode? : Extrapolation = Clamp,
) -> Array[Transform2D] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[Transform2D] = []
  for index in 0.. Double {
  let span = last - first
  if span <= 0.0 || mode is Clamp {
    return if time < first { first } else if time > last { last } else { time }
  }
  if time >= first && time <= last {
    return time
  }
  match mode {
    Continue => time
    Repeat => {
      let offset = time - first
      let cycles = @math.floor(offset / span)
      first + offset - cycles * span
    }
    Mirror => {
      let offset = time - first
      let cycles = @math.floor(offset / span)
      let part = offset - cycles * span
      if cycles.to_int() % 2 == 0 {
        first + part
      } else {
        last - part
      }
    }
    Clamp => if time < first { first } else { last }
  }
}