///|
/// Exception Handling Lowering
/// Lowers IR exception opcodes to MachV instructions that call C runtime helpers

///|
/// Helper to get vmctx register as vreg
fn emit_load_vmctx(
  ctx : @lower.LoweringContext,
  block : @block.Block,
) -> @abi.VReg {
  let vmctx_vreg = ctx.machv_function().new_vreg(Int)
  let vmctx_mov = @instr.Inst(Move)
  vmctx_mov.add_def({ reg: Virtual(vmctx_vreg) })
  vmctx_mov.add_use(Physical({ index: @lower.context_index(ctx), class: Int }))
  block.add_inst(vmctx_mov)
  vmctx_vreg
}

///|
/// Lower Throw(tag_idx) instruction
/// Calls exception_throw(ctx, tag_idx, values_ptr, count)
pub fn lower_throw(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  tag_idx : Int,
) -> Unit {
  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Materialize tag index
  let tag_idx_vreg = @lower.materialize_imm(ctx, block, tag_idx.to_int64())

  // Allocate space for exception values (if any).
  //
  // Important: do NOT dynamically adjust SP here.
  // Our JIT assumes a fixed SP within the function body (after prologue), so
  // spills/stack slots use constant offsets from SP.
  //
  // Instead, reuse the pre-allocated outgoing-args area at SP and ensure it is
  // large enough for the values array.
  let num_operands = inst.operands.length()
  let (values_ptr_vreg, count_vreg) = if num_operands > 0 {
    let stack_space = (num_operands * 8 + 15) / 16 * 16
    ctx.machv_function().update_max_outgoing_args_size(stack_space)

    // Store each value to the outgoing args area at SP.
    for i in 0.. Unit {
  guard inst.operands.length() > 0 else { return }

  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Get exnref from operand
  let exnref_vreg = ctx.get_vreg_for_use(inst.operands[0], block)

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_throw_ref(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_throw_ref(ctx, exnref)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, exnref_vreg],
    None,
  )
}

///|
/// Lower TryTableBegin(handler_id) instruction
/// Calls exception_try_begin(ctx, handler_id) -> jmp_buf ptr
/// Then calls sigsetjmp(jmp_buf_ptr, 0) -> result
///
/// The TryTableBegin IR instruction returns 0 normally, or handler_id on catch.
/// This is the result of sigsetjmp.
pub fn lower_try_table_begin(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  handler_id : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)

  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Materialize handler_id
  let handler_id_vreg = @lower.materialize_imm(
    ctx,
    block,
    handler_id.to_int64(),
  )

  // Load function pointer for try_begin
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_try_begin(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_try_begin(ctx, handler_id) -> jmp_buf ptr
  let jmp_buf_ptr = ctx.machv_function().new_vreg(Int)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, handler_id_vreg],
    Some(jmp_buf_ptr),
  )

  // Now call sigsetjmp(jmp_buf_ptr, 0) to actually set the jump point
  // sigsetjmp returns 0 normally, or handler_id when longjmp is called
  //
  // Load sigsetjmp function pointer
  let setjmp_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_setjmp = @instr.Inst(
    LoadExternalFuncAddr(rt_exception_sigsetjmp(ctx)),
  )
  load_setjmp.add_def({ reg: Virtual(setjmp_ptr_vreg) })
  block.add_inst(load_setjmp)

  // Second argument: savemask = 0 (don't save signal mask)
  let savemask_vreg = @lower.materialize_imm(ctx, block, 0L)

  // Call: sigsetjmp(jmp_buf_ptr, 0) -> result
  // Result is 0 on first call, handler_id on longjmp
  @lower.lower_c_libcall(
    ctx,
    block,
    setjmp_ptr_vreg,
    [jmp_buf_ptr, savemask_vreg],
    Some(dst),
  )
}

