///|
/// Easing curves for timeline and motion interpolation.
pub(all) enum Easing {
  Linear
  Step
  EaseIn
  EaseOut
  EaseInOut
  SmoothStep
  SineLike
  Bounce
  Pulse
} derive(Eq, Debug)

///|
pub fn Easing::sample(self : Easing, numerator : Int, denominator : Int) -> Int {
  if denominator <= 0 {
    1000
  } else {
    let t = if numerator < 0 {
      0
    } else if numerator > denominator {
      denominator
    } else {
      numerator
    }
    let unit = t * 1000 / denominator
    match self {
      Linear => unit
      Step => if unit < 1000 { 0 } else { 1000 }
      EaseIn => unit * unit / 1000
      EaseOut => 1000 - (1000 - unit) * (1000 - unit) / 1000
      EaseInOut =>
        if unit < 500 {
          2 * unit * unit / 1000
        } else {
          1000 - 2 * (1000 - unit) * (1000 - unit) / 1000
        }
      SmoothStep => {
        let squared = unit * unit / 1000
        squared * (3000 - 2 * unit) / 1000
      }
      SineLike =>
        if unit < 500 {
          unit * unit * 2 / 1000
        } else {
          1000 - (1000 - unit) * (1000 - unit) * 2 / 1000
        }
      Bounce => if unit < 500 { unit * 2 } else { 1000 - (unit - 500) * 2 }
      Pulse => if unit < 500 { unit * 2 } else { (1000 - unit) * 2 }
    }
  }
}

///|
/// Position, scale, rotation, and opacity values sampled along a track.
pub(all) struct MotionKeyframe {
  time_cs : Int
  x : Int
  y : Int
  scale_per_mille : Int
  rotation : CanvasRotation
  opacity : Byte
} derive(Debug)

///|
pub fn MotionKeyframe::new(
  time_cs~ : Int,
  x~ : Int,
  y~ : Int,
  scale_per_mille~ : Int,
  rotation~ : CanvasRotation,
  opacity~ : Byte,
) -> MotionKeyframe {
  {
    time_cs,
    x,
    y,
    scale_per_mille: if scale_per_mille < 0 {
      0
    } else {
      scale_per_mille
    },
    rotation,
    opacity,
  }
}

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

///|
pub fn MotionKeyframe::position(self : MotionKeyframe) -> (Int, Int) {
  (self.x, self.y)
}

///|
pub fn MotionKeyframe::scale(self : MotionKeyframe) -> Int {
  self.scale_per_mille
}

///|
pub fn MotionKeyframe::rotation(self : MotionKeyframe) -> CanvasRotation {
  self.rotation
}

///|
pub fn MotionKeyframe::opacity(self : MotionKeyframe) -> Byte {
  self.opacity
}

///|
pub(all) struct MotionSample {
  x : Int
  y : Int
  scale_per_mille : Int
  opacity : Byte
  rotation : CanvasRotation
} derive(Debug)

///|
pub fn MotionSample::position(self : MotionSample) -> (Int, Int) {
  (self.x, self.y)
}

///|
pub fn MotionSample::scale(self : MotionSample) -> Int {
  self.scale_per_mille
}

///|
pub fn MotionSample::opacity(self : MotionSample) -> Byte {
  self.opacity
}

///|
pub fn MotionSample::rotation(self : MotionSample) -> CanvasRotation {
  self.rotation
}

///|
/// A sorted motion track. Keys added out of order are inserted into timeline
/// order, making it safe for editors to build tracks incrementally.
pub(all) struct MotionTrack {
  keys : Array[MotionKeyframe]
  easing : Easing
} derive(Debug)

///|
pub fn MotionTrack::new(easing~ : Easing) -> MotionTrack {
  { keys: [], easing }
}

///|
pub fn MotionTrack::keys(self : MotionTrack) -> ArrayView[MotionKeyframe] {
  self.keys[:]
}

///|
pub fn MotionTrack::easing(self : MotionTrack) -> Easing {
  self.easing
}

///|
pub fn MotionTrack::add(
  self : MotionTrack,
  key : MotionKeyframe,
) -> MotionTrack {
  let mut index = self.keys.length()
  for i, existing in self.keys {
    if key.time() < existing.time() {
      index = i
      break
    }
  }
  self.keys.push(key)
  if index < self.keys.length() - 1 {
    let mut i = self.keys.length() - 1
    while i > index {
      self.keys[i] = self.keys[i - 1]
      i -= 1
    }
    self.keys[index] = key
  }
  self
}

///|
fn interpolate_int(left : Int, right : Int, amount : Int) -> Int {
  left + (right - left) * amount / 1000
}

