/// Inter-block motion-vector candidate derivation (AV1 §7.10.2).
///
/// The candidate stack is a pure function of the blocks already decoded around
/// the current one and reads no bitstream syntax. Its output decides both the
/// entropy contexts used by the inter mode tree and the predictor a NEWMV
/// residual is added to, so the scan order, the weights and the weight-based
/// sort are all normative.

///|
/// Inter block modes, continuing the intra numbering of AV1 §6.10.24. The
/// values are the indices into the mode CDFs, so they are bitstream-visible.
let av1_mode_nearest_mv : Int = 13

///|
let av1_mode_near_mv : Int = 14

///|
let av1_mode_global_mv : Int = 15

///|
let av1_mode_new_mv : Int = 16

///|
let av1_mode_nearest_nearest_mv : Int = 17

///|
let av1_mode_near_near_mv : Int = 18

///|
let av1_mode_nearest_new_mv : Int = 19

///|
let av1_mode_new_nearest_mv : Int = 20

///|
let av1_mode_near_new_mv : Int = 21

///|
let av1_mode_new_near_mv : Int = 22

///|
let av1_mode_global_global_mv : Int = 23

///|
let av1_mode_new_new_mv : Int = 24

///|
/// `MAX_REF_MV_STACK_SIZE`.
let av1_ref_mv_stack_size : Int = 8

///|
/// `REF_CAT_LEVEL`: the weight that marks a candidate found next to the block.
let av1_ref_cat_level : Int = 640

///|
/// `MV_BORDER`, in eighth-pel, added to the block's own extent when clamping.
let av1_mv_border : Int = 128

///|
/// Per-4x4 motion state of one frame. Every tile shares it: the candidate scans
/// read neighbours decoded earlier in raster order, including the row above.
pub(all) struct Av1MotionField {
  cols : Int
  rows : Int
  /// 4x4 extents of the block covering each unit, so a scan steps over a whole
  /// neighbour block rather than re-reading the same vector.
  width4 : Array[Int]
  height4 : Array[Int]
  y_modes : Array[Int]
  is_inter : Array[Bool]
  written : Array[Bool]
  /// Skip and skip-mode flags, which the transform-size contexts read back.
  skips : Array[Int]
  skip_modes : Array[Int]
  /// Luma transform extents and the two subpel filter choices, so a later block
  /// can measure this one's neighbours.
  tx_width : Array[Int]
  tx_height : Array[Int]
  /// Luma transform types, which a chroma transform of an inter block inherits.
  tx_types : Array[Int]
  interp_v : Array[Int]
  interp_h : Array[Int]
  /// Two reference names and two motion vectors per unit, interleaved by unit.
  refs : Array[Int]
  mv_rows : Array[Int]
  mv_cols : Array[Int]
  /// The compound symbols each block read (`compound_idx` / `comp_group_idx`),
  /// which the next compound block's contexts add up.
  compound_idxs : Array[Int]
  comp_group_idxs : Array[Int]
}

///|
/// An empty motion field. Intra blocks leave `is_inter` false, which is what the
/// candidate scans test before considering a neighbour at all.
pub fn av1_motion_field(cols : Int, rows : Int) -> Av1MotionField {
  let size = cols * rows
  {
    cols,
    rows,
    width4: Array::make(size, 1),
    height4: Array::make(size, 1),
    y_modes: Array::make(size, av1_intra_frame),
    is_inter: Array::make(size, false),
    written: Array::make(size, false),
    skips: Array::make(size, 0),
    skip_modes: Array::make(size, 0),
    tx_width: Array::make(size, 4),
    tx_height: Array::make(size, 4),
    tx_types: Array::make(size, 0),
    interp_v: Array::make(size, av1_interp_eighttap),
    interp_h: Array::make(size, av1_interp_eighttap),
    refs: Array::make(size * 2, av1_intra_frame),
    mv_rows: Array::make(size * 2, 0),
    mv_cols: Array::make(size * 2, 0),
    compound_idxs: Array::make(size, 0),
    comp_group_idxs: Array::make(size, 0),
  }
}

///|
fn av1_field_unit(field : Av1MotionField, row : Int, col : Int) -> Int {
  row * field.cols + col
}

