///|
/// Bounded AVIF grid and animation metadata extraction.
///
/// This module only parses ISO BMFF structure and returns item/sample extents.
/// It never invokes a browser codec or an external decoder.  Pixel reconstruction
/// remains in the AV1 pipeline; callers can feed the returned extents to it.
pub struct AvifGridTile {
  item_id : Int
  extents : Array[AvifGridExtent]
}

///|
pub struct AvifGridExtent {
  offset : Int
  length : Int
  construction_method : Int
}

///|
pub struct AvifGridDescriptor {
  item_id : Int
  rows : Int
  columns : Int
  output_width : Int
  output_height : Int
  tiles : Array[AvifGridTile]
}

///|
pub struct AvifAnimationFrame {
  index : Int
  offset : Int
  length : Int
  timestamp : Int
  duration : Int
  timescale : Int
}

///|
pub struct AvifAnimationDescriptor {
  timescale : Int
  frames : Array[AvifAnimationFrame]
}

///|
/// A successfully decoded animation sample with its track timing.
pub struct AvifAnimationPixelFrame {
  index : Int
  timestamp : Int
  duration : Int
  timescale : Int
  image : Image
}

///|
/// Decoded animation samples. Samples are supplied in the order returned by
/// `avif_animation_descriptor`; each sample is passed to the pure AV1 decoder.
pub struct AvifAnimationPixels {
  timescale : Int
  frames : Array[AvifAnimationPixelFrame]
}

///|
/// Decode an ordered set of AV1 animation samples using the native decoder.
/// A malformed or unsupported sample rejects the whole sequence so callers do
/// not accidentally present a partial animation.
fn avif_decode_animation_frames_limited(
  descriptor : AvifAnimationDescriptor,
  samples : Array[Array[Byte]],
  limits? : DecodeLimits? = None,
) -> AvifAnimationPixels? {
  if !decode_samples_allowed(samples, limits) {
    return None
  }
  let budget = decode_budget(limits)
  if descriptor.timescale <= 0 ||
    descriptor.frames.length() == 0 ||
    descriptor.frames.length() != samples.length() {
    return None
  }
  let frames : Array[AvifAnimationPixelFrame] = []
  // One stateful decoder for the whole sequence: later items may omit the
  // sequence header, and every item may predict from an earlier frame.
  let video = av1_video_decoder()
  let mut expected_timestamp = 0
  for i in 0.. value
      None => return None
    }
    // A BMFF sample is one temporal unit with one presentation time. Decode
    // every hidden/reference frame, but require one presented image for it.
    if displayed.length() != 1 {
      return None
    }
    let image = match av1_frame_to_rgba(displayed[0]) {
      Some(value) => value
      None => return None
    }
    frames.push({
      index: timing.index,
      timestamp: timing.timestamp,
      duration: timing.duration,
      timescale: timing.timescale,
      image,
    })
    // Guard the cumulative presentation timestamp against signed overflow.
    // A wrapped value would make subsequent monotonicity checks meaningless
    // and could select an incorrect frame during playback.
    let next_timestamp = expected_timestamp + timing.duration
    if next_timestamp < expected_timestamp {
      return None
    }
    expected_timestamp = next_timestamp
  }
  Some({ timescale: descriptor.timescale, frames, })
}

///|
/// Convenience entrypoint for callers that already extracted sample timing
/// tables. The arrays must match the sample order and length.
fn avif_decode_animation_samples_limited(
  timescale : Int,
  timestamps : Array[Int],
  durations : Array[Int],
  samples : Array[Array[Byte]],
  limits? : DecodeLimits? = None,
) -> AvifAnimationPixels? {
  if timescale <= 0 ||
    timestamps.length() != samples.length() ||
    durations.length() != samples.length() ||
    samples.length() == 0 {
    return None
  }
  let frames : Array[AvifAnimationFrame] = []
  for i in 0.. (AvifAnimationDescriptor, Array[Array[Byte]])? {
  let descriptor = match avif_animation_descriptor(data) {
    Some(value) => value
    None => return None
  }
  let samples : Array[Array[Byte]] = []
  for frame in descriptor.frames {
    if frame.offset < 0 ||
      frame.length < 0 ||
      frame.offset > data.length() - frame.length {
      return None
    }
    let sample : Array[Byte] = []
    for i in frame.offset..<(frame.offset + frame.length) {
      sample.push(data[i])
    }
    samples.push(sample)
  }
  Some((descriptor, samples))
}

///|
/// Parse and decode all animation samples from a self-contained BMFF buffer.
fn avif_decode_animation_limited(
  data : Array[Byte],
  limits? : DecodeLimits? = None,
) -> AvifAnimationPixels? {
  let decoded = match avif_decode_animation_planes(data, limits~) {
    Some(value) => value
    None => return None
  }
  let frames : Array[AvifAnimationPixelFrame] = []
  for i in 0.. av1_frame_to_rgba(decoded.color[i])
      Some(alpha) =>
        av1_frame_with_alpha(decoded.color[i], alpha[i], decoded.premultiplied)
    }
    let image = match converted {
      Some(value) => value
      None => return None
    }
    let timing = decoded.descriptor.frames[i]
    frames.push({
      index: i,
      timestamp: timing.timestamp,
      duration: timing.duration,
      timescale: timing.timescale,
      image,
    })
  }
  Some({ timescale: decoded.descriptor.timescale, frames, })
}

