//
// PORT NOTE: This is the Rust-native translation of lapi.c.
// The C-API surface (lua_State *, int stack-index protocol) is replaced by
// methods on LuaState.  `lua_lock` / `lua_unlock` are dropped (no-op in the
// single-threaded default build).  `api_incr_top` is dropped; `state.push()`
// already increments.  Stack pointers (StkId) become StackIdx (u32).

#![allow(dead_code)]

#[allow(unused_imports)]
use crate::prelude::*;
use std::convert::Infallible;

use crate::state::{
    FinalizerObject, LuaCFunction, LuaCallable, LuaState, LuaTableRefExt, LuaTypeExt,
    LuaUserDataRefExt, LuaValueExt, StackIdx, StackIdxExt, WeakTableEntry,
};
use lua_types::value::LuaTable;
use lua_types::{
    GcRef, LuaClosure, LuaError, LuaStatus, LuaString, LuaType, LuaUserData, LuaValue,
};

pub const LUA_IDENT: &[u8] = b"$LuaVersion: Lua 5.4.7  Copyright (C) 1994-2024 Lua.org, PUC-Rio $\
      $LuaAuthors: R. Ierusalimschy, L. H. Figueiredo, W. Celes $";

const LUA_REGISTRYINDEX: i32 = -(1_000_000) - 1000;

const LUA_MULTRET: i32 = -1;

const LUA_RIDX_GLOBALS: i64 = 2;

const MAX_UPVAL: u8 = 255;

#[inline]
fn is_pseudo(idx: i32) -> bool {
    idx <= LUA_REGISTRYINDEX
}

#[inline]
fn is_upvalue(idx: i32) -> bool {
    idx < LUA_REGISTRYINDEX
}

// PORT NOTE: In C, the only "invalid" TValue is the global nilvalue singleton
// pointer returned by index2value when the index is out of range. In Rust we
// cannot do pointer-equality on a singleton, so validity is decided by whether
// the index resolves to a real stack/upvalue slot — see `is_valid_index`.
#[inline]
fn is_valid_index(state: &LuaState, idx: i32) -> bool {
    if idx == 0 {
        return false;
    }
    let ci = state.current_call_info();
    if idx > 0 {
        let slot = ci.func + idx;
        slot.0 < state.top_idx().0
    } else if !is_pseudo(idx) {
        (-idx) as u32 <= state.top_idx().0.saturating_sub(ci.func.0 + 1)
    } else if idx == LUA_REGISTRYINDEX {
        true
    } else {
        let upval_n = (LUA_REGISTRYINDEX - idx) as usize;
        let func_val = state.get_at(ci.func);
        if let LuaValue::Function(LuaClosure::C(ref ccl)) = func_val {
            let upvalues = ccl.upvalues.borrow();
            upval_n >= 1 && upval_n <= upvalues.len()
        } else {
            false
        }
    }
}

// ── index helpers ─────────────────────────────────────────────────────────────

// PORT NOTE: In Rust we cannot return a pointer; we return a cloned LuaValue.
// Writers use a companion index_to_stack_idx() for actual stack slots.
fn index_to_value(state: &LuaState, idx: i32) -> LuaValue {
    let ci = state.current_call_info();
    if idx > 0 {
        let func_idx = ci.func;
        let slot = func_idx + idx;
        debug_assert!(
            idx as u32 <= ci.top.saturating_sub(func_idx + 1),
            "unacceptable index"
        );
        if slot.0 >= state.top_idx().0 {
            LuaValue::Nil
        } else {
            state.get_at(slot)
        }
    } else if !is_pseudo(idx) {
        // negative index
        debug_assert!(idx != 0, "invalid index");
        let top = state.top_idx();
        let slot = (top.0 as i32 + idx) as u32;
        state.get_at(slot)
    } else if idx == LUA_REGISTRYINDEX {
        state.registry_value()
    } else {
        // upvalues: idx = LUA_REGISTRYINDEX - idx  (idx < LUA_REGISTRYINDEX)
        let upval_n = (LUA_REGISTRYINDEX - idx) as usize;
        debug_assert!(upval_n <= MAX_UPVAL as usize + 1, "upvalue index too large");
        let func_val = state.get_at(ci.func);
        if let LuaValue::Function(LuaClosure::C(ref ccl)) = func_val {
            // C closure upvalue
            let upvalues = ccl.upvalues.borrow();
            if upval_n >= 1 && upval_n <= upvalues.len() {
                upvalues[upval_n - 1].clone()
            } else {
                LuaValue::Nil
            }
        } else {
            LuaValue::Nil
        }
    }
}

/// Fast path for ordinary positive stack indices.
///
/// Stdlib C callbacks receive their arguments at positive indices. For hot
/// callbacks such as `ipairsaux`, this avoids the negative/pseudo-index
/// branches in `index_to_value` while preserving the same "missing argument is
/// nil" behavior for positive indices.
#[inline(always)]
pub fn positive_index_value(state: &LuaState, idx: i32) -> LuaValue {
    debug_assert!(idx > 0, "positive_index_value requires a positive index");
    let ci = state.current_call_info();
    let func_idx = ci.func;
    let slot = func_idx + idx;
    debug_assert!(
        idx as u32 <= ci.top.saturating_sub(func_idx + 1),
        "unacceptable index"
    );
    if slot.0 >= state.top_idx().0 {
        LuaValue::Nil
    } else {
        state.get_at(slot)
    }
}

// Returns a StackIdx for a valid (non-pseudo) actual stack slot.
#[inline]
fn index_to_stack_idx(state: &LuaState, idx: i32) -> StackIdx {
    let ci = state.current_call_info();
    if idx > 0 {
        let slot = ci.func + idx;
        debug_assert!(slot.0 < state.top_idx().0, "invalid index");
        slot
    } else {
        debug_assert!(idx != 0 && !is_pseudo(idx), "invalid index");
        StackIdx((state.top_idx().0 as i32 + idx) as u32)
    }
}

// ── stack manipulation ────────────────────────────────────────────────────────

pub fn check_stack(state: &mut LuaState, n: i32) -> bool {
    debug_assert!(n >= 0, "negative 'n'");
    let available = state.stack_available();
    let res = if available > n as usize {
        true
    } else {
        crate::do_::grow_stack(state, n, false).unwrap_or(false)
    };
    if res {
        let needed_top = state.top_idx() + n as i32;
        let ci_idx = state.current_ci_idx();
        if state.get_ci(ci_idx).top.0 < needed_top.0 {
            let live_top = state.top_idx();
            state.get_ci_mut(ci_idx).top = needed_top;
            state.clear_stack_range(live_top, needed_top);
        }
    }
    res
}

/// Move the top `n` values from `from`'s stack onto `to`'s stack.
///
/// Both threads must share the same `GlobalState` (i.e. one is a
/// coroutine the other created via `coroutine.create`). Calling with
/// `from` == `to` is a no-op. Equivalent to:
///
/// ```text
/// args = from.stack[top-n..top].clone();
/// from.set_top(top - n);
/// for v in args { to.push(v); }
/// ```
///
///
/// Phase E-3: implemented for the same-`GlobalState` case (the only one
/// `lua-stdlib` uses today). `lua-vm` callers should prefer this helper
/// over hand-rolling the snapshot/push dance.
pub fn xmove(from: &mut LuaState, to: &mut LuaState, n: i32) {
    if n <= 0 {
        return;
    }
    if std::ptr::eq(from as *const LuaState, to as *const LuaState) {
        return;
    }
    let abs_top = from.top_idx().0 as i32;
    debug_assert!(abs_top >= n, "lua_xmove: from stack underflow");
    let first_abs = abs_top - n;
    let mut buf: Vec<lua_types::LuaValue> = Vec::with_capacity(n as usize);
    for i in 0..n {
        let idx = StackIdx((first_abs + i) as u32);
        buf.push(from.get_at(idx));
    }
    from.set_top(StackIdx(first_abs as u32));
    for v in buf {
        to.push(v);
    }
}

pub fn at_panic(
    state: &mut LuaState,
    panicf: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
) -> Option<fn(&mut LuaState) -> Result<usize, LuaError>> {
    let old = state.global_mut().panic;
    state.global_mut().panic = panicf;
    old
}

pub fn version(_state: &LuaState) -> f64 {
    504.0
}

pub fn abs_index(state: &LuaState, idx: i32) -> i32 {
    //          : cast_int(L->top.p - L->ci->func.p) + idx;
    if idx > 0 || is_pseudo(idx) {
        idx
    } else {
        let ci = state.current_call_info();
        (state.top_idx().0 as i32 - ci.func.0 as i32) + idx
    }
}

pub fn get_top(state: &LuaState) -> i32 {
    let ci = state.current_call_info();
    (state.top_idx().0 as i32) - (ci.func.0 as i32 + 1)
}

pub fn set_top(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
    let func = state.current_call_info().func;
    let ci_top = state.current_call_info().top;
    if idx >= 0 {
        debug_assert!(
            idx as u32 <= ci_top.saturating_sub(func + 1),
            "new top too large"
        );
        let new_top = func + 1 + idx as i32;
        let old_top = state.top_idx();
        if new_top.0 > old_top.0 {
            for i in old_top.0..new_top.0 {
                state.set_at(i, LuaValue::Nil);
            }
        }
        // TODO(port): to-be-closed variable closing on stack shrink;
        // luaF_close not yet translated. Skipping close logic for Phase A.
        state.set_top_idx(new_top);
    } else {
        debug_assert!(
            -(idx + 1) <= (state.top_idx().0 as i32 - (func.0 as i32 + 1)),
            "invalid new top"
        );
        let new_top = (state.top_idx().0 as i32 + idx + 1) as u32;
        // TODO(port): to-be-closed variable closing on stack shrink (same as above)
        state.set_top_idx(new_top);
    }
    Ok(())
}

pub fn close_slot(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
    let level = index_to_stack_idx(state, idx);
    // TODO(port): tbc-list check and luaF_close not yet translated.
    state.set_at(level, LuaValue::Nil);
    Ok(())
}

#[inline]
fn reverse_segment(state: &mut LuaState, from: StackIdx, to: StackIdx) {
    let mut lo = from.0;
    let mut hi = to.0;
    while lo < hi {
        let temp = state.get_at(StackIdx(lo));
        let hi_val = state.get_at(StackIdx(hi));
        state.set_at(StackIdx(lo), hi_val);
        state.set_at(StackIdx(hi), temp);
        lo += 1;
        hi -= 1;
    }
}

pub fn rotate(state: &mut LuaState, idx: i32, n: i32) {
    let t = state.top_idx() - 1;
    let p = index_to_stack_idx(state, idx);
    debug_assert!(
        (n.unsigned_abs() as i32) <= ((t.0 as i32) - (p.0 as i32) + 1),
        "invalid 'n'"
    );
    let m = if n >= 0 {
        t - n
    } else {
        StackIdx((p.0 as i32 - n - 1) as u32)
    };
    reverse_segment(state, p, m);
    reverse_segment(state, m + 1, t);
    reverse_segment(state, p, t);
}

pub fn copy(state: &mut LuaState, fromidx: i32, toidx: i32) {
    let fr = index_to_value(state, fromidx);
    if is_upvalue(toidx) {
        // Writing to a function upvalue pseudo-index
        let upval_n = (LUA_REGISTRYINDEX - toidx) as usize;
        let func_val = state.get_at(state.current_call_info().func);
        if let LuaValue::Function(LuaClosure::C(ref ccl)) = func_val {
            let wrote = {
                let mut upvalues = ccl.upvalues.borrow_mut();
                if upval_n >= 1 && upval_n <= upvalues.len() {
                    upvalues[upval_n - 1] = fr.clone();
                    true
                } else {
                    false
                }
            };
            if wrote {
                state.gc().barrier(ccl, &fr);
            }
        }
    } else if toidx == LUA_REGISTRYINDEX {
        // TODO(port): write to registry — needs GlobalState::set_registry(fr)
    } else {
        let to_slot = index_to_stack_idx(state, toidx);
        state.set_at(to_slot, fr);
    }
}

pub fn push_value(state: &mut LuaState, idx: i32) {
    let v = index_to_value(state, idx);
    state.push(v);
}

/// Inherent `push_copy` so the `LuaStateStubExt::push_copy` default
/// `todo!()` no longer fires. Phase-A `state.push_copy(idx)` call-sites
/// (base.rs, etc.) duplicate the value at `idx` onto the top of the stack —
/// the same semantics as `lua_pushvalue`.
impl LuaState {
    pub fn push_copy(&mut self, idx: i32) -> Result<(), LuaError> {
        push_value(self, idx);
        Ok(())
    }

    pub fn push_value_at(&mut self, idx: i32) -> Result<(), LuaError> {
        push_value(self, idx);
        Ok(())
    }

    pub fn insert(&mut self, idx: i32) -> Result<(), LuaError> {
        rotate(self, idx, 1);
        Ok(())
    }

