///|
/// A half-open range of frame indices.
pub(all) struct FrameRange {
  start : Int
  end : Int
} derive(Eq, Debug)

///|
pub fn FrameRange::new(start~ : Int, end~ : Int) -> FrameRange {
  {
    start: if start < 0 {
      0
    } else {
      start
    },
    end: if end < start {
      start
    } else {
      end
    },
  }
}

///|
pub fn FrameRange::start(self : FrameRange) -> Int {
  self.start
}

///|
pub fn FrameRange::end(self : FrameRange) -> Int {
  self.end
}

///|
pub fn FrameRange::length(self : FrameRange) -> Int {
  self.end - self.start
}

///|
pub fn FrameRange::contains(self : FrameRange, index : Int) -> Bool {
  index >= self.start && index < self.end
}

///|
pub(all) enum TimelinePolicy {
  KeepDurations
  Uniform
  PreserveTotal
  ClampToGif
} derive(Eq, Debug)

///|
pub(all) struct EditReport {
  input_frames : Int
  output_frames : Int
  removed_frames : Int
  duration_before : Int
  duration_after : Int
} derive(Debug)

///|
pub fn EditReport::input_frames(self : EditReport) -> Int {
  self.input_frames
}

///|
pub fn EditReport::output_frames(self : EditReport) -> Int {
  self.output_frames
}

///|
pub fn EditReport::removed_frames(self : EditReport) -> Int {
  self.removed_frames
}

///|
pub fn EditReport::duration_before(self : EditReport) -> Int {
  self.duration_before
}

///|
pub fn EditReport::duration_after(self : EditReport) -> Int {
  self.duration_after
}

///|
fn animation_report(before : Animation, after : Animation) -> EditReport {
  EditReport::{
    input_frames: before.frame_count(),
    output_frames: after.frame_count(),
    removed_frames: if before.frame_count() > after.frame_count() {
      before.frame_count() - after.frame_count()
    } else {
      0
    },
    duration_before: before.duration_cs(),
    duration_after: after.duration_cs(),
  }
}

///|
/// Remove a half-open frame range and return the resulting animation.
pub fn Animation::remove_range(
  self : Animation,
  range : FrameRange,
) -> Animation {
  let result : Array[Frame] = []
  for i, frame in self.frames {
    if !range.contains(i) {
      result.push(frame)
    }
  }
  Animation::{ ..self, frames: result }
}

///|
/// Replace a range with a new sequence while preserving logical dimensions.
pub fn Animation::replace_range(
  self : Animation,
  range : FrameRange,
  replacement : ArrayView[Frame],
) -> Animation {
  let result : Array[Frame] = []
  for i, frame in self.frames {
    if i == range.start() {
      for item in replacement {
        result.push(item)
      }
    }
    if !range.contains(i) {
      result.push(frame)
    }
  }
  if range.start() >= self.frames.length() {
    for item in replacement {
      result.push(item)
    }
  }
  Animation::{ ..self, frames: result }
}

///|
/// Insert a frame before the requested index. Out-of-range indices append.
pub fn Animation::insert_frame(
  self : Animation,
  index : Int,
  frame : Frame,
) -> Animation {
  let result : Array[Frame] = []
  let target = if index < 0 {
    0
  } else if index > self.frames.length() {
    self.frames.length()
  } else {
    index
  }
  for i, existing in self.frames {
    if i == target {
      result.push(frame)
    }
    result.push(existing)
  }
  if target == self.frames.length() {
    result.push(frame)
  }
  Animation::{ ..self, frames: result }
}