///|
/// Fully validated, synchronized track samples before output conversion.
priv struct AvifAnimationPlanes {
  descriptor : AvifAnimationDescriptor
  color : Array[Av1FramePlanes]
  alpha : Array[Av1FramePlanes]?
  premultiplied : Bool
}

///|
/// The native and RGBA APIs share track selection, stateful reconstruction,
/// exact time alignment, color metadata and alpha geometry validation.
fn avif_decode_animation_planes(
  data : Array[Byte],
  limits? : DecodeLimits? = None,
) -> AvifAnimationPlanes? {
  if !decode_input_allowed(data.length(), limits) {
    return None
  }
  let budget = decode_budget(limits)
  let (color, alpha) = match ga_animation_tracks(data) {
    Some(value) => value
    None => return None
  }
  let color_planes = match ga_animation_decode_track(data, color, budget~) {
    Some(value) => value
    None => return None
  }
  let alpha_planes = match alpha {
    None => None
    Some(track) => {
      if track.descriptor.frames.length() != color.descriptor.frames.length() {
        return None
      }
      // Compare media times exactly, including tracks using different scales.
      for i in 0.. Some(value)
        None => return None
      }
    }
  }
  let frames : Array[Av1FramePlanes] = []
  for i in 0.. color.premultiplied_by.contains(track.id)
      None => false
    },
  })
}

///|
/// Decode an AVIF sequence and select the image visible at `timestamp` in
/// track timescale units.  The full sequence is validated before selection;
/// malformed timing or pixel samples therefore cannot produce a partial frame.
fn avif_decode_animation_frame_limited(
  data : Array[Byte],
  timestamp : Int,
  limits? : DecodeLimits? = None,
) -> Image? {
  let animation = match avif_decode_animation_limited(data, limits~) {
    Some(value) => value
    None => return None
  }
  avif_animation_frame_at(animation, timestamp)
}

///|
/// Select the frame visible at a media timestamp. Playback loops at the
/// track duration; negative timestamps are rejected.
pub fn avif_animation_frame_at(
  animation : AvifAnimationPixels,
  timestamp : Int,
) -> Image? {
  if timestamp < 0 || animation.timescale <= 0 || animation.frames.length() == 0 {
    return None
  }
  let mut total = 0
  for frame in animation.frames {
    if frame.duration <= 0 {
      return None
    }
    let next_total = total + frame.duration
    if next_total < total {
      return None
    }
    total = next_total
  }
  if total <= 0 {
    return None
  }
  let t = timestamp % total
  for frame in animation.frames {
    if t < frame.timestamp + frame.duration {
      return Some(frame.image)
    }
  }
  Some(animation.frames[animation.frames.length() - 1].image)
}

/// Timing metadata must describe the same ordered sequence as the samples.
/// Accepting an out-of-order table would render valid pixels at the wrong
/// presentation time, which is especially hard to diagnose in a player.
/// This regression stays beside the private record fields so it can construct
/// an intentionally malformed descriptor without widening the public API.

