///|
priv struct DirectStackObjectRequest {
  size : Int32
  alignment : Int32
}

///|
/// State owned by one streaming VCode construction, shared by native targets.
pub struct DirectLoweringState[Inst, Failure] {
  builder : CheckedBuilder[Inst]
  context : LoweringContext
  protocol : @native.CallProtocol
  signature : @native.Signature
  result_area : Value?
  on_event : (TargetCompileEvent) -> Unit
  priv values : Array[Value]
  priv value_types : Array[@native.ValueType]
  priv blocks : Array[Block]
  priv layout : Array[Block]
  priv stack_objects : Array[DirectStackObjectRequest]
  priv operands : Array[Value]
  priv operand_types : Array[@native.ValueType]
  priv roots : Array[Value]
  priv mut instruction_index : Int32
  priv mut first_error : Failure?
  priv mut constructed : Function[Inst]?
}

///|
pub fn[Inst, Failure] DirectLoweringState::new(
  builder : CheckedBuilder[Inst],
  context : LoweringContext,
  protocol : @native.CallProtocol,
  signature : @native.Signature,
  values : Array[Value],
  result_area : Value?,
  on_event : (TargetCompileEvent) -> Unit,
) -> DirectLoweringState[Inst, Failure] {
  let entry = builder.entry_block()
  {
    builder,
    context,
    protocol,
    signature,
    result_area,
    on_event,
    values,
    value_types: signature.params.copy(),
    blocks: [entry],
    layout: [entry],
    stack_objects: [],
    operands: [],
    operand_types: [],
    roots: [],
    instruction_index: 0,
    first_error: None,
    constructed: None,
  }
}

///|
pub fn[Inst, Failure] DirectLoweringState::record_error(
  self : DirectLoweringState[Inst, Failure],
  error : Failure,
) -> Unit {
  if self.first_error is None {
    self.first_error = Some(error)
  }
}

///|
pub fn[Inst, Failure] DirectLoweringState::target_value(
  self : DirectLoweringState[Inst, Failure],
  id : Int32,
) -> Value {
  self.values[id]
}

///|
fn[Inst, Failure] DirectLoweringState::target_values_into(
  self : DirectLoweringState[Inst, Failure],
  ids : Array[Int32],
  values : Array[Value],
) -> Array[Value] {
  values.clear()
  for id in ids {
    values.push(self.target_value(id))
  }
  values
}

///|
pub fn[Inst, Failure] DirectLoweringState::target_values(
  self : DirectLoweringState[Inst, Failure],
  ids : Array[Int32],
) -> Array[Value] {
  ids.map(id => self.target_value(id))
}

///|
pub fn[Inst, Failure] DirectLoweringState::create_block(
  self : DirectLoweringState[Inst, Failure],
  parameter_types : Array[@native.ValueType],
) -> (Int32, Array[Int32]) {
  let block = self.builder.create_block(parameter_types)
  let block_id = self.blocks.length()
  self.blocks.push(block)
  let parameters : Array[Int32] = []
  for index, ty in parameter_types {
    let value = self.builder.block_parameter(block, index) catch {
      _ => abort("direct lowering could not read a new block parameter")
    }
    parameters.push(self.values.length())
    self.values.push(value)
    self.value_types.push(ty)
  }
  (block_id, parameters)
}

///|
pub fn[Inst, Failure] DirectLoweringState::create_stack_object(
  self : DirectLoweringState[Inst, Failure],
  size : Int32,
  alignment : Int32,
) -> Int32 {
  let id = self.stack_objects.length()
  self.stack_objects.push({ size, alignment, })
  id
}

///|
pub fn[Inst, Failure] DirectLoweringState::switch_to_block(
  self : DirectLoweringState[Inst, Failure],
  block_id : Int32,
) -> Unit {
  let block = self.blocks[block_id]
  self.layout.push(block)
}

///|
pub fn[Inst, Failure] DirectLoweringState::block(
  self : DirectLoweringState[Inst, Failure],
  id : Int32,
) -> Block {
  self.blocks[id]
}

///|
pub fn[Inst, Failure] DirectLoweringState::value_type(
  self : DirectLoweringState[Inst, Failure],
  id : Int32,
) -> @native.ValueType {
  self.value_types[id]
}

///|
pub fn[Inst, Failure] DirectLoweringState::stack_object(
  self : DirectLoweringState[Inst, Failure],
  id : Int32,
) -> (Int32, Int32) {
  let request = self.stack_objects[id]
  (request.size, request.alignment)
}

///|
pub fn[Inst, Failure] DirectLoweringState::instruction_index(
  self : DirectLoweringState[Inst, Failure],
) -> Int32 {
  self.instruction_index
}

///|
pub fn[Inst, Failure] DirectLoweringState::first_error(
  self : DirectLoweringState[Inst, Failure],
) -> Failure? {
  self.first_error
}

