///|
/// Byte offsets for lightweight GIF inspection and resumable processing.
pub(all) struct GifIndexEntry {
  frame_index : Int
  marker_offset : Int
  descriptor_offset : Int
  extension_offset : Int?
} derive(Debug)

///|
pub fn GifIndexEntry::frame_index(self : GifIndexEntry) -> Int {
  self.frame_index
}

///|
pub fn GifIndexEntry::marker_offset(self : GifIndexEntry) -> Int {
  self.marker_offset
}

///|
pub fn GifIndexEntry::descriptor_offset(self : GifIndexEntry) -> Int {
  self.descriptor_offset
}

///|
pub fn GifIndexEntry::extension_offset(self : GifIndexEntry) -> Int? {
  self.extension_offset
}

///|
pub(all) struct GifByteIndex {
  bytes : Int
  header_bytes : Int
  trailer_offset : Int?
  frames : Array[GifIndexEntry]
  extension_count : Int
} derive(Debug)

///|
pub fn GifByteIndex::bytes(self : GifByteIndex) -> Int {
  self.bytes
}

///|
pub fn GifByteIndex::header_bytes(self : GifByteIndex) -> Int {
  self.header_bytes
}

///|
pub fn GifByteIndex::trailer_offset(self : GifByteIndex) -> Int? {
  self.trailer_offset
}

///|
pub fn GifByteIndex::frames(self : GifByteIndex) -> ArrayView[GifIndexEntry] {
  self.frames[:]
}

///|
pub fn GifByteIndex::extension_count(self : GifByteIndex) -> Int {
  self.extension_count
}

///|
fn skip_index_sub_blocks(data : Bytes, position : Int) -> Int? {
  let mut offset = position
  let mut done = false
  while !done && offset < data.length() {
    let size = data[offset].to_int()
    offset += 1
    if size == 0 {
      done = true
    } else if offset + size <= data.length() {
      offset += size
    } else {
      return None
    }
  }
  if done {
    Some(offset)
  } else {
    None
  }
}

///|
/// Build a conservative frame/extension index without decoding LZW payloads.
pub fn index_gif(data : Bytes) -> GifByteIndex raise GifError {
  if data.length() < 13 {
    raise UnexpectedEnd(context="GIF index header")
  }
  if !(data[0] == b'G' && data[1] == b'I' && data[2] == b'F') {
    raise InvalidSignature
  }
  let packed = data[10].to_int()
  let global_size = if (packed & 0x80) == 0 {
    0
  } else {
    3 * (1 << ((packed & 7) + 1))
  }
  let mut offset = 13 + global_size
  let entries : Array[GifIndexEntry] = []
  let mut extensions = 0
  let mut pending_extension : Int? = None
  let mut trailer : Int? = None
  while offset < data.length() {
    let marker_offset = offset
    let marker = data[offset].to_int()
    offset += 1
    match marker {
      0x3b => {
        trailer = Some(marker_offset)
        offset = data.length()
      }
      0x21 => {
        extensions += 1
        pending_extension = Some(marker_offset)
        if offset >= data.length() {
          raise UnexpectedEnd(context="GIF extension label")
        }
        let label = data[offset].to_int()
        offset += 1
        if label == 0xf9 || label == 0x01 {
          if offset >= data.length() {
            raise UnexpectedEnd(context="GIF extension block")
          }
          let size = data[offset].to_int()
          offset += 1 + size
          if offset > data.length() {
            raise UnexpectedEnd(context="GIF extension payload")
          }
          match skip_index_sub_blocks(data, offset) {
            None => raise UnexpectedEnd(context="GIF extension sub-blocks")
            Some(next) => offset = next
          }
        } else if label == 0xff {
          if offset >= data.length() {
            raise UnexpectedEnd(context="GIF application extension")
          }
          let size = data[offset].to_int()
          offset += 1 + size
          match skip_index_sub_blocks(data, offset) {
            None => raise UnexpectedEnd(context="GIF application payload")
            Some(next) => offset = next
          }
        } else {
          match skip_index_sub_blocks(data, offset) {
            None => raise UnexpectedEnd(context="GIF extension sub-blocks")
            Some(next) => offset = next
          }
        }
      }
      0x2c => {
        let descriptor = marker_offset
        if offset + 9 > data.length() {
          raise UnexpectedEnd(context="GIF image descriptor")
        }
        let packed_image = data[offset + 8].to_int()
        offset += 9
        if (packed_image & 0x80) != 0 {
          offset += 3 * (1 << ((packed_image & 7) + 1))
        }
        if offset >= data.length() {
          raise UnexpectedEnd(context="GIF LZW code size")
        }
        offset += 1
        match skip_index_sub_blocks(data, offset) {
          None => raise UnexpectedEnd(context="GIF image sub-blocks")
          Some(next) => offset = next
        }
        entries.push(GifIndexEntry::{
          frame_index: entries.length(),
          marker_offset,
          descriptor_offset: descriptor,
          extension_offset: pending_extension,
        })
        pending_extension = None
      }
      _ => raise InvalidBlock(context="unknown GIF marker in index")
    }
  }
  GifByteIndex::{
    bytes: data.length(),
    header_bytes: 13 + global_size,
    trailer_offset: trailer,
    frames: entries,
    extension_count: extensions,
  }
}

///|
/// Check whether an index points to a complete GIF stream.
pub fn GifByteIndex::is_complete(self : GifByteIndex) -> Bool {
  self.trailer_offset is Some(_) && self.frames.length() > 0
}

///|
/// Return frame offsets in decode order for diagnostic output.
pub fn GifByteIndex::frame_offsets(self : GifByteIndex) -> Array[Int] {
  let result : Array[Int] = []
  for entry in self.frames {
    result.push(entry.marker_offset())
  }
  result
}