///|
test "animation decoder rejects noncontiguous timing tables" {
  let sample : Array[Byte] = [
    b'\x0A', b'\x06', b'\x18', b'\x15', b'\x7F', b'\xFD', b'\xB0', b'\x08', b'\x32',
    b'\x13', b'\x18', b'\x00', b'\x00', b'\x00', b'\x50', b'\x00', b'\x00', b'\x00',
    b'\x0A', b'\x05', b'\x79', b'\x52', b'\x6E', b'\x43', b'\xD9', b'\x3A', b'\x46',
    b'\x02', b'\x28',
  ]
  let descriptor : AvifAnimationDescriptor = {
    timescale: 1000,
    frames: [
      {
        index: 0,
        offset: 0,
        length: sample.length(),
        timestamp: 0,
        duration: 10,
        timescale: 1000,
      },
      {
        index: 1,
        offset: 0,
        length: sample.length(),
        timestamp: 20,
        duration: 10,
        timescale: 1000,
      },
    ],
  }
  assert_true(
    avif_decode_animation_frames_limited(descriptor, [sample, sample]) is None,
  )
}

///|
/// Unknown iloc construction methods must fail closed.  In particular, method
/// 2 must not be interpreted as a file-relative extent (method 0).
test "iloc rejects unsupported construction method" {
  let data : Array[Byte] = [
    b'\x01', b'\x00', b'\x00', b'\x00', // version 1 + flags
     b'\x44', b'\x00', // 4-byte offset/length, no base
     b'\x00', b'\x01', // one item
     b'\x00', b'\x01', // item id
     b'\x00', b'\x02', // construction method 2
     b'\x00', b'\x00', // data reference index
     b'\x00', b'\x00', // zero extents
  ]
  let box : GaBox = {
    typ: ga_iloc,
    start: 0,
    payload: 0,
    end: data.length(),
    parent: -1,
  }
  let items : Array[GaItem] = [
    { id: 1, typ: ga_grid, method_id: 0, base: 0, offs: [], lens: [], },
  ]
  let rejected = try {
    ga_parse_iloc(data, box, items)
    false
  } catch {
    _ => true
  }
  assert_true(rejected)
}

///|
test "iloc resolves idat construction method extents" {
  let data : Array[Byte] = [
    b'\x01', b'\x00', b'\x00', b'\x00', b'\x44', b'\x00', b'\x00', b'\x01', b'\x00',
    b'\x01', b'\x00', b'\x01', // construction method 1
     b'\x00', b'\x00', b'\x00', b'\x01', b'\x00', b'\x00', b'\x00', b'\x05', b'\x00',
    b'\x00', b'\x00', b'\x03',
  ]
  let box : GaBox = {
    typ: ga_iloc,
    start: 0,
    payload: 0,
    end: data.length(),
    parent: -1,
  }
  let items : Array[GaItem] = [
    { id: 1, typ: ga_grid, method_id: 0, base: 0, offs: [], lens: [], },
  ]
  ga_parse_iloc(data, box, items)
  assert_eq(items[0].method_id, 1)
  assert_eq(items[0].offs[0], 5)
  assert_eq(items[0].lens[0], 3)
}

///|
priv suberror GridAnimError {
  Invalid
  Unsupported
}

///|
priv struct GaBox {
  typ : Int
  start : Int
  payload : Int
  end : Int
  parent : Int
}

///|
priv struct GaItem {
  id : Int
  typ : Int
  mut method_id : Int
  mut base : Int
  offs : Array[Int]
  lens : Array[Int]
}

///|
priv struct GaAnimationTrack {
  box_start : Int
  properties : Array[GaBox]
  id : Int
  enabled : Bool
  descriptor : AvifAnimationDescriptor
  config_obus : Array[Byte]
  config_depth : Int
  config_monochrome : Bool
  config_width : Int
  config_height : Int
  nclx_primaries : Int?
  nclx_transfer : Int?
  nclx_matrix : Int?
  nclx_full_range : Bool?
  aux_for : Array[Int]
  premultiplied_by : Array[Int]
  is_alpha : Bool
}

///|
fn ga_cstring_matches(
  data : Array[Byte],
  from : Int,
  end : Int,
  value : String,
) -> Bool raise GridAnimError {
  let chars = value.to_array()
  let mut matches = true
  for p in from..= chars.length() || byte != chars[p - from].to_int() {
      matches = false
    }
  }
  raise GridAnimError::Invalid
}