///|
/// Record one decoded block over its 4x4 footprint. The specification writes
/// this before transform sizing and before prediction, so a block's own entries
/// are already visible when motion compensation reads them back.
pub fn av1_motion_field_store(
  field : Av1MotionField,
  row : Int,
  col : Int,
  width4 : Int,
  height4 : Int,
  y_mode : Int,
  refs : Array[Int],
  mv_rows : Array[Int],
  mv_cols : Array[Int],
  skip? : Int = 0,
  skip_mode? : Int = 0,
  interp? : Array[Int] = [],
  compound_idx? : Int = 0,
  comp_group_idx? : Int = 0,
) -> Unit {
  for r = row; r < row + height4 && r < field.rows; r = r + 1 {
    for c = col; c < col + width4 && c < field.cols; c = c + 1 {
      let unit = av1_field_unit(field, r, c)
      field.width4[unit] = width4
      field.height4[unit] = height4
      field.y_modes[unit] = y_mode
      field.is_inter[unit] = refs[0] > av1_intra_frame
      field.written[unit] = true
      field.skips[unit] = skip
      field.skip_modes[unit] = skip_mode
      field.compound_idxs[unit] = compound_idx
      field.comp_group_idxs[unit] = comp_group_idx
      if interp.length() >= 2 {
        field.interp_v[unit] = interp[0]
        field.interp_h[unit] = interp[1]
      }
      field.refs[unit * 2] = refs[0]
      field.refs[unit * 2 + 1] = refs[1]
      field.mv_rows[unit * 2] = mv_rows[0]
      field.mv_cols[unit * 2] = mv_cols[0]
      field.mv_rows[unit * 2 + 1] = mv_rows[1]
      field.mv_cols[unit * 2 + 1] = mv_cols[1]
    }
  }
}

///|
/// Record the luma transform extents of one block over its 4x4 footprint. The
/// transform-size contexts of later blocks measure neighbours against these
/// (AV1 §8.3, get_above_tx_width and get_left_tx_height).
fn av1_motion_field_store_tx(
  field : Av1MotionField,
  row : Int,
  col : Int,
  width4 : Int,
  height4 : Int,
  tx_width : Int,
  tx_height : Int,
) -> Unit {
  for r = row; r < row + height4 && r < field.rows; r = r + 1 {
    for c = col; c < col + width4 && c < field.cols; c = c + 1 {
      let unit = av1_field_unit(field, r, c)
      field.tx_width[unit] = tx_width
      field.tx_height[unit] = tx_height
    }
  }
}

///|
/// get_above_tx_width( row, col ) (AV1 §9.3.4): the transform width of the unit
/// above one position. `block_row` is the top MI row of the coding block being
/// sized, and a skipped inter block sitting on that edge reports its own block
/// width, because it coded no residual to subdivide. An unavailable edge reports
/// the largest possible width.
fn av1_above_tx_width(
  field : Av1MotionField,
  block_row : Int,
  row : Int,
  col : Int,
  avail_u : Bool,
) -> Int {
  if row <= 0 || col < 0 || col >= field.cols {
    return 64
  }
  let unit = (row - 1) * field.cols + col
  if row == block_row {
    if !avail_u {
      return 64
    }
    if field.skips[unit] != 0 && field.is_inter[unit] {
      return field.width4[unit] * 4
    }
  }
  field.tx_width[unit]
}

///|
/// get_left_tx_height( row, col ), the vertical twin of the above (AV1 §9.3.4).
fn av1_left_tx_height(
  field : Av1MotionField,
  block_col : Int,
  row : Int,
  col : Int,
  avail_l : Bool,
) -> Int {
  if row < 0 || row >= field.rows || col <= 0 {
    return 64
  }
  let unit = row * field.cols + (col - 1)
  if col == block_col {
    if !avail_l {
      return 64
    }
    if field.skips[unit] != 0 && field.is_inter[unit] {
      return field.height4[unit] * 4
    }
  }
  field.tx_height[unit]
}

///|
/// The frame-level state the candidate derivation reads. Naming it explicitly
/// keeps the derivation independent of the rest of the frame frame.
pub(all) struct Av1MvFrame {
  allow_high_precision_mv : Bool
  /// Temporal motion-vector projection is a separate stage; a frame that asks
  /// for it is refused rather than derived from an incomplete candidate list.
  use_ref_frame_mvs : Bool
  force_integer_mv : Int
  /// `is_motion_mode_switchable` gates the per-block `use_obmc` / `motion_mode`
  /// symbol of AV1 §5.11.27.
  is_motion_mode_switchable : Bool
  /// `allow_warped_motion` picks the three-symbol `motion_mode` branch over the
  /// binary `use_obmc` one.
  allow_warped_motion : Bool
  gm_type : Array[Int]
  gm_params : Array[Array[Int]]
  ref_frame_sign_bias : Array[Int]
  /// Coded frame dimensions, which `is_scaled` compares against each stored
  /// reference (AV1 §5.11.26: a reference is scaled when its visible extent
  /// differs from the current frame's).
  frame_width : Int
  frame_height : Int
  /// The 8x8 temporal grid this frame needs, or `None` when the frame does not
  /// set `use_ref_frame_mvs`.
  temporal : Av1MotionFieldGrid?
}

