///|
/// Compatibility helpers for common GIF producer and consumer conventions.
pub(all) enum DisposalHint {
  Preserve
  ClearToBackground
  RestorePrevious
} derive(Eq, Debug)

///|
pub fn DisposalHint::to_disposal(self : DisposalHint) -> Disposal {
  match self {
    Preserve => Keep
    ClearToBackground => Background
    RestorePrevious => Previous
  }
}

///|
pub fn disposal_hint(disposal : Disposal) -> DisposalHint {
  match disposal {
    Keep => Preserve
    Background => ClearToBackground
    Previous => RestorePrevious
  }
}

///|
/// Normalize a frame for viewers that reject zero-sized or negative offsets.
pub fn normalize_frame(
  frame : Frame,
  screen_width : Int,
  screen_height : Int,
) -> Frame? {
  let width = if screen_width < 1 { 1 } else { screen_width }
  let height = if screen_height < 1 { 1 } else { screen_height }
  let canvas = frame.canvas()
  let left = if frame.left < 0 { 0 } else { frame.left }
  let top = if frame.top < 0 { 0 } else { frame.top }
  let available_width = width - left
  let available_height = height - top
  if canvas.width <= 0 ||
    canvas.height <= 0 ||
    available_width <= 0 ||
    available_height <= 0 {
    None
  } else {
    let output_width = if canvas.width > available_width {
      available_width
    } else {
      canvas.width
    }
    let output_height = if canvas.height > available_height {
      available_height
    } else {
      canvas.height
    }
    Some(
      frame
      .with_canvas(
        canvas.crop(x=0, y=0, width=output_width, height=output_height),
      )
      .with_offset(left~, top~),
    )
  }
}

///|
/// Normalize all frames and preserve a useful placeholder when input is empty.
pub fn normalize_animation(animation : Animation) -> Animation {
  let result = Animation::new(
      width=if animation.width < 1 { 1 } else { animation.width },
      height=if animation.height < 1 { 1 } else { animation.height },
    )
    .with_loop_count(loop_count=animation.loop_count)
    .with_background(background=animation.background)
  for frame in animation.frames {
    match normalize_frame(frame, result.width, result.height) {
      None => ()
      Some(value) => ignore(result.add_frame(value))
    }
  }
  if result.frame_count() == 0 {
    ignore(
      result.add_frame(
        Frame::new(
          canvas=Canvas::new(
            width=result.width,
            height=result.height,
            fill=animation.background,
          ),
          delay_cs=0,
        ),
      ),
    )
  }
  result
}

///|
/// Construct a GIF89a-compatible animation with a safe default delay.
pub fn compatible_animation(
  width : Int,
  height : Int,
  frames : ArrayView[Canvas],
  delay_cs : Int,
) -> Animation {
  let result = Animation::new(
    width=if width < 1 { 1 } else { width },
    height=if height < 1 { 1 } else { height },
  )
  let delay = if delay_cs < 0 {
    0
  } else if delay_cs > 65535 {
    65535
  } else {
    delay_cs
  }
  for canvas in frames {
    ignore(result.add_frame(Frame::new(canvas~, delay_cs=delay)))
  }
  normalize_animation(result)
}

///|
/// Return whether all frames use metadata accepted by the encoder.
pub fn Animation::uses_compatible_metadata(self : Animation) -> Bool {
  for frame in self.frames {
    let (left, top) = frame.offset()
    if left < 0 ||
      top < 0 ||
      frame.canvas().width <= 0 ||
      frame.canvas().height <= 0 {
      return false
    }
  }
  true
}

///|
/// Convert centiseconds to a display-friendly millisecond duration.
pub fn centiseconds_to_milliseconds(value : Int) -> Int {
  if value < 0 {
    0
  } else {
    value * 10
  }
}

///|
/// Convert milliseconds to centiseconds using nearest integer rounding.
pub fn milliseconds_to_centiseconds(value : Int) -> Int {
  if value < 0 {
    0
  } else {
    (value + 5) / 10
  }
}