///|
fn ga_animation_track(
  data : Array[Byte],
  bs : Array[GaBox],
  trak : GaBox,
  legacy : Bool,
) -> GaAnimationTrack? raise GridAnimError {
  let ti = ga_idx(bs, trak.start)
  let mut id = 1
  let mut enabled = true
  match ga_find(bs, ti, 0x746B6864) { // tkhd
    Some(header) => {
      let version = ga_u8(data, header.payload, header.end)
      if version > 1 {
        raise GridAnimError::Unsupported
      }
      id = ga_u32(
        data,
        header.payload + (if version == 0 { 12 } else { 20 }),
        header.end,
      )
      enabled = (ga_u8(data, header.payload + 3, header.end) & 1) != 0
      if id <= 0 {
        raise GridAnimError::Invalid
      }
    }
    None => if !legacy { raise GridAnimError::Invalid }
  }
  let mdia = match ga_find(bs, ti, ga_mdia) {
    Some(value) => value
    None => raise GridAnimError::Invalid
  }
  let di = ga_idx(bs, mdia.start)
  match ga_find(bs, di, 0x68646C72) { // hdlr
    Some(handler) => {
      let kind = ga_u32(data, handler.payload + 8, handler.end)
      if kind == 0x61757876 { // auxv is never a primary picture.
        enabled = false
      } else if kind != 0x70696374 {
        return None
      }
    }
    None => if !legacy { raise GridAnimError::Invalid }
  }
  let minf = match ga_find(bs, di, ga_minf) {
    Some(value) => value
    None => raise GridAnimError::Invalid
  }
  let stbl = match ga_find(bs, ga_idx(bs, minf.start), ga_stbl) {
    Some(value) => value
    None => raise GridAnimError::Invalid
  }
  let mut config_depth = 0
  let mut config_monochrome = false
  let mut config_width = 0
  let mut config_height = 0
  let mut nclx_primaries : Int? = None
  let mut nclx_transfer : Int? = None
  let mut nclx_matrix : Int? = None
  let mut nclx_full_range : Bool? = None
  let config_obus : Array[Byte] = []
  let mut stored_properties : Array[GaBox] = []
  let mut is_alpha = false
  match ga_find(bs, ga_idx(bs, stbl.start), 0x73747364) { // stsd
    None => if !legacy { raise GridAnimError::Invalid }
    Some(stsd) => {
      if ga_u32(data, stsd.payload, stsd.end) != 0 ||
        ga_u32(data, stsd.payload + 4, stsd.end) != 1 {
        raise GridAnimError::Unsupported
      }
      let entries : Array[GaBox] = []
      ga_walk(data, stsd.payload + 8, stsd.end, -1, entries)
      if entries.length() != 1 || entries[0].typ != 0x61763031 {
        return None
      }
      let entry = entries[0]
      if entry.end - entry.payload < 78 {
        raise GridAnimError::Invalid
      }
      config_width = ga_u16(data, entry.payload + 24, entry.end)
      config_height = ga_u16(data, entry.payload + 26, entry.end)
      if config_width <= 0 || config_height <= 0 {
        raise GridAnimError::Invalid
      }
      let properties : Array[GaBox] = []
      ga_walk(data, entry.payload + 78, entry.end, -1, properties)
      stored_properties = properties
      for property in properties {
        if property.typ == 0x636F6C72 &&
          ga_u32(data, property.payload, property.end) == 0x6E636C78 {
          if nclx_primaries is Some(_) {
            raise GridAnimError::Invalid
          }
          nclx_primaries = Some(
            ga_u16(data, property.payload + 4, property.end),
          )
          nclx_transfer = Some(ga_u16(data, property.payload + 6, property.end))
          nclx_matrix = Some(ga_u16(data, property.payload + 8, property.end))
          nclx_full_range = Some(
            (ga_u8(data, property.payload + 10, property.end) & 0x80) != 0,
          )
        }
      }
      let config = match ga_find(properties, -1, 0x61763143) { // av1C
        Some(value) => value
        None => raise GridAnimError::Invalid
      }
      if config.end - config.payload < 4 ||
        ga_u8(data, config.payload, config.end) != 0x81 {
        raise GridAnimError::Invalid
      }
      let flags = ga_u8(data, config.payload + 2, config.end)
      config_depth = if (flags & 0x40) == 0 {
        8
      } else if (flags & 0x20) == 0 {
        10
      } else {
        12
      }
      config_monochrome = (flags & 0x10) != 0
      for p in (config.payload + 4).. ()
        Some(auxi) => {
          if ga_u32(data, auxi.payload, auxi.end) != 0 {
            raise GridAnimError::Unsupported
          }
          is_alpha = ga_cstring_matches(
            data,
            auxi.payload + 4,
            auxi.end,
            "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
          )
        }
      }
    }
  }
  let aux_for : Array[Int] = []
  let premultiplied_by : Array[Int] = []
  match ga_find(bs, ti, 0x74726566) { // tref
    None => ()
    Some(references) => {
      match ga_find(bs, ga_idx(bs, references.start), 0x6175786C) { // auxl
        None => ()
        Some(auxl) => {
          if auxl.end == auxl.payload || (auxl.end - auxl.payload) % 4 != 0 {
            raise GridAnimError::Invalid
          }
          for p = auxl.payload; p < auxl.end; p = p + 4 {
            let master = ga_u32(data, p, auxl.end)
            if master <= 0 || master == id {
              raise GridAnimError::Invalid
            }
            aux_for.push(master)
          }
        }
      }
      match ga_find(bs, ga_idx(bs, references.start), 0x7072656D) { // prem
        None => ()
        Some(prem) => {
          if prem.end == prem.payload || (prem.end - prem.payload) % 4 != 0 {
            raise GridAnimError::Invalid
          }
          for p = prem.payload; p < prem.end; p = p + 4 {
            let auxiliary = ga_u32(data, p, prem.end)
            premultiplied_by.push(auxiliary)
          }
        }
      }
    }
  }
  let descriptor = match ga_animation_track_descriptor(data, bs, trak) {
    Some(value) => value
    None => raise GridAnimError::Invalid
  }
  Some({
    id,
    enabled,
    box_start: trak.start,
    properties: stored_properties,
    descriptor,
    config_obus,
    config_depth,
    config_monochrome,
    config_width,
    config_height,
    nclx_primaries,
    nclx_transfer,
    nclx_matrix,
    nclx_full_range,
    aux_for,
    premultiplied_by,
    is_alpha,
  })
}