///|
/// Project a parsed frame header onto what motion derivation needs. A frame
/// that sets `use_ref_frame_mvs` also carries the 8x8 grid its temporal scan
/// reads; `av1_inter_frame_of` supplies it.
pub fn av1_mv_frame_of(
  hf : Av1FrameHeaderInfo,
  temporal? : Av1MotionFieldGrid? = None,
) -> Av1MvFrame {
  {
    allow_high_precision_mv: hf.allow_high_precision_mv,
    use_ref_frame_mvs: hf.use_ref_frame_mvs,
    force_integer_mv: hf.force_integer_mv,
    is_motion_mode_switchable: hf.is_motion_mode_switchable,
    allow_warped_motion: hf.allow_warped_motion,
    gm_type: hf.gm_type,
    gm_params: hf.gm_params,
    ref_frame_sign_bias: hf.ref_frame_sign_bias,
    frame_width: hf.frame_width,
    frame_height: hf.frame_height,
    temporal,
  }
}

///|
/// The block being predicted, plus the tile bounds that limit the scans.
pub(all) struct Av1MvBlock {
  mi_row : Int
  mi_col : Int
  /// 4x4-unit extent.
  width4 : Int
  height4 : Int
  /// Pixel extent, which sets the clamp margin.
  block_width : Int
  block_height : Int
  /// Reference names per list; `av1_none_frame` when the list is unused.
  refs : Array[Int]
  tile_row_start : Int
  tile_row_end : Int
  tile_col_start : Int
  tile_col_end : Int
}

///|
/// The derived candidate stack and the contexts the inter mode tree consumes.
pub(all) struct Av1MvStack {
  /// Flattened `[index * 2 + list]` pairs of (row, col) in eighth-pel.
  mv_rows : Array[Int]
  mv_cols : Array[Int]
  weights : Array[Int]
  mut count : Int
  mut newmv_count : Int
  mut found_match : Int
  mut close_matches : Int
  mut total_matches : Int
  mut zero_mv_context : Int
  mut newmv_context : Int
  mut refmv_context : Int
  drl_contexts : Array[Int]
  global_rows : Array[Int]
  global_cols : Array[Int]
}

///|
fn av1_mv_stack_new() -> Av1MvStack {
  {
    mv_rows: Array::make(av1_ref_mv_stack_size * 2, 0),
    mv_cols: Array::make(av1_ref_mv_stack_size * 2, 0),
    weights: Array::make(av1_ref_mv_stack_size, 0),
    count: 0,
    newmv_count: 0,
    found_match: 0,
    close_matches: 0,
    total_matches: 0,
    zero_mv_context: 0,
    newmv_context: 0,
    refmv_context: 0,
    drl_contexts: Array::make(av1_ref_mv_stack_size, 0),
    global_rows: Array::make(2, 0),
    global_cols: Array::make(2, 0),
  }
}

///|
fn av1_stack_mv(stack : Av1MvStack, index : Int, list : Int) -> (Int, Int) {
  (stack.mv_rows[index * 2 + list], stack.mv_cols[index * 2 + list])
}

///|
fn av1_stack_set_mv(
  stack : Av1MvStack,
  index : Int,
  list : Int,
  row : Int,
  col : Int,
) -> Unit {
  stack.mv_rows[index * 2 + list] = row
  stack.mv_cols[index * 2 + list] = col
}

///|
fn av1_mv_inside(block : Av1MvBlock, row : Int, col : Int) -> Bool {
  row >= block.tile_row_start &&
  row < block.tile_row_end &&
  col >= block.tile_col_start &&
  col < block.tile_col_end
}

///|
/// Does this mode carry a freshly coded motion vector? How often the answer is
/// yes feeds the new-mv context.
fn av1_mode_has_newmv(mode : Int) -> Bool {
  // The single-reference NEWMV and the five compound modes with a NEWMV
  // constituent in either list: NEARST_NEWMV, NEW_NEARSTMV, NEAR_NEWMV,
  // NEW_NEARMV and NEW_NEWMV.
  mode == av1_mode_new_mv ||
  mode == av1_mode_nearest_new_mv ||
  mode == av1_mode_new_nearest_mv ||
  mode == av1_mode_near_new_mv ||
  mode == av1_mode_new_near_mv ||
  mode == av1_mode_new_new_mv
}

