///|
/// Timeline-wide canvas transform.
pub(all) enum AnimationTransform {
  Identity
  Grayscale
  Invert
  FlipHorizontal
  FlipVertical
} derive(Eq, Debug)

///|
fn transform_canvas(canvas : Canvas, transform : AnimationTransform) -> Canvas {
  match transform {
    Identity => canvas.copy()
    Grayscale => canvas.grayscale()
    Invert => canvas.invert()
    FlipHorizontal => canvas.flip_horizontal()
    FlipVertical => canvas.flip_vertical()
  }
}

///|
/// Apply the same canvas transform to every frame.
pub fn Animation::map_transform(
  self : Animation,
  transform : AnimationTransform,
) -> Animation {
  let frames : Array[Frame] = []
  for frame in self.frames {
    frames.push({ ..frame, canvas: transform_canvas(frame.canvas, transform) })
  }
  Animation::{ ..self, frames, }
}

///|
pub fn Animation::grayscale(self : Animation) -> Animation {
  self.map_transform(Grayscale)
}

///|
pub fn Animation::invert(self : Animation) -> Animation {
  self.map_transform(Invert)
}

///|
pub fn Animation::flip_horizontal(self : Animation) -> Animation {
  self.map_transform(FlipHorizontal)
}

///|
pub fn Animation::flip_vertical(self : Animation) -> Animation {
  self.map_transform(FlipVertical)
}

///|
/// Resize every frame and update the logical screen dimensions.
pub fn Animation::resize_nearest(
  self : Animation,
  width~ : Int,
  height~ : Int,
) -> Animation {
  let frames : Array[Frame] = []
  for frame in self.frames {
    frames.push({
      ..frame,
      canvas: frame.canvas.resize_nearest(width~, height~),
      left: 0,
      top: 0,
    })
  }
  Animation::{ ..self, width, height, frames }
}

///|
/// Crop every frame to the same logical rectangle.
pub fn Animation::crop(
  self : Animation,
  x~ : Int,
  y~ : Int,
  width~ : Int,
  height~ : Int,
) -> Animation {
  let frames : Array[Frame] = []
  for frame in self.frames {
    frames.push({
      ..frame,
      canvas: frame.canvas.crop(x~, y~, width~, height~),
      left: 0,
      top: 0,
    })
  }
  Animation::{ ..self, width, height, frames }
}

///|
/// Add a constant delay to every frame, clamping to GIF's practical range.
pub fn Animation::delay_all(self : Animation, delay_cs : Int) -> Animation {
  let safe_delay = if delay_cs < 0 {
    0
  } else if delay_cs > 65535 {
    65535
  } else {
    delay_cs
  }
  let frames : Array[Frame] = []
  for frame in self.frames {
    frames.push(frame.with_delay(delay_cs=frame.delay_cs + safe_delay))
  }
  Animation::{ ..self, frames, }
}

///|
/// Set every frame to one delay value.
pub fn Animation::with_delay(self : Animation, delay_cs : Int) -> Animation {
  let safe_delay = if delay_cs < 0 {
    0
  } else if delay_cs > 65535 {
    65535
  } else {
    delay_cs
  }
  let frames : Array[Frame] = []
  for frame in self.frames {
    frames.push(frame.with_delay(delay_cs=safe_delay))
  }
  Animation::{ ..self, frames, }
}

///|
/// Concatenate two animations with identical logical dimensions.
pub fn Animation::append(self : Animation, other : Animation) -> Animation {
  if self.width != other.width || self.height != other.height {
    panic()
  }
  let frames : Array[Frame] = []
  for frame in self.frames {
    frames.push(frame)
  }
  for frame in other.frames {
    frames.push(frame)
  }
  Animation::{ ..self, frames, }
}

///|
/// Generate a cross-fade animation from the last frame of `left` to the first
/// frame of `right`. `steps` is the number of intervals, so `steps=1` emits
/// the two endpoint frames.
pub fn crossfade(
  left : Animation,
  right : Animation,
  steps : Int,
  delay_cs? : Int = 5,
) -> Animation {
  if left.width != right.width ||
    left.height != right.height ||
    left.frames.length() == 0 ||
    right.frames.length() == 0 {
    panic()
  }
  let count = if steps < 1 { 1 } else { steps }
  let start = left.frames[left.frames.length() - 1]
  let finish = right.frames[0]
  let frames : Array[Frame] = []
  for i in 0..<(count + 1) {
    let canvas = start.canvas.fade_to(finish.canvas, i, count)
    frames.push(Frame::new(canvas~, delay_cs~))
  }
  Animation::{
    width: left.width,
    height: left.height,
    frames,
    loop_count: left.loop_count,
    background: left.background,
  }
}

///|
/// Insert a hold after each frame, keeping the original delay on the first copy.
pub fn Animation::hold(self : Animation, copies : Int) -> Animation {
  let result : Array[Frame] = []
  let count = if copies < 1 { 1 } else { copies }
  for frame in self.frames {
    for i in 0.. Animation {
  let snapshots = compose(self)
  let frames : Array[Frame] = []
  for i, canvas in snapshots {
    frames.push({ ..self.frames[i], canvas, left: 0, top: 0 })
  }
  Animation::{ ..self, frames, }
}

///|
/// Remove disposal history by flattening and making each frame persistent.
pub fn Animation::persistent(self : Animation) -> Animation {
  let flattened = self.flatten_frames()
  let frames : Array[Frame] = []
  for frame in flattened.frames {
    frames.push({ ..frame, disposal: Keep })
  }
  Animation::{ ..flattened, frames, }
}

///|
/// Return the first frame's canvas, if present.
pub fn Animation::first_canvas(self : Animation) -> Canvas? {
  if self.frames.length() == 0 {
    None
  } else {
    Some(self.frames[0].canvas)
  }
}

///|
/// Return the last frame's canvas, if present.
pub fn Animation::last_canvas(self : Animation) -> Canvas? {
  if self.frames.length() == 0 {
    None
  } else {
    Some(self.frames[self.frames.length() - 1].canvas)
  }
}