///|
/// Select by track identity and auxl association, never by physical trak order.
/// If several enabled primary picture tracks exist, the lowest track ID wins.
fn ga_animation_tracks(
  data : Array[Byte],
  diagnostic? : DecodeDiagnostic? = None,
) -> (GaAnimationTrack, GaAnimationTrack?)? {
  try {
    let bs : Array[GaBox] = []
    ga_walk(data, 0, data.length(), -1, bs)
    let moov = match ga_find(bs, -1, ga_moov) {
      Some(value) => value
      None => return None
    }
    let mi = ga_idx(bs, moov.start)
    let tracks = bs.filter(box => box.parent == mi && box.typ == ga_trak)
    let parsed : Array[GaAnimationTrack] = []
    for track in tracks {
      match ga_animation_track(data, bs, track, tracks.length() == 1) {
        None => ()
        Some(value) => {
          if parsed.any(previous => previous.id == value.id) {
            return None
          }
          parsed.push(value)
        }
      }
    }
    let mut color : GaAnimationTrack? = None
    for track in parsed {
      if track.enabled && track.aux_for.length() == 0 && !track.is_alpha {
        match color {
          None => color = Some(track)
          Some(previous) => if track.id < previous.id { color = Some(track) }
        }
      }
    }
    let color = match color {
      Some(value) => value
      None => return None
    }
    let mut alpha : GaAnimationTrack? = None
    for track in parsed {
      if track.is_alpha && track.aux_for.contains(color.id) {
        if alpha is Some(_) ||
          !track.config_monochrome ||
          track.config_depth != color.config_depth {
          return None
        }
        alpha = Some(track)
      }
    }
    Some((color, alpha))
  } catch {
    GridAnimError::Unsupported => {
      decode_trace_error(
        diagnostic,
        decode_error(
          Unsupported,
          "animation.container",
          "Unsupported animation container syntax",
        ),
      )
      None
    }
    _ => {
      decode_trace_error(
        diagnostic,
        decode_error(
          InvalidData,
          "animation.container",
          "Invalid animation track, timing or sample table",
        ),
      )
      None
    }
  }
}

///|
/// Metadata for the selected primary AV1 picture track.
pub fn avif_animation_descriptor(
  data : Array[Byte],
) -> AvifAnimationDescriptor? {
  match ga_animation_tracks(data) {
    Some((color, _)) => Some(color.descriptor)
    None => None
  }
}