///|
fn interpolate_rotation(
  left : CanvasRotation,
  right : CanvasRotation,
  amount : Int,
) -> CanvasRotation {
  if amount < 500 {
    left
  } else {
    right
  }
}

///|
pub fn MotionTrack::sample(self : MotionTrack, time_cs : Int) -> MotionSample {
  if self.keys.length() == 0 {
    MotionSample::{
      x: 0,
      y: 0,
      scale_per_mille: 1000,
      opacity: 255,
      rotation: Clockwise90,
    }
  } else if self.keys.length() == 1 || time_cs <= self.keys[0].time() {
    let key = self.keys[0]
    MotionSample::{
      x: key.x,
      y: key.y,
      scale_per_mille: key.scale_per_mille,
      opacity: key.opacity,
      rotation: key.rotation,
    }
  } else {
    let last = self.keys[self.keys.length() - 1]
    if time_cs >= last.time() {
      MotionSample::{
        x: last.x,
        y: last.y,
        scale_per_mille: last.scale_per_mille,
        opacity: last.opacity,
        rotation: last.rotation,
      }
    } else {
      let mut left = self.keys[0]
      let mut right = last
      for i in 1.. Canvas {
  if scale_per_mille <= 0 {
    Canvas::new(width=1, height=1, fill=rgba(0, 0, 0, 0))
  } else {
    let width = if self.width * scale_per_mille / 1000 < 1 {
      1
    } else {
      self.width * scale_per_mille / 1000
    }
    let height = if self.height * scale_per_mille / 1000 < 1 {
      1
    } else {
      self.height * scale_per_mille / 1000
    }
    self.resize_nearest(width~, height~)
  }
}

///|
fn place_canvas(
  background : Canvas,
  source : Canvas,
  left : Int,
  top : Int,
  opacity : Byte,
) -> Canvas {
  let result = background.copy()
  let layer = if opacity == 255 { source } else { source.scale_alpha(opacity) }
  ignore(result.blit(layer, destination_x=left, destination_y=top))
  result
}

///|
fn apply_motion_sample(
  animation : Animation,
  frame : Frame,
  sample : MotionSample,
) -> Frame {
  let rotated = frame.canvas.rotate(sample.rotation)
  let scaled = rotated.scale_centered(sample.scale_per_mille)
  let background = Canvas::new(
    width=animation.width,
    height=animation.height,
    fill=animation.background,
  )
  let left = sample.x - scaled.width / 2
  let top = sample.y - scaled.height / 2
  let placed = place_canvas(background, scaled, left, top, sample.opacity)
  Frame::new(canvas=placed, delay_cs=frame.delay_cs()).with_disposal(
    disposal=frame.disposal(),
  )
}

///|
/// Apply a motion track to each frame at its timeline start time.
pub fn Animation::apply_motion(
  self : Animation,
  track : MotionTrack,
) -> Animation {
  let result : Array[Frame] = []
  let starts = self.timeline_starts()
  for i, frame in self.frames {
    result.push(apply_motion_sample(self, frame, track.sample(starts[i])))
  }
  Animation::{ ..self, frames: result }
}

///|
/// Generate a moving copy of an animation between two positions.
pub fn Animation::move_between(
  self : Animation,
  from~ : (Int, Int),
  to~ : (Int, Int),
  easing~ : Easing,
) -> Animation {
  let track = MotionTrack::new(easing~)
    .add(
      MotionKeyframe::new(
        time_cs=0,
        x=from.0,
        y=from.1,
        scale_per_mille=1000,
        rotation=Clockwise90,
        opacity=255,
      ),
    )
    .add(
      MotionKeyframe::new(
        time_cs=if self.duration_cs() <= 0 { 1 } else { self.duration_cs() },
        x=to.0,
        y=to.1,
        scale_per_mille=1000,
        rotation=Clockwise90,
        opacity=255,
      ),
    )
  self.apply_motion(track)
}

///|
/// Add a pulsing opacity effect without changing frame dimensions.
pub fn Animation::pulse(
  self : Animation,
  minimum_alpha~ : Byte,
  easing~ : Easing,
) -> Animation {
  let result : Array[Frame] = []
  let duration = if self.duration_cs() <= 0 { 1 } else { self.duration_cs() }
  for start, frame in self.frames {
    let time = self.timeline_starts()[start]
    let phase = time % duration
    let amount = easing.sample(phase, duration)
    let alpha = minimum_alpha.to_int() +
      (255 - minimum_alpha.to_int()) * amount / 1000
    result.push(frame.with_canvas(frame.canvas().scale_alpha(alpha.to_byte())))
  }
  Animation::{ ..self, frames: result }
}

///|
/// Repeat each frame with alternating opacity to create a reliable blink.
pub fn Animation::blink(
  self : Animation,
  visible_copies : Int,
  hidden_copies : Int,
) -> Animation {
  let visible = if visible_copies < 1 { 1 } else { visible_copies }
  let hidden = if hidden_copies < 0 { 0 } else { hidden_copies }
  let result : Array[Frame] = []
  for frame in self.frames {
    for _ in 0.. Animation {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let result : Array[Frame] = []
  let mut state = seed
  for frame in self.frames {
    state = (state * 1103515245 + 12345) & 0x7fffffff
    let x = if safe_radius == 0 {
      0
    } else {
      state % (safe_radius * 2 + 1) - safe_radius
    }
    state = (state * 1103515245 + 12345) & 0x7fffffff
    let y = if safe_radius == 0 {
      0
    } else {
      state % (safe_radius * 2 + 1) - safe_radius
    }
    let background = Canvas::new(
      width=self.width,
      height=self.height,
      fill=self.background,
    )
    let shaken = place_canvas(background, frame.canvas(), x, y, 255)
    result.push(
      Frame::new(canvas=shaken, delay_cs=frame.delay_cs()).with_disposal(
        disposal=frame.disposal(),
      ),
    )
  }
  Animation::{ ..self, frames: result }
}

///|
/// Add a zoom-in or zoom-out camera move around the logical screen center.
pub fn Animation::zoom(
  self : Animation,
  from_per_mille~ : Int,
  to_per_mille~ : Int,
  easing~ : Easing,
) -> Animation {
  let result : Array[Frame] = []
  let duration = if self.duration_cs() <= 0 { 1 } else { self.duration_cs() }
  let starts = self.timeline_starts()
  for i, frame in self.frames {
    let amount = easing.sample(starts[i], duration)
    let scale = interpolate_int(from_per_mille, to_per_mille, amount)
    let centered = frame.canvas().scale_centered(scale)
    let background = Canvas::new(
      width=self.width,
      height=self.height,
      fill=self.background,
    )
    let placed = place_canvas(
      background,
      centered,
      (self.width - centered.width) / 2,
      (self.height - centered.height) / 2,
      255,
    )
    result.push(
      Frame::new(canvas=placed, delay_cs=frame.delay_cs()).with_disposal(
        disposal=frame.disposal(),
      ),
    )
  }
  Animation::{ ..self, frames: result }
}

///|
/// Create a frame-by-frame color wipe from one edge of the logical screen.
pub fn Animation::wipe(
  self : Animation,
  direction : Int,
  color : Rgba,
) -> Animation {
  let result : Array[Frame] = []
  for i, frame in self.frames {
    let progress = (i + 1) * 1000 / self.frames.length()
    let canvas = frame.canvas().copy()
    let width = self.width * progress / 1000
    let height = self.height * progress / 1000
    if direction % 4 == 0 {
      ignore(
        canvas.fill_rect(
          x=0,
          y=0,
          width=self.width - width,
          height=self.height,
          color~,
        ),
      )
    } else if direction % 4 == 1 {
      ignore(
        canvas.fill_rect(
          x=width,
          y=0,
          width=self.width - width,
          height=self.height,
          color~,
        ),
      )
    } else if direction % 4 == 2 {
      ignore(
        canvas.fill_rect(
          x=0,
          y=0,
          width=self.width,
          height=self.height - height,
          color~,
        ),
      )
    } else {
      ignore(
        canvas.fill_rect(
          x=0,
          y=height,
          width=self.width,
          height=self.height - height,
          color~,
        ),
      )
    }
    result.push(Frame::new(canvas~, delay_cs=frame.delay_cs()))
  }
  Animation::{ ..self, frames: result }
}

///|
/// Create intermediate frames between two canvases using an easing curve.
pub fn interpolate_canvases(
  first : Canvas,
  second : Canvas,
  count : Int,
  easing~ : Easing,
) -> Array[Canvas] {
  if first.width != second.width || first.height != second.height || count <= 0 {
    []
  } else {
    let result : Array[Canvas] = []
    for index in 0.. Animation {
  if between <= 0 || self.frames.length() <= 1 {
    self
  } else {
    let result : Array[Frame] = []
    for i in 0.. Animation {
  let result : Array[Frame] = []
  for frame in self.frames {
    result.push(
      Frame::new(
        canvas=frame.canvas().adjust(effect),
        delay_cs=frame.delay_cs(),
      ).with_disposal(disposal=frame.disposal()),
    )
  }
  Animation::{ ..self, frames: result }
}