    /// Inherent `length_at` mirroring `luaL_len` from `lauxlib.c`: push the
    /// value's length onto the stack (honouring `__len`), pop it as an
    /// integer, and error if the result is not an integer. Defined on
    /// `LuaState` so it overrides the `LuaStateStubExt::length_at` trait
    /// default `todo!()`.
    pub fn length_at(&mut self, idx: i32) -> Result<i64, LuaError> {
        len(self, idx)?;
        let l = match to_integer_x(self, -1) {
            Some(n) => n,
            None => {
                return Err(LuaError::runtime(format_args!(
                    "object length is not an integer"
                )));
            }
        };
        self.pop_n(1);
        Ok(l)
    }

    /// Write `msg` bytes verbatim to standard output. Mirrors the C macro
    /// `lua_writestring(s, l) = fwrite(s, 1, l, stdout)` from `lauxlib.h`,
    /// used by `print` and friends. A failed write is propagated as a
    /// `LuaError::runtime`; this matches C-Lua's behaviour where an I/O
    /// error during `lua_writestring` would surface through the host's
    /// error handling.
    pub fn write_output(&mut self, msg: &[u8]) -> Result<(), LuaError> {
        if let Some(write_fn) = self.global().stdout_hook {
            write_fn(msg).map_err(|e| LuaError::runtime(format_args!("{}", e)))?;
            return Ok(());
        }

        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
        {
            let _ = msg;
            Err(LuaError::runtime(format_args!(
                "stdout not available in this host"
            )))
        }

        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
        {
            use std::io::Write;
            let stdout = std::io::stdout();
            let mut handle = stdout.lock();
            handle
                .write_all(msg)
                .map_err(|e| LuaError::runtime(format_args!("{}", e)))?;
            handle
                .flush()
                .map_err(|e| LuaError::runtime(format_args!("{}", e)))?;
            Ok(())
        }
    }

    /// Convert the value at `idx` to a display string, push the result onto
    /// the stack, and return a copy of its bytes. Mirrors `luaL_tolstring`
    /// from `lauxlib.c`. The default Lua formatting is used for primitives
    /// (`"true"`/`"false"`/`"nil"`, `%I` integers, `%.14g` floats); for other
    /// reference types the result is `"<typename>: 0x<hex pointer>"`.
    ///
    /// If the value has a `__tostring` metamethod, it is invoked first and its
    /// (string) result is used in place of the default formatting (matching
    pub fn to_display_string(&mut self, idx: i32) -> Result<Vec<u8>, LuaError> {
        let abs = abs_index(self, idx);
        let v = index_to_value(self, abs);
        let mt: Option<GcRef<LuaTable>> = match &v {
            LuaValue::Table(t) => t.metatable(),
            LuaValue::UserData(u) => u.metatable(),
            _ => self.global().mt[v.base_type() as usize].clone(),
        };
        if let Some(mt_ref) = mt {
            let key = self.intern_str(b"__tostring")?;
            let f = mt_ref.get_short_str(&key);
            if !matches!(f, LuaValue::Nil) {
                let func_idx = self.top_idx();
                self.push(f);
                self.push(v.clone());
                if self.current_ci().is_lua_code() {
                    self.do_call(func_idx, 1)?;
                } else {
                    self.do_call_no_yield(func_idx, 1)?;
                }
                let top = self.top_idx();
                let result = self.get_at(StackIdx(top.0 - 1));
                if let LuaValue::Str(s) = result {
                    return Ok(s.as_bytes().to_vec());
                }
                let legacy_no_check = matches!(
                    self.global().lua_version,
                    lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
                );
                if matches!(result, LuaValue::Int(_) | LuaValue::Float(_)) {
                    return Ok(crate::object::num_to_string(self, &result)?
                        .as_bytes()
                        .to_vec());
                }
                if legacy_no_check {
                    let display_idx = StackIdx(top.0 - 1).0 as i32;
                    return display_default_bytes(self, &result, display_idx);
                }
                return Err(crate::debug::c_api_runtime(
                    self,
                    b"'__tostring' must return a string".to_vec(),
                ));
            }
        }
        let bytes: Vec<u8> = match &v {
            LuaValue::Str(s) => {
                let out = s.as_bytes().to_vec();
                self.push(LuaValue::Str(s.clone()));
                out
            }
            LuaValue::Int(_) | LuaValue::Float(_) => {
                let s = crate::object::num_to_string(self, &v)?;
                let out = s.as_bytes().to_vec();
                self.push(LuaValue::Str(s));
                out
            }
            LuaValue::Bool(b) => {
                let lit: &[u8] = if *b { b"true" } else { b"false" };
                let s = self.intern_str(lit)?;
                self.push(LuaValue::Str(s));
                lit.to_vec()
            }
            LuaValue::Nil => {
                let s = self.intern_str(b"nil")?;
                self.push(LuaValue::Str(s));
                b"nil".to_vec()
            }
            _ => {
                let kind = crate::tagmethods::obj_type_name(self, &v)?;
                let ptr = to_pointer(self, abs).unwrap_or(0);
                let mut buf = kind;
                buf.extend_from_slice(b": 0x");
                buf.extend_from_slice(format!("{:x}", ptr).as_bytes());
                let s = self.intern_str(&buf)?;
                self.push(LuaValue::Str(s));
                buf
            }
        };
        Ok(bytes)
    }

    /// (stack top minus the slot just after the frame's `func`).
    ///
    /// Receiver is `&mut self` to match the `LuaStateStubExt::top` trait
    /// signature exactly; with a different receiver shape (`&self`), Rust's
    /// method-resolution picks the trait default and the program panics on
    /// `todo!("phase-b-reconcile: top")`.
    pub fn top(&mut self) -> i32 {
        get_top(self)
    }

    /// `LuaStateStubExt::get_top` trait method. Inherent method shadows the
    /// trait default so the `todo!("phase-b-reconcile: get_top")` shim never
    /// fires.
    pub fn get_top(&mut self) -> i32 {
        get_top(self)
    }

    /// stack index `idx`, or `LuaType::None` if `idx` falls outside the
    /// active call frame. Inherent method shadows the
    /// `LuaStateStubExt::type_at` trait default so the `todo!()` shim
    /// never fires.
    pub fn type_at(&mut self, idx: i32) -> LuaType {
        lua_type_at(self, idx)
    }