///|
fn ga_animation_decode_track(
  data : Array[Byte],
  track : GaAnimationTrack,
  budget? : DecodeBudget? = None,
) -> Array[Av1FramePlanes]? {
  let cursor = ga_track_decoder(track)
  let frames : Array[Av1FramePlanes] = []
  let mut expected_timestamp = 0
  for i in 0.. 0x7FFFFFFF - expected_timestamp {
      return None
    }
    match ga_track_decode_one(cursor, data, i, budget) {
      Some(frame) => frames.push(frame)
      None => return None
    }
    expected_timestamp += timing.duration
  }
  Some(frames)
}

///|
let ga_meta = 0x6D657461

///|
let ga_iinf = 0x69696E66

///|
let ga_infe = 0x696E6665

///|
let ga_iloc = 0x696C6F63

///|
let ga_idat = 0x69646174

///|
let ga_iref = 0x69726566

///|
let ga_dimg = 0x64696D67

///|
let ga_grid = 0x67726964

///|
let ga_pitm = 0x7069746D

///|
let ga_moov = 0x6D6F6F76

///|
let ga_trak = 0x7472616B

///|
let ga_mdia = 0x6D646961

///|
let ga_minf = 0x6D696E66

///|
let ga_stbl = 0x7374626C

///|
let ga_mdhd = 0x6D646864

///|
let ga_stts = 0x73747473

///|
let ga_stsz = 0x7374737A

///|
let ga_stsc = 0x73747363

///|
let ga_stco = 0x7374636F

///|
let ga_co64 = 0x636F3634

///|
fn ga_u8(d : Array[Byte], p : Int, e : Int) -> Int raise GridAnimError {
  if p < 0 || p >= e || p >= d.length() {
    raise GridAnimError::Invalid
  }
  d[p].to_int()
}

///|
fn ga_u16(d : Array[Byte], p : Int, e : Int) -> Int raise GridAnimError {
  ga_u8(d, p, e) * 256 + ga_u8(d, p + 1, e)
}

///|
fn ga_u32(d : Array[Byte], p : Int, e : Int) -> Int raise GridAnimError {
  ga_u8(d, p, e) * 16777216 +
  ga_u8(d, p + 1, e) * 65536 +
  ga_u8(d, p + 2, e) * 256 +
  ga_u8(d, p + 3, e)
}

///|
fn ga_u64(d : Array[Byte], p : Int, e : Int) -> Int raise GridAnimError {
  for i in 0..<4 {
    if ga_u8(d, p + i, e) != 0 {
      raise GridAnimError::Unsupported
    }
  }
  if ga_u8(d, p + 4, e) > 127 {
    raise GridAnimError::Unsupported
  }
  ga_u32(d, p + 4, e)
}

