///|
/// Metadata extension kinds that can be inserted into a GIF89a stream.
pub(all) enum GifMetadataKind {
  Comment
  Application(identifier~ : String, authentication~ : String)
} derive(Eq, Debug)

///|
/// A metadata record stored between the logical screen and image blocks.
pub(all) struct GifMetadata {
  kind : GifMetadataKind
  payload : Bytes
} derive(Debug)

///|
pub fn GifMetadata::comment(text : String) -> GifMetadata {
  { kind: Comment, payload: @utf8.encode(text) }
}

///|
pub fn GifMetadata::application(
  identifier : String,
  authentication : String,
  payload : Bytes,
) -> GifMetadata {
  { kind: Application(identifier~, authentication~), payload }
}

///|
pub fn GifMetadata::kind(self : GifMetadata) -> GifMetadataKind {
  self.kind
}

///|
pub fn GifMetadata::payload(self : GifMetadata) -> Bytes {
  self.payload
}

///|
fn metadata_ascii(value : String, width : Int) -> Array[Byte] {
  let result : Array[Byte] = []
  for byte in @utf8.encode(value) {
    if result.length() < width {
      result.push(byte)
    }
  }
  while result.length() < width {
    result.push(b' ')
  }
  result
}

///|
fn metadata_sub_blocks(payload : Bytes) -> Array[Byte] {
  let result : Array[Byte] = []
  let mut offset = 0
  while offset < payload.length() {
    let remaining = payload.length() - offset
    let size = if remaining > 255 { 255 } else { remaining }
    result.push(size.to_byte())
    for i in 0.. Array[Byte] {
  let result : Array[Byte] = []
  match metadata.kind {
    Comment => {
      result.push(0x21)
      result.push(0xfe)
      for byte in metadata_sub_blocks(metadata.payload) {
        result.push(byte)
      }
    }
    Application(identifier~, authentication~) => {
      result.push(0x21)
      result.push(0xff)
      result.push(0x0b)
      for byte in metadata_ascii(identifier, 8) {
        result.push(byte)
      }
      for byte in metadata_ascii(authentication, 3) {
        result.push(byte)
      }
      for byte in metadata_sub_blocks(metadata.payload) {
        result.push(byte)
      }
    }
  }
  result
}

///|
fn insert_metadata(bytes : Bytes, metadata : ArrayView[GifMetadata]) -> Bytes {
  let trailer = if bytes.length() == 0 { 0 } else { bytes.length() - 1 }
  let result : Array[Byte] = []
  for i in 0.. 0 {
    result.push(bytes[trailer])
  }
  Bytes::from_array(result[:])
}

///|
/// Encode an animation and insert comment/application extensions before the trailer.
pub fn encode_with_metadata(
  animation : Animation,
  metadata : ArrayView[GifMetadata],
  options? : EncodeOptions = EncodeOptions::default(),
) -> Bytes raise GifError {
  let encoded = encode_with_options(animation, options)
  insert_metadata(encoded, metadata)
}

///|
/// Add a UTF-8 comment to a freshly encoded animation.
pub fn encode_with_comment(
  animation : Animation,
  comment : String,
  options? : EncodeOptions = EncodeOptions::default(),
) -> Bytes raise GifError {
  let item = GifMetadata::comment(comment)
  encode_with_metadata(animation, [item][:], options~)
}

///|
/// Add a Netscape-compatible application payload without changing loop timing.
pub fn encode_with_application(
  animation : Animation,
  identifier : String,
  authentication : String,
  payload : Bytes,
  options? : EncodeOptions = EncodeOptions::default(),
) -> Bytes raise GifError {
  let item = GifMetadata::application(identifier, authentication, payload)
  encode_with_metadata(animation, [item][:], options~)
}

///|
/// Read all comment extension payloads without decoding pixel data.
pub fn extract_comments(data : Bytes) -> Array[String] raise GifError {
  let result : Array[String] = []
  let mut i = 0
  while i + 1 < data.length() {
    if data[i] == 0x21 && data[i + 1] == 0xfe {
      let payload : Array[Byte] = []
      i += 2
      while i < data.length() {
        let size = data[i].to_int()
        i += 1
        if size == 0 {
          break
        }
        if i + size > data.length() {
          raise UnexpectedEnd(context="comment extension")
        }
        for j in 0.. raise InvalidBlock(context="comment is not valid UTF-8")
      }
    } else {
      i += 1
    }
  }
  result
}

///|
/// Return the number of extension bytes added by a metadata record.
pub fn GifMetadata::encoded_length(self : GifMetadata) -> Int {
  metadata_extension(self).length()
}

///|
/// Validate the identifier widths required by an application extension.
pub fn GifMetadata::is_valid(self : GifMetadata) -> Bool {
  match self.kind {
    Comment => true
    Application(identifier~, authentication~) =>
      @utf8.encode(identifier).length() <= 8 &&
      @utf8.encode(authentication).length() <= 3
  }
}