///|
/// Drop the precision the frame is not allowed to signal (AV1 §7.10.2.10). The
/// two branches round in opposite directions on purpose: forced-integer motion
/// rounds away from zero, dropping only the high-precision bit rounds toward it.
fn av1_lower_mv_precision(
  row : Int,
  col : Int,
  frame : Av1MvFrame,
) -> (Int, Int) {
  if frame.allow_high_precision_mv {
    return (row, col)
  }
  let lower = fn(value : Int) -> Int {
    if frame.force_integer_mv != 0 {
      let magnitude = (value.abs() + 3) >> 3 << 3
      if value > 0 {
        magnitude
      } else {
        -magnitude
      }
    } else if (value & 1) != 0 {
      if value > 0 {
        value - 1
      } else {
        value + 1
      }
    } else {
      value
    }
  }
  (lower(row), lower(col))
}

///|
/// The block's motion implied by the frame-level warp model for one list (AV1
/// §7.10.2.1). Identity and intra references give a zero vector.
fn av1_setup_global_mv(
  list : Int,
  block : Av1MvBlock,
  frame : Av1MvFrame,
) -> (Int, Int) {
  let name = block.refs[list]
  if name <= av1_intra_frame {
    return (0, 0)
  }
  let typ = frame.gm_type[name]
  if typ == av1_gm_identity {
    return (0, 0)
  }
  let params = frame.gm_params[name]
  if typ == av1_gm_translation {
    return av1_lower_mv_precision(
      params[0] >> (av1_warped_model_prec_bits - 3),
      params[1] >> (av1_warped_model_prec_bits - 3),
      frame,
    )
  }
  // Rotate-zoom and affine are evaluated at the block centre, then lowered to
  // the precision the frame allows.
  let centre_x = block.mi_col * 4 + block.block_width / 2 - 1
  let centre_y = block.mi_row * 4 + block.block_height / 2 - 1
  let bias = 1 << av1_warped_model_prec_bits
  let xc = (params[2] - bias) * centre_x + params[3] * centre_y + params[0]
  let yc = params[4] * centre_x + (params[5] - bias) * centre_y + params[1]
  let raw = if frame.allow_high_precision_mv {
    (
      av1_mc_round2_signed(yc, av1_warped_model_prec_bits - 3),
      av1_mc_round2_signed(xc, av1_warped_model_prec_bits - 3),
    )
  } else {
    (
      av1_mc_round2_signed(yc, av1_warped_model_prec_bits - 2) * 2,
      av1_mc_round2_signed(xc, av1_warped_model_prec_bits - 2) * 2,
    )
  }
  av1_lower_mv_precision(raw.0, raw.1, frame)
}

