/// Temporal scan (AV1 §7.10.2.5) and temporal sample (§7.10.2.6): the motion
/// vectors that `motion_field_estimation` projected onto this frame's 8x8 grid
/// are looked up at the block's own cells and at three offsets just outside the
/// block, and pushed onto the candidate stack with weight 2.
///
/// The scan runs after the near-neighbour weighting and before the corner scan,
/// and only when the frame sets `use_ref_frame_mvs`. It is also the stage that
/// decides `zero_mv_context`: the block's own cell sets it to 1, and the
/// comparison against the global motion vector may set it back to 0.

///|
/// Scan the block's own cells, then the three offsets outside the block that
/// stay inside the same 64x64 superblock (§7.10.2.5).
///
/// Compound entries are complete vector pairs: both reference projections must
/// be present before either vector can contribute to the candidate stack.
fn av1_temporal_scan(
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  is_compound : Bool,
) -> Bool {
  let grid = match frame.temporal {
    Some(value) => value
    None => return false
  }
  let bw4 = block.width4
  let bh4 = block.height4
  let step_w4 = if bw4 >= 16 { 4 } else { 2 }
  let step_h4 = if bh4 >= 16 { 4 } else { 2 }
  let row_limit = if bh4 < 16 { bh4 } else { 16 }
  let col_limit = if bw4 < 16 { bw4 } else { 16 }
  let mut delta_row = 0
  while delta_row < row_limit {
    let mut delta_col = 0
    while delta_col < col_limit {
      av1_temporal_sample(
        stack,
        block,
        frame,
        grid,
        delta_row,
        delta_col,
        is_compound~,
      )
      delta_col = delta_col + step_w4
    }
    delta_row = delta_row + step_h4
  }
  // A block of at least 8x8 and less than 64x64 also probes the cells just
  // below it and to its right, as long as those stay in its own superblock.
  let allow_extension = bh4 >= 2 && bh4 < 16 && bw4 >= 2 && bw4 < 16
  if allow_extension {
    let offsets : Array[(Int, Int)] = [(bh4, -2), (bh4, bw4), (bh4 - 2, bw4)]
    for offset in offsets {
      if av1_tpl_sb_border(block, offset.0, offset.1) {
        av1_temporal_sample(
          stack,
          block,
          frame,
          grid,
          offset.0,
          offset.1,
          is_compound~,
        )
      }
    }
  }
  true
}

///|
/// Does the offset stay inside the block's own 64x64 superblock? Both axes are
/// checked against the 16 MI units of a superblock.
fn av1_tpl_sb_border(
  block : Av1MvBlock,
  delta_row : Int,
  delta_col : Int,
) -> Bool {
  let row = (block.mi_row & 15) + delta_row
  let col = (block.mi_col & 15) + delta_col
  row >= 0 && row < 16 && col >= 0 && col < 16
}

///|
/// Look up the projected vector at one offset and push it onto the stack
/// (§7.10.2.6). A compound pair is merged only when both list vectors match;
/// sharing the first vector alone does not make two candidates identical.
fn av1_temporal_sample(
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  grid : Av1MotionFieldGrid,
  delta_row : Int,
  delta_col : Int,
  is_compound? : Bool = false,
) -> Unit {
  let mv_row = (block.mi_row + delta_row) | 1
  let mv_col = (block.mi_col + delta_col) | 1
  if !av1_mv_inside(block, mv_row, mv_col) {
    return
  }
  let x8 = mv_col >> 1
  let y8 = mv_row >> 1
  if x8 < 0 || y8 < 0 || x8 >= grid.w8 || y8 >= grid.h8 {
    return
  }
  if delta_row == 0 && delta_col == 0 {
    stack.zero_mv_context = 1
  }
  let slot = block.refs[0] - av1_last_frame
  if slot < 0 || slot >= grid.mv_rows.length() {
    return
  }
  let index = y8 * grid.w8 + x8
  let cand_row = grid.mv_rows[slot][index]
  let cand_col = grid.mv_cols[slot][index]
  if cand_row == av1_mv_invalid {
    return
  }
  // The grid holds projected vectors, which carry one more bit of precision
  // than the frame is allowed to signal.
  let candidate = av1_lower_mv_precision(cand_row, cand_col, frame)
  let other = if is_compound {
    let other_slot = block.refs[1] - av1_last_frame
    if other_slot < 0 || other_slot >= grid.mv_rows.length() {
      return
    }
    let other_row = grid.mv_rows[other_slot][index]
    let other_col = grid.mv_cols[other_slot][index]
    if other_row == av1_mv_invalid {
      return
    }
    av1_lower_mv_precision(other_row, other_col, frame)
  } else {
    (0, 0)
  }
  if delta_row == 0 && delta_col == 0 {
    if (candidate.0 - stack.global_rows[0]).abs() >= 16 ||
      (candidate.1 - stack.global_cols[0]).abs() >= 16 ||
      (
        is_compound &&
        (
          (other.0 - stack.global_rows[1]).abs() >= 16 ||
          (other.1 - stack.global_cols[1]).abs() >= 16
        )
      ) {
      stack.zero_mv_context = 1
    } else {
      stack.zero_mv_context = 0
    }
  }
  for idx in 0..