///|
priv struct GaTrackDecoder {
  track : GaAnimationTrack
  decoder : Av1VideoDecoder
  mut sequence_payload : Array[Byte]?
}

///|
fn ga_track_decoder(track : GaAnimationTrack) -> GaTrackDecoder {
  { track, decoder: av1_video_decoder(), sequence_payload: None, }
}

///|
fn ga_track_sample(
  data : Array[Byte],
  track : GaAnimationTrack,
  index : Int,
  limit : Int,
) -> Array[Byte]? {
  if index < 0 || index >= track.descriptor.frames.length() {
    return None
  }
  let timing = track.descriptor.frames[index]
  let sample = if index == 0 { track.config_obus.copy() } else { [] }
  if timing.offset < 0 ||
    timing.length < 0 ||
    timing.offset > data.length() - timing.length ||
    timing.length > limit - sample.length() {
    return None
  }
  for i in timing.offset..<(timing.offset + timing.length) {
    sample.push(data[i])
  }
  Some(sample)
}

///|
/// Shared by eager and incremental animation APIs; only returned-frame storage
/// differs. Both retain the same sequence/configuration and sample contracts.
fn ga_track_decode_one(
  cursor : GaTrackDecoder,
  data : Array[Byte],
  index : Int,
  budget : DecodeBudget?,
) -> Av1FramePlanes? {
  let track = cursor.track
  let timing = track.descriptor.frames[index]
  decode_context(budget, "animation.sample", Some(timing.offset))
  if timing.index != index ||
    timing.timescale != track.descriptor.timescale ||
    timing.duration <= 0 {
    return None
  }
  let limit = match budget {
    Some(value) => value.limits.max_input_bytes
    None => 0x7FFFFFFF
  }
  let sample = match ga_track_sample(data, track, index, limit) {
    Some(value) => value
    None => {
      decode_fail(
        budget,
        LimitExceeded,
        "Sample extents or assembled payload exceed the input boundary",
      )
      return None
    }
  }
  for payload in av1_obu_payloads(sample, 1) {
    match cursor.sequence_payload {
      None => cursor.sequence_payload = Some(payload)
      Some(previous) => if previous != payload { return None }
    }
  }
  let displayed = match
    av1_video_decode_temporal_unit_planes(
      cursor.decoder,
      sample,
      allow_monochrome=true,
      budget~,
    ) {
    Some(value) => value
    None => return None
  }
  decode_context(budget, "animation.sample", Some(timing.offset))
  if displayed.length() != 1 {
    return None
  }
  let frame = displayed[0]
  if track.config_width != 0 {
    let sequence = match cursor.decoder.sequence {
      Some(value) => value
      None => return None
    }
    if sequence.width != track.config_width ||
      sequence.height != track.config_height {
      return None
    }
  }
  if track.config_depth != 0 &&
    (
      frame.bit_depth != track.config_depth ||
      (frame.planes.length() == 1) != track.config_monochrome
    ) {
    return None
  }
  Some(frame)
}