///|
/// Push one candidate, merging into an existing entry that already holds the
/// same vector (AV1 §7.10.2.5). Sets `found_match` when anything was found.
fn av1_search_stack(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  mv_row : Int,
  mv_col : Int,
  cand_list : Int,
  weight : Int,
) -> Unit {
  let unit = av1_field_unit(field, mv_row, mv_col)
  let mode = field.y_modes[unit]
  let large = field.width4[unit] >= 2 && field.height4[unit] >= 2
  let candidate = if (
      mode == av1_mode_global_mv || mode == av1_mode_global_global_mv
    ) &&
    frame.gm_type[block.refs[0]] > av1_gm_translation &&
    large {
    (stack.global_rows[0], stack.global_cols[0])
  } else {
    (field.mv_rows[unit * 2 + cand_list], field.mv_cols[unit * 2 + cand_list])
  }
  // Precision is lowered on the candidates, never on a decoded MV: the coder
  // already cannot produce an unallowed one.
  let mv = av1_lower_mv_precision(candidate.0, candidate.1, frame)
  if av1_mode_has_newmv(mode) {
    stack.newmv_count = stack.newmv_count + 1
  }
  stack.found_match = 1
  for idx in 0.. Unit {
  let unit = av1_field_unit(field, mv_row, mv_col)
  let mode = field.y_modes[unit]
  // Affine/rotzoom global motion varies with block centre. As in the single
  // reference search, replace the neighbour's stored vector for each list
  // before lowering precision and merging candidate pairs (AV1 7.10.2.5).
  let global = mode == av1_mode_global_global_mv &&
    field.width4[unit] >= 2 &&
    field.height4[unit] >= 2
  let first_candidate = if global &&
    frame.gm_type[block.refs[0]] > av1_gm_translation {
    (stack.global_rows[0], stack.global_cols[0])
  } else {
    (field.mv_rows[unit * 2], field.mv_cols[unit * 2])
  }
  let second_candidate = if global &&
    frame.gm_type[block.refs[1]] > av1_gm_translation {
    (stack.global_rows[1], stack.global_cols[1])
  } else {
    (field.mv_rows[unit * 2 + 1], field.mv_cols[unit * 2 + 1])
  }
  let first = av1_lower_mv_precision(
    first_candidate.0,
    first_candidate.1,
    frame,
  )
  let second = av1_lower_mv_precision(
    second_candidate.0,
    second_candidate.1,
    frame,
  )
  stack.found_match = 1
  for idx in 0.. Unit {
  let unit = av1_field_unit(field, mv_row, mv_col)
  if !field.is_inter[unit] {
    return
  }
  if !is_compound {
    // A neighbour may hold this block's reference in either list; each match
    // contributes its own candidate.
    for cand_list = 0; cand_list < 2; cand_list = cand_list + 1 {
      if field.refs[unit * 2 + cand_list] == block.refs[0] {
        av1_search_stack(
          field, stack, block, frame, mv_row, mv_col, cand_list, weight,
        )
      }
    }
  } else if field.refs[unit * 2] == block.refs[0] &&
    field.refs[unit * 2 + 1] == block.refs[1] {
    av1_compound_search_stack(
      field, stack, block, frame, mv_row, mv_col, weight,
    )
  }
}

///|
/// Walk one row of neighbours above or below the block (AV1 §7.10.2.2). The
/// far offsets are shifted by the block's own alignment so that a wide block
/// reaches the same source samples its neighbours were derived from.
fn av1_scan_row(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  delta_row : Int,
  is_compound : Bool,
) -> Unit {
  let mut end4 = block.width4
  let room_cols = field.cols - block.mi_col
  if end4 > room_cols {
    end4 = room_cols
  }
  if end4 > 16 {
    end4 = 16
  }
  let far = delta_row.abs() > 1
  let row_shift = if far { delta_row + (block.mi_row & 1) } else { delta_row }
  let col_shift = if far { 1 - (block.mi_col & 1) } else { 0 }
  let mut i = 0
  while i < end4 {
    let mv_row = block.mi_row + row_shift
    let mv_col = block.mi_col + col_shift + i
    if !av1_mv_inside(block, mv_row, mv_col) {
      break
    }
    let unit = av1_field_unit(field, mv_row, mv_col)
    let mut length = block.width4
    if field.width4[unit] < length {
      length = field.width4[unit]
    }
    if far && length < 2 {
      length = 2
    }
    if block.width4 >= 16 && length < 4 {
      length = 4
    }
    av1_add_ref_mv_candidate(
      field,
      stack,
      block,
      frame,
      mv_row,
      mv_col,
      is_compound,
      length * 2,
    )
    i = i + length
  }
}

///|
/// Walk one column of neighbours to the left or right (AV1 §7.10.2.3).
fn av1_scan_col(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  delta_col : Int,
  is_compound : Bool,
) -> Unit {
  let room = field.rows - block.mi_row
  let mut end4 = block.height4
  if end4 > room {
    end4 = room
  }
  if end4 > 16 {
    end4 = 16
  }
  let far = delta_col.abs() > 1
  let col_shift = if far { delta_col + (block.mi_col & 1) } else { delta_col }
  let row_shift = if far { 1 - (block.mi_row & 1) } else { 0 }
  let mut i = 0
  while i < end4 {
    let mv_row = block.mi_row + row_shift + i
    let mv_col = block.mi_col + col_shift
    if !av1_mv_inside(block, mv_row, mv_col) {
      break
    }
    let unit = av1_field_unit(field, mv_row, mv_col)
    let mut length = block.height4
    if field.height4[unit] < length {
      length = field.height4[unit]
    }
    if far && length < 2 {
      length = 2
    }
    if block.height4 >= 16 && length < 4 {
      length = 4
    }
    av1_add_ref_mv_candidate(
      field,
      stack,
      block,
      frame,
      mv_row,
      mv_col,
      is_compound,
      length * 2,
    )
    i = i + length
  }
}

///|
fn av1_scan_point(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  delta_row : Int,
  delta_col : Int,
  is_compound : Bool,
) -> Unit {
  let mv_row = block.mi_row + delta_row
  let mv_col = block.mi_col + delta_col
  if !av1_mv_inside(block, mv_row, mv_col) {
    return
  }
  let unit = av1_field_unit(field, mv_row, mv_col)
  if !field.written[unit] {
    return
  }
  av1_add_ref_mv_candidate(
    field, stack, block, frame, mv_row, mv_col, is_compound, 4,
  )
}

///|
/// The extra search used when the neighbourhood yielded fewer than two
/// candidates (AV1 §7.10.2.12). A single-reference block falls back to any
/// inter neighbour's vector, sign-flipped when it looks the other way in time,
/// and finally to the global-motion vector.
fn av1_extra_search(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  is_compound : Bool,
) -> Unit {
  let mut w4 = block.width4
  if w4 > 16 {
    w4 = 16
  }
  let room_cols = field.cols - block.mi_col
  if w4 > room_cols {
    w4 = room_cols
  }
  let mut h4 = block.height4
  if h4 > 16 {
    h4 = 16
  }
  let room_rows = field.rows - block.mi_row
  if h4 > room_rows {
    h4 = room_rows
  }
  let num4x4 = if w4 < h4 { w4 } else { h4 }
  for pass in 0..<2 {
    let mut idx = 0
    while idx < num4x4 && stack.count < 2 {
      let mv_row = if pass == 0 { block.mi_row - 1 } else { block.mi_row + idx }
      let mv_col = if pass == 0 { block.mi_col + idx } else { block.mi_col - 1 }
      if !av1_mv_inside(block, mv_row, mv_col) {
        break
      }
      let unit = av1_field_unit(field, mv_row, mv_col)
      av1_add_extra_mv_candidate(field, stack, block, frame, unit, is_compound)
      idx = idx +
        (if pass == 0 { field.width4[unit] } else { field.height4[unit] })
    }
  }
  if !is_compound {
    for idx = stack.count; idx < 2; idx = idx + 1 {
      av1_stack_set_mv(
        stack,
        idx,
        0,
        stack.global_rows[0],
        stack.global_cols[0],
      )
    }
    return
  }
  if stack.count >= 2 {
    return
  }
  // The compound extended search (§7.10.2.12). Four scratch slots per list:
  // slots 0 and 1 take the candidates that share this block's references, the
  // global motion fills whatever is left of them, and slots 2 and 3 take the
  // candidates named by some other reference, sign-corrected for whichever
  // list they are being pushed onto. The merge then folds slot 2 (and 3 when
  // it exists) underneath a short same-reference list.
  let ext_rows = Array::make(8, 0)
  let ext_cols = Array::make(8, 0)
  let counts = Array::make(4, 0)
  let sz4 = if w4 < h4 { w4 } else { h4 }
  let mut idx = 0
  while idx < sz4 {
    let mv_row = block.mi_row - 1
    let mv_col = block.mi_col + idx
    if !av1_mv_inside(block, mv_row, mv_col) {
      break
    }
    let unit = av1_field_unit(field, mv_row, mv_col)
    av1_add_compound_extended(
      ext_rows, ext_cols, counts, field, block, frame, unit,
    )
    idx = idx + field.width4[unit]
  }
  idx = 0
  while idx < sz4 {
    let mv_row = block.mi_row + idx
    let mv_col = block.mi_col - 1
    if !av1_mv_inside(block, mv_row, mv_col) {
      break
    }
    let unit = av1_field_unit(field, mv_row, mv_col)
    av1_add_compound_extended(
      ext_rows, ext_cols, counts, field, block, frame, unit,
    )
    idx = idx + field.height4[unit]
  }
  for list in 0..<2 {
    let mut m = counts[list]
    if m >= 2 {
      continue
    }
    let diff = counts[list + 2]
    if diff != 0 {
      // The different-reference candidates live in scratch slots 2 and 3; the
      // list picks which field of them is read, so one slot covers both lists.
      av1_ext_move(ext_rows, ext_cols, m, list, 2, list)
      m = m + 1
      if m == 2 {
        continue
      }
      if diff == 2 {
        av1_ext_move(ext_rows, ext_cols, m, list, 3, list)
        continue
      }
    }
    while m < 2 {
      let global = if list == 0 {
        (stack.global_rows[0], stack.global_cols[0])
      } else {
        (stack.global_rows[1], stack.global_cols[1])
      }
      ext_rows[m * 2 + list] = global.0
      ext_cols[m * 2 + list] = global.1
      m = m + 1
    }
  }
  // The merged candidates land right after the entries already on the stack,
  // so a one-entry stack is topped up and an empty one is filled from zero.
  let base = stack.count
  for slot in 0..<2 {
    av1_stack_set_mv(
      stack,
      base + slot,
      0,
      ext_rows[slot * 2 + 0],
      ext_cols[slot * 2 + 0],
    )
    av1_stack_set_mv(
      stack,
      base + slot,
      1,
      ext_rows[slot * 2 + 1],
      ext_cols[slot * 2 + 1],
    )
    stack.weights[base + slot] = 2
  }
  if base == 1 &&
    av1_stack_mv(stack, 0, 0) == av1_stack_mv(stack, 1, 0) &&
    av1_stack_mv(stack, 0, 1) == av1_stack_mv(stack, 1, 1) {
    av1_stack_set_mv(stack, 1, 0, ext_rows[1 * 2 + 0], ext_cols[1 * 2 + 0])
    av1_stack_set_mv(stack, 1, 1, ext_rows[1 * 2 + 1], ext_cols[1 * 2 + 1])
  }
  stack.count = 2
}

///|
/// One compound extended candidate: fold the neighbour's two lists into the
/// same-reference and different-reference scratch slots (§7.10.2.12). The
/// different-reference slot takes the vector sign-corrected for the list it is
/// pushed onto, which is what makes a vector from a third reference usable by
/// either side of this block's pair.
fn av1_add_compound_extended(
  ext_rows : Array[Int],
  ext_cols : Array[Int],
  counts : Array[Int],
  field : Av1MotionField,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  unit : Int,
) -> Unit {
  // counts: 0 and 1 are the same-reference slots for each list, 2 and 3 the
  // different-reference ones.
  for n in 0..<2 {
    let cand_ref = field.refs[unit * 2 + n]
    if cand_ref <= av1_intra_frame {
      break
    }
    let cand_row = field.mv_rows[unit * 2 + n]
    let cand_col = field.mv_cols[unit * 2 + n]
    if cand_ref == block.refs[0] {
      if counts[0] < 2 {
        av1_ext_slot(ext_rows, ext_cols, counts[0], 0, cand_row, cand_col)
        counts[0] = counts[0] + 1
      }
      if counts[3] < 2 {
        let bias = frame.ref_frame_sign_bias[block.refs[1]] !=
          frame.ref_frame_sign_bias[cand_ref]
        av1_ext_slot(
          ext_rows,
          ext_cols,
          counts[3] + 2,
          1,
          if bias {
            -cand_row
          } else {
            cand_row
          },
          if bias {
            -cand_col
          } else {
            cand_col
          },
        )
        counts[3] = counts[3] + 1
      }
    } else if cand_ref == block.refs[1] {
      if counts[1] < 2 {
        av1_ext_slot(ext_rows, ext_cols, counts[1], 1, cand_row, cand_col)
        counts[1] = counts[1] + 1
      }
      if counts[2] < 2 {
        let bias = frame.ref_frame_sign_bias[block.refs[0]] !=
          frame.ref_frame_sign_bias[cand_ref]
        av1_ext_slot(
          ext_rows,
          ext_cols,
          counts[2] + 2,
          0,
          if bias {
            -cand_row
          } else {
            cand_row
          },
          if bias {
            -cand_col
          } else {
            cand_col
          },
        )
        counts[2] = counts[2] + 1
      }
    } else {
      let bias0 = frame.ref_frame_sign_bias[block.refs[0]] !=
        frame.ref_frame_sign_bias[cand_ref]
      if counts[2] < 2 {
        av1_ext_slot(
          ext_rows,
          ext_cols,
          counts[2] + 2,
          0,
          if bias0 {
            -cand_row
          } else {
            cand_row
          },
          if bias0 {
            -cand_col
          } else {
            cand_col
          },
        )
        counts[2] = counts[2] + 1
      }
      let bias1 = frame.ref_frame_sign_bias[block.refs[1]] !=
        frame.ref_frame_sign_bias[cand_ref]
      if counts[3] < 2 {
        av1_ext_slot(
          ext_rows,
          ext_cols,
          counts[3] + 2,
          1,
          if bias1 {
            -cand_row
          } else {
            cand_row
          },
          if bias1 {
            -cand_col
          } else {
            cand_col
          },
        )
        counts[3] = counts[3] + 1
      }
    }
  }
}

///|
/// Write one scratch slot's list field.
fn av1_ext_slot(
  ext_rows : Array[Int],
  ext_cols : Array[Int],
  slot : Int,
  list : Int,
  row : Int,
  col : Int,
) -> Unit {
  ext_rows[slot * 2 + list] = row
  ext_cols[slot * 2 + list] = col
}

///|
/// Copy one scratch slot's list field over another's.
fn av1_ext_move(
  ext_rows : Array[Int],
  ext_cols : Array[Int],
  to_slot : Int,
  to_list : Int,
  from_slot : Int,
  from_list : Int,
) -> Unit {
  ext_rows[to_slot * 2 + to_list] = ext_rows[from_slot * 2 + from_list]
  ext_cols[to_slot * 2 + to_list] = ext_cols[from_slot * 2 + from_list]
}

///|
fn av1_add_extra_mv_candidate(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  unit : Int,
  is_compound : Bool,
) -> Unit {
  if is_compound {
    return
  }
  for cand_list in 0..<2 {
    let cand_ref = field.refs[unit * 2 + cand_list]
    if cand_ref <= av1_intra_frame {
      continue
    }
    let mut row = field.mv_rows[unit * 2 + cand_list]
    let mut col = field.mv_cols[unit * 2 + cand_list]
    if frame.ref_frame_sign_bias[cand_ref] !=
      frame.ref_frame_sign_bias[block.refs[0]] {
      row = -row
      col = -col
    }
    let mut found = false
    for idx in 0.. Unit {
  let lists = if is_compound { 2 } else { 1 }
  let mut stop = end
  while stop > start {
    let mut new_end = start
    for idx = start + 1; idx < stop; idx = idx + 1 {
      if stack.weights[idx - 1] < stack.weights[idx] {
        let weight = stack.weights[idx - 1]
        stack.weights[idx - 1] = stack.weights[idx]
        stack.weights[idx] = weight
        for list in 0.. Int {
  let to_start = -(unit * 4 * 8)
  let to_end = (frame_units - extent - unit) * 4 * 8
  av1_mc_clip(to_start - border, to_end + border, value)
}

///|
/// Derive the DRL contexts, clamp every candidate and set the three mode-tree
/// contexts (AV1 §7.10.2.14).
fn av1_stack_contexts(
  field : Av1MotionField,
  stack : Av1MvStack,
  block : Av1MvBlock,
  is_compound : Bool,
  new_count : Int,
) -> Unit {
  for idx in 0..= av1_ref_cat_level {
        if stack.weights[idx + 1] < av1_ref_cat_level {
          z = 1
        }
      } else {
        z = 2
      }
    }
    stack.drl_contexts[idx] = z
  }
  let lists = if is_compound { 2 } else { 1 }
  let row_border = av1_mv_border + block.block_height * 8
  let col_border = av1_mv_border + block.block_width * 8
  for list in 0.. 2 { 2 } else { stack.total_matches }
    stack.refmv_context = near
  } else if stack.close_matches == 1 {
    stack.newmv_context = 3 - (if new_count < 1 { new_count } else { 1 })
    // The near-motion row scales the match count by three before saturating,
    // so a block with one near match but no further match starts the context
    // at zero rather than two (AV1 §8.3).
    let scaled = stack.total_matches * 3
    stack.refmv_context = if scaled > 4 { 4 } else { scaled }
  } else {
    stack.newmv_context = 5 - (if new_count < 1 { new_count } else { 1 })
    stack.refmv_context = 5
  }
}

///|
/// Build the candidate stack and the inter-mode contexts for one block (AV1
/// §7.10.2). Motion-vector reference storage (`use_ref_frame_mvs`) adds a
/// temporal scan between the near weighting and the corner scan; it reads the
/// frame's projected 8x8 grid, and a frame that asks for it without one is
/// refused rather than decoded with a candidate list that lost its temporal
/// half.
pub fn av1_find_mv_stack(
  field : Av1MotionField,
  block : Av1MvBlock,
  frame : Av1MvFrame,
  is_compound : Bool,
) -> Av1MvStack? {
  let stack = av1_mv_stack_new()
  let bw4 = block.width4
  let bh4 = block.height4
  let global = av1_setup_global_mv(0, block, frame)
  stack.global_rows[0] = global.0
  stack.global_cols[0] = global.1
  if is_compound {
    let other = av1_setup_global_mv(1, block, frame)
    stack.global_rows[1] = other.0
    stack.global_cols[1] = other.1
  }
  stack.found_match = 0
  av1_scan_row(field, stack, block, frame, -1, is_compound)
  let mut found_above_match = stack.found_match
  stack.found_match = 0
  av1_scan_col(field, stack, block, frame, -1, is_compound)
  let mut found_left_match = stack.found_match
  stack.found_match = 0
  if (if bw4 > bh4 { bw4 } else { bh4 }) <= 16 {
    av1_scan_point(field, stack, block, frame, -1, bw4, is_compound)
  }
  if stack.found_match == 1 {
    found_above_match = 1
  }
  stack.close_matches = found_above_match + found_left_match
  let num_nearest = stack.count
  let num_new = stack.newmv_count
  for idx in 0.. 1 {
    av1_scan_row(field, stack, block, frame, -5, is_compound)
    if stack.found_match == 1 {
      found_above_match = 1
    }
    stack.found_match = 0
  }
  if bw4 > 1 {
    av1_scan_col(field, stack, block, frame, -5, is_compound)
    if stack.found_match == 1 {
      found_left_match = 1
    }
    stack.found_match = 0
  }
  stack.total_matches = found_above_match + found_left_match
  av1_sort_mv_stack(stack, 0, num_nearest, is_compound)
  av1_sort_mv_stack(stack, num_nearest, stack.count, is_compound)
  if stack.count < 2 {
    av1_extra_search(field, stack, block, frame, is_compound)
  }
  av1_stack_contexts(field, stack, block, is_compound, num_new)
  Some(stack)
}