///|
fn disposal_bits(disposal : Disposal) -> Int {
  match disposal {
    Keep => 1
    Background => 2
    Previous => 3
  }
}

///|
fn min_lzw_code_size(bits : Int) -> Int {
  if bits < 2 {
    2
  } else {
    bits
  }
}

///|
fn validate_frame(animation : Animation, frame : Frame) -> Unit raise GifError {
  let canvas = frame.canvas
  let (left, top) = frame.offset()
  if canvas.width <= 0 ||
    canvas.height <= 0 ||
    left < 0 ||
    top < 0 ||
    left + canvas.width > animation.width ||
    top + canvas.height > animation.height {
    raise InvalidDimensions
  }
}

///|
fn write_graphic_control_extension(
  writer : GifWriter,
  frame : Frame,
  palette : Palette,
) -> Unit {
  let transparent_index = palette.transparent_index()
  let transparent_bit = match transparent_index {
    None => 0
    Some(_) => 1
  }
  let packed = (disposal_bits(frame.disposal) << 2) | transparent_bit
  writer.bytes([0x21, 0xf9, 0x04])
  writer.byte(packed)
  writer.u16(frame.delay_cs)
  let transparent_byte = match transparent_index {
    None => 0
    Some(index) => index
  }
  writer.byte(transparent_byte)
  writer.byte(0)
}

///|
fn write_image(
  writer : GifWriter,
  frame : Frame,
  quantized : QuantizedFrame,
  local_table : Bool,
) -> Unit raise GifError {
  let palette = quantized.palette()
  let (table_size, table_bits) = palette_table_size(palette)
  let (left, top) = frame.offset()
  let canvas = frame.canvas
  let packed = if local_table { 0x80 | (table_bits - 1) } else { 0 }
  writer.byte(0x2c)
  writer.u16(left)
  writer.u16(top)
  writer.u16(canvas.width)
  writer.u16(canvas.height)
  writer.byte(packed)
  if local_table {
    write_palette(writer, palette, table_size)
  }
  let payload = lzw_encode(quantized.indices(), min_lzw_code_size(table_bits))
  writer.byte(min_lzw_code_size(table_bits))
  write_sub_blocks(writer, payload)
}

///|
/// Encode an animation as a standards-compliant GIF89a byte sequence.
///
/// Each frame receives a local palette except the first frame, whose palette
/// becomes the logical screen's global table. This keeps frame-local colors
/// independent while retaining a valid global screen descriptor.
pub fn encode_with_options(
  animation : Animation,
  options : EncodeOptions,
) -> Bytes raise GifError {
  if animation.width <= 0 ||
    animation.height <= 0 ||
    animation.width > 65535 ||
    animation.height > 65535 ||
    animation.frames.length() == 0 {
    raise InvalidDimensions
  }
  let quantized : Array[QuantizedFrame] = []
  for frame in animation.frames {
    validate_frame(animation, frame)
    match frame.transparent {
      None =>
        quantized.push(quantize(frame.canvas, max_colors=options.max_colors))
      Some(color) =>
        quantized.push(
          quantize(
            frame.canvas,
            max_colors=options.max_colors,
            transparent=color,
          ),
        )
    }
  }
  let first_palette = quantized[0].palette()
  let (global_size, global_bits) = palette_table_size(first_palette)
  let writer = GifWriter::new()
  writer.bytes([b'G', b'I', b'F', b'8', b'9', b'a'])
  writer.u16(animation.width)
  writer.u16(animation.height)
  writer.byte(0x80 | (7 << 4) | (global_bits - 1))
  writer.byte(0)
  writer.byte(0)
  write_palette(writer, first_palette, global_size)
  if options.emit_loop_extension {
    match animation.loop_count {
      None => ()
      Some(loop_count) => {
        writer.bytes([0x21, 0xff, 0x0b])
        writer.bytes([
          b'N', b'E', b'T', b'S', b'C', b'A', b'P', b'E', b'2', b'.', b'0',
        ])
        writer.byte(3)
        writer.byte(1)
        writer.u16(
          if loop_count < 0 {
            0
          } else if loop_count > 65535 {
            65535
          } else {
            loop_count
          },
        )
        writer.byte(0)
      }
    }
  }
  for i, frame in animation.frames {
    let frame_palette = quantized[i].palette()
    let frame_for_encoding = if frame.delay_cs == 0 &&
      options.default_delay_cs > 0 {
      frame.with_delay(delay_cs=options.default_delay_cs)
    } else {
      frame
    }
    write_graphic_control_extension(writer, frame_for_encoding, frame_palette)
    write_image(writer, frame_for_encoding, quantized[i], i != 0)
  }
  writer.byte(0x3b)
  writer.finish()
}

///|
/// Encode with the standard 256-color palette budget and loop metadata.
pub fn encode(animation : Animation) -> Bytes raise GifError {
  encode_with_options(animation, EncodeOptions::default())
}