///|
fn ga_track_index(
  data : Array[Byte],
  track : GaAnimationTrack,
  limit : Int,
) -> Result[(Av1SequenceInfo, Array[Byte], Array[Bool]), DecodeError] {
  let mut sequence : Av1SequenceInfo? = None
  let mut header : Array[Byte]? = None
  let keys : Array[Bool] = []
  let mut time = 0
  for i in 0.. 0x7FFFFFFF - time {
      return Err(
        decode_error(
          InvalidData,
          "animation.timing",
          "Track timestamps/durations are not contiguous",
        ),
      )
    }
    time += timing.duration
    let sample = match ga_track_sample(data, track, i, limit) {
      Some(value) => value
      None =>
        return Err(
          decode_error(
            LimitExceeded,
            "animation.sample",
            "Sample exceeds the supplied bytes or policy",
            offset=Some(timing.offset),
          ),
        )
    }
    let packets = match av1_obu_packets_checked(sample, limit~) {
      Ok(value) => value
      Err(error) =>
        return Err({
          ..error,
          byte_offset: None,
          context: "animation/" + error.context,
        })
    }
    let mut frames = 0
    let mut key = false
    for packet in packets {
      if packet.kind == 1 {
        match header {
          Some(previous) =>
            if previous != packet.payload {
              return Err(
                decode_error(
                  InvalidData,
                  "animation.sequence",
                  "Sequence header changes within a track",
                ),
              )
            }
          None => {
            header = Some(packet.payload)
            sequence = av1_parse_sequence_payload(packet.payload)
            if sequence is None {
              return Err(
                decode_error(
                  InvalidData,
                  "animation.sequence",
                  "Invalid sequence header",
                ),
              )
            }
          }
        }
      }
      if packet.kind == 3 || packet.kind == 6 {
        let seq = match sequence {
          Some(value) => value
          None =>
            return Err(
              decode_error(
                MissingSequence,
                "animation.sequence",
                "Track has no initial sequence header",
              ),
            )
        }
        if !av1_obu_selected(packet, seq) {
          continue
        }
        if packet.payload.is_empty() {
          return Err(
            decode_error(
              TruncatedInput,
              "animation.sample",
              "Empty frame header",
            ),
          )
        }
        frames += 1
        let first = packet.payload[0].to_int()
        key = seq.reduced_still_picture_header ||
          ((first & 128) == 0 && ((first >> 5) & 3) == 0 && (first & 16) != 0)
      }
    }
    // Conservative random access: exactly one selected, shown key picture.
    // Multi-picture units remain decodable sequentially or by replay from zero.
    keys.push(frames == 1 && key)
  }
  match (sequence, header) {
    (Some(seq), Some(payload)) => Ok((seq, payload, keys))
    _ =>
      Err(
        decode_error(
          MissingSequence,
          "animation.sequence",
          "Track has no sequence header",
        ),
      )
  }
}

///|
/// A lazy, independently owned AVIF animation session. Container/timing and
/// OBU envelopes are checked at construction; entropy/pixels are checked only
/// when consumed. Returned pictures survive next_frame(), seek_frame(), reset().
pub struct AvifAnimationDecoder {
  priv data : Array[Byte]
  priv limits : DecodeLimits
  priv mut color : GaTrackDecoder
  priv mut alpha : GaTrackDecoder?
  priv color_sequence : Av1SequenceInfo
  priv color_header : Array[Byte]
  priv alpha_sequence : Av1SequenceInfo?
  priv alpha_header : Array[Byte]?
  priv keys : Array[Bool]
  priv metadata_value : AvifMetadata
  priv mut next_index : Int
  priv mut failure : DecodeError?
}

///|
pub fn AvifAnimationDecoder::new(
  data : Array[Byte],
  limits? : DecodeLimits = DecodeLimits::default(),
) -> Result[AvifAnimationDecoder, DecodeError] {
  let budget = match decode_checked_budget(data.length(), limits) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let (color, alpha) = match
    ga_animation_tracks(data, diagnostic=budget.diagnostic) {
    Some(value) => value
    None => return Err(decode_failure(budget))
  }
  if color.descriptor.frames.is_empty() || color.descriptor.timescale <= 0 {
    return Err(
      decode_error(
        InvalidData,
        "animation.timing",
        "Empty track or invalid timescale",
      ),
    )
  }
  let (color_sequence, color_header, keys) = match
    ga_track_index(data, color, limits.max_input_bytes) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let mut alpha_sequence : Av1SequenceInfo? = None
  let mut alpha_header : Array[Byte]? = None
  if alpha is Some(track) {
    if track.descriptor.frames.length() != keys.length() {
      return Err(
        decode_error(
          InvalidData,
          "animation.alpha",
          "Color and alpha frame counts differ",
        ),
      )
    }
    let (seq, payload, alpha_keys) = match
      ga_track_index(data, track, limits.max_input_bytes) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    alpha_sequence = Some(seq)
    alpha_header = Some(payload)
    for i in 0.. value
    Err(error) => return Err(error)
  }
  Ok({
    data: data.copy(),
    limits,
    color: ga_track_decoder(color),
    alpha: alpha.map(ga_track_decoder),
    color_sequence,
    color_header,
    alpha_sequence,
    alpha_header,
    keys,
    metadata_value,
    next_index: 0,
    failure: None,
  })
}

