///|
/// Compose decoded grid tiles into one RGBA image.
///
/// Tile bytes are decoded by the caller through the normal AV1 path. This
/// helper only performs bounded placement and crops tiles at the output edge.
pub fn avif_reconstruct_grid(
  rows : Int,
  columns : Int,
  output_width : Int,
  output_height : Int,
  tiles : Array[Image],
) -> Image? {
  if rows <= 0 ||
    columns <= 0 ||
    output_width <= 0 ||
    output_height <= 0 ||
    rows > 256 ||
    columns > 256 ||
    tiles.length() != rows * columns {
    return None
  }
  if output_width > MAX_IMAGE_PIXELS / output_height {
    return None
  }
  let first = tiles[0]
  if first.width <= 0 || first.height <= 0 {
    return None
  }
  // Every cell has the same coded dimensions. Only the output rectangle is
  // cropped; it must include part of the final row and column, without holes.
  if (output_width - 1) / first.width != columns - 1 ||
    (output_height - 1) / first.height != rows - 1 {
    return None
  }
  for tile in tiles {
    if tile.width != first.width || tile.height != first.height {
      return None
    }
  }
  let out = Image::new(output_width, output_height)
  for index in 0.. Image? {
  if !decode_samples_allowed(samples, limits) {
    return None
  }
  let budget = decode_budget(limits)
  if !decode_reserve(
      budget,
      output_width,
      output_height,
      picture=false,
      padded=false,
    ) {
    return None
  }
  if rows <= 0 ||
    columns <= 0 ||
    rows > 256 ||
    columns > 256 ||
    samples.length() != rows * columns {
    return None
  }
  let images : Array[Image] = []
  for sample in samples {
    let frame = match
      av1_decode_frame_planes(sample, allow_monochrome=true, budget~) {
      Some(value) => value
      None => return None
    }
    let image = match av1_frame_to_rgba(frame) {
      Some(value) => value
      None => return None
    }
    images.push(image)
  }
  avif_reconstruct_grid(rows, columns, output_width, output_height, images)
}

///|
/// Decode a grid directly from a BMFF buffer and its parsed descriptor.
/// Each tile may contain one or more iloc extents; extents are concatenated
/// before passing the sample to the pure AV1 decoder.
fn avif_decode_grid_limited(
  data : Array[Byte],
  descriptor : AvifGridDescriptor,
  limits? : DecodeLimits? = None,
) -> Image? {
  if !decode_input_allowed(data.length(), limits) {
    return None
  }
  match avif_decode_grid_frame(data, descriptor, budget=decode_budget(limits)) {
    Some(frame) => {
      let colored = match avif_grid_descriptor(data) {
        Some(primary) if primary.item_id == descriptor.item_id => {
          let container = match avif_container_parse_limited(data, limits~) {
            Some(value) => value
            None => return None
          }
          avif_frame_apply_color(frame, container)
        }
        _ => frame
      }
      av1_frame_to_rgba(colored)
    }
    None => None
  }
}

///|
/// Decode and compose a grid directly from a BMFF buffer.  This convenience
/// entrypoint performs descriptor extraction and pixel reconstruction as one
/// operation, so callers cannot accidentally skip the `dimg` ordering or
/// extent validation step.
fn avif_decode_grid_auto_limited(
  data : Array[Byte],
  limits? : DecodeLimits? = None,
) -> Image? {
  if !decode_input_allowed(data.length(), limits) {
    return None
  }
  let descriptor = match avif_grid_descriptor(data) {
    Some(value) => value
    None => return None
  }
  if !avif_grid_properties_match(data, descriptor, limits~) {
    return None
  }
  avif_decode_grid_limited(data, descriptor, limits~)
}

///|
test "grid reconstruction crops edge tiles and preserves order" {
  let a = Image::new(2, 2)
  a.set_pixel(0, 0, b'\x11', b'\x00', b'\x00', b'\xFF')
  let b = Image::new(2, 2)
  b.set_pixel(0, 0, b'\x22', b'\x00', b'\x00', b'\xFF')
  let out = avif_reconstruct_grid(1, 2, 3, 2, [a, b]).unwrap()
  assert_eq(out.get_pixel(0, 0), (b'\x11', b'\x00', b'\x00', b'\xFF'))
  assert_eq(out.get_pixel(2, 0), (b'\x22', b'\x00', b'\x00', b'\xFF'))
}

///|
test "grid reconstruction rejects mismatched tile counts" {
  assert_true(avif_reconstruct_grid(2, 2, 4, 4, []) is None)
}

///|
test "grid reconstruction rejects unequal cells and uncovered output" {
  let tile = Image::new(2, 2)
  assert_true(
    avif_reconstruct_grid(1, 2, 4, 2, [tile, Image::new(1, 2)]) is None,
  )
  assert_true(avif_reconstruct_grid(1, 2, 5, 2, [tile, tile]) is None)
  assert_true(avif_reconstruct_grid(2, 1, 2, 5, [tile, tile]) is None)
  assert_true(avif_reconstruct_grid(1, 2, 2, 2, [tile, tile]) is None)
}

///|
/// Multiple iloc extents are concatenated before AV1 decoding.  Construction
/// method is metadata only at this stage; the descriptor has already resolved
/// each extent to a data-relative offset.
test "grid decoder concatenates split construction-method extents" {
  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 data : Array[Byte] = []
  for b in sample {
    data.push(b)
  }
  let descriptor : AvifGridDescriptor = {
    item_id: 1,
    rows: 1,
    columns: 1,
    output_width: 64,
    output_height: 64,
    tiles: [
      {
        item_id: 2,
        extents: [
          { offset: 0, length: 8, construction_method: 1, },
          { offset: 8, length: sample.length() - 8, construction_method: 1, },
        ],
      },
    ],
  }
  let image = avif_decode_grid_limited(data, descriptor).unwrap()
  assert_eq((image.width, image.height), (64, 64))
  assert_eq(image.get_pixel(0, 0).3, b'\xFF')
}

///|
test "grid tile decoder composes ordered AV1 samples" {
  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 image = avif_decode_grid_tiles_limited(1, 2, 128, 64, [sample, sample]).unwrap()
  assert_eq(image.width, 128)
  assert_eq(image.height, 64)
  assert_eq(image.get_pixel(0, 0).3, b'\xFF')
}