/// Intra block copy uses inter transforms and a separate integer displacement
/// vector context, while its reference samples belong to the current tile.

///|
priv struct Av1IntrabcState {
  use_cdf : Array[Int]
  info : Av1InterInfo
  frame_mi_cols : Int
  frame_mi_rows : Int
  tile_col : Int
  tile_row : Int
}

///|
fn av1_intrabc_state(
  width : Int,
  height : Int,
  frame_mi_cols? : Int = 0,
  frame_mi_rows? : Int = 0,
  tile_col? : Int = 0,
  tile_row? : Int = 0,
) -> Av1IntrabcState {
  let cols = width / 4
  let rows = height / 4
  {
    use_cdf: [30531, 32768, 0],
    info: {
      cdfs: av1_inter_cdfs(),
      field: av1_motion_field(cols, rows),
      frame: {
        allow_high_precision_mv: false,
        use_ref_frame_mvs: false,
        force_integer_mv: 1,
        is_motion_mode_switchable: false,
        allow_warped_motion: false,
        gm_type: Array::make(8, 0),
        gm_params: Array::makei(8, _ => av1_identity_gm_params()),
        ref_frame_sign_bias: Array::make(8, 0),
        frame_width: width,
        frame_height: height,
        temporal: None,
      },
      refs: Array::make(8, None),
      interpolation_filter: 3,
      enable_dual_filter: false,
      skip_mode_present: false,
      skip_mode_frame: [],
      reference_select: false,
      enable_interintra_compound: false,
      ref_distances: Array::make(8, 0),
      enable_masked_compound: false,
      enable_jnt_comp: false,
      mi_cols: cols,
      mi_rows: rows,
      tile_row_start: 0,
      tile_col_start: 0,
      tile_row_end: rows,
      tile_col_end: cols,
    },
    frame_mi_cols: if frame_mi_cols > 0 {
      frame_mi_cols
    } else {
      tile_col + cols
    },
    frame_mi_rows: if frame_mi_rows > 0 {
      frame_mi_rows
    } else {
      tile_row + rows
    },
    tile_col,
    tile_row,
  }
}

///|
/// Inter residuals read the same transform tables for either kind of motion.
/// Keeping this lookup separate leaves intra-frame mode parsing unchanged.
fn av1_block_motion_info(state : Av1IntraState) -> Av1InterInfo? {
  match state.inter {
    Some(info) => Some(info)
    None => state.intrabc.map(value => value.info)
  }
}

///|
/// find_mv_stack with INTRA_FRAME. The spatial search and stable sort are the
/// ordinary MV processes. The extended search has no inter-frame references
/// to add, and the temporal and global predictors are zero for this frame.
/// Clamp in full-frame coordinates, although the motion field is tile-local.
fn av1_intrabc_predictor(
  ibc : Av1IntrabcState,
  block : Av1SbBlock,
  superblock_size : Int,
) -> (Int, Int) {
  let field = ibc.info.field
  let frame = ibc.info.frame
  let current : Av1MvBlock = {
    mi_row: block.row / 4,
    mi_col: block.col / 4,
    width4: block.width / 4,
    height4: block.height / 4,
    block_width: block.width,
    block_height: block.height,
    refs: [av1_intra_frame, av1_none_frame],
    tile_row_start: 0,
    tile_row_end: field.rows,
    tile_col_start: 0,
    tile_col_end: field.cols,
  }
  let stack = av1_mv_stack_new()
  av1_scan_row(field, stack, current, frame, -1, false)
  av1_scan_col(field, stack, current, frame, -1, false)
  if block.width <= 64 && block.height <= 64 {
    av1_scan_point(field, stack, current, frame, -1, current.width4, false)
  }
  let nearest = stack.count
  for index in 0.. 1 {
    av1_scan_row(field, stack, current, frame, -5, false)
  }
  if current.width4 > 1 {
    av1_scan_col(field, stack, current, frame, -5, false)
  }
  av1_sort_mv_stack(stack, 0, nearest, false)
  av1_sort_mv_stack(stack, nearest, stack.count, false)
  for index in 0..<2 {
    let mv = av1_stack_mv(stack, index, 0)
    let row = av1_clamp_mv(
      mv.0,
      ibc.tile_row + current.mi_row,
      current.height4,
      ibc.frame_mi_rows,
      av1_mv_border + block.height * 8,
    )
    let col = av1_clamp_mv(
      mv.1,
      ibc.tile_col + current.mi_col,
      current.width4,
      ibc.frame_mi_cols,
      av1_mv_border + block.width * 8,
    )
    if row != 0 || col != 0 {
      return (row, col)
    }
  }
  if block.row < superblock_size {
    (0, -(superblock_size + 256) * 8)
  } else {
    (-superblock_size * 8, 0)
  }
}