    /// #N (value expected)` error if the slot at `arg` is `LUA_TNONE`
    /// (i.e. beyond the active call frame's top). Otherwise a no-op.
    ///
    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_arg_any`
    /// trait default so the `todo!()` shim never fires.
    pub fn check_arg_any(&mut self, arg: i32) -> Result<(), LuaError> {
        if lua_type_at(self, arg) == LuaType::None {
            return Err(crate::debug::arg_error_impl(self, arg, b"value expected"));
        }
        Ok(())
    }

    /// at `arg` to a string via `lua_tolstring` (which coerces numbers to
    /// their string form) and returns the bytes. Raises
    /// `bad argument #N (string expected, got <type>)` if the value is not a
    /// string and not number-coercible.
    ///
    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_arg_string`
    /// trait default so the `todo!()` shim never fires. Uses the free `to_lua_string`
    /// helper here rather than `auxlib::check_lstring`, which routes through
    /// `state.to_lua_string` / `state.type_name` — both still trait stubs.
    pub fn check_arg_string(&mut self, arg: i32) -> Result<Vec<u8>, LuaError> {
        match to_lua_string(self, arg)? {
            Some(s) => Ok(s.as_bytes().to_vec()),
            None => {
                let got = index_to_value(self, arg);
                let got_name = if lua_type_at(self, arg) == LuaType::None {
                    b"no value".to_vec()
                } else {
                    crate::tagmethods::obj_type_name(self, &got)?
                };
                let extramsg = format!(
                    "string expected, got {}",
                    String::from_utf8_lossy(&got_name)
                );
                Err(crate::debug::arg_error_impl(self, arg, extramsg.as_bytes()))
            }
        }
    }

    /// `arg` to a `lua_Integer` (i64) via `lua_tointegerx` (which accepts
    /// ints, floats with exact integer value, and string-form integers).
    /// Raises `bad argument #N (number has no integer representation)` if
    /// the value is a number but not representable as an integer, or
    /// `bad argument #N (number expected, got <type>)` otherwise.
    ///
    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_arg_integer`
    /// trait default so the `todo!()` shim never fires. Uses the free
    /// `to_integer_x` / `is_number` helpers in this file rather than
    /// `auxlib::check_integer`, which routes through `state.to_integer_x`
    /// and `state.type_name` — both still trait stubs.
    pub fn check_arg_integer(&mut self, arg: i32) -> Result<i64, LuaError> {
        match to_integer_x(self, arg) {
            Some(d) => Ok(d),
            None => {
                if is_number(self, arg) {
                    Err(crate::debug::arg_error_impl(
                        self,
                        arg,
                        b"number has no integer representation",
                    ))
                } else {
                    let got = index_to_value(self, arg);
                    let got_name = if lua_type_at(self, arg) == LuaType::None {
                        b"no value".to_vec()
                    } else {
                        crate::tagmethods::obj_type_name(self, &got)?
                    };
                    let extramsg = format!(
                        "number expected, got {}",
                        String::from_utf8_lossy(&got_name)
                    );
                    Err(crate::debug::arg_error_impl(self, arg, extramsg.as_bytes()))
                }
            }
        }
    }

    /// `arg` to an `f64` via `lua_tonumberx` (which accepts ints, floats,
    /// and number-shaped strings) and raises `bad argument #N (number
    /// expected, got <type>)` if the value is not number-coercible.
    ///
    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_number`
    /// trait default so the `todo!()` shim never fires. Uses the free
    /// `to_number_x` helper here rather than `auxlib::check_number`, which
    /// routes through `state.to_number_x` and `state.type_name` — both still
    /// trait stubs.
    pub fn check_number(&mut self, arg: i32) -> Result<f64, LuaError> {
        match to_number_x(self, arg) {
            Some(d) => Ok(d),
            None => {
                let got = index_to_value(self, arg);
                let got_name = if lua_type_at(self, arg) == LuaType::None {
                    b"no value".to_vec()
                } else {
                    crate::tagmethods::obj_type_name(self, &got)?
                };
                let extramsg = format!(
                    "number expected, got {}",
                    String::from_utf8_lossy(&got_name)
                );
                Err(crate::debug::arg_error_impl(self, arg, extramsg.as_bytes()))
            }
        }
    }

    /// `arg` is absent (`LUA_TNONE`) or `nil`, return `def`; otherwise
    /// convert it to an integer (with the same string-to-number coercion
    /// `lua_tointegerx` applies) and raise on failure.
    ///
    /// Inherent method on LuaState shadows the `LuaStateStubExt::opt_arg_integer`
    /// trait default so the `todo!()` shim never fires. Implemented with the
    /// free-function helpers in this file rather than `auxlib::opt_integer`
    /// because the latter routes through `state.is_none_or_nil` and
    /// `state.to_integer_x`, which are themselves stubbed.
    pub fn opt_arg_integer(&mut self, arg: i32, def: i64) -> Result<i64, LuaError> {
        match lua_type_at(self, arg) {
            LuaType::None | LuaType::Nil => Ok(def),
            _ => match to_integer_x(self, arg) {
                Some(d) => Ok(d),
                None => {
                    if is_number(self, arg) {
                        Err(LuaError::arg_error(
                            arg,
                            "number has no integer representation",
                        ))
                    } else {
                        let got = index_to_value(self, arg);
                        Err(LuaError::type_arg_error(arg, "number", &got))
                    }
                }
            },
        }
    }

    /// `lua_pcallk` with no continuation. Defers to the existing `pcall_k`
    /// free function, which routes through `protected_call_raw` and
    /// surfaces any runtime / syntax error as `Err(LuaError::Runtime|Syntax)`.
    ///
    /// Inherent method on LuaState shadows the `LuaStateStubExt::protected_call`
    /// trait default so the `todo!()` shim never fires.
    pub fn protected_call(&mut self, nargs: i32, nresults: i32, msgh: i32) -> Result<(), LuaError> {
        pcall_k(self, nargs, nresults, msgh, 0, None).map(|_| ())
    }

    /// protected call. When `k` is set and the thread is yieldable, an
    /// inner yield propagates as `LuaError::Yield` and the continuation
    /// fires on resume via `finishCcall` → `finishpcallk`.
    pub fn protected_call_k(
        &mut self,
        nargs: i32,
        nresults: i32,
        msgh: i32,
        ctx: isize,
        k: Option<crate::state::LuaKFunction>,
    ) -> Result<(), LuaError> {
        pcall_k(self, nargs, nresults, msgh, ctx, k).map(|_| ())
    }

    pub fn push_string(&mut self, s: &[u8]) -> Result<(), LuaError> {
        push_lstring(self, s)?;
        Ok(())
    }

    pub fn push_c_closure(
        &mut self,
        f: fn(&mut LuaState) -> Result<usize, LuaError>,
        n: i32,
    ) -> Result<(), LuaError> {
        push_cclosure(self, f, n)
    }

    pub fn raw_seti(&mut self, idx: i32, n: i64) -> Result<(), LuaError> {
        raw_set_i(self, idx, n)
    }

    pub fn table_set_i(&mut self, idx: i32, n: i64) -> Result<(), LuaError> {
        set_i(self, idx, n)
    }

    /// Get `t[n]` where `t` is a pre-resolved `LuaValue`, bypassing stack-index
    /// resolution. Use this in tight loops that operate on the same table
    /// repeatedly to avoid the `index_to_value` call per iteration.
    pub fn table_get_i_value(&mut self, t: &LuaValue, n: i64) -> Result<LuaType, LuaError> {
        get_i_value(self, t, n)
    }

    /// Set `t[n] = stack_top` (then pop) where `t` is a pre-resolved `LuaValue`,
    /// bypassing stack-index resolution. Use this in tight loops that operate on
    /// the same table repeatedly to avoid the `index_to_value` call per iteration.
    pub fn table_set_i_value(&mut self, t: &LuaValue, n: i64) -> Result<(), LuaError> {
        set_i_value(self, t, n)
    }

    pub fn create_table(&mut self, narr: i32, nrec: i32) -> Result<(), LuaError> {
        create_table(self, narr, nrec)
    }

    /// Pop the value on top of the stack and store it in the registry under
    /// the string `key`.
    ///
    pub fn registry_set(&mut self, key: &[u8]) -> Result<(), LuaError> {
        set_field(self, LUA_REGISTRYINDEX, key)
    }

    /// Create a new metatable in the registry under key `tname`. Leaves the
    /// new metatable on top of the stack and returns `true` when newly
    /// created. If `registry[tname]` already exists, leaves it on top of the
    /// stack and returns `false`.
    ///
    pub fn new_metatable(&mut self, tname: &[u8]) -> Result<bool, LuaError> {
        if get_field(self, LUA_REGISTRYINDEX, tname)? != LuaType::Nil {
            return Ok(false);
        }
        self.pop_n(1);
        create_table(self, 0, 2)?;
        push_lstring(self, tname)?;
        set_field(self, -2, b"__name")?;
        push_value(self, -1);
        set_field(self, LUA_REGISTRYINDEX, tname)?;
        Ok(true)
    }

    /// Create a new library table sized for `funcs` and register each entry as
    /// a closure field on it. Leaves the table on the top of the stack.
    ///
    ///   `luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)`.
    /// `luaL_checkversion` is a no-op here (no ABI-version mismatch is
    /// possible inside the Rust port).
    pub fn new_lib(&mut self, funcs: &[(&[u8], LuaCFunction)]) -> Result<(), LuaError> {
        create_table(self, 0, funcs.len() as i32)?;
        for (name, f) in funcs {
            push_cclosure(self, *f, 0)?;
            set_field(self, -2, name)?;
        }
        Ok(())
    }

    /// Create and populate a library table for `funcs`, leaving it on top of
    /// the stack. The `_name` argument is informational and matches the
    /// `luaL_register`-style call sites in the Phase-A stdlib; the actual
    /// global binding for the library happens later via `luaL_requiref`.
    ///
    pub fn register_lib(
        &mut self,
        _name: &[u8],
        funcs: &[(&[u8], LuaCFunction)],
    ) -> Result<(), LuaError> {
        self.new_lib(funcs)
    }

    /// Create a new empty table presized to hold every entry in `funcs`, and
    /// leave it on top of the stack. No registration is performed — callers
    /// typically follow up with `set_funcs` / `set_funcs_with_upvalues` to
    /// populate the table.
    ///
    ///   `lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)`. The C macro's
    /// `- 1` discounts the sentinel `{NULL, NULL}` entry; the Rust slice has
    /// no sentinel, so we use `funcs.len()` directly.
    pub fn new_lib_table(&mut self, funcs: &[(&[u8], LuaCFunction)]) -> Result<(), LuaError> {
        create_table(self, 0, funcs.len() as i32)
    }

    /// Register each entry in `funcs` as a C closure on the table at index
    /// `-(nup + 2)`, sharing the `nup` values currently on top of the stack
    /// as upvalues. The upvalues are popped at the end.
    ///
    pub fn set_funcs_with_upvalues(
        &mut self,
        funcs: &[(&[u8], LuaCFunction)],
        nup: i32,
    ) -> Result<(), LuaError> {
        check_stack(self, nup);
        for (name, f) in funcs {
            for _ in 0..nup {
                push_value(self, -nup);
            }
            push_cclosure(self, *f, nup)?;
            set_field(self, -(nup + 2), name)?;
        }
        self.pop_n(nup as usize);
        Ok(())
    }

    pub fn set_metatable(&mut self, objindex: i32) -> Result<(), LuaError> {
        set_metatable(self, objindex)?;
        Ok(())
    }

    /// Fetch the metatable registered under `name` in the registry and assign
    /// it as the metatable of the value currently on top of the stack. The
    /// fetched metatable is popped after assignment, leaving the original top
    /// value in place.
    ///
    pub fn set_metatable_by_name(&mut self, name: &[u8]) -> Result<(), LuaError> {
        get_field(self, LUA_REGISTRYINDEX, name)?;
        set_metatable(self, -2)?;
        Ok(())
    }

    /// Ensure `registry[name]` is a table; push it onto the stack.
    /// Returns `true` if the table already existed, `false` if newly created.
    ///
    pub fn get_subtable_registry(&mut self, name: &[u8]) -> Result<bool, LuaError> {
        if get_field(self, LUA_REGISTRYINDEX, name)? == LuaType::Table {
            return Ok(true);
        }
        self.pop_n(1);
        let idx = abs_index(self, LUA_REGISTRYINDEX);
        let new_tbl = self.new_table();
        self.push(LuaValue::Table(new_tbl));
        push_value(self, -1);
        set_field(self, idx, name)?;
        Ok(false)
    }

    /// Allocate a fresh full-userdata block of `size` bytes with `nuvalue`
    /// nil-initialised user-value slots, push it on the stack, and return a
    /// `GcRef` to it. The `_name` parameter is advisory — callers typically
    /// follow up with `set_metatable_by_name(name)` to attach the registered
    /// metatable.
    ///
    /// C-correspondent: `lua_newuserdatauv(L, size, nuvalue)` (no name
    /// parameter on the C side; the Rust signature carries it for callers'
    /// convenience).
    pub fn new_userdata_typed(
        &mut self,
        _name: &[u8],
        size: usize,
        nuvalue: i32,
    ) -> Result<GcRef<LuaUserData>, LuaError> {
        debug_assert!(nuvalue >= 0 && nuvalue < u16::MAX as i32, "invalid value");
        // TODO(D-1c-bridge): state.new_userdata is still todo!(); keep direct alloc
        let u = GcRef::new(LuaUserData {
            data: vec![0u8; size].into_boxed_slice(),
            uv: std::cell::RefCell::new(vec![LuaValue::Nil; nuvalue as usize]),
            metatable: std::cell::RefCell::new(None),
            host_value: std::cell::RefCell::new(None),
        });
        u.account_buffer(u.buffer_bytes() as isize);
        self.push(LuaValue::UserData(u.clone()));
        self.gc_pre_collect_clear();
        self.gc().check_step();
        Ok(u)
    }
}

/// Opaque identity pointer for a `LuaValue`, mirroring `to_pointer` but keyed on
/// the value rather than a stack index. Returns `None` for value-typed values
/// that have no identity (nil, bool, numbers).
fn value_pointer(o: &LuaValue) -> Option<usize> {
    match o {
        LuaValue::Function(LuaClosure::LightC(f)) => Some(*f as usize),
        LuaValue::LightUserData(p) => Some(*p as usize),
        LuaValue::Str(s) => Some(GcRef::identity(s)),
        LuaValue::Table(t) => Some(GcRef::identity(t)),
        LuaValue::Function(LuaClosure::Lua(f)) => Some(GcRef::identity(f)),
        LuaValue::Function(LuaClosure::C(f)) => Some(GcRef::identity(f)),
        LuaValue::UserData(u) => Some(GcRef::identity(u)),
        LuaValue::Thread(t) => Some(GcRef::identity(t)),
        _ => None,
    }
}

/// Formats `val` to its default `tostring` bytes without consulting any
/// metamethod (`"true"`/`"false"`/`"nil"`, numbers, or `"<type>: 0x<addr>"`).
/// Used for the Lua 5.1/5.2 `tostring` path, where `__tostring` may return any
/// type and the raw return value is used as-is. `_idx` identifies the slot the
/// value already occupies; callers must not push a new copy.
fn display_default_bytes(
    state: &mut LuaState,
    val: &LuaValue,
    _idx: i32,
) -> Result<Vec<u8>, LuaError> {
    match val {
        LuaValue::Str(s) => Ok(s.as_bytes().to_vec()),
        LuaValue::Int(_) | LuaValue::Float(_) => {
            Ok(crate::object::num_to_string(state, val)?.as_bytes().to_vec())
        }
        LuaValue::Bool(b) => Ok(if *b { b"true".to_vec() } else { b"false".to_vec() }),
        LuaValue::Nil => Ok(b"nil".to_vec()),
        _ => {
            let kind = crate::tagmethods::obj_type_name(state, val)?;
            let ptr = value_pointer(val).unwrap_or(0);
            let mut buf = kind;
            buf.extend_from_slice(b": 0x");
            buf.extend_from_slice(format!("{:x}", ptr).as_bytes());
            Ok(buf)
        }
    }
}

// ── access functions (stack → Rust) ──────────────────────────────────────────

pub fn lua_type_at(state: &LuaState, idx: i32) -> LuaType {
    if !is_valid_index(state, idx) {
        return LuaType::None;
    }
    index_to_value(state, idx).base_type()
}

pub fn type_name(_state: &LuaState, t: LuaType) -> &'static [u8] {
    t.type_name()
}

pub fn is_cfunction(state: &LuaState, idx: i32) -> bool {
    let o = index_to_value(state, idx);
    matches!(
        o,
        LuaValue::Function(LuaClosure::LightC(_)) | LuaValue::Function(LuaClosure::C(_))
    )
}

pub fn is_integer(state: &LuaState, idx: i32) -> bool {
    let o = index_to_value(state, idx);
    matches!(o, LuaValue::Int(_))
}

pub fn is_number(state: &LuaState, idx: i32) -> bool {
    let o = index_to_value(state, idx);
    o.to_number_with_strconv().is_some()
}

pub fn is_string(state: &LuaState, idx: i32) -> bool {
    let o = index_to_value(state, idx);
    matches!(o, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_))
}

pub fn is_userdata(state: &LuaState, idx: i32) -> bool {
    let o = index_to_value(state, idx);
    matches!(o, LuaValue::UserData(_) | LuaValue::LightUserData(_))
}

pub fn raw_equal(state: &LuaState, index1: i32, index2: i32) -> bool {
    if !is_valid_index(state, index1) || !is_valid_index(state, index2) {
        return false;
    }
    let o1 = index_to_value(state, index1);
    let o2 = index_to_value(state, index2);
    state.equal_obj(None, &o1, &o2)
}

// PORT NOTE: LUA_OPUNM / LUA_OPBNOT are unary; all others are binary.
pub fn arith(state: &mut LuaState, op: i32) -> Result<(), LuaError> {
    // TODO(port): LUA_OPUNM and LUA_OPBNOT constant values not yet defined in
    // Rust; using raw i32 comparison for now.
    const LUA_OPUNM: i32 = 12;
    const LUA_OPBNOT: i32 = 14;
    if op == LUA_OPUNM || op == LUA_OPBNOT {
        // unary — duplicate top as fake second operand
        let top_val = state.get_at(state.top_idx() - 1);
        state.push(top_val);
    }
    let top = state.top_idx();
    let a = state.get_at(top - 2);
    let b = state.get_at(top - 1);
    let result = state.arith_op(op, &a, &b)?;
    state.set_at(top - 2, result);
    state.pop();
    Ok(())
}

pub fn compare(state: &mut LuaState, index1: i32, index2: i32, op: i32) -> Result<bool, LuaError> {
    let valid = is_valid_index(state, index1) && is_valid_index(state, index2);
    let o1 = index_to_value(state, index1);
    let o2 = index_to_value(state, index2);
    if valid {
        match op {
            0 => Ok(state.equal_obj_with_tm(&o1, &o2)?),
            1 => state.less_than(&o1, &o2),
            2 => state.less_equal(&o1, &o2),
            _ => {
                debug_assert!(false, "invalid option");
                Ok(false)
            }
        }
    } else {
        Ok(false)
    }
}

pub fn string_to_number(state: &mut LuaState, s: &[u8]) -> usize {
    // TODO(port): luaO_str2num not yet translated; push result if successful.
    match state.str_to_num(s) {
        Some((val, consumed)) => {
            state.push(val);
            consumed
        }
        None => 0,
    }
}

pub fn to_number_x(state: &LuaState, idx: i32) -> Option<f64> {
    let o = index_to_value(state, idx);
    o.to_number_with_strconv()
}

pub fn to_integer_x(state: &LuaState, idx: i32) -> Option<i64> {
    let o = index_to_value(state, idx);
    o.to_integer_with_strconv()
}

pub fn to_boolean(state: &LuaState, idx: i32) -> bool {
    let o = index_to_value(state, idx);
    !matches!(o, LuaValue::Nil | LuaValue::Bool(false))
}

// PORT NOTE: returns Option<GcRef<LuaString>> instead of raw C pointer+len.
pub fn to_lua_string(state: &mut LuaState, idx: i32) -> Result<Option<GcRef<LuaString>>, LuaError> {
    let o = index_to_value(state, idx);
    if let LuaValue::Str(s) = &o {
        return Ok(Some(s.clone()));
    }
    if !matches!(o, LuaValue::Int(_) | LuaValue::Float(_)) {
        return Ok(None);
    }
    state.obj_to_string(idx)?;
    state.gc_pre_collect_clear();
    state.gc().check_step();
    let updated = index_to_value(state, idx);
    if let LuaValue::Str(s) = updated {
        Ok(Some(s))
    } else {
        Ok(None)
    }
}

pub fn raw_len(state: &LuaState, idx: i32) -> u64 {
    let o = index_to_value(state, idx);
    match &o {
        LuaValue::Str(s) => s.len() as u64,
        LuaValue::UserData(u) => u.len() as u64,
        LuaValue::Table(t) => state.table_getn(t) as u64,
        _ => 0,
    }
}

pub fn to_cfunction(
    state: &LuaState,
    idx: i32,
) -> Option<fn(&mut LuaState) -> Result<usize, LuaError>> {
    let o = index_to_value(state, idx);
    match o {
        // TODO(phase-b): lua-types `LuaClosure::LightC` carries a placeholder
        // `fn() -> i32` until it can reference `LuaState`. The real cast
        // happens once lua-types absorbs the LuaState-aware signature.
        LuaValue::Function(LuaClosure::LightC(_f)) => None,
        LuaValue::Function(LuaClosure::C(_ccl)) => None,
        _ => None,
    }
}

#[inline]
fn to_userdata_ptr(o: &LuaValue) -> Option<*mut core::ffi::c_void> {
    match o {
        LuaValue::UserData(u) => {
            // TODO(port): getudatamem returns a pointer to the raw byte payload of Udata.
            // In Rust, LuaUserData carries a Box<[u8]>; we'd need to return a raw ptr.
            // This is only safe inside lua-gc; stubbing with None for Phase A.
            let _ = u;
            None
        }
        LuaValue::LightUserData(p) => Some(*p),
        _ => None,
    }
}

pub fn to_userdata(state: &LuaState, idx: i32) -> Option<*mut core::ffi::c_void> {
    let o = index_to_value(state, idx);
    to_userdata_ptr(&o)
}

pub fn to_thread(state: &LuaState, idx: i32) -> Option<GcRef<lua_types::value::LuaThread>> {
    // TODO(phase-b): lua-vm's rich LuaState is not the same type as
    // lua_types::value::LuaThread; the latter is a placeholder. Resolve in
    // Phase B by unifying thread types.
    let o = index_to_value(state, idx);
    if let LuaValue::Thread(t) = o {
        Some(t)
    } else {
        None
    }
}

// PORT NOTE: returns a usize (opaque identity) rather than a raw void*.
// Raw pointers are only allowed in lua-gc / lua-coro.
pub fn to_pointer(state: &LuaState, idx: i32) -> Option<usize> {
    let o = index_to_value(state, idx);
    // TODO(port): returning a raw pointer here is not safe outside lua-gc.
    // Returning the GC identity as a usize for opaque pointer identity purposes.
    match &o {
        LuaValue::Function(LuaClosure::LightC(f)) => Some(*f as usize),
        LuaValue::LightUserData(p) => Some(*p as usize),
        LuaValue::Str(s) => Some(GcRef::identity(s)),
        LuaValue::Table(t) => Some(GcRef::identity(t)),
        LuaValue::Function(LuaClosure::Lua(f)) => Some(GcRef::identity(f)),
        LuaValue::Function(LuaClosure::C(f)) => Some(GcRef::identity(f)),
        LuaValue::UserData(u) => Some(GcRef::identity(u)),
        LuaValue::Thread(t) => Some(GcRef::identity(t)),
        _ => None,
    }
}

// ── push functions (Rust → stack) ────────────────────────────────────────────

pub fn push_nil(state: &mut LuaState) {
    state.push(LuaValue::Nil);
}

pub fn push_number(state: &mut LuaState, n: f64) {
    state.push(LuaValue::Float(n));
}

pub fn push_integer(state: &mut LuaState, n: i64) {
    state.push(LuaValue::Int(n));
}

// PORT NOTE: returns the interned LuaString instead of a raw C pointer.
pub fn push_lstring(state: &mut LuaState, s: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
    let ts = state.intern_str(s)?;
    state.push(LuaValue::Str(ts.clone()));
    state.gc_pre_collect_clear();
    state.gc().check_step();
    Ok(ts)
}

pub fn push_string(
    state: &mut LuaState,
    s: Option<&[u8]>,
) -> Result<Option<GcRef<LuaString>>, LuaError> {
    match s {
        None => {
            state.push(LuaValue::Nil);
            state.gc_pre_collect_clear();
            state.gc().check_step();
            Ok(None)
        }
        Some(bytes) => {
            let ts = state.intern_str(bytes)?;
            state.push(LuaValue::Str(ts.clone()));
            state.gc_pre_collect_clear();
            state.gc().check_step();
            Ok(Some(ts))
        }
    }
}

// PORT NOTE: va_list is not representable in safe Rust; callers pass a pre-formatted &[u8].
// TODO(port): lua_pushvfstring uses C varargs (va_list); no direct Rust equivalent.
// The Rust API uses state.push_fstring(format_args!(...)) instead.
pub fn push_vfstring(state: &mut LuaState, formatted: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
    let ts = state.intern_str(formatted)?;
    state.push(LuaValue::Str(ts.clone()));
    state.gc_pre_collect_clear();
    state.gc().check_step();
    Ok(ts)
}

// PORT NOTE: C varargs not used; callers use format_args! and push_fstring.
pub fn push_fstring(state: &mut LuaState, formatted: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
    push_vfstring(state, formatted)
}

pub fn push_cclosure(
    state: &mut LuaState,
    f: fn(&mut LuaState) -> Result<usize, LuaError>,
    n: i32,
) -> Result<(), LuaError> {
    //    if (n == 0) { setfvalue(s2v(L->top.p), fn); api_incr_top(L); }
    //    else { api_checknelems(L, n); api_check(L, n <= MAXUPVAL, ...);
    //           cl = luaF_newCclosure(L, n); cl->f = fn;
    //           L->top.p -= n;
    //           while (n--) setobj2n(L, &cl->upvalue[n], s2v(L->top.p + n));
    //           setclCvalue(L, s2v(L->top.p), cl); api_incr_top(L);
    //           luaC_checkGC(L); }
    //    lua_unlock(L);
    //
    // PORT NOTE: `LuaClosure::LightC` and `LuaCClosure` carry a `LuaCFnPtr`
    // (a `usize` index into `GlobalState.c_functions`) rather than the raw
    // function pointer, because lua-types cannot reference `LuaState`. We
    // register `f` in the per-state registry and store the resulting index.
    let idx: lua_types::closure::LuaCFnPtr = {
        let mut g = state.global_mut();
        if n == 0 {
            match g.c_functions.iter().position(|existing| {
                existing
                    .as_bare()
                    .is_some_and(|existing| std::ptr::fn_addr_eq(existing, f))
            }) {
                Some(i) => i,
                None => {
                    let i = g.c_functions.len();
                    g.c_functions.push(LuaCallable::bare(f));
                    i
                }
            }
        } else {
            let i = g.c_functions.len();
            g.c_functions.push(LuaCallable::bare(f));
            i
        }
    };
    if n == 0 {
        state.push(LuaValue::Function(LuaClosure::LightC(idx)));
    } else {
        debug_assert!(
            n > 0 && (n as u32) <= MAX_UPVAL as u32,
            "upvalue index too large"
        );
        let n_usize = n as usize;
        let top = state.top_idx();
        debug_assert!((top.0 as usize) >= n_usize, "not enough elements on stack");
        let base = top.0 as usize - n_usize;
        let mut upvalues: Vec<LuaValue> = Vec::with_capacity(n_usize);
        for i in 0..n_usize {
            upvalues.push(state.get_at(crate::state::StackIdx((base + i) as u32)));
        }
        state.pop_n(n_usize);
        // TODO(D-1c-bridge): state.new_c_closure is still todo!(); keep direct alloc
        let cl = LuaClosure::C(GcRef::new(lua_types::closure::LuaCClosure {
            func: idx,
            upvalues: std::cell::RefCell::new(upvalues),
        }));
        if let LuaClosure::C(ccl) = &cl {
            ccl.account_buffer(ccl.buffer_bytes() as isize);
        }
        state.push(LuaValue::Function(cl));
        state.gc_pre_collect_clear();
        state.gc().check_step();
    }
    Ok(())
}

pub fn push_boolean(state: &mut LuaState, b: bool) {
    state.push(LuaValue::Bool(b));
}

pub fn push_light_userdata(state: &mut LuaState, p: *mut core::ffi::c_void) {
    state.push(LuaValue::LightUserData(p));
}

// Returns true if pushed thread is the main thread.
pub fn push_thread(state: &mut LuaState) -> bool {
    let (value, is_main) = {
        let g = state.global();
        let id = g.current_thread_id;
        let v = g
            .thread_value_for(id)
            .expect("current_thread_id must always resolve to a registered thread");
        (v, id == g.main_thread_id)
    };
    state.push(LuaValue::Thread(value));
    is_main
}

// ── get functions (Lua → stack) ───────────────────────────────────────────────

fn aux_get_str(state: &mut LuaState, t: LuaValue, k: &[u8]) -> Result<LuaType, LuaError> {
    let str_val = {
        let ts = state.intern_str(k)?;
        LuaValue::Str(ts)
    };
    // TODO(port): luaV_fastget / luaV_finishget not yet translated; using
    // a simplified table_get that may miss metamethod chains.
    let result = state.table_get_with_tm(&t, &str_val)?;
    state.push(result);
    let top = state.top_idx();
    Ok(state.get_at(top - 1).base_type())
}

fn get_global_table(state: &LuaState) -> LuaValue {
    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder has
    // no storage, so we cannot fetch the globals table from the registry's
    // array slot. init_registry now stashes globals in a direct
    // GlobalState field; read it from there until the LuaTable placeholder
    // reconciles with lua-vm::table::LuaTable.
    //
    // Lua 5.1 has a per-thread global table (`lua_State.l_gt`): a freshly
    // loaded top-level chunk takes the *running* thread's `l_gt` as its
    // environment, and `setfenv(0, t)` from inside a coroutine must affect
    // only that coroutine, never the main thread's globals. Resolve through
    // the running thread's `l_gt` on 5.1; every other version shares one
    // global table and reads the `globals` field directly.
    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
        let running = state.global().current_thread_id;
        return state.v51_thread_lgt(running);
    }
    state.global().globals.clone()
}

pub fn get_global(state: &mut LuaState, name: &[u8]) -> Result<LuaType, LuaError> {
    let g = get_global_table(state);
    aux_get_str(state, g, name)
}

pub fn get_table(state: &mut LuaState, idx: i32) -> Result<LuaType, LuaError> {
    let t = index_to_value(state, idx);
    let top = state.top_idx();
    let key = state.get_at(top - 1);
    let result = state.table_get_with_tm(&t, &key)?;
    state.set_at(top - 1, result);
    let val = state.get_at(top - 1);
    Ok(val.base_type())
}

pub fn get_field(state: &mut LuaState, idx: i32, k: &[u8]) -> Result<LuaType, LuaError> {
    let t = index_to_value(state, idx);
    aux_get_str(state, t, k)
}

pub fn get_i(state: &mut LuaState, idx: i32, n: i64) -> Result<LuaType, LuaError> {
    let t = index_to_value(state, idx);
    let key = LuaValue::Int(n);
    let result = state.table_get_with_tm(&t, &key)?;
    state.push(result);
    let top = state.top_idx();
    Ok(state.get_at(top - 1).base_type())
}

/// Variant of `get_i` that accepts a pre-resolved table value instead of a
/// stack index. Callers that invoke `get_i` repeatedly on the same table
/// (e.g. the shift loops in `table.remove` / `table.insert`) should resolve
/// the table once and use this function to avoid calling `index_to_value`
/// on every iteration.
pub fn get_i_value(state: &mut LuaState, t: &LuaValue, n: i64) -> Result<LuaType, LuaError> {
    let key = LuaValue::Int(n);
    let result = state.table_get_with_tm(t, &key)?;
    state.push(result);
    let top = state.top_idx();
    Ok(state.get_at(top - 1).base_type())
}

fn finish_raw_get(state: &mut LuaState, val: Option<LuaValue>) -> LuaType {
    let v = val.unwrap_or(LuaValue::Nil);
    state.push(v);
    let top = state.top_idx();
    state.get_at(top - 1).base_type()
}

fn get_table_value(state: &LuaState, idx: i32) -> Option<GcRef<LuaTable>> {
    let t = index_to_value(state, idx);
    debug_assert!(matches!(t, LuaValue::Table(_)), "table expected");
    if let LuaValue::Table(tbl) = t {
        Some(tbl)
    } else {
        None
    }
}

pub fn raw_get(state: &mut LuaState, idx: i32) -> LuaType {
    let t = get_table_value(state, idx);
    let top = state.top_idx();
    let key = state.get_at(top - 1);
    let val = t.as_ref().map(|tbl| tbl.get(&key));
    state.set_top_idx(top - 1);
    finish_raw_get(state, val)
}

pub fn raw_get_i(state: &mut LuaState, idx: i32, n: i64) -> LuaType {
    let t = get_table_value(state, idx);
    let val = t.as_ref().map(|tbl| tbl.get_int(n));
    finish_raw_get(state, val)
}

pub fn raw_get_p(state: &mut LuaState, idx: i32, p: *const core::ffi::c_void) -> LuaType {
    let t = get_table_value(state, idx);
    let key = LuaValue::LightUserData(p as *mut core::ffi::c_void);
    let val = t.as_ref().map(|tbl| tbl.get(&key));
    finish_raw_get(state, val)
}

pub fn create_table(state: &mut LuaState, narray: i32, nrec: i32) -> Result<(), LuaError> {
    let t = state.new_table();
    if narray > 0 || nrec > 0 {
        t.resize(state, narray as usize, nrec as usize)?;
    }
    state.push(LuaValue::Table(t));
    state.gc_pre_collect_clear();
    state.gc().check_step();
    Ok(())
}

pub fn get_metatable(state: &mut LuaState, objindex: i32) -> bool {
    let obj = index_to_value(state, objindex);
    let mt: Option<GcRef<LuaTable>> = match &obj {
        LuaValue::Table(t) => t.metatable(),
        LuaValue::UserData(u) => u.metatable(),
        other => {
            let idx = other.base_type() as usize;
            state.global().mt[idx].clone()
        }
    };
    if let Some(mt_table) = mt {
        state.push(LuaValue::Table(mt_table));
        true
    } else {
        false
    }
}

pub fn get_i_uservalue(state: &mut LuaState, idx: i32, n: i32) -> LuaType {
    let o = index_to_value(state, idx);
    debug_assert!(matches!(o, LuaValue::UserData(_)), "full userdata expected");
    if let LuaValue::UserData(ref u) = o {
        let uv = u.uv.borrow();
        let uv_count = uv.len() as i32;
        if n <= 0 || n > uv_count {
            state.push(LuaValue::Nil);
            LuaType::None
        } else {
            let val = uv[(n - 1) as usize].clone();
            let t = val.base_type();
            state.push(val);
            t
        }
    } else {
        state.push(LuaValue::Nil);
        LuaType::None
    }
}

// ── set functions (stack → Lua) ───────────────────────────────────────────────

fn aux_set_str(state: &mut LuaState, t: LuaValue, k: &[u8]) -> Result<(), LuaError> {
    let str_val = {
        let ts = state.intern_str(k)?;
        LuaValue::Str(ts)
    };
    //       luaV_finishfastset(L, t, slot, s2v(L->top.p - 1)); L->top.p--;
    //    else { setsvalue2s L->top.p str; api_incr_top;
    //           luaV_finishset(L, t, s2v(L->top.p-1), s2v(L->top.p-2), slot);
    //           L->top.p -= 2; }
    let top = state.top_idx();
    let val = state.get_at(top - 1);
    state.table_set_with_tm(&t, str_val, val)?;
    state.pop();
    Ok(())
}

pub fn set_global(state: &mut LuaState, name: &[u8]) -> Result<(), LuaError> {
    let g = get_global_table(state);
    aux_set_str(state, g, name)
}

pub fn set_table(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
    let t = index_to_value(state, idx);
    let top = state.top_idx();
    let key = state.get_at(top - 2);
    let val = state.get_at(top - 1);
    state.table_set_with_tm(&t, key, val)?;
    state.set_top_idx(top - 2);
    Ok(())
}

pub fn set_field(state: &mut LuaState, idx: i32, k: &[u8]) -> Result<(), LuaError> {
    let t = index_to_value(state, idx);
    aux_set_str(state, t, k)
}

pub fn set_i(state: &mut LuaState, idx: i32, n: i64) -> Result<(), LuaError> {
    let t = index_to_value(state, idx);
    let top = state.top_idx();
    let val = state.get_at(top - 1);
    let key = LuaValue::Int(n);
    state.table_set_with_tm(&t, key, val)?;
    state.pop();
    Ok(())
}

/// Variant of `set_i` that accepts a pre-resolved table value instead of a
/// stack index. Callers that invoke `set_i` repeatedly on the same table
/// (e.g. the shift loops in `table.remove` / `table.insert`) should resolve
/// the table once and use this function to avoid calling `index_to_value`
/// on every iteration.
pub fn set_i_value(state: &mut LuaState, t: &LuaValue, n: i64) -> Result<(), LuaError> {
    let top = state.top_idx();
    let val = state.get_at(top - 1);
    let key = LuaValue::Int(n);
    state.table_set_with_tm(t, key, val)?;
    state.pop();
    Ok(())
}

fn aux_raw_set(state: &mut LuaState, idx: i32, key: LuaValue, n: u32) -> Result<(), LuaError> {
    let t = get_table_value(state, idx)
        .ok_or_else(|| LuaError::runtime(format_args!("table expected")))?;
    let top = state.top_idx();
    let val = state.get_at(top - 1);
    t.raw_set(state, key, val)?;
    t.invalidate_tm_cache();
    let top_val = state.get_at(top - 1);
    state.gc().table_barrier_back(&t, &top_val);
    state.set_top_idx(top - n as i32);
    Ok(())
}

pub fn raw_set(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
    let top = state.top_idx();
    let key = state.get_at(top - 2);
    aux_raw_set(state, idx, key, 2)
}

pub fn raw_set_p(
    state: &mut LuaState,
    idx: i32,
    p: *const core::ffi::c_void,
) -> Result<(), LuaError> {
    let key = LuaValue::LightUserData(p as *mut core::ffi::c_void);
    aux_raw_set(state, idx, key, 1)
}

pub fn raw_set_i(state: &mut LuaState, idx: i32, n: i64) -> Result<(), LuaError> {
    let t = get_table_value(state, idx)
        .ok_or_else(|| LuaError::runtime(format_args!("table expected")))?;
    let top = state.top_idx();
    let val = state.get_at(top - 1);
    t.raw_set_int(state, n, val)?;
    let top_val = state.get_at(top - 1);
    state.gc().table_barrier_back(&t, &top_val);
    state.pop();
    Ok(())
}

/// Returns true if `mt` (a metatable) holds a non-nil `__gc` entry.
///
/// PORT NOTE: Mirrors the body of C's `tofinalize` in `lgc.c` minus the bits
/// that consult per-object GC bits (irrelevant in Phase B's Rc world).
fn metatable_has_gc(state: &LuaState, mt: &GcRef<LuaTable>) -> bool {
    let name = state.global().tmname[crate::tagmethods::TagMethod::Gc as usize].clone();
    !matches!(mt.get_short_str(&name), LuaValue::Nil)
}

fn register_finalizable_object(state: &mut LuaState, object: FinalizerObject) {
    let heap_ptr = object.heap_ptr();
    let mut g = state.global_mut();
    if g.finalizers.push_pending_unique(object) {
        if let Some(ptr) = heap_ptr {
            g.heap.move_allgc_to_finobj(ptr);
        }
    }
}

/// Lua 5.1 collect-time finalizability probe for one userdata.
///
/// Holds only a weak handle, so it never roots the userdata — the collector's
/// roster can keep it indefinitely without preventing collection. `is_alive`
/// reports whether the box has been swept (via the weak handle's allocation
/// token), letting the collector prune dead probes safely. See
/// [`lua_gc::Udata51Probe`] and C 5.1 `luaC_separateudata`.
///
/// `submitted` is C's permanent `FINALIZEDBIT`: once the scan hands a userdata
/// to the finalizer machinery it is never re-submitted, so each userdata is
/// finalized at most once. (The shared `is_finalized` header flag cannot serve
/// this role — `pop_to_be_finalized` resets it so the object can be collected
/// normally after its `__gc` runs, which would otherwise let the scan
/// re-finalize it on every later collect.)
struct Udata51RosterEntry {
    weak: lua_types::gc::GcWeak<LuaUserData>,
    submitted: std::cell::Cell<bool>,
}

impl lua_gc::Udata51Probe for Udata51RosterEntry {
    fn is_alive(&self) -> bool {
        self.weak.upgrade().is_some()
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// Record a userdata in the Lua 5.1 collect-time finalizability roster.
///
/// 5.1 decides finalizability at collect time by re-reading each userdata's
/// live metatable, so a `__gc` installed on a shared metatable *after*
/// `setmetatable` still takes effect (the `gc.lua` newproxy case). The eager
/// `setmetatable`-time registration cannot see such late `__gc`, so on 5.1 we
/// additionally roster every userdata that carries a metatable and re-scan
/// them at each explicit collection. No-op on 5.2–5.5, which match C's eager
/// `luaC_checkfinalizer` exactly.
fn roster_v51_userdata(state: &LuaState, ud: &GcRef<LuaUserData>) {
    if !matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
        return;
    }
    let probe = std::rc::Rc::new(Udata51RosterEntry {
        weak: ud.downgrade(),
        submitted: std::cell::Cell::new(false),
    });
    state.global().heap.register_v51_udata(probe);
}

/// Lua 5.1 collect-time finalizability scan, run before an explicit collect.
///
/// Mirrors C 5.1 `luaC_separateudata`: walk every rostered userdata and, for
/// each one whose live metatable now carries `__gc`, register it for
/// finalization. `register_finalizable_object` is idempotent (it skips
/// already-finalized objects), so a userdata registered eagerly at
/// `setmetatable` or by an earlier scan is not double-registered. Reachability
/// is decided later by the collector's post-mark hook: a still-reachable
/// userdata stays pending and is finalized only once it actually dies.
///
/// No-op on 5.2–5.5.
pub fn scan_v51_finalizable_userdata(state: &mut LuaState) {
    if !matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
        return;
    }
    let probes = state.global().heap.scan_v51_finalizable();
    if probes.is_empty() {
        return;
    }
    let gc_name = state.global().tmname[crate::tagmethods::TagMethod::Gc as usize].clone();
    for probe in &probes {
        let entry = match probe.as_any().downcast_ref::<Udata51RosterEntry>() {
            Some(entry) => entry,
            None => continue,
        };
        if entry.submitted.get() {
            continue;
        }
        let ud = match entry.weak.upgrade() {
            Some(ud) => ud,
            None => continue,
        };
        let has_gc = match ud.metatable() {
            Some(mt) => !matches!(mt.get_short_str(&gc_name), LuaValue::Nil),
            None => false,
        };
        if has_gc {
            entry.submitted.set(true);
            register_finalizable_object(state, FinalizerObject::UserData(ud));
        }
    }
}

/// Drain objects already promoted from `pending_finalizers` to
/// `to_be_finalized` by the heap mark phase.
pub fn run_pending_finalizers(state: &mut LuaState) {
    let _ = run_pending_finalizers_inner(state, false);
}

const GC_FIN_MAX: usize = 10;

/// `__gc` driver that mirrors C-Lua's `GCTM(L, propagateerrors)`.
///
/// `propagate` corresponds to C's `propagateerrors` argument. C calls
/// `GCTM(L, 1)` from `runafewfinalizers` (the explicit-collect / automatic
/// step paths) and `GCTM(L, 0)` from `callallpendingfinalizers` (the
/// `lua_close` path). When a finalizer errors and `propagate` is set, the
/// disposition is version-specific:
///
/// - 5.2 / 5.3: wrap the error object as `error in __gc metamethod (%s)`
///   (matching `GCTM`'s `LUA_ERRGCMM` branch) and re-throw, aborting the
///   drain. Returned here as `Err(LuaError)` for the caller to propagate.
/// - 5.4 / 5.5: `luaE_warnerror(L, "__gc")` — emit a warning and discard the
///   error. The warning is silent unless the program enabled warnings
///   (`warn("@on")`), which matches the reference default of swallowing
///   `__gc` errors with warnings off.
/// - 5.1: errors are silently swallowed (`luaC_callGCTM` ignores the status).
///
/// `propagate = false` (the close path) swallows the error on every version,
/// matching `callallpendingfinalizers`'s `GCTM(L, 0)`.
pub fn run_pending_finalizers_inner(state: &mut LuaState, propagate: bool) -> Result<(), LuaError> {
    run_pending_finalizers_limited(state, propagate, None).map(|_| ())
}

pub(crate) fn run_some_pending_finalizers_inner(
    state: &mut LuaState,
    propagate: bool,
) -> Result<usize, LuaError> {
    run_pending_finalizers_limited(state, propagate, Some(GC_FIN_MAX))
}

fn run_pending_finalizers_limited(
    state: &mut LuaState,
    propagate: bool,
    limit: Option<usize>,
) -> Result<usize, LuaError> {
    let version = state.global().lua_version;
    let mut processed = 0usize;
    loop {
        if limit.map_or(false, |limit| processed >= limit) {
            break;
        }
        // `to_be_finalized` is stored in tobefnz head order. Newly unreachable
        // objects are appended behind any older queued finalizers, matching
        // C-Lua's `finobj`/`tobefnz` ordering.
        if !state.global().finalizers.has_to_be_finalized() {
            break;
        }
        // The Phase-A pre-finalizer weak-value sweep (mirroring C-Lua's
        // `clearbyvalues(g, g->weak, NULL)` from `atomic()`) is no longer
        // needed: under D-2, weak-table sweeping runs inside the post-mark
        // hook of `Heap::full_collect_with_post_mark`, which uses
        // reachability instead of strong_count and therefore clears such
        // entries BEFORE this finalizer pass runs. The full "bug-in-5.1"
        // ordering (finalizer-visible state) still requires reachability-
        // based detection of which finalizable tables are about to die — a
        // gap tracked under D-2 ephemeron/finalizer follow-up.
        let object = {
            let mut g = state.global_mut();
            let object = g
                .finalizers
                .pop_to_be_finalized()
                .expect("to-be-finalized checked non-empty");
            if let Some(ptr) = object.heap_ptr() {
                g.heap.move_tobefnz_to_allgc(ptr);
            }
            object
        };
        processed += 1;
        let mt = object.metatable();
        let gc_fn = match mt {
            Some(ref m) => {
                let name = state.global().tmname[crate::tagmethods::TagMethod::Gc as usize].clone();
                m.get_short_str(&name)
            }
            None => LuaValue::Nil,
        };
        if !matches!(gc_fn, LuaValue::Function(_)) {
            continue;
        }
        let saved_top = state.top_idx();
        let ci_top = state.current_call_info().top;
        if saved_top.0 < ci_top.0 {
            state.clear_stack_range(saved_top, ci_top);
            state.set_top(ci_top);
        }
        state.push(gc_fn);
        state.push(object.as_lua_value());
        let func_idx = state.top_idx() - 2;
        let _heap_guard = {
            let g = state.global.borrow();
            lua_gc::HeapGuard::push(&g.heap)
        };
        let old_allowhook = state.allowhook;
        let old_gcstp = state.global_mut().stop_gc_internal();
        state.allowhook = false;
        let caller_ci = state.ci;
        let caller_status = state.get_ci(caller_ci).callstatus;
        state.get_ci_mut(caller_ci).callstatus = caller_status | crate::state::CIST_FIN;
        let status = crate::do_::pcall(state, |s| s.call_no_yield(func_idx, 0), func_idx, 0);
        // On error, `pcall` left the error object on top of the stack. Capture
        // it before `set_top` truncates so the version-specific disposition
        // below (propagate on 5.2/5.3, warn on 5.4/5.5) can use it.
        let finalizer_error: Option<LuaValue> = if status != LuaStatus::Ok {
            Some(state.get_at(state.top_idx() - 1).clone())
        } else {
            None
        };
        state.get_ci_mut(caller_ci).callstatus = caller_status;
        state.allowhook = old_allowhook;
        state.global_mut().set_gc_stop_flags(old_gcstp);
        state.set_top(saved_top);

        if let Some(errobj) = finalizer_error {
            match version {
                lua_types::LuaVersion::V51 if propagate => {
                    // C 5.1 `GCTM` invokes the finalizer with `luaD_call`
                    // (unprotected), so a raised error propagates UNWRAPPED out
                    // of `luaC_fullgc` / `lua_gc` to the enclosing pcall — no
                    // `error in __gc metamethod (...)` decoration (that wrapping
                    // arrived in 5.2). Re-throw the raw error object and abort
                    // the drain, matching the C longjmp.
                    return Err(LuaError::from_value(errobj));
                }
                lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53 if propagate => {
                    // C `GCTM`: wrap a string error object as
                    // `error in __gc metamethod (%s)` and re-throw. A
                    // non-string object becomes `(no message)`. Aborts the
                    // drain (the remaining `to_be_finalized` entries are
                    // left for a later pass, matching the C longjmp out of
                    // `runafewfinalizers`).
                    let msg: Vec<u8> = match &errobj {
                        LuaValue::Str(s) => s.as_bytes().to_vec(),
                        _ => b"no message".to_vec(),
                    };
                    let mut wrapped = b"error in __gc metamethod (".to_vec();
                    wrapped.extend_from_slice(&msg);
                    wrapped.push(b')');
                    let interned = state.intern_str(&wrapped)?;
                    return Err(LuaError::from_value(LuaValue::Str(interned)));
                }
                lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55 if propagate => {
                    // C `luaE_warnerror(L, "__gc")`: emit
                    // `error in __gc (<msg>)` through the warning system.
                    // Silent unless warnings are enabled.
                    let msg: Vec<u8> = match &errobj {
                        LuaValue::Str(s) => s.as_bytes().to_vec(),
                        _ => b"error object is not a string".to_vec(),
                    };
                    state.emit_warning(b"error in ", true);
                    state.emit_warning(b"__gc", true);
                    state.emit_warning(b" (", true);
                    state.emit_warning(&msg, true);
                    state.emit_warning(b")", false);
                }
                _ => {}
            }
        }
    }
    // Post-finalizer weak sweep is also obsolete: any weak entries newly
    // exposed by the finalizer pass will be cleared on the NEXT
    // `Heap::full_collect_with_post_mark`. We accept the one-cycle lag.
    Ok(processed)
}

/// Run every still-pending `__gc` finalizer at state close.
///
/// Mirrors C-Lua's `luaC_freeallobjects` (`lgc.c`), which calls
/// `separatetobefnz(g, 1)` to move *all* remaining finalizable objects
/// (regardless of reachability) into the to-be-finalized list, then
/// `callallpendingfinalizers` to invoke each `__gc` before the objects are
/// freed. At `lua_close`, objects the program kept alive to program end —
/// e.g. a table held by a global — still have their finalizer run; that is
/// what emits messages like `>>> closing state <<<` from `gc.lua`.
///
/// The typed registry of finalizable objects is `pending_finalizers`. A single
/// snapshot of that list is promoted into `to_be_finalized` and drained by
/// [`run_pending_finalizers`]. We snapshot
/// once (matching C's single `separatetobefnz` call): a finalizer may
/// resurrect its object or register new finalizables via `setmetatable`, but
/// C does not re-finalize those at close (`gcstp = GCSTPCLS`), so neither do
/// we — the freshly-registered entries are left in `pending_finalizers` and
/// simply dropped with the state.
pub fn run_close_finalizers(state: &mut LuaState) {
    let pending: Vec<FinalizerObject> = state.global_mut().finalizers.take_pending();
    if pending.is_empty() {
        return;
    }
    let mut seen = std::collections::HashSet::<usize>::new();
    {
        let mut g = state.global_mut();
        for object in pending.into_iter().rev() {
            let heap_ptr = object.heap_ptr();
            if seen.insert(object.identity()) {
                g.finalizers.push_to_be_finalized(object);
                if let Some(ptr) = heap_ptr {
                    g.heap.move_finobj_to_tobefnz(ptr);
                }
            }
        }
    }
    run_pending_finalizers(state);
}

/// Snapshot the currently-live weak tables from
/// `GlobalState.weak_tables_registry`, deduplicating by Rc pointer and
/// dropping any whose backing storage has been freed. Used by both the
/// pre-finalizer and post-finalizer sweeps in [`run_pending_finalizers`]
/// and by the explicit `collectgarbage("collect")` path.
fn collect_live_weak_tables(state: &mut LuaState) -> Vec<GcRef<lua_types::value::LuaTable>> {
    let mut g = state.global_mut();
    g.weak_tables_registry.live_snapshot()
}

pub fn set_metatable(state: &mut LuaState, objindex: i32) -> Result<bool, LuaError> {
    let top = state.top_idx();
    let mt_val = state.get_at(top - 1);
    let mt: Option<GcRef<LuaTable>> = if matches!(mt_val, LuaValue::Nil) {
        None
    } else {
        debug_assert!(matches!(mt_val, LuaValue::Table(_)), "table expected");
        if let LuaValue::Table(t) = mt_val {
            Some(t)
        } else {
            None
        }
    };

    let obj = index_to_value(state, objindex);
    match obj {
        LuaValue::Table(ref tbl) => {
            if mt.is_some() {
                state.gc().obj_barrier(tbl, mt.as_ref().unwrap());
            }
            tbl.set_metatable(mt.clone());
            if tbl.weak_mode() == 0 {
                state
                    .global_mut()
                    .weak_tables_registry
                    .remove_identity(tbl.identity());
            } else {
                state
                    .global_mut()
                    .weak_tables_registry
                    .push_unique(WeakTableEntry::new(tbl));
            }
            // Phase-B finalizer registration: if the new metatable carries
            // `__gc` and `obj` was not already registered, pin `obj` in the
            // pending-finalizers list so that `run_pending_finalizers` can
            // invoke the finalizer before the object is freed.
            //
            // Lua 5.1 has no `__gc` on tables — only userdata can be finalized.
            // Setting `__gc` on a table metatable is inert under V51 (no call,
            // no error). `__gc` on tables was added in 5.2, so only register
            // table finalizers off V51.
            let tables_finalizable =
                !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
            if tables_finalizable {
                if let Some(ref mt_table) = mt {
                    if metatable_has_gc(state, mt_table) {
                        register_finalizable_object(state, FinalizerObject::Table(tbl.clone()));
                    }
                }
            }
        }
        LuaValue::UserData(ref ud) => {
            if let Some(ref mt_table) = mt {
                state.gc().obj_barrier(ud, mt_table);
                if metatable_has_gc(state, mt_table) {
                    register_finalizable_object(state, FinalizerObject::UserData(ud.clone()));
                }
                // Lua 5.1 decides finalizability at collect time, so a `__gc`
                // added to this (possibly shared) metatable later must still
                // take effect. Roster the userdata for the collect-time scan;
                // no-op on 5.2–5.5.
                roster_v51_userdata(state, ud);
            }
            ud.set_metatable(mt);
        }
        ref other => {
            let idx = other.base_type() as usize;
            state.global_mut().mt[idx] = mt;
        }
    }
    state.pop();
    Ok(true)
}

pub fn set_i_uservalue(state: &mut LuaState, idx: i32, n: i32) -> Result<bool, LuaError> {
    let o = index_to_value(state, idx);
    debug_assert!(matches!(o, LuaValue::UserData(_)), "full userdata expected");
    let top = state.top_idx();
    let val = state.get_at(top - 1);
    let res = if let LuaValue::UserData(ref ud) = o {
        let mut uv = ud.uv.borrow_mut();
        let nuvalue = uv.len() as i32;
        if n < 1 || n > nuvalue {
            false
        } else {
            uv[(n - 1) as usize] = val.clone();
            drop(uv);
            state.gc().barrier_back(ud, &val);
            true
        }
    } else {
        false
    };
    state.pop();
    Ok(res)
}

// ── load/call functions ───────────────────────────────────────────────────────

//                            lua_KContext ctx, lua_KFunction k)
pub fn call_k(
    state: &mut LuaState,
    nargs: i32,
    nresults: i32,
    ctx: isize,
    k: Option<fn(&mut LuaState, i32, isize) -> Result<usize, LuaError>>,
) -> Result<(), LuaError> {
    let top = state.top_idx();
    let func_idx = top - (nargs + 1);
    //      L->ci->u.c.k = k; L->ci->u.c.ctx = ctx;
    //      luaD_call(L, func, nresults);
    //    } else {
    //      luaD_callnoyield(L, func, nresults);
    //    }
    if k.is_some() && state.is_yieldable() {
        let ci_idx = state.ci;
        {
            let ci = state.get_ci_mut(ci_idx);
            ci.set_u_c_k(k);
            ci.set_u_c_ctx(ctx);
        }
        state.call_at(func_idx, nresults)?;
    } else {
        state.call_no_yield(func_idx, nresults)?;
    }
    state.adjust_results(nresults);
    Ok(())
}

//                            lua_KContext ctx, lua_KFunction k)
pub fn pcall_k(
    state: &mut LuaState,
    nargs: i32,
    nresults: i32,
    errfunc: i32,
    ctx: isize,
    k: Option<fn(&mut LuaState, i32, isize) -> Result<usize, LuaError>>,
) -> Result<LuaStatus, LuaError> {
    // Phase D-1c: activate the heap for the duration of this protected call.
    // GcRef::new (post D-1e) and any future allocator-aware code will route
    // through state.global.heap via with_current_heap(...). Stacked so nested
    // pcalls inside the same thread don't clobber each other.
    let _heap_guard = {
        let g = state.global.borrow();
        // The HeapGuard borrows &Heap; we let it live for the function scope.
        // The borrow of `g` is dropped immediately; the guard's NonNull
        // outlives it (the heap field is pinned inside GlobalState which
        // is Rc-managed and won't move).
        lua_gc::HeapGuard::push(&g.heap)
    };
    let err_handler_idx: isize = if errfunc == 0 {
        0
    } else {
        let o = index_to_stack_idx(state, errfunc);
        debug_assert!(
            matches!(state.get_at(o), LuaValue::Function(_)),
            "error handler must be a function"
        );
        o.0 as isize
    };
    let top = state.top_idx();
    let func_idx = top - (nargs + 1);
    if k.is_none() || !state.is_yieldable() {
        state.protected_call_raw(func_idx, nresults, StackIdx(err_handler_idx as u32))?;
        state.adjust_results(nresults);
        return Ok(LuaStatus::Ok);
    }
    // Yieldable continuation path: arrange for an interrupted call (yield or
    // recoverable error) to be resumable. The call is already protected by
    // `lua_resume`; real errors must propagate with CIST_YPCALL still set so
    // `precover` can run `finish_pcallk`.
    //
    let ci_idx = state.ci;
    let allow = state.allowhook;
    let saved_errfunc = state.errfunc;
    {
        let ci = state.get_ci_mut(ci_idx);
        ci.set_u_c_k(k);
        ci.set_u_c_ctx(ctx);
        ci.set_u2_funcidx(func_idx.0 as i32);
        ci.set_u_c_old_errfunc(saved_errfunc);
        ci.set_oah(allow);
        ci.callstatus |= crate::state::CIST_YPCALL;
    }
    state.errfunc = err_handler_idx;
    let call_result = crate::do_::call(state, func_idx, nresults);
    match call_result {
        Ok(()) => {
            //    L->errfunc = ci->u.c.old_errfunc;
            //    status = LUA_OK;
            state.get_ci_mut(ci_idx).callstatus &= !crate::state::CIST_YPCALL;
            state.errfunc = saved_errfunc;
            state.adjust_results(nresults);
            Ok(LuaStatus::Ok)
        }
        Err(crate::state::LuaError::Yield) => {
            // Yield must propagate up to lua_resume. The recovery prep stays
            // on `ci_idx` so that on resume, `finishCcall` will call
            // `finishpcallk` followed by the continuation `k`.
            Err(crate::state::LuaError::Yield)
        }
        Err(e) => {
            // Real errors take the same path as C longjmp: they unwind to
            // lua_resume's protected runner, which calls precover and then
            // finish_pcallk while this C frame still advertises CIST_YPCALL.
            Err(e)
        }
    }
}

//                          const char *chunkname, const char *mode)
// PORT NOTE: lua_Reader (void* callback) is replaced by Box<dyn FnMut>; mode
// is &[u8].
/// Whether the freshly loaded closure's first upvalue is the synthetic `_ENV`
/// cell, used to decide if `load` should seed it with the globals table on
/// Lua 5.1.
///
/// Lua 5.1 has no `_ENV` syntax — globals reach their environment through the
/// running thread's `l_gt` (fenv model), not an injected upvalue. Our core
/// nonetheless gives a source-loaded 5.1 chunk a synthetic `_ENV` upvalue at
/// index 0 (the Option-B fenv model) so global access can route through
/// `GETTABUP`. That synthetic cell must be seeded with the globals table. A
/// function reconstructed from `string.dump` bytecode, by contrast, carries
/// its real upvalues (e.g. `a`, `b`) at index 0 and must be left uninitialised
/// (nil) to match the reference. Distinguishing them by whether upvalue 0 is
/// named `_ENV` reproduces both behaviours.
fn first_upvalue_is_env(lcl: &lua_types::closure::LuaLClosure) -> bool {
    lcl.proto
        .upvalues
        .first()
        .and_then(|uv| uv.name.as_ref())
        .is_some_and(|s| s.as_bytes() == b"_ENV")
}

pub fn load(
    state: &mut LuaState,
    reader: crate::zio::ChunkReader,
    chunkname: Option<&[u8]>,
    mode: Option<&[u8]>,
) -> Result<LuaStatus, LuaError> {
    let name = chunkname.unwrap_or(b"?");
    let z = crate::zio::ZIO::new(reader);
    let status = state.protected_parser(z, name, mode);
    if status == LuaStatus::Ok {
        let top = state.top_idx();
        let func_val = state.get_at(top - 1);
        if let LuaValue::Function(LuaClosure::Lua(lcl)) = func_val {
            let inject_env = match state.global().lua_version {
                lua_types::LuaVersion::V51 => first_upvalue_is_env(&lcl),
                lua_types::LuaVersion::V52 => lcl.upvals.len() == 1,
                _ => !lcl.upvals.is_empty(),
            };
            if inject_env {
                let gt = get_global_table(state);
                let uv = state.new_upval_closed(gt);
                lcl.set_upval(0, uv);
                state.gc().obj_barrier(&lcl, &uv);
            }
        }
    }
    Ok(status)
}

pub fn dump(
    state: &LuaState,
    writer: &mut dyn FnMut(&[u8]) -> Result<(), LuaError>,
    strip: bool,
) -> Result<bool, LuaError> {
    let top = state.top_idx();
    let o = state.get_at(top - 1);
    if let LuaValue::Function(LuaClosure::Lua(ref lcl)) = o {
        crate::dump::dump(state, &lcl.proto, writer, strip)?;
        Ok(true)
    } else {
        Ok(false)
    }
}

pub fn status(state: &LuaState) -> LuaStatus {
    LuaStatus::from_raw(state.status as i32)
}

// ── garbage collection ────────────────────────────────────────────────────────

/// GC operation codes (C: LUA_GC* constants)
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GcWhat {
    Stop = 0,
    Restart = 1,
    Collect = 2,
    Count = 3,
    CountB = 4,
    Step = 5,
    SetPause = 6,
    SetStepMul = 7,
    IsRunning = 9,
    Gen = 10,
    Inc = 11,
}

// PORT NOTE: C varargs replaced by explicit GcArgs enum; callers supply parameters directly.
pub enum GcArgs {
    Stop,
    Restart,
    Collect,
    Count,
    CountB,
    Step {
        data: i32,
    },
    SetPause {
        value: i32,
    },
    SetStepMul {
        value: i32,
    },
    IsRunning,
    Gen {
        minormul: i32,
        majormul: i32,
    },
    Inc {
        pause: i32,
        stepmul: i32,
        stepsize: i32,
    },
    /// Lua 5.5 `collectgarbage("param", name [, value])`. `param` is the
    /// 0-based param index; `value < 0` means "read only".
    Param {
        param: usize,
        value: i64,
    },
}

pub fn gc(state: &mut LuaState, args: GcArgs) -> i32 {
    // Lua 5.5 `collectgarbage("param", ...)` reads/writes a param and is not
    // gated by the finalizer (gc-stopped-internally) guard. Handled first so a
    // param read is never clobbered by the -1 sentinel.
    if let GcArgs::Param { param, value } = &args {
        return state.global_mut().gc55_param(*param, *value) as i32;
    }
    if state.global().is_gc_stopped_internally() {
        return -1;
    }
    match args {
        GcArgs::Stop => {
            state.global_mut().set_gc_stop_user();
        }
        GcArgs::Restart => {
            {
                let mut g = state.global_mut();
                crate::state::set_debt(&mut *g, 0);
            }
            state.global_mut().clear_gc_stop();
        }
        GcArgs::Collect => {
            if !state.allowhook {
                return 0;
            }
            // Lua 5.1 only: re-scan rostered userdata for a late-added `__gc`
            // before the collect, mirroring C's collect-time
            // `luaC_separateudata`. No-op on 5.2–5.5.
            scan_v51_finalizable_userdata(state);
            // Under D-2, weak-table sweep happens INSIDE the heap's
            // post-mark hook (see GcHandle::full_collect), driven by
            // reachability rather than strong_count. The standalone weak
            // sweep that used to run here would now be a no-op against an
            // already-clean state and is removed.
            state.gc().full_collect();
            // Phase-B: drain pending __gc finalizers for tables whose user
            // refs have all been dropped. Kept for legacy compat; runs
            // after the heap's collect so weak entries have been cleared.
            // This is C-Lua's explicit-collect path (`runafewfinalizers` with
            // `propagateerrors = 1`): on 5.2/5.3 a finalizer error is wrapped
            // and re-thrown. `gc()` returns `i32`, so park the error in the
            // global for the `collectgarbage` built-in to re-raise.
            if let Err(e) = run_pending_finalizers_inner(state, true) {
                state.global_mut().gc_finalizer_error = Some(e.into_value());
            }
        }
        GcArgs::Count => {
            let g = state.global();
            let total = g.heap.bytes_used();
            return (total >> 10) as i32;
        }
        GcArgs::CountB => {
            let g = state.global();
            let total = g.heap.bytes_used();
            return (total & 0x3ff) as i32;
        }
        GcArgs::Step { data } => {
            let old_stp = {
                let mut g = state.global_mut();
                let old = g.gc_stop_flags();
                g.clear_gc_stop();
                old
            };
            // C-Lua converts `data` KiB of added debt into work units via
            // `stepmul`. We use a simpler mapping: the work-unit count is
            // `data * stepmul` after decoding the packed GC parameter, with a
            // floor of 1 unit. When
            // `data == 0` the call still performs one basic step (matching
            // C-Lua's `luaC_step(L)` after `setdebt(g, 0)`).
            let stepmul = (state.global().gc_stepmul_param() as isize | 1).max(1);
            let work_units = if data == 0 {
                stepmul
            } else {
                let raw = (data as isize).saturating_mul(stepmul);
                raw.max(1)
            };
            let debt_for_result = if data == 0 {
                let mut g = state.global_mut();
                crate::state::set_debt(&mut *g, 0);
                0
            } else {
                let debt = data as isize * 1024 + state.global().gc_debt();
                let mut g = state.global_mut();
                crate::state::set_debt(&mut *g, debt);
                debt
            };
            let gen_mode = state.global().is_gen_mode();
            let cycle_complete = if gen_mode {
                if data == 0 {
                    state.gc().generational_step_minor_only();
                } else {
                    state.gc().generational_step();
                }
                if state.global().finalizers.has_to_be_finalized() {
                    if let Err(e) = run_pending_finalizers_inner(state, true) {
                        state.global_mut().gc_finalizer_error = Some(e.into_value());
                    }
                }
                debt_for_result > 0 && state.global().gc_at_pause()
            } else if state.global().heap.gc_state() == lua_gc::GcState::CallFin
                && state.global().finalizers.has_to_be_finalized()
            {
                if let Err(e) = run_some_pending_finalizers_inner(state, true) {
                    state.global_mut().gc_finalizer_error = Some(e.into_value());
                }
                if state.global().finalizers.has_to_be_finalized() {
                    false
                } else {
                    state.global().heap.finish_callfin_phase()
                }
            } else {
                let completed = state.gc().incremental_step(work_units);
                if state.global().heap.gc_state() == lua_gc::GcState::CallFin
                    && state.global().finalizers.has_to_be_finalized()
                {
                    if let Err(e) = run_some_pending_finalizers_inner(state, true) {
                        state.global_mut().gc_finalizer_error = Some(e.into_value());
                    }
                    if state.global().finalizers.has_to_be_finalized() {
                        false
                    } else {
                        state.global().heap.finish_callfin_phase()
                    }
                } else {
                    completed
                }
            };
            state.global_mut().set_gc_stop_flags(old_stp);
            // Sync the global gcstate byte for `gc_at_pause()` callers.
            {
                let heap_state = state.global().heap.gc_state();
                let mut g = state.global_mut();
                g.gcstate = if heap_state.is_pause() { 0 } else { 1 };
            }
            return if cycle_complete { 1 } else { 0 };
        }
        GcArgs::SetPause { value } => {
            let old = state.global().gc_pause_param();
            state.global_mut().set_gc_pause_param(value);
            return old;
        }
        GcArgs::SetStepMul { value } => {
            let old = state.global().gc_stepmul_param();
            state.global_mut().set_gc_stepmul_param(value);
            return old;
        }
        GcArgs::IsRunning => {
            return state.global().gc_running() as i32;
        }
        GcArgs::Gen { minormul, majormul } => {
            let old_mode = if state.global().is_gen_mode() {
                10i32
            } else {
                11i32
            };
            if minormul != 0 {
                state.global_mut().genminormul = minormul as u8;
            }
            if majormul != 0 {
                state.global_mut().set_gc_genmajormul(majormul);
            }
            state.gc().change_mode(crate::state::GcKind::Generational);
            return old_mode;
        }
        GcArgs::Inc {
            pause,
            stepmul,
            stepsize,
        } => {
            let old_mode = if state.global().is_gen_mode() {
                10i32
            } else {
                11i32
            };
            if pause != 0 {
                state.global_mut().set_gc_pause_param(pause);
            }
            if stepmul != 0 {
                state.global_mut().set_gc_stepmul_param(stepmul);
            }
            if stepsize != 0 {
                state.global_mut().gcstepsize = stepsize as u8;
            }
            state.gc().change_mode(crate::state::GcKind::Incremental);
            return old_mode;
        }
        GcArgs::Param { .. } => unreachable!("Param handled before the finalizer guard"),
    }
    0
}

/// Configure the hosted-runtime GC mode after the standard libraries are open.
///
/// Upstream 5.4/5.5 initialize a fresh raw state in incremental mode, then the
/// standalone/runtime startup path restarts the collector and enters
/// generational mode. Keep `new_state()` semantics raw-state faithful and apply
/// the observable hosted default at this layer.
pub fn configure_startup_gc_mode(state: &mut LuaState) {
    if matches!(
        state.global().lua_version,
        lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55
    ) {
        let _ = gc(state, GcArgs::Restart);
        let _ = gc(
            state,
            GcArgs::Gen {
                minormul: 0,
                majormul: 0,
            },
        );
    }
}

// ── miscellaneous functions ───────────────────────────────────────────────────

// PORT NOTE: returns Result<Infallible, _> — semantically "always Err". The
// translator originally wrote `Result<!, _>` but the `!` type in a return
// position is still nightly-only as of Rust 1.93; Infallible is the stable
// stand-in. Callsites just pattern-match on Err.
pub fn lua_error(state: &mut LuaState) -> Result<Infallible, LuaError> {
    //      luaM_error(L);  /* memory error */
    //    else
    //      luaG_errormsg(L);  /* regular error */
    let top = state.top_idx();
    let errobj = state.get_at(top - 1);
    let is_mem_err = if let LuaValue::Str(ref s) = errobj {
        let memerr = state.global().memerrmsg.clone();
        GcRef::ptr_eq(s, &memerr)
    } else {
        false
    };
    if is_mem_err {
        Err(LuaError::Memory)
    } else {
        Err(LuaError::from_value(errobj))
    }
}

/// Normalize an integer-valued float resumption key to its integer key on the
/// float-only number model (5.1/5.2).
///
/// Those versions have no integer subtype, so a loop variable used as a table
/// key arrives as `Float(k.0)` while `new_key` stored it as `Int(k)` (key
/// normalization runs on every version). Without this the `next` resumption key
/// would miss the stored key and raise `invalid key to 'next'`. The dual-number
/// versions (5.3+) must NOT normalize here: reference Lua errors on `next(t, 2.0)`
/// against an integer key, and that fidelity is preserved by leaving the key as-is.
fn normalize_float_only_next_key(state: &LuaState, key: LuaValue) -> LuaValue {
    if state.global().lua_version.number_model() != lua_types::NumberModel::FloatOnly {
        return key;
    }
    if let LuaValue::Float(f) = key {
        let k = f as i64;
        if k as f64 == f {
            return LuaValue::Int(k);
        }
    }
    key
}

pub fn next(state: &mut LuaState, idx: i32) -> Result<bool, LuaError> {
    let t = get_table_value(state, idx)
        .ok_or_else(|| LuaError::runtime(format_args!("table expected")))?;
    let top = state.top_idx();
    let key = normalize_float_only_next_key(state, state.get_at(top - 1));
    match t.next(key)? {
        Some((next_key, next_val)) => {
            state.set_at(top - 1, next_key);
            state.push(next_val);
            Ok(true)
        }
        None => {
            state.set_top_idx(top - 1);
            Ok(false)
        }
    }
}

pub fn to_close(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
    let _level = index_to_stack_idx(state, idx);
    // TODO(port): luaF_newtbcupval and to-be-closed variable infrastructure
    // not yet translated. Stubbing for Phase A.
    Ok(())
}

pub fn concat(state: &mut LuaState, n: i32) -> Result<(), LuaError> {
    if n > 0 {
        state.concat(n)?;
    } else {
        let empty = state.intern_str(b"")?;
        state.push(LuaValue::Str(empty));
    }
    state.gc_pre_collect_clear();
    state.gc().check_step();
    Ok(())
}

pub fn len(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
    let t = index_to_value(state, idx);
    let result = state.obj_len(&t)?;
    state.push(result);
    Ok(())
}

// PORT NOTE: The custom allocator hook is not exposed in the Rust-native API.
// Rust's allocator handles all allocation.
// These are intentionally omitted.

pub fn set_warn_f(state: &mut LuaState, f: Option<Box<dyn FnMut(&[u8], bool)>>) {
    // PORT NOTE: ud_warn userdata is folded into the closure per types.tsv.
    state.global_mut().warnf = f;
}

pub fn warning(state: &mut LuaState, msg: &[u8], tocont: bool) {
    state.emit_warning(msg, tocont);
}

pub fn new_userdata_uv(
    state: &mut LuaState,
    size: usize,
    nuvalue: i32,
) -> Result<GcRef<LuaUserData>, LuaError> {
    debug_assert!(nuvalue >= 0 && nuvalue < u16::MAX as i32, "invalid value");
    let u = state.new_userdata(size, nuvalue as usize)?;
    state.push(LuaValue::UserData(u.clone()));
    state.gc_pre_collect_clear();
    state.gc().check_step();
    Ok(u)
}

// ── upvalue access ────────────────────────────────────────────────────────────

// PORT NOTE: Returns (name, value) instead of mutating output pointers. The name
// is returned as an owned Vec<u8> because Lua upvalue names live in the proto's
// LuaString table (GC heap), not in static storage.
fn aux_upvalue(state: &LuaState, fi: &LuaValue, n: i32) -> Option<(Vec<u8>, LuaValue)> {
    match fi {
        LuaValue::Function(LuaClosure::C(ccl)) => {
            let upvalues = ccl.upvalues.borrow();
            let nupvalues = upvalues.len() as i32;
            if n < 1 || n > nupvalues {
                return None;
            }
            Some((Vec::new(), upvalues[(n - 1) as usize].clone()))
        }
        LuaValue::Function(LuaClosure::Lua(lcl)) => {
            let nupvalues = lcl.upvals.len() as i32;
            if n < 1 || n > nupvalues {
                return None;
            }
            let val = state.upvalue_get(lcl, (n - 1) as usize);
            // The proto records the static name of each upvalue (e.g. "_ENV"
            // for the main chunk's environment upvalue). Stripped chunks have
            // no upvalue-name debug info; Lua reports those as "(no name)".
            let no_name: &[u8] = if state.global().lua_version == lua_types::LuaVersion::V53 {
                b"(*no name)"
            } else {
                b"(no name)"
            };
            let name: Vec<u8> = lcl
                .proto
                .upvalues
                .get((n - 1) as usize)
                .and_then(|ud| ud.name.as_ref())
                .map(|s| s.as_bytes().to_vec())
                .unwrap_or_else(|| no_name.to_vec());
            Some((name, val))
        }
        _ => None,
    }
}

pub fn get_upvalue(state: &mut LuaState, funcindex: i32, n: i32) -> Option<Vec<u8>> {
    let fi = index_to_value(state, funcindex);
    if let Some((name, val)) = aux_upvalue(state, &fi, n) {
        state.push(val);
        Some(name)
    } else {
        None
    }
}

pub fn setup_value(state: &mut LuaState, funcindex: i32, n: i32) -> Option<Vec<u8>> {
    let fi = index_to_value(state, funcindex);
    let (name, _) = aux_upvalue(state, &fi, n)?;
    let new_val = state.pop();
    match &fi {
        LuaValue::Function(LuaClosure::Lua(lcl)) => {
            state.upvalue_set(lcl, (n - 1) as usize, new_val).ok()?;
        }
        LuaValue::Function(LuaClosure::C(ccl)) => {
            let idx = (n - 1) as usize;
            {
                let mut upvalues = ccl.upvalues.borrow_mut();
                if idx >= upvalues.len() {
                    return None;
                }
                upvalues[idx] = new_val.clone();
            }
            state.gc().barrier(ccl, &new_val);
        }
        _ => return None,
    }
    Some(name)
}

// PORT NOTE: returns an index into the upvals vec rather than a pointer-to-pointer.
// Returns None if n is out of range.
fn get_upval_ref_idx(state: &LuaState, fidx: i32, n: i32) -> Option<usize> {
    let fi = index_to_value(state, fidx);
    debug_assert!(
        matches!(fi, LuaValue::Function(LuaClosure::Lua(_))),
        "Lua function expected"
    );
    if let LuaValue::Function(LuaClosure::Lua(ref lcl)) = fi {
        let sizeupvalues = lcl.upvals.len() as i32;
        if n >= 1 && n <= sizeupvalues {
            Some((n - 1) as usize)
        } else {
            None
        }
    } else {
        None
    }
}

// PORT NOTE: Returns Option<usize> identity instead of raw void*.
pub fn upvalue_id(state: &LuaState, fidx: i32, n: i32) -> Option<usize> {
    let fi = index_to_value(state, fidx);
    match &fi {
        LuaValue::Function(LuaClosure::Lua(lcl)) => {
            let idx = get_upval_ref_idx(state, fidx, n)?;
            // Return the identity of the UpVal GcRef
            Some(GcRef::identity(&lcl.upval(idx)))
        }
        LuaValue::Function(LuaClosure::C(ccl)) => {
            let upvalues = ccl.upvalues.borrow();
            if n >= 1 && n <= upvalues.len() as i32 {
                // TODO(port): returning address of upvalue slot not possible without raw ptr.
                // Return a synthetic identity based on the closure's identity + n.
                Some(GcRef::identity(ccl) ^ (n as usize))
            } else {
                None
            }
        }
        LuaValue::Function(LuaClosure::LightC(_)) => None,
        _ => {
            debug_assert!(false, "function expected");
            None
        }
    }
}

//                                               int fidx2, int n2)
pub fn upvalue_join(state: &mut LuaState, fidx1: i32, n1: i32, fidx2: i32, n2: i32) {
    let idx1 = match get_upval_ref_idx(state, fidx1, n1) {
        Some(i) => i,
        None => return,
    };
    let idx2 = match get_upval_ref_idx(state, fidx2, n2) {
        Some(i) => i,
        None => return,
    };
    let f1 = index_to_value(state, fidx1);
    let f2 = index_to_value(state, fidx2);
    if let (LuaValue::Function(LuaClosure::Lua(lcl1)), LuaValue::Function(LuaClosure::Lua(lcl2))) =
        (&f1, &f2)
    {
        let shared = lcl2.upval(idx2);
        lcl1.set_upval(idx1, shared);
        state.gc().obj_barrier(lcl1, &shared);
    }
}

// ──────────────────────────────────────────────────────────────────────────
// PORT STATUS
//   source:        src/lapi.c  (1464 lines, ~47 functions)
//   target_crate:  lua-vm
//   confidence:    low
//   todos:         18
//   port_notes:    8
//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
//   notes:         Heavy use of interior mutability TODOs (GcRef writes for
//                  metatables, upvalue writes, userdata uv writes). The
//                  index2value helper returns cloned LuaValue not a pointer,
//                  so write-back paths that C achieves with TValue* are
//                  stubbed. Stack pointer arithmetic faithfully translated to
//                  StackIdx (u32) arithmetic. va_list functions (pushvfstring,
//                  pushfstring) replaced by &[u8] forwarders. lua_gc varargs
//                  replaced by explicit GcArgs enum. Raw pointer returns
//                  (topointer, touserdata, upvalueid) return Option<usize>
//                  identity values; actual *mut void only legal in lua-gc.
//                  lua_pushthread stubbed (needs self_gcref()), lua_xmove
//                  stubbed (split-borrow), upvalue_join stubbed (GcRef write).
//                  Phase B must wire up: state.grow_stack, state.call_no_yield,
//                  state.protected_call_raw, state.adjust_results,
//                  state.table_get_with_tm, state.table_set_with_tm,
//                  state.arith_op, state.concat, state.obj_len,
//                  state.obj_to_string, state.str_to_num, state.table_getn,
//                  state.registry_value, state.registry_get,
//                  GcRef::identity, GcRef::ptr_eq, GlobalState GC accessors.
// ──────────────────────────────────────────────────────────────────────────
