///|
pub(all) enum ThreadArg {
  PhysicalArg(Int, Int)
  VirtualArg(Int, Int)
} derive(Eq, Debug)

///|
pub(all) struct ThreadBlock {
  id : Int
  params : Array[Int]
  target : Int?
  target_args : Array[ThreadArg]
  is_empty : Bool
} derive(Debug)

///|
pub(all) struct ThreadedJump {
  target : Int
  args : Array[ThreadArg]
  changed : Bool
} derive(Debug)

///|
fn substitute_thread_args(
  forwarded_args : Array[ThreadArg],
  params : Array[Int],
  incoming_args : Array[ThreadArg],
) -> Array[ThreadArg]? {
  let rewritten : Array[ThreadArg] = []
  for arg in forwarded_args {
    match arg {
      PhysicalArg(_, _) => rewritten.push(arg)
      VirtualArg(vreg_id, _) => {
        let mut mapped : ThreadArg? = None
        for i, param_id in params {
          if param_id == vreg_id {
            mapped = Some(incoming_args[i])
            break
          }
        }
        match mapped {
          Some(value) => rewritten.push(value)
          None => return None
        }
      }
    }
  }
  Some(rewritten)
}

///|
fn build_thread_block_index(blocks : Array[ThreadBlock]) -> Map[Int, Int] {
  let block_idx_by_id : Map[Int, Int] = {}
  for i, block in blocks {
    block_idx_by_id.set(block.id, i)
  }
  block_idx_by_id
}

///|
/// Resolve a jump target through chains of empty jump-only blocks.
///
/// A block can be threaded when it is empty, jumps to another block, and its
/// jump arguments can be expressed as forwarding this block's parameters.
pub fn resolve_threaded_jump(
  blocks : Array[ThreadBlock],
  start_target : Int,
  start_args : Array[ThreadArg],
) -> ThreadedJump {
  let block_idx_by_id = build_thread_block_index(blocks)
  let mut current = start_target
  let mut current_args = start_args.copy()
  let visited : Map[Int, Bool] = {}
  let mut changed = false
  while true {
    if visited.get(current).unwrap_or(false) {
      return { target: start_target, args: start_args, changed: false }
    }
    visited.set(current, true)
    match block_idx_by_id.get(current) {
      Some(block_idx) => {
        let block = blocks[block_idx]
        if block.is_empty &&
          block.target is Some(next) &&
          next != current &&
          block.params.length() == current_args.length() &&
          substitute_thread_args(block.target_args, block.params, current_args)
          is Some(mapped) {
          current = next
          current_args = mapped
          changed = true
          continue
        }
        return { target: current, args: current_args, changed }
      }
      None => return { target: current, args: current_args, changed }
    }
  }
  { target: start_target, args: start_args, changed: false }
}