///|
/// Displacement-vector syntax uses MV_INTRABC_CONTEXT and omits all fractional
/// and high-precision symbols, even when chroma subsampling produces half pels.
fn av1_intrabc_read_dv(
  decoder : Av1SymbolDecoder,
  ibc : Av1IntrabcState,
  predicted : (Int, Int),
) -> (Int, Int)? {
  let joint = decoder.symbol(ibc.info.cdfs.mv_joint[1])
  if joint < 0 || joint > 3 {
    return None
  }
  let mut row = predicted.0
  let mut col = predicted.1
  if joint == 2 || joint == 3 {
    let delta = av1_read_mv_component(decoder, ibc.info, 1, 0)
    if delta == 0 {
      return None
    }
    row = row + delta
  }
  if joint == 1 || joint == 3 {
    let delta = av1_read_mv_component(decoder, ibc.info, 1, 1)
    if delta == 0 {
      return None
    }
    col = col + delta
  }
  if row < -32768 ||
    row > 32767 ||
    col < -32768 ||
    col > 32767 ||
    (row & 7) != 0 ||
    (col & 7) != 0 {
    None
  } else {
    Some((row, col))
  }
}

///|
/// Conforming DV bounds, including the four-SB64 delay and wavefront limit
/// from libaom av1/common/mvref_common.h:av1_is_dv_valid. All tile starts align
/// to superblocks, so subtracting their origin preserves the SB differences.
fn av1_intrabc_dv_valid(
  state : Av1IntraState,
  block : Av1SbBlock,
  row : Int,
  col : Int,
) -> Bool {
  if (row & 7) != 0 || (col & 7) != 0 {
    return false
  }
  let left = block.col + (col >> 3)
  let top = block.row + (row >> 3)
  let right = left + block.width
  let bottom = top + block.height
  if left < 0 || top < 0 || right > state.cols * 4 || bottom > state.rows * 4 {
    return false
  }
  if av1_intra_has_chroma(state, block) {
    if (block.width < 8 && state.sub_x == 1 && left < 4) ||
      (block.height < 8 && state.sub_y == 1 && top < 4) {
      return false
    }
  }
  let active_row = block.row / state.superblock_size
  let active_col = block.col / 64
  let source_row = (bottom - 1) / state.superblock_size
  let source_col = (right - 1) / 64
  let columns = (state.cols + 15) / 16
  if source_row * columns + source_col >= active_row * columns + active_col - 4 {
    return false
  }
  let gradient = if state.superblock_size == 128 { 6 } else { 5 }
  source_row <= active_row &&
  source_col < active_col - 4 + gradient * (active_row - source_row)
}

///|
/// Only samples reconstructed by a completed coding block may be copied.
fn av1_intrabc_sample_ready(
  ibc : Av1IntrabcState,
  plane : Av1IntraPlane,
  x : Int,
  y : Int,
  sub_x : Int,
  sub_y : Int,
) -> Bool {
  x >= 0 &&
  y >= 0 &&
  x < plane.width &&
  y < plane.height &&
  ibc.info.field.written[(y << sub_y) / 4 * ibc.info.field.cols +
  (x << sub_x) / 4]
}

///|
/// Integer luma DV becomes integer or half-pel motion on chroma. BILINEAR's
/// two nonzero taps at those phases reduce exactly to rounded averages at all
/// three supported bit depths, including the two-stage 12-bit rounding.
fn av1_intrabc_predict(
  state : Av1IntraState,
  ibc : Av1IntrabcState,
  block : Av1SbBlock,
  row : Int,
  col : Int,
) -> Bool {
  if !av1_intrabc_dv_valid(state, block, row, col) {
    return false
  }
  let planes = if av1_intra_has_chroma(state, block) { 3 } else { 1 }
  for plane_index in 0..> (3 + sx)
    let dy = row >> (3 + sy)
    let half_x = if sx == 1 { (col >> 3) & 1 } else { 0 }
    let half_y = if sy == 1 { (row >> 3) & 1 } else { 0 }
    for y in 0..= plane.height {
        break
      }
      for x in 0..= plane.width {
          break
        }
        let source_x = origin_x + x + dx
        let source_y = origin_y + y + dy
        let mut sum = 0
        for yy in 0..<(1 + half_y) {
          for xx in 0..<(1 + half_x) {
            if !av1_intrabc_sample_ready(
                ibc,
                plane,
                source_x + xx,
                source_y + yy,
                sx,
                sy,
              ) {
              return false
            }
            sum = sum +
              plane.pixels[(source_y + yy) * plane.width + source_x + xx]
          }
        }
        let shift = half_x + half_y
        plane.pixels[(origin_y + y) * plane.width + origin_x + x] = if shift ==
          0 {
          sum
        } else {
          (sum + (1 << (shift - 1))) >> shift
        }
      }
    }
  }
  true
}

