///|
fn resolve_edge_moves(
  function : @vcode.Function[X64Inst],
  allocation : @vcode.Allocation,
  block : @vcode.Block,
  successor_index : Int32,
) -> @vcode.ResolvedMovePlan raise X64EmitError {
  @vcode.resolve_edge_moves(
    function,
    allocation,
    block,
    successor_index,
    int_transfer_scratch(),
    fp_transfer_scratch(),
  ) catch {
    error => raise InvalidParallelMove(cause=error)
  }
}

///|
fn emit_resolved_moves(
  buffer : CodeBuffer,
  frame : X64Frame,
  plan : @vcode.ResolvedMovePlan,
) -> Unit raise X64EmitError {
  @vcode.emit_resolved_moves(
    plan,
    buffer,
    frame,
    X64Frame::slot_offset,
    X64Frame::emergency_move_offset,
    emit_move,
    emit_stack_access,
    UnresolvedStackMove,
    MissingEmergencyMoveArea,
  )
}

///|
fn emit_edge_moves(
  buffer : CodeBuffer,
  function : @vcode.Function[X64Inst],
  allocation : @vcode.Allocation,
  frame : X64Frame,
  block : @vcode.Block,
  successor_index : Int32,
) -> Unit raise X64EmitError {
  emit_resolved_moves(
    buffer,
    frame,
    resolve_edge_moves(function, allocation, block, successor_index),
  )
}

///|
fn threaded_branch_target(
  function : @vcode.Function[X64Inst],
  allocation : @vcode.Allocation,
  target : @vcode.Block,
) -> @vcode.Block raise X64EmitError {
  @vcode.threaded_branch_target(
    function,
    allocation,
    target,
    Jump,
    int_transfer_scratch(),
    fp_transfer_scratch(),
  ) catch {
    error => raise InvalidParallelMove(cause=error)
  }
}

///|
// The caller emits CMP/TEST. Test its flags before edge moves can clobber them.
fn emit_conditional_edges(
  buffer : CodeBuffer,
  function : @vcode.Function[X64Inst],
  allocation : @vcode.Allocation,
  frame : X64Frame,
  block : @vcode.Block,
  instruction : @vcode.Instruction,
  condition : X64Condition,
  next_block : @vcode.Block?,
) -> Unit raise X64EmitError {
  let layout = @vcode.plan_conditional_edges(
    function,
    allocation,
    block,
    instruction,
    next_block,
    Jump,
    int_transfer_scratch(),
    fp_transfer_scratch(),
    split_false_first=false,
  ) catch {
    error => raise InvalidParallelMove(cause=error)
  }
  let first_target = function.block_index(layout.first_target).unwrap()
  let second_target = function.block_index(layout.second_target).unwrap()
  let inverse = inverse_condition(condition)
  let first_condition = if layout.first_when_true { condition } else { inverse }
  if layout.direct_branch {
    buffer.x86_emit_jcc_rel32(raw_condition(first_condition), first_target)
  } else {
    let second_condition = if layout.first_when_true {
      inverse
    } else {
      condition
    }
    buffer.emit_byte(0x0F)
    buffer.emit_byte(x86_cond_to_jcc_opcode(raw_condition(second_condition)))
    let past_moves = buffer.position()
    emit_u32_le(buffer.encoder, 0)
    emit_resolved_moves(buffer, frame, layout.first_moves)
    buffer.x86_emit_jmp_rel32(first_target)
    patch_relative_branch(buffer, past_moves, buffer.position(), 32)
  }
  emit_resolved_moves(buffer, frame, layout.second_moves)
  if !layout.second_falls_through {
    buffer.x86_emit_jmp_rel32(second_target)
  }
}

///|
fn emit_switch_compare(
  buffer : CodeBuffer,
  width : GprWidth,
  index : @vcode.PhysicalReg,
  bits : UInt64,
) -> Unit {
  if bits <= 0x7FFFFFFFUL {
    if width == W32 {
      buffer.encoder.x86_emit_cmp_r32_imm32(index.id, bits.to_int())
    } else {
      buffer.encoder.x86_emit_cmp_r_imm32(index.id, bits.to_int())
    }
  } else {
    let scratch = @vcode.PhysicalReg::new(11, Int)
    emit_constant(buffer, width, scratch, bits)
    if width == W32 {
      buffer.encoder.x86_emit_gpr(Cmp, R32, index.id, scratch.id)
    } else {
      buffer.encoder.x86_emit_gpr(Cmp, R64, index.id, scratch.id)
    }
  }
}

///|
fn emit_switch_edges(
  buffer : CodeBuffer,
  function : @vcode.Function[X64Inst],
  allocation : @vcode.Allocation,
  frame : X64Frame,
  block : @vcode.Block,
  instruction : @vcode.Instruction,
  width : GprWidth,
  cases : Array[UInt64],
  index : @vcode.PhysicalReg,
) -> Unit raise X64EmitError {
  if cases.is_empty() {
    emit_edge_moves(buffer, function, allocation, frame, block, 0)
    buffer.x86_emit_jmp_rel32(
      function
      .block_index(
        threaded_branch_target(
          function,
          allocation,
          function.instruction_successor_at(instruction, 0).unwrap().target,
        ),
      )
      .unwrap(),
    )
    return
  }
  let case_branches : Array[(Int32, Int32)] = []
  for successor_index, bits in cases {
    emit_switch_compare(buffer, width, index, bits)
    buffer.emit_byte(0x0F)
    buffer.emit_byte(x86_cond_to_jcc_opcode(RawEq))
    case_branches.push((buffer.position(), successor_index))
    emit_u32_le(buffer.encoder, 0)
  }
  emit_edge_moves(buffer, function, allocation, frame, block, cases.length())
  buffer.x86_emit_jmp_rel32(
    function
    .block_index(
      threaded_branch_target(
        function,
        allocation,
        function.instruction_successor_at(instruction, cases.length()).unwrap().target,
      ),
    )
    .unwrap(),
  )
  for entry in case_branches {
    let (branch, successor_index) = entry
    patch_relative_branch(buffer, branch, buffer.position(), 32)
    emit_edge_moves(buffer, function, allocation, frame, block, successor_index)
    buffer.x86_emit_jmp_rel32(
      function
      .block_index(
        threaded_branch_target(
          function,
          allocation,
          function
          .instruction_successor_at(instruction, successor_index)
          .unwrap().target,
        ),
      )
      .unwrap(),
    )
  }
}