///|
/// Resolve the parallel moves attached to one allocated successor edge.
pub fn[Inst] resolve_edge_moves(
  function : Function[Inst],
  allocation : Allocation,
  block : Block,
  successor_index : Int32,
  integer_scratch : PhysicalReg,
  fp_scratch : PhysicalReg,
) -> ResolvedMovePlan raise MoveResolveError {
  if allocation.edge_edits_at(block, successor_index).is_empty() {
    return ResolvedMovePlan::empty()
  }
  plan_parallel_moves(
    allocation_edge_moves(function, allocation, block, successor_index),
    integer_scratch,
    fp_scratch,
  )
}

///|
/// Chase short chains of unconditional forwarding edges. A block can only be
/// bypassed if it has no body, no terminator edits, and no resolved edge moves.
/// Each target still checks the displacement of its final branch encoding.
pub fn[Inst : Eq] threaded_branch_target(
  function : Function[Inst],
  allocation : Allocation,
  target : Block,
  jump : Inst,
  integer_scratch : PhysicalReg,
  fp_scratch : PhysicalReg,
) -> Block raise MoveResolveError {
  let mut current = target
  // Forwarding chains longer than a few hops do not occur in practice; the
  // bound also terminates on a cycle of empty blocks.
  for _hop in 0..<4 {
    if !function.block_body(current).is_empty() {
      break
    }
    guard function.block_terminator(current) is Some(terminator) else { break }
    if function.instruction(terminator) != Some(jump) {
      break
    }
    if !allocation.edits_at(terminator, Before).is_empty() ||
      !allocation.edits_at(terminator, After).is_empty() {
      break
    }
    if !resolve_edge_moves(
        function, allocation, current, 0, integer_scratch, fp_scratch,
      ).steps.is_empty() {
      break
    }
    guard function.instruction_successor_at(terminator, 0) is Some(edge) else {
      break
    }
    if edge.target == current {
      break
    }
    current = edge.target
  }
  current
}

///|
#valtype
pub struct ConditionalEdgeLayout {
  first_target : Block
  first_moves : ResolvedMovePlan
  second_target : Block
  second_moves : ResolvedMovePlan
  first_when_true : Bool
  direct_branch : Bool
  second_falls_through : Bool
}

///|
/// Resolve moves and choose an emission order. Compare the original blocks
/// for fallthrough, and use the threaded destinations for explicit branches.
pub fn[Inst : Eq] plan_conditional_edges(
  function : Function[Inst],
  allocation : Allocation,
  block : Block,
  instruction : Instruction,
  next_block : Block?,
  jump : Inst,
  integer_scratch : PhysicalReg,
  fp_scratch : PhysicalReg,
  split_false_first~ : Bool,
) -> ConditionalEdgeLayout raise MoveResolveError {
  let when_true = function.instruction_successor_at(instruction, 0).unwrap().target
  let when_false = function.instruction_successor_at(instruction, 1).unwrap().target
  let true_moves = resolve_edge_moves(
    function, allocation, block, 0, integer_scratch, fp_scratch,
  )
  let false_moves = resolve_edge_moves(
    function, allocation, block, 1, integer_scratch, fp_scratch,
  )
  let true_target = threaded_branch_target(
    function, allocation, when_true, jump, integer_scratch, fp_scratch,
  )
  let false_target = threaded_branch_target(
    function, allocation, when_false, jump, integer_scratch, fp_scratch,
  )
  let true_empty = true_moves.steps.is_empty()
  let false_empty = false_moves.steps.is_empty()
  let (first_when_true, direct_branch, second_falls_through) = if next_block ==
    Some(when_false) {
    (true, true_empty, true)
  } else if next_block == Some(when_true) {
    (false, false_empty, true)
  } else if true_empty {
    (true, true, false)
  } else if split_false_first {
    (false, false, false)
  } else if false_empty {
    (false, true, false)
  } else {
    (true, false, false)
  }
  let (first_target, first_moves, second_target, second_moves) = if first_when_true {
    (true_target, true_moves, false_target, false_moves)
  } else {
    (false_target, false_moves, true_target, true_moves)
  }
  {
    first_target,
    first_moves,
    second_target,
    second_moves,
    first_when_true,
    direct_branch,
    second_falls_through,
  }
}