///|
/// Publish both ordinary intra blocks and copy blocks to the candidate and
/// transform contexts. INTRA_FRAME with is_inter=true distinguishes intrabc.
fn av1_intrabc_record(
  ibc : Av1IntrabcState,
  block : Av1SbBlock,
  copy : Bool,
  row : Int,
  col : Int,
  skip : Int,
  tx_width : Int,
  tx_height : Int,
) -> Unit {
  av1_motion_field_store(
    ibc.info.field,
    block.row / 4,
    block.col / 4,
    block.width / 4,
    block.height / 4,
    AV1_INTRA_DC,
    [av1_intra_frame, av1_none_frame],
    [row, 0],
    [col, 0],
    skip~,
    interp=[3, 3],
  )
  let field = ibc.info.field
  for y in (block.row / 4)..<((block.row + block.height) / 4) {
    if y >= field.rows {
      break
    }
    for x in (block.col / 4)..<((block.col + block.width) / 4) {
      if x >= field.cols {
        break
      }
      field.is_inter[y * field.cols + x] = copy
    }
  }
  if !copy {
    av1_motion_field_store_tx(
      field,
      block.row / 4,
      block.col / 4,
      block.width / 4,
      block.height / 4,
      tx_width,
      tx_height,
    )
  }
}

///|
fn av1_intrabc_block(
  decoder : Av1SymbolDecoder,
  state : Av1IntraState,
  ibc : Av1IntrabcState,
  block : Av1SbBlock,
  block_index : Int,
  skip : Int,
) -> Bool {
  let predicted = av1_intrabc_predictor(ibc, block, state.superblock_size)
  let (row, col) = match av1_intrabc_read_dv(decoder, ibc, predicted) {
    Some(value) => value
    None => return false
  }
  if !av1_intrabc_predict(state, ibc, block, row, col) {
    return false
  }
  state.palette.y_colors = []
  state.palette.u_colors = []
  state.palette.v_colors = []
  state.palette_y_map = []
  state.palette_uv_map = []
  av1_palette_record(state.palette, block)
  let current = av1_inter_block(
    block.row / 4,
    block.col / 4,
    block.width / 4,
    block.height / 4,
    block_index,
    block.row > 0,
    block.col > 0,
  )
  current.is_inter = true
  current.skip = skip
  current.mv_row = row
  current.mv_col = col
  let (tx_width, tx_height, roots) = match
    av1_read_block_tx_size(decoder, state, ibc.info, current) {
    Some(value) => value
    None => return false
  }
  state.current = Some(current)
  av1_intra_record_modes(
    state,
    block,
    AV1_INTRA_DC,
    AV1_INTRA_DC,
    av1_intra_has_chroma(state, block),
    skip,
  )
  if !av1_intrabc_residual(
      decoder, state, current, block, tx_width, tx_height, roots,
    ) {
    return false
  }
  av1_intrabc_record(ibc, block, true, row, col, skip, tx_width, tx_height)
  av1_tx_traversal_record_size(
    state.tx_neighbors,
    {
      row: block.row,
      col: block.col,
      width: block.width,
      height: block.height,
    },
    block.width,
    block.height,
  )
  state.current = None
  true
}

///|
/// The inter walker handles arbitrary luma leaves. Align only the chroma
/// transform origins for subsampled four-pixel owners (AV1 is_chroma_reference).
fn av1_intrabc_residual(
  decoder : Av1SymbolDecoder,
  state : Av1IntraState,
  current : Av1InterBlock,
  block : Av1SbBlock,
  tx_width : Int,
  tx_height : Int,
  roots : Array[Array[Av1TxNode]],
) -> Bool {
  av1_cfl_begin_block(
    state.cfl,
    block,
    !state.monochrome && !av1_intra_has_chroma(state, block),
  )
  if (state.sub_x == 0 || block.width >= 8) &&
    (state.sub_y == 0 || block.height >= 8) {
    return av1_inter_residual(
      decoder, state, current, block, tx_width, tx_height, roots,
    )
  }
  // A sub-8x8 owner fits one residual chunk. Its luma tree still precedes U/V.
  if roots.is_empty() {
    if !av1_inter_residual_raster(
        decoder,
        state,
        current,
        0,
        block.col,
        block.row,
        block.width,
        block.height,
        tx_width,
        tx_height,
      ) {
      return false
    }
  } else {
    for leaves in roots {
      for leaf in leaves {
        if !av1_intra_transform(
            decoder,
            state,
            0,
            0,
            leaf.col,
            leaf.row,
            leaf.width,
            leaf.height,
            block.width,
            block.height,
            current.skip,
            inter=true,
          ) {
          return false
        }
      }
    }
  }
  if av1_intra_has_chroma(state, block) {
    for plane in 1..<3 {
      let (width, height) = av1_plane_residual_size(
        block.width,
        block.height,
        plane,
        state.sub_x,
        state.sub_y,
      )
      let (cw, ch) = av1_chroma_tx_size(width, height, state.seg_lossless)
      if !av1_inter_residual_raster(
          decoder,
          state,
          current,
          plane,
          block.col / (4 << state.sub_x) * 4,
          block.row / (4 << state.sub_y) * 4,
          width,
          height,
          cw,
          ch,
        ) {
        return false
      }
    }
  }
  true
}