// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// Frames, and how each one replaces the one before.
//
// Three formats animate, and they agree on the shape even where they disagree on
// the arithmetic: a canvas, a rectangle per frame, a delay, a rule for what to
// do with the pixels underneath, and a rule for what to leave behind when the
// frame is over. APNG, GIF and WebP each spell those two rules differently but
// mean the same four cases.
//
// A frame keeps its own rectangle rather than a full canvas, because that is
// what the file holds and because a sixty-frame animation of a small moving
// sprite stores sixty small rectangles rather than sixty canvases. `compose`
// turns them into the full pictures a viewer shows.

///|
/// What to do with the frame's rectangle once the frame has been shown.
pub(all) enum Dispose {
  /// Leave it: the next frame draws over it.
  Keep
  /// Clear it to transparent black.
  Background
  /// Put back whatever was under it before it was drawn.
  Previous
} derive(Eq, Debug)

///|
pub extend Dispose with Eq::{equal, not_equal}

///|
pub extend Dispose with Debug::{to_repr}

///|
/// How a frame's pixels meet what is already on the canvas.
pub(all) enum Blend {
  /// Replace, alpha and all.
  Source
  /// Composite, the usual `over`.
  Over
} derive(Eq, Debug)

///|
pub extend Blend with Eq::{equal, not_equal}

///|
pub extend Blend with Debug::{to_repr}

///|
/// One frame: a picture, where it goes, and how long it stays.
pub(all) struct Frame {
  /// The pixels of this frame, already in a pixel format.
  image : Image
  /// Where the frame's top-left corner sits on the canvas.
  x : Int32
  /// Where the frame's top-left corner sits on the canvas.
  y : Int32
  /// How long the frame is shown, in milliseconds.
  delay : Int32
  /// What to do with the rectangle afterwards.
  dispose : Dispose
  /// How to lay the frame over what is there.
  blend : Blend
}

///|
/// A frame that covers the canvas, shown for a tenth of a second, drawn over
/// what is there.
pub fn Frame::new(
  image : Image,
  x? : Int32,
  y? : Int32,
  delay? : Int32,
  dispose? : Dispose,
  blend? : Blend,
) -> Frame {
  {
    image,
    x: x.unwrap_or(0),
    y: y.unwrap_or(0),
    delay: delay.unwrap_or(100),
    dispose: dispose.unwrap_or(Keep),
    blend: blend.unwrap_or(Over),
  }
}

///|
/// A sequence of frames over one canvas.
pub(all) struct Animation {
  /// The canvas width every frame is placed on.
  width : Int32
  /// The canvas height every frame is placed on.
  height : Int32
  /// How many times the animation plays. 0 means forever, which is what an
  /// animation without a repeat count means in all three formats.
  loops : Int32
  /// The frames, in order.
  frames : Array[Frame]
  /// Metadata that belongs to the animation as a whole.
  metadata : Metadata
}

///|
/// An animation over a canvas, with no frames yet unless given some.
pub fn Animation::new(
  width : Int32,
  height : Int32,
  frames : Array[Frame],
  loops? : Int32,
  metadata? : Metadata,
) -> Animation {
  {
    width,
    height,
    loops: loops.unwrap_or(0),
    frames,
    metadata: metadata.unwrap_or(Metadata::default()),
  }
}

///|
/// Every frame as a full canvas, in order, with each rule applied as it goes.
///
/// The result is what a viewer shows: the same length as `frames`, each one the
/// whole canvas. This is the expensive way to watch an animation and the only
/// way to see it out of context, which is what a test does.
///
/// The canvas is sixteen-bit, so that an eight-bit animation and a sixteen-bit
/// one come out of the same code without either losing its precision.
pub fn Animation::compose(self : Animation) -> Array[Image] {
  let width = self.width
  let height = self.height
  let canvas : FixedArray[Byte] = FixedArray::make(width * height * 8, b'\x00')
  let pictures : Array[Image] = []
  for frame in self.frames {
    let x0 = clamp(frame.x, 0, width)
    let y0 = clamp(frame.y, 0, height)
    let x1 = clamp(frame.x + frame.image.width, 0, width)
    let y1 = clamp(frame.y + frame.image.height, 0, height)
    let saved = if frame.dispose == Previous {
      copy_rect(canvas, width, x0, y0, x1, y1)
    } else {
      FixedArray::make(0, b'\x00')
    }
    draw(canvas, width, frame, x0, y0, x1, y1)
    pictures.push(
      assemble(width, height, Rgba16, canvas.copy(), false, self.metadata),
    )
    match frame.dispose {
      Keep => ()
      Background => clear_rect(canvas, width, x0, y0, x1, y1)
      Previous => restore_rect(canvas, width, x0, y0, x1, y1, saved)
    }
  }
  pictures
}

///|
fn clamp(value : Int32, low : Int32, high : Int32) -> Int32 {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
/// Composite one frame onto the canvas, in the rectangle that overlaps it.
fn draw(
  canvas : FixedArray[Byte],
  width : Int32,
  frame : Frame,
  x0 : Int32,
  y0 : Int32,
  x1 : Int32,
  y1 : Int32,
) -> Unit {
  if x1 <= x0 || y1 <= y0 {
    return
  }
  let picture = if frame.image.premultiplied {
    frame.image.to_straight()
  } else {
    frame.image
  }
  let bpp = picture.format.bytes()
  for y in y0.. Pixel {
  {
    r: word_of(canvas, at).to_double() / 65535.0,
    g: word_of(canvas, at + 2).to_double() / 65535.0,
    b: word_of(canvas, at + 4).to_double() / 65535.0,
    a: word_of(canvas, at + 6).to_double() / 65535.0,
  }
}

///|
/// Source over destination, straight alpha, in the units `Pixel` uses.
fn over(source : Pixel, target : Pixel) -> Pixel {
  let sa = source.a
  if sa >= 1.0 {
    return source
  }
  if sa <= 0.0 {
    return target
  }
  let da = target.a
  let out = sa + da * (1.0 - sa)
  if out <= 0.0 {
    return { r: 0.0, g: 0.0, b: 0.0, a: 0.0, }
  }
  {
    r: (source.r * sa + target.r * da * (1.0 - sa)) / out,
    g: (source.g * sa + target.g * da * (1.0 - sa)) / out,
    b: (source.b * sa + target.b * da * (1.0 - sa)) / out,
    a: out,
  }
}

///|
fn clear_rect(
  canvas : FixedArray[Byte],
  width : Int32,
  x0 : Int32,
  y0 : Int32,
  x1 : Int32,
  y1 : Int32,
) -> Unit {
  for y in y0.. FixedArray[Byte] {
  let columns = if x1 > x0 { x1 - x0 } else { 0 }
  let rows = if y1 > y0 { y1 - y0 } else { 0 }
  let saved : FixedArray[Byte] = FixedArray::make(columns * rows * 8, b'\x00')
  for row in 0.. Unit {
  let columns = if x1 > x0 { x1 - x0 } else { 0 }
  let rows = if y1 > y0 { y1 - y0 } else { 0 }
  for row in 0..