///|
pub fn[Inst, Failure] DirectLoweringState::constructed_function(
  self : DirectLoweringState[Inst, Failure],
) -> Function[Inst]? {
  self.constructed
}

///|
/// Return a new semantic value ID backed by the same untraced machine word.
pub fn[Inst, Failure] DirectLoweringState::alias_word_bitcast(
  self : DirectLoweringState[Inst, Failure],
  operand_ids : Array[Int32],
  result_types : Array[@native.ValueType],
  from : @native.ValueType,
  to : @native.ValueType,
) -> Int32? {
  if operand_ids is [operand_id] &&
    result_types is [result_type] &&
    self.value_types[operand_id] == from &&
    result_type == to &&
    ((from == I64 && to == Ptr64) || (from == Ptr64 && to == I64)) {
    let id = self.values.length()
    self.values.push(self.target_value(operand_id))
    self.value_types.push(result_type)
    self.instruction_index += 1
    Some(id)
  } else {
    None
  }
}

///|
/// These buffers are consumed synchronously by selection.
pub fn[Inst, Failure] DirectLoweringState::instruction_operands(
  self : DirectLoweringState[Inst, Failure],
  operand_ids : Array[Int32],
  root_ids : Array[Int32],
) -> (Array[Value], Array[@native.ValueType], Array[Value]) {
  let operands = self.target_values_into(operand_ids, self.operands)
  self.operand_types.clear()
  for id in operand_ids {
    self.operand_types.push(self.value_types[id])
  }
  let roots = self.target_values_into(root_ids, self.roots)
  (operands, self.operand_types, roots)
}

///|
pub fn[Inst, Failure] DirectLoweringState::record_results(
  self : DirectLoweringState[Inst, Failure],
  results : Array[Value],
  result_types : Array[@native.ValueType],
) -> Array[Int32] {
  self.instruction_index += 1
  let ids : Array[Int32] = []
  for index, value in results {
    ids.push(self.values.length())
    self.values.push(value)
    self.value_types.push(result_types[index])
  }
  ids
}

///|
pub fn[Inst, Failure] DirectLoweringState::finish_construction(
  self : DirectLoweringState[Inst, Failure],
  build_failure : (VCodeBuildError) -> Failure,
) -> Unit {
  if self.constructed is Some(_) {
    return
  }
  let lowered = self.builder.finish() catch {
    error => {
      self.record_error(build_failure(error))
      return
    }
  }
  lowered.set_layout(self.layout) catch {
    error => {
      self.record_error(build_failure(error))
      return
    }
  }
  self.constructed = Some(lowered)
}

///|
/// Materialize typed definitions only when a raw word alias crosses a block edge.
pub fn[Inst, Failure] DirectLoweringState::edge(
  self : DirectLoweringState[Inst, Failure],
  block : Block,
  target : Int32,
  ids : Array[Int32],
  make_move : (@native.ValueType) -> Inst,
) -> Edge raise VCodeBuildError {
  let values : Array[Value] = []
  for id in ids {
    let value = self.target_value(id)
    let expected = self.value_type(id)
    let needs_move = match expected {
      Ptr64 => self.builder.value_type(value) == Some(I64)
      I64 => self.builder.value_type(value) == Some(Ptr64)
      _ => false
    }
    if needs_move {
      let (_, results) = self.builder.append_body(
        block,
        make_move(expected),
        [Input::any(value)],
        [Output::any(expected)],
        [],
        InstructionMetadata::empty(),
      )
      values.push(results[0])
    } else {
      values.push(value)
    }
  }
  Edge::new(self.block(target), values)
}

///|
pub fn[Inst, Failure] DirectLoweringState::switch_targets(
  self : DirectLoweringState[Inst, Failure],
  block : Block,
  cases : Array[@lowering.TargetSwitchCase],
  default_target : Int32,
  default_arguments : Array[Int32],
  make_move : (@native.ValueType) -> Inst,
) -> (Array[UInt64], Array[Edge]) raise VCodeBuildError {
  let keys : Array[UInt64] = []
  let edges : Array[Edge] = []
  for case in cases {
    keys.push(case.bits)
    edges.push(self.edge(block, case.target, case.arguments, make_move))
  }
  edges.push(self.edge(block, default_target, default_arguments, make_move))
  (keys, edges)
}

///|
/// Attach the recorded live roots when sealing a block.
pub fn[Inst, Failure] DirectLoweringState::terminator_metadata(
  self : DirectLoweringState[Inst, Failure],
  source : @native.SourceLocation?,
  live_gc_roots : Array[Int32],
) -> InstructionMetadata {
  InstructionMetadata::new(
    source?,
    live_gc_roots=self.target_values(live_gc_roots),
  )
}