///|
/// Insert `count` transparent frames with the requested delay.
pub fn Animation::insert_gap(
  self : Animation,
  index : Int,
  count : Int,
  delay_cs~ : Int,
) -> Animation {
  let mut result = self
  let safe_count = if count < 0 { 0 } else { count }
  for _ in 0.. Animation {
  if range.length() <= 0 || range.start() >= self.frames.length() {
    self
  } else {
    let extracted : Array[Frame] = []
    let remaining : Array[Frame] = []
    for i, frame in self.frames {
      if range.contains(i) {
        extracted.push(frame)
      } else {
        remaining.push(frame)
      }
    }
    let safe_destination = if destination < 0 {
      0
    } else if destination > remaining.length() {
      remaining.length()
    } else {
      destination
    }
    let result : Array[Frame] = []
    for i, frame in remaining {
      if i == safe_destination {
        for item in extracted {
          result.push(item)
        }
      }
      result.push(frame)
    }
    if safe_destination == remaining.length() {
      for item in extracted {
        result.push(item)
      }
    }
    Animation::{ ..self, frames: result }
  }
}

///|
/// Split every frame into a fixed number of duration-preserving copies.
pub fn Animation::split_frames(self : Animation, pieces : Int) -> Animation {
  if pieces <= 1 {
    self
  } else {
    let result : Array[Frame] = []
    for frame in self.frames {
      let each = frame.delay_cs() / pieces
      let remainder = frame.delay_cs() - each * pieces
      for part in 0.. Animation {
  let result : Array[Frame] = []
  let mut previous : Canvas? = None
  for frame in self.frames {
    let keep = match previous {
      None => true
      Some(canvas) => changed_rect(canvas, frame.canvas(), threshold) is Some(_)
    }
    if keep {
      result.push(frame)
      previous = Some(frame.canvas())
    } else if result.length() > 0 {
      let last = result[result.length() - 1]
      result[result.length() - 1] = last.with_delay(
        delay_cs=last.delay_cs() + frame.delay_cs(),
      )
    }
  }
  Animation::{ ..self, frames: result }
}

///|
/// Merge adjacent frames when their rendered pixels are identical.
pub fn Animation::merge_equal_frames(self : Animation) -> Animation {
  if self.frames.length() <= 1 {
    self
  } else {
    let result : Array[Frame] = [self.frames[0]]
    for i in 1.. Animation {
  if tick_cs <= 0 || self.frames.length() == 0 {
    self
  } else {
    let result : Array[Frame] = []
    let mut assigned = 0
    for i, frame in self.frames {
      let delay = match policy {
        KeepDurations => frame.delay_cs()
        Uniform => tick_cs
        PreserveTotal =>
          if i == self.frames.length() - 1 {
            let target = self.duration_cs()
            if target > assigned {
              target - assigned
            } else {
              1
            }
          } else {
            let units = (frame.delay_cs() + tick_cs / 2) / tick_cs
            if units <= 0 {
              tick_cs
            } else {
              units * tick_cs
            }
          }
        ClampToGif =>
          if frame.delay_cs() > 65535 {
            65535
          } else if frame.delay_cs() < 0 {
            0
          } else {
            frame.delay_cs()
          }
      }
      assigned += delay
      result.push(frame.with_delay(delay_cs=delay))
    }
    Animation::{ ..self, frames: result }
  }
}

///|
/// Normalize a time range into a sequence of uniformly sampled snapshots.
pub fn Animation::resample(
  self : Animation,
  frame_count : Int,
  policy : TimelinePolicy,
) -> Animation {
  if frame_count <= 0 || self.frames.length() == 0 {
    Animation::{ ..self, frames: [] }
  } else {
    let result : Array[Frame] = []
    let duration = if self.duration_cs() <= 0 {
      self.frames.length()
    } else {
      self.duration_cs()
    }
    let delay = if policy == Uniform { duration / frame_count } else { 0 }
    for i in 0.. ()
        Some(frame) =>
          result.push(
            frame.with_delay(
              delay_cs=if delay <= 0 { frame.delay_cs() } else { delay },
            ),
          )
      }
    }
    Animation::{ ..self, frames: result }
  }
}

///|
/// A small non-destructive editor with undo snapshots for CLI and UI clients.
pub(all) struct AnimationEditor {
  mut current : Animation
  mut history : Array[Animation]
  mut cursor : Int
} derive(Debug)

///|
pub fn AnimationEditor::new(animation : Animation) -> AnimationEditor {
  { current: animation, history: [], cursor: 0 }
}

///|
pub fn AnimationEditor::current(self : AnimationEditor) -> Animation {
  self.current
}

///|
pub fn AnimationEditor::can_undo(self : AnimationEditor) -> Bool {
  self.history.length() > 0
}

///|
pub fn AnimationEditor::can_redo(self : AnimationEditor) -> Bool {
  self.cursor + 1 < self.history.length()
}

///|
fn AnimationEditor::record(
  self : AnimationEditor,
  next : Animation,
) -> AnimationEditor {
  self.history.push(self.current)
  self.cursor = self.history.length()
  self.current = next
  self
}

///|
pub fn AnimationEditor::remove(
  self : AnimationEditor,
  range : FrameRange,
) -> AnimationEditor {
  self.record(self.current.remove_range(range))
}

///|
pub fn AnimationEditor::insert(
  self : AnimationEditor,
  index : Int,
  frame : Frame,
) -> AnimationEditor {
  self.record(self.current.insert_frame(index, frame))
}

///|
pub fn AnimationEditor::transform(
  self : AnimationEditor,
  transform : AnimationTransform,
) -> AnimationEditor {
  self.record(self.current.map_transform(transform))
}

///|
pub fn AnimationEditor::undo(self : AnimationEditor) -> AnimationEditor {
  if self.history.length() == 0 {
    self
  } else {
    let previous = self.history[self.history.length() - 1]
    ignore(self.history.remove(self.history.length() - 1))
    self.current = previous
    self
  }
}

///|
pub fn AnimationEditor::build(self : AnimationEditor) -> Animation {
  self.current
}

///|
/// Return a report for an edit operation without hiding the before/after data.
pub fn edit_report(before : Animation, after : Animation) -> EditReport {
  animation_report(before, after)
}

///|
/// Split a timeline at every delay boundary and return the generated ranges.
pub fn Animation::ranges(self : Animation) -> Array[FrameRange] {
  let result : Array[FrameRange] = []
  for i in 0.. Animation {
  let replacement : Array[Frame] = []
  let start = if range.start() < 0 { 0 } else { range.start() }
  let end = if range.end() > self.frames.length() {
    self.frames.length()
  } else {
    range.end()
  }
  if start < end {
    for i in start.. Animation {
  if cycles <= 0 || self.frames.length() == 0 {
    Animation::{ ..self, frames: [] }
  } else {
    let result : Array[Frame] = []
    for _ in 0..