///|
/// Summary values useful for timeline UIs and encoder diagnostics.
pub(all) struct AnimationStats {
  frame_count : Int
  duration_cs : Int
  transparent_frames : Int
  changed_pixels : Int
} derive(Debug)

///|
pub fn AnimationStats::frame_count(self : AnimationStats) -> Int {
  self.frame_count
}

///|
pub fn AnimationStats::duration_cs(self : AnimationStats) -> Int {
  self.duration_cs
}

///|
pub fn AnimationStats::transparent_frames(self : AnimationStats) -> Int {
  self.transparent_frames
}

///|
pub fn AnimationStats::changed_pixels(self : AnimationStats) -> Int {
  self.changed_pixels
}

///|
pub fn Frame::with_delay(self : Frame, delay_cs~ : Int) -> Frame {
  { ..self, delay_cs: if delay_cs < 0 { 0 } else { delay_cs } }
}

///|
/// Sum frame delays in GIF centiseconds.
pub fn Animation::duration_cs(self : Animation) -> Int {
  let mut duration = 0
  for frame in self.frames {
    duration += frame.delay_cs
  }
  duration
}

///|
/// Build a timeline from canvases with one delay value.
pub fn animation_from_canvases(
  canvases : ArrayView[Canvas],
  delay_cs~ : Int,
  loop_count? : Int,
) -> Animation {
  if canvases.length() == 0 {
    Animation::new(width=0, height=0)
  } else {
    let first = canvases[0]
    let mut animation = Animation::new(width=first.width, height=first.height)
    match loop_count {
      None => ()
      Some(value) =>
        animation = animation.with_loop_count(loop_count=Some(value))
    }
    for canvas in canvases {
      if canvas.width != first.width || canvas.height != first.height {
        panic()
      }
      animation = animation.add_frame(Frame::new(canvas~, delay_cs~))
    }
    animation
  }
}

///|
/// Return a timeline with frames in reverse display order.
pub fn Animation::reverse(self : Animation) -> Animation {
  let reversed : Array[Frame] = []
  let mut i = self.frames.length() - 1
  while i >= 0 {
    reversed.push(self.frames[i])
    i -= 1
  }
  Animation::{ ..self, frames: reversed }
}

///|
/// Repeat the timeline `times` times. A zero or negative count returns no frames.
pub fn Animation::repeat(self : Animation, times : Int) -> Animation {
  let result : Array[Frame] = []
  if times > 0 {
    for _ in 0.. Animation {
  let safe_start = if start < 0 {
    0
  } else if start > self.frames.length() {
    self.frames.length()
  } else {
    start
  }
  let safe_end = if end < safe_start {
    safe_start
  } else if end > self.frames.length() {
    self.frames.length()
  } else {
    end
  }
  let result : Array[Frame] = []
  for i in safe_start.. Int {
  pixel_difference(left.canvas, right.canvas)
}

///|
/// Return the smallest rectangle containing changed pixels, or `None` when equal.
pub fn changed_bounds(left : Canvas, right : Canvas) -> (Int, Int, Int, Int)? {
  if left.width != right.width || left.height != right.height {
    Some(
      (
        0,
        0,
        if right.width > left.width {
          right.width
        } else {
          left.width
        },
        if right.height > left.height {
          right.height
        } else {
          left.height
        },
      ),
    )
  } else {
    let mut min_x = left.width
    let mut min_y = left.height
    let mut max_x = -1
    let mut max_y = -1
    for y in 0.. max_x {
            max_x = x
          }
          if y > max_y {
            max_y = y
          }
        }
      }
    }
    if max_x < 0 {
      None
    } else {
      Some((min_x, min_y, max_x - min_x + 1, max_y - min_y + 1))
    }
  }
}

///|
/// Compute statistics without encoding the animation.
pub fn Animation::stats(self : Animation) -> AnimationStats {
  let mut transparent_frames = 0
  let mut changed_pixels = 0
  let mut previous : Canvas? = None
  for frame in self.frames {
    if frame.transparent is Some(_) || !frame.canvas.is_opaque() {
      transparent_frames += 1
    }
    match previous {
      None => changed_pixels += frame.canvas.width * frame.canvas.height
      Some(canvas) => changed_pixels += pixel_difference(canvas, frame.canvas)
    }
    previous = Some(frame.canvas)
  }
  AnimationStats::{
    frame_count: self.frames.length(),
    duration_cs: self.duration_cs(),
    transparent_frames,
    changed_pixels,
  }
}

///|
/// Mutable-style builder that keeps construction readable in CLI tools.
pub(all) struct AnimationBuilder {
  animation : Animation
} derive(Debug)

///|
pub fn AnimationBuilder::new(width~ : Int, height~ : Int) -> AnimationBuilder {
  { animation: Animation::new(width~, height~) }
}

///|
pub fn AnimationBuilder::background(
  self : AnimationBuilder,
  color : Rgba,
) -> AnimationBuilder {
  { animation: self.animation.with_background(background=color) }
}

///|
pub fn AnimationBuilder::loop_count(
  self : AnimationBuilder,
  value : Int?,
) -> AnimationBuilder {
  { animation: self.animation.with_loop_count(loop_count=value) }
}

///|
pub fn AnimationBuilder::frame(
  self : AnimationBuilder,
  frame : Frame,
) -> AnimationBuilder {
  { animation: self.animation.add_frame(frame) }
}

///|
pub fn AnimationBuilder::solid_frame(
  self : AnimationBuilder,
  color~ : Rgba,
  delay_cs~ : Int,
) -> AnimationBuilder {
  let canvas = Canvas::new(
    width=self.animation.width,
    height=self.animation.height,
    fill=color,
  )
  self.frame(Frame::new(canvas~, delay_cs~))
}

///|
pub fn AnimationBuilder::build(self : AnimationBuilder) -> Animation {
  self.animation
}