///|
fn ga_idx(bs : Array[GaBox], start : Int) -> Int {
  for i in 0.. GaBox? {
  for b in bs {
    if b.parent == parent && b.typ == typ {
      return Some(b)
    }
  }
  None
}

///|
fn ga_walk(
  d : Array[Byte],
  from : Int,
  to : Int,
  parent : Int,
  out : Array[GaBox],
  depth? : Int = 0,
) -> Unit raise GridAnimError {
  if depth > AVIF_MAX_BOX_DEPTH {
    raise GridAnimError::Invalid
  }
  let mut p = from
  while p < to {
    if to - p < 8 {
      raise GridAnimError::Invalid
    }
    let s = ga_u32(d, p, to)
    let typ = ga_u32(d, p + 4, to)
    let mut h = 8
    let mut size = s
    if s == 1 {
      h = 16
      size = ga_u64(d, p + 8, to)
    } else if s == 0 {
      size = to - p
    }
    if size < h || size > to - p {
      raise GridAnimError::Invalid
    }
    let end = p + size
    let payload = p + h
    let bi = out.length()
    out.push({ typ, start: p, payload, end, parent, })
    // FullBox containers carry a four-byte version/flags prefix.
    if typ == ga_meta {
      if end - payload < 4 {
        raise GridAnimError::Invalid
      }
      ga_walk(d, payload + 4, end, bi, out, depth=depth + 1)
    } else if typ == ga_iinf {
      let v = ga_u8(d, payload, end)
      let q = if v == 0 { payload + 6 } else { payload + 8 }
      if q > end {
        raise GridAnimError::Invalid
      }
      ga_walk(d, q, end, bi, out, depth=depth + 1)
    } else if typ == ga_moov ||
      typ == ga_trak ||
      typ == ga_mdia ||
      typ == ga_minf ||
      typ == ga_stbl ||
      typ == 0x74726566 {
      ga_walk(d, payload, end, bi, out, depth=depth + 1)
    }
    p = end
  }
}

///|
fn ga_items(
  d : Array[Byte],
  bs : Array[GaBox],
  meta : GaBox,
) -> Array[GaItem] raise GridAnimError {
  let mi = ga_idx(bs, meta.start)
  let ii = match ga_find(bs, mi, ga_iinf) {
    Some(x) => x
    None => raise GridAnimError::Invalid
  }
  let xi = ga_idx(bs, ii.start)
  let out : Array[GaItem] = []
  for b in bs {
    if b.parent == xi && b.typ == ga_infe {
      let v = ga_u8(d, b.payload, b.end)
      if v != 2 && v != 3 {
        raise GridAnimError::Unsupported
      }
      let id = if v >= 3 {
        ga_u32(d, b.payload + 4, b.end)
      } else {
        ga_u16(d, b.payload + 4, b.end)
      }
      let tp = b.payload + (if v >= 3 { 10 } else { 8 })
      out.push({
        id,
        typ: ga_u32(d, tp, b.end),
        method_id: 0,
        base: 0,
        offs: [],
        lens: [],
      })
    }
  }
  out
}

///|
fn ga_item_idx(xs : Array[GaItem], id : Int) -> Int {
  for i in 0.. Unit raise GridAnimError {
  let p = b.payload
  let v = ga_u8(d, p, b.end)
  if v > 2 {
    raise GridAnimError::Unsupported
  }
  let a = ga_u8(d, p + 4, b.end)
  let os = a >> 4
  let ls = a & 15
  let z = ga_u8(d, p + 5, b.end)
  let bs = z >> 4
  let isz = if v >= 1 { z & 15 } else { 0 }
  if (os != 0 && os != 4 && os != 8) ||
    (ls != 0 && ls != 4 && ls != 8) ||
    (bs != 0 && bs != 4 && bs != 8) {
    raise GridAnimError::Unsupported
  }
  let mut q = p + 6
  let n = if v < 2 { ga_u16(d, q, b.end) } else { ga_u32(d, q, b.end) }
  q = q + (if v < 2 { 2 } else { 4 })
  for _ in 0..= 1 { ga_u16(d, q, b.end) & 15 } else { 0 }
    // ISO BMFF iloc defines construction_method 0 (file) and 1 (idat).
    // Treat all other values as unsupported instead of accidentally treating
    // them as method 0 and reading bytes from the wrong base.
    if method_id != 0 && method_id != 1 {
      raise GridAnimError::Unsupported
    }
    if v >= 1 {
      q = q + 2
    }
    if ga_u16(d, q, b.end) != 0 {
      raise GridAnimError::Unsupported
    }
    q = q + 2
    let base = if bs == 0 {
      0
    } else if bs == 4 {
      ga_u32(d, q, b.end)
    } else {
      ga_u64(d, q, b.end)
    }
    q = q + bs
    let ec = ga_u16(d, q, b.end)
    q = q + 2
    let xi = ga_item_idx(xs, id)
    for _ in 0..= 1 && isz != 0 {
        q = q + isz
      }
      let off = if os == 0 {
        0
      } else if os == 4 {
        ga_u32(d, q, b.end)
      } else {
        ga_u64(d, q, b.end)
      }
      q = q + os
      let len = if ls == 0 {
        0
      } else if ls == 4 {
        ga_u32(d, q, b.end)
      } else {
        ga_u64(d, q, b.end)
      }
      q = q + ls
      if xi >= 0 {
        xs[xi].method_id = method_id
        xs[xi].base = base
        xs[xi].offs.push(off)
        xs[xi].lens.push(len)
      }
    }
  }
}