///|
pub fn AvifAnimationDecoder::frame_count(self : AvifAnimationDecoder) -> Int {
  self.keys.length()
}

///|
pub fn AvifAnimationDecoder::position(self : AvifAnimationDecoder) -> Int {
  self.next_index
}

///|
pub fn AvifAnimationDecoder::metadata(
  self : AvifAnimationDecoder,
) -> AvifMetadata {
  avif_metadata_copy(self.metadata_value)
}

///|
pub fn AvifAnimationDecoder::frame_timing(
  self : AvifAnimationDecoder,
  index : Int,
) -> Result[AvifAnimationFrame, DecodeError] {
  if index < 0 || index >= self.keys.length() {
    return Err(
      decode_error(
        InvalidData,
        "animation.index",
        "Frame index is outside the track",
      ),
    )
  }
  Ok(self.color.track.descriptor.frames[index])
}

///|
pub fn AvifAnimationDecoder::is_keyframe(
  self : AvifAnimationDecoder,
  index : Int,
) -> Result[Bool, DecodeError] {
  match self.frame_timing(index) {
    Err(error) => Err(error)
    Ok(_) => Ok(self.keys[index])
  }
}

///|
/// Nearest conservatively indexed independent sample, shared by color/alpha.
/// None means seeking must replay from sample zero to preserve dependencies.
pub fn AvifAnimationDecoder::nearest_keyframe(
  self : AvifAnimationDecoder,
  index : Int,
) -> Result[Int?, DecodeError] {
  match self.frame_timing(index) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  for i = index; i >= 0; i = i - 1 {
    if self.keys[i] {
      return Ok(Some(i))
    }
  }
  Ok(None)
}

///|
fn AvifAnimationDecoder::reset_to(
  self : AvifAnimationDecoder,
  index : Int,
) -> Unit {
  self.color = ga_track_decoder(self.color.track)
  self.alpha = self.alpha.map(cursor => ga_track_decoder(cursor.track))
  if index > 0 {
    self.color.decoder.sequence = Some(self.color_sequence)
    self.color.sequence_payload = Some(self.color_header)
    if self.alpha is Some(cursor) {
      cursor.decoder.sequence = self.alpha_sequence
      cursor.sequence_payload = self.alpha_header
    }
  }
  self.next_index = index
  self.failure = None
}

///|
pub fn AvifAnimationDecoder::reset(self : AvifAnimationDecoder) -> Unit {
  self.reset_to(0)
}

///|
fn animation_decode_error(
  cursor : GaTrackDecoder,
  index : Int,
  budget : DecodeBudget,
  role : String,
) -> DecodeError {
  let error = decode_failure(budget)
  let prefix = if index == 0 { cursor.track.config_obus.length() } else { 0 }
  let at = cursor.track.descriptor.frames[index].offset.to_int64()
  let byte_offset = if error.context == "animation.sample" {
    error.byte_offset
  } else {
    match error.byte_offset {
      Some(offset) if offset >= prefix.to_int64() =>
        Some(at + offset - prefix.to_int64())
      _ => None
    }
  }
  {
    ..error,
    byte_offset,
    context: "animation." + role + "/" + error.context,
    message: "Sample \{index}: \{error.message}",
  }
}