///|
/// Lower TryTableEnd(handler_id) instruction
/// Calls exception_try_end(ctx, handler_id)
pub fn lower_try_table_end(
  ctx : @lower.LoweringContext,
  _inst : @milkir.Inst,
  block : @block.Block,
  handler_id : Int,
) -> Unit {
  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Materialize handler_id
  let handler_id_vreg = @lower.materialize_imm(
    ctx,
    block,
    handler_id.to_int64(),
  )

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_try_end(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_try_end(ctx, handler_id)
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, handler_id_vreg],
    None,
  )
}

///|
/// Lower GetExceptionTag instruction
/// Calls exception_get_tag(ctx) -> tag
pub fn lower_get_exception_tag(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)

  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_get_tag(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_get_tag(ctx) -> tag
  @lower.lower_c_libcall(ctx, block, func_ptr_vreg, [vmctx_vreg], Some(dst))
}

///|
/// Lower GetExceptionValue(idx) instruction
/// Calls exception_get_value(ctx, idx) -> value
pub fn lower_get_exception_value(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)

  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Materialize index
  let idx_vreg = @lower.materialize_imm(ctx, block, idx.to_int64())

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_get_value(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_get_value(ctx, idx) -> value
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, idx_vreg],
    Some(dst),
  )
}

///|
/// Lower GetExceptionValueCount instruction
/// Calls exception_get_value_count(ctx) -> count
pub fn lower_get_exception_value_count(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)

  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(
    LoadExternalFuncAddr(rt_exception_get_value_count(ctx)),
  )
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_get_value_count(ctx) -> count
  @lower.lower_c_libcall(ctx, block, func_ptr_vreg, [vmctx_vreg], Some(dst))
}

///|
/// Lower Delegate(depth) instruction
/// Calls exception_delegate(ctx, depth)
pub fn lower_delegate(
  ctx : @lower.LoweringContext,
  _inst : @milkir.Inst,
  block : @block.Block,
  depth : Int,
) -> Unit {
  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Materialize depth
  let depth_vreg = @lower.materialize_imm(ctx, block, depth.to_int64())

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(LoadExternalFuncAddr(rt_exception_delegate(ctx)))
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_delegate(ctx, depth)
  // This is noreturn
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, depth_vreg],
    None,
  )
}

///|
/// Lower SpillLocalsForThrow(count) instruction
/// Calls exception_spill_locals(ctx, locals_ptr, count)
/// This saves all locals to memory so catch handlers can see throw-time values
pub fn lower_spill_locals_for_throw(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  count : Int,
) -> Unit {
  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // If no locals, pass null and 0
  let (locals_ptr_vreg, count_vreg) = if count > 0 {
    // Use pre-allocated outgoing args area for temporary local snapshots.
    // Do not adjust SP dynamically inside function bodies.
    let stack_space = (count * 8 + 15) / 16 * 16
    ctx.machv_function().update_max_outgoing_args_size(stack_space)

    // Store each local value to outgoing args area at SP + offset.
    for i in 0.. value
pub fn lower_get_spilled_local(
  ctx : @lower.LoweringContext,
  inst : @milkir.Inst,
  block : @block.Block,
  idx : Int,
) -> Unit {
  guard inst.first_result() is Some(result) else { return }
  let dst = ctx.get_vreg(result)

  // Get vmctx
  let vmctx_vreg = emit_load_vmctx(ctx, block)

  // Materialize index
  let idx_vreg = @lower.materialize_imm(ctx, block, idx.to_int64())

  // Load function pointer
  let func_ptr_vreg = ctx.machv_function().new_vreg(Int)
  let load_fp = @instr.Inst(
    LoadExternalFuncAddr(rt_exception_get_spilled_local(ctx)),
  )
  load_fp.add_def({ reg: Virtual(func_ptr_vreg) })
  block.add_inst(load_fp)

  // Call: exception_get_spilled_local(ctx, idx) -> value
  @lower.lower_c_libcall(
    ctx,
    block,
    func_ptr_vreg,
    [vmctx_vreg, idx_vreg],
    Some(dst),
  )
}