///|
/// Extract bounded samples from one already-selected track.
fn ga_animation_track_descriptor(
  data : Array[Byte],
  bs : Array[GaBox],
  trak : GaBox,
) -> AvifAnimationDescriptor? {
  try {
    let ti = ga_idx(bs, trak.start)
    let mdia = match ga_find(bs, ti, ga_mdia) {
      Some(x) => x
      None => raise GridAnimError::Invalid
    }
    let di = ga_idx(bs, mdia.start)
    let mdhd = match ga_find(bs, di, ga_mdhd) {
      Some(x) => x
      None => raise GridAnimError::Invalid
    }
    let ver = ga_u8(data, mdhd.payload, mdhd.end)
    if ver > 1 {
      raise GridAnimError::Unsupported
    }
    let tspos = mdhd.payload + (if ver == 0 { 12 } else { 20 })
    let scale = ga_u32(data, tspos, mdhd.end)
    if scale <= 0 {
      raise GridAnimError::Invalid
    }
    let minf = match ga_find(bs, di, ga_minf) {
      Some(x) => x
      None => raise GridAnimError::Invalid
    }
    let ni = ga_idx(bs, minf.start)
    let stbl = match ga_find(bs, ni, ga_stbl) {
      Some(x) => x
      None => raise GridAnimError::Invalid
    }
    let si = ga_idx(bs, stbl.start)
    let stsz = match ga_find(bs, si, ga_stsz) {
      Some(x) => x
      None => raise GridAnimError::Invalid
    }
    let szp = stsz.payload + 4
    let sample_size = ga_u32(data, szp, stsz.end)
    let count = ga_u32(data, szp + 4, stsz.end)
    if count <= 0 || count > 4096 {
      raise GridAnimError::Unsupported
    }
    let sizes : Array[Int] = []
    if sample_size != 0 {
      for _ in 0.. x
      None =>
        match ga_find(bs, si, ga_co64) {
          Some(x) => x
          None => raise GridAnimError::Invalid
        }
    }
    let cp = co.payload + 4
    let nc = ga_u32(data, cp, co.end)
    // A chunk may contain more than one sample.  The common one-sample-per-
    // chunk layout has `nc == count`; for the compact layout emitted by a
    // number of AVIF writers a single chunk carries the complete sequence.
    // We can resolve that case from the sample sizes without requiring the
    // optional stsc table.  Reject other ambiguous mappings instead of
    // attaching pixels to guessed offsets.
    let stsc = ga_find(bs, si, ga_stsc)
    if nc <= 0 || (stsc is None && nc != count && nc != 1) {
      raise GridAnimError::Unsupported
    }
    // Resolve the standard sample-to-chunk table when present.  A single
    // chunk may contain several samples, and conversely a writer may group
    // multiple samples per chunk; relying on contiguous offsets alone is
    // incorrect for either layout.
    let sample_offsets : Array[Int] = []
    match stsc {
      None =>
        if nc == count {
          for i in 0.. {
        let ep = table.payload + 4
        let entries = ga_u32(data, ep, table.end)
        if entries <= 0 || entries > 4096 || table.end - ep < 4 + entries * 12 {
          raise GridAnimError::Invalid
        }
        let first_chunks : Array[Int] = []
        let per_chunk : Array[Int] = []
        for j in 0.. 0 && first_chunk <= first_chunks[j - 1]) {
            raise GridAnimError::Invalid
          }
          first_chunks.push(first_chunk)
          per_chunk.push(samples_per_chunk)
        }
        let mut sample_index = 0
        for chunk in 1..<(nc + 1) {
          let mut entry = -1
          for j in 0..= count {
              raise GridAnimError::Invalid
            }
            sample_offsets.push(offset)
            offset = offset + sizes[sample_index]
            sample_index = sample_index + 1
          }
        }
        if sample_index != count {
          raise GridAnimError::Invalid
        }
      }
    }
    let stts = match ga_find(bs, si, ga_stts) {
      Some(x) => x
      None => raise GridAnimError::Invalid
    }
    let tp = stts.payload + 4
    let ne = ga_u32(data, tp, stts.end)
    if ne <= 0 || ne > 4096 {
      raise GridAnimError::Unsupported
    }
    let durations : Array[Int] = []
    for i in 0.. count - durations.length() || dt <= 0 {
        raise GridAnimError::Invalid
      }
      for _ in 0.. data.length() - l {
        raise GridAnimError::Invalid
      }
      frames.push({
        index: i,
        offset: o,
        length: l,
        timestamp: t,
        duration: durations[i],
        timescale: scale,
      })
      let next_t = t + durations[i]
      if next_t < t {
        raise GridAnimError::Invalid
      }
      t = next_t
    }
    Some({ timescale: scale, frames, })
  } catch {
    _ => None
  }
}