///|
fn AvifAnimationDecoder::read_planes(
  self : AvifAnimationDecoder,
  index : Int,
  budget : DecodeBudget,
) -> Result[(Av1FramePlanes, Av1FramePlanes?, Bool), DecodeError] {
  let decoded = match
    ga_track_decode_one(self.color, self.data, index, Some(budget)) {
    Some(value) => value
    None =>
      return Err(animation_decode_error(self.color, index, budget, "color"))
  }
  let track = self.color.track
  let color = av1_frame_apply_nclx(
    decoded,
    track.nclx_primaries,
    track.nclx_transfer,
    track.nclx_matrix,
    track.nclx_full_range,
  )
  let mut alpha : Av1FramePlanes? = None
  let mut premultiplied = false
  if self.alpha is Some(cursor) {
    let plane = match
      ga_track_decode_one(cursor, self.data, index, Some(budget)) {
      Some(value) => value
      None => return Err(animation_decode_error(cursor, index, budget, "alpha"))
    }
    if plane.width != color.width ||
      plane.height != color.height ||
      plane.bit_depth != color.bit_depth ||
      plane.planes.length() != 1 {
      return Err(
        decode_error(
          InvalidData,
          "animation.alpha",
          "Alpha geometry or depth does not match color",
          offset=Some(cursor.track.descriptor.frames[index].offset),
        ),
      )
    }
    alpha = Some(plane)
    premultiplied = track.premultiplied_by.contains(cursor.track.id)
  }
  Ok((color, alpha, premultiplied))
}

///|
fn animation_native_frame(
  timing : AvifAnimationFrame,
  planes : (Av1FramePlanes, Av1FramePlanes?, Bool),
) -> AvifNativeAnimationFrame {
  let (color, alpha, premultiplied) = planes
  {
    index: timing.index,
    timestamp: timing.timestamp,
    duration: timing.duration,
    timescale: timing.timescale,
    image: {
      color: av1_frame_to_native(color),
      alpha: alpha.map(av1_frame_to_native),
      premultiplied,
    },
  }
}

///|
/// Ok(None) is end of sequence. A later corrupt sample can fail after earlier
/// frames were delivered; failure remains sticky until reset(). Work limits
/// apply to this operation, shared by the color and alpha reconstruction.
pub fn AvifAnimationDecoder::next_frame(
  self : AvifAnimationDecoder,
) -> Result[AvifNativeAnimationFrame?, DecodeError] {
  if self.failure is Some(error) {
    return Err(error)
  }
  if self.next_index == self.keys.length() {
    return Ok(None)
  }
  let budget = match decode_checked_budget(0, self.limits) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match self.read_planes(self.next_index, budget) {
    Err(error) => {
      self.failure = Some(error)
      Err(error)
    }
    Ok(planes) => {
      let frame = animation_native_frame(
        self.color.track.descriptor.frames[self.next_index],
        planes,
      )
      self.next_index += 1
      Ok(Some(frame))
    }
  }
}

///|
/// Decode from the nearest common independent key sample (or zero). All replay
/// work counts against one operation budget. The next read returns index + 1.
pub fn AvifAnimationDecoder::seek_frame(
  self : AvifAnimationDecoder,
  index : Int,
) -> Result[AvifNativeAnimationFrame, DecodeError] {
  if self.failure is Some(error) {
    return Err(error)
  }
  let start = match self.nearest_keyframe(index) {
    Err(error) => return Err(error)
    Ok(Some(value)) => value
    Ok(None) => 0
  }
  self.reset_to(start)
  let budget = match decode_checked_budget(0, self.limits) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  for i in start..<=index {
    match self.read_planes(i, budget) {
      Err(error) => {
        self.failure = Some(error)
        return Err(error)
      }
      Ok(planes) => {
        self.next_index = i + 1
        if i == index {
          return Ok(
            animation_native_frame(
              self.color.track.descriptor.frames[i],
              planes,
            ),
          )
        }
      }
    }
  }
  Err(
    decode_error(InvalidState, "animation.seek", "No target frame was decoded"),
  )
}