use crate::lua_value::{LuaValue, lua_value_to_udvalue, udvalue_to_lua_value};
use crate::lua_vm::call_info::call_status::CIST_PENDING_FINISH;
use crate::lua_vm::execute::call::{self, call_c_function};
use crate::lua_vm::execute::execute_loop::lua_execute;
use crate::lua_vm::execute::helper::{get_binop_metamethod, get_metamethod_from_meta_ptr};
/// Metamethod operations
///
/// Implements MMBIN, MMBINI, MMBINK opcodes
/// Based on Lua 5.5 ltm.c
use crate::lua_vm::{LuaError, LuaResult, LuaState, get_metamethod_event};
use crate::stdlib::debug;
use crate::{CallInfo, gc::TablePtr};

/// Try unary metamethod (for __unm, __bnot)
/// Port of luaT_trybinTM for unary operations
pub fn try_unary_tm(
    lua_state: &mut LuaState,
    operand: LuaValue,
    result_pos: usize,
    tm_kind: TmKind,
) -> LuaResult<()> {
    // Try trait-based __unm for userdata
    if tm_kind == TmKind::Unm
        && operand.ttisfulluserdata()
        && let Some(ud) = operand.as_userdata_mut()
        && let Some(udv) = ud.get_trait().lua_unm()
    {
        let result = udvalue_to_lua_value(lua_state, udv)?;
        let stack = lua_state.stack_mut();
        stack[result_pos] = result;
        return Ok(());
    }

    // Try to get metamethod from operand
    let metamethod = get_metamethod_event(lua_state, &operand, tm_kind);
    if let Some(mm) = metamethod {
        // Call metamethod: mm(operand, operand) -> result
        let result = call_tm_res(lua_state, mm, operand, operand)?;

        // Store result
        let stack = lua_state.stack_mut();
        stack[result_pos] = result;
        Ok(())
    } else {
        // No metamethod found
        if tm_kind == TmKind::Bnot && operand.is_number() {
            // Float that can't be converted to integer
            Err(lua_state.error("number has no integer representation".to_string()))
        } else {
            // Use descriptive operation name like C Lua
            let op_desc = match tm_kind {
                TmKind::Bnot => "perform bitwise operation on",
                TmKind::Unm => "perform arithmetic on",
                TmKind::Len => "get length of",
                _ => "perform arithmetic on",
            };
            Err(crate::stdlib::debug::typeerror(
                lua_state, &operand, op_desc,
            ))
        }
    }
}

/// Try binary metamethod
/// Corresponds to luaT_trybinTM in ltm.c
/// Like Lua 5.5's luaT_trybinTM:
/// ```c
/// void luaT_trybinTM (lua_State *L, const TValue *p1, const TValue *p2,
///                     StkId res, TMS event) {
///   if (l_unlikely(callbinTM(L, p1, p2, res, event) < 0)) {
///     switch (event) {
///       case TM_BAND: case TM_BOR: case TM_BXOR:
///       case TM_SHL: case TM_SHR: case TM_BNOT: {
///         if (ttisnumber(p1) && ttisnumber(p2))
///           luaG_tointerror(L, p1, p2);
///         else
///           luaG_opinterror(L, p1, p2, "perform bitwise operation on");
///       }
///       /* calls never return, but to avoid warnings: *//* FALLTHROUGH */
///       default:
///         luaG_opinterror(L, p1, p2, "perform arithmetic on");
///     }
///   }
/// }
/// ```
/// Try to convert a LuaValue to integer (NO string coercion).
/// Returns Some(i64) if the value is an integer or an integral float.
pub fn try_bin_tm(
    lua_state: &mut LuaState,
    p1: LuaValue,
    p2: LuaValue,
    res: u32,
    p1_reg: u32,
    p2_reg: u32,
    tm_kind: TmKind,
) -> LuaResult<()> {
    // Try trait-based arithmetic for userdata
    if p1.ttisfulluserdata() || p2.ttisfulluserdata() {
        let trait_result = if let Some(ud) = p1.as_userdata_mut() {
            let other = lua_value_to_udvalue(&p2);
            match tm_kind {
                TmKind::Add => ud.get_trait().lua_add(&other),
                TmKind::Sub => ud.get_trait().lua_sub(&other),
                TmKind::Mul => ud.get_trait().lua_mul(&other),
                TmKind::Div => ud.get_trait().lua_div(&other),
                TmKind::Mod => ud.get_trait().lua_mod(&other),
                _ => None,
            }
        } else {
            None
        };
        if let Some(udv) = trait_result {
            unsafe {
                *lua_state.stack_mut().get_unchecked_mut(res as usize) =
                    udvalue_to_lua_value(lua_state, udv)?;
            }
            return Ok(());
        }
        let trait_result2 = if let Some(ud) = p2.as_userdata_mut() {
            let other = lua_value_to_udvalue(&p1);
            match tm_kind {
                TmKind::Add => ud.get_trait().lua_add(&other),
                TmKind::Sub => ud.get_trait().lua_sub(&other),
                TmKind::Mul => ud.get_trait().lua_mul(&other),
                TmKind::Div => ud.get_trait().lua_div(&other),
                TmKind::Mod => ud.get_trait().lua_mod(&other),
                _ => None,
            }
        } else {
            None
        };
        if let Some(udv) = trait_result2 {
            unsafe {
                *lua_state.stack_mut().get_unchecked_mut(res as usize) =
                    udvalue_to_lua_value(lua_state, udv)?;
            }
            return Ok(());
        }
    }

    // Try to get metamethod from p1, then p2
    let metamethod = get_binop_metamethod(lua_state, &p1, &p2, tm_kind);
    if let Some(mm) = metamethod {
        // Call metamethod with (p1, p2) as arguments
        let r = call_tm_res(lua_state, mm, p1, p2)?;
        unsafe {
            *lua_state.stack_mut().get_unchecked_mut(res as usize) = r;
        }
        Ok(())
    } else {
        // No metamethod found, return error
        let msg = match tm_kind {
            TmKind::Band
            | TmKind::Bor
            | TmKind::Bxor
            | TmKind::Shl
            | TmKind::Shr
            | TmKind::Bnot => {
                // Check if both values are numbers — if so, the issue is
                // that they can't be converted to integers.
                // Mirror C Lua's luaG_tointerror: include varinfo for the
                // problematic operand (the float that can't convert to int).
                if p1.is_number() && p2.is_number() {
                    // Blame the operand that's a float (not integer)
                    let blame_reg = if !p1.is_integer() { p1_reg } else { p2_reg };
                    let info = debug::varinfo_for_reg(lua_state, blame_reg);
                    return Err(
                        lua_state.error(format!("number has no integer representation{}", info))
                    );
                } else {
                    "perform bitwise operation on"
                }
            }
            _ => "perform arithmetic on",
        };
        Err(debug::opinterror(lua_state, p1_reg, p2_reg, &p1, &p2, msg))
    }
}

/// Call a metamethod with two arguments
/// Based on Lua 5.5's luaT_callTMres - returns the result value directly
/// Port of Lua 5.5's luaT_callTMres from ltm.c:119
/// ```c
/// lu_byte luaT_callTMres (lua_State *L, const TValue *f, const TValue *p1,
///                         const TValue *p2, StkId res) {
///   ptrdiff_t result = savestack(L, res);
///   StkId func = L->top.p;
///   setobj2s(L, func, f);  /* push function (assume EXTRA_STACK) */
///   setobj2s(L, func + 1, p1);  /* 1st argument */
///   setobj2s(L, func + 2, p2);  /* 2nd argument */
///   L->top.p += 3;
///   /* metamethod may yield only when called from Lua code */
///   if (isLuacode(L->ci))
///     luaD_call(L, func, 1);
///   else
///     luaD_callnoyield(L, func, 1);
///   res = restorestack(L, result);
///   setobjs2s(L, res, --L->top.p);  /* move result to its place */
///   return ttypetag(s2v(res));  /* return tag of the result */
/// }
/// ```
pub fn call_tm_res(
    lua_state: &mut LuaState,
    metamethod: LuaValue,
    arg1: LuaValue,
    arg2: LuaValue,
) -> LuaResult<LuaValue> {
    // Sync top to ci_top — callers in the inline hot path already did set_top_raw(ci_top),
    // so the comparison is almost always true. We still check for safety in other callers.
    let func_pos = {
        let ci_top = lua_state.current_frame_top_unchecked();
        let top = lua_state.get_top();
        if top != ci_top {
            lua_state.set_top_raw(ci_top);
        }
        ci_top
    };

    // Direct stack write using raw pointers — like Lua 5.5's setobj2s.
    // EXTRA_STACK (5 slots) guarantees space above ci->top.
    unsafe {
        let sp = lua_state.stack_mut().as_mut_ptr();
        *sp.add(func_pos) = metamethod;
        *sp.add(func_pos + 1) = arg1;
        *sp.add(func_pos + 2) = arg2;
    }
    lua_state.set_top_raw(func_pos + 3);

    // Call the metamethod with nresults=1
    if metamethod.is_lua_function() {
        let lua_func = unsafe { metamethod.as_lua_function_unchecked() };
        let chunk = lua_func.chunk();
        let upvalue_ptrs = lua_func.upvalues().as_ptr();

        let new_base = func_pos + 1;
        let caller_depth = lua_state.call_depth();

        if !(chunk.param_count == 2
            && lua_state.try_push_lua_frame_exact(
                new_base,
                1,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?)
        {
            lua_state.push_lua_frame(
                new_base,
                2,
                1,
                chunk.param_count,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?;
        }
        lua_state.inc_n_ccalls()?;
        let r = lua_execute(lua_state, caller_depth);
        lua_state.dec_n_ccalls();
        r?;
    } else if metamethod.is_cfunction() {
        call_c_function(lua_state, func_pos, 2, 1)?;
    } else {
        return Err(crate::stdlib::debug::callerror(lua_state, &metamethod));
    }

    let result_val = unsafe { *lua_state.stack_mut().as_ptr().add(func_pos) };
    lua_state.set_top_raw(func_pos);

    Ok(result_val)
}

pub fn call_tm_res1(
    lua_state: &mut LuaState,
    metamethod: LuaValue,
    arg1: LuaValue,
) -> LuaResult<LuaValue> {
    let func_pos = {
        let ci_top = lua_state.current_frame_top_unchecked();
        let top = lua_state.get_top();
        if top != ci_top {
            lua_state.set_top_raw(ci_top);
        }
        ci_top
    };

    unsafe {
        let sp = lua_state.stack_mut().as_mut_ptr();
        *sp.add(func_pos) = metamethod;
        *sp.add(func_pos + 1) = arg1;
    }
    lua_state.set_top_raw(func_pos + 2);

    if metamethod.is_lua_function() {
        let lua_func = unsafe { metamethod.as_lua_function_unchecked() };
        let chunk = lua_func.chunk();
        let upvalue_ptrs = lua_func.upvalues().as_ptr();

        let new_base = func_pos + 1;
        let caller_depth = lua_state.call_depth();

        if !(chunk.param_count == 1
            && lua_state.try_push_lua_frame_exact(
                new_base,
                1,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?)
        {
            lua_state.push_lua_frame(
                new_base,
                1,
                1,
                chunk.param_count,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?;
        }
        lua_state.inc_n_ccalls()?;
        let r = lua_execute(lua_state, caller_depth);
        lua_state.dec_n_ccalls();
        r?;
    } else if metamethod.is_cfunction() {
        call_c_function(lua_state, func_pos, 1, 1)?;
    } else {
        return Err(crate::stdlib::debug::callerror(lua_state, &metamethod));
    }

    let result_val = unsafe { *lua_state.stack_mut().as_ptr().add(func_pos) };
    lua_state.set_top_raw(func_pos);

    Ok(result_val)
}

pub fn call_tm_res_into(
    lua_state: &mut LuaState,
    metamethod: LuaValue,
    arg1: LuaValue,
    arg2: LuaValue,
    dest_reg: usize,
) -> LuaResult<()> {
    let func_pos = {
        let ci_top = lua_state.current_frame_top_unchecked();
        let top = lua_state.get_top();
        if top != ci_top {
            lua_state.set_top_raw(ci_top);
        }
        ci_top
    };

    unsafe {
        let sp = lua_state.stack_mut().as_mut_ptr();
        *sp.add(func_pos) = metamethod;
        *sp.add(func_pos + 1) = arg1;
        *sp.add(func_pos + 2) = arg2;
    }
    lua_state.set_top_raw(func_pos + 3);

    if metamethod.is_lua_function() {
        let lua_func = unsafe { metamethod.as_lua_function_unchecked() };
        let chunk = lua_func.chunk();
        let upvalue_ptrs = lua_func.upvalues().as_ptr();

        let new_base = func_pos + 1;
        let caller_depth = lua_state.call_depth();

        if !(chunk.param_count == 2
            && lua_state.try_push_lua_frame_exact(
                new_base,
                1,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?)
        {
            lua_state.push_lua_frame(
                new_base,
                2,
                1,
                chunk.param_count,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?;
        }
        lua_state.inc_n_ccalls()?;
        let r = lua_execute(lua_state, caller_depth);
        lua_state.dec_n_ccalls();
        r?;
    } else if metamethod.is_cfunction() {
        call_c_function(lua_state, func_pos, 2, 1)?;
    } else {
        return Err(crate::stdlib::debug::callerror(lua_state, &metamethod));
    }

    unsafe {
        let sp = lua_state.stack_mut().as_mut_ptr();
        *sp.add(dest_reg) = *sp.add(func_pos);
    }
    lua_state.set_top_raw(func_pos);
    Ok(())
}

/// Port of Lua 5.5's luaT_callTM from ltm.c:103
/// Calls metamethod without expecting a return value
/// ```c
/// void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1,
///                   const TValue *p2, const TValue *p3) {
///   StkId func = L->top.p;
///   setobj2s(L, func, f);  /* push function (assume EXTRA_STACK) */
///   setobj2s(L, func + 1, p1);  /* 1st argument */
///   setobj2s(L, func + 2, p2);  /* 2nd argument */
///   setobj2s(L, func + 3, p3);  /* 3rd argument */
///   L->top.p = func + 4;
///   /* metamethod may yield only when called from Lua code */
///   if (isLuacode(L->ci))
///     luaD_call(L, func, 0);
///   else
///     luaD_callnoyield(L, func, 0);
/// }
/// ```
pub fn call_tm(
    lua_state: &mut LuaState,
    metamethod: LuaValue,
    arg1: LuaValue,
    arg2: LuaValue,
    arg3: LuaValue,
) -> LuaResult<()> {
    // Sync top to ci_top
    let func_pos = {
        let ci_top = lua_state.current_frame_top_unchecked();
        let top = lua_state.get_top();
        if top != ci_top {
            lua_state.set_top_raw(ci_top);
        }
        ci_top
    };

    // Direct stack write using raw pointers
    unsafe {
        let sp = lua_state.stack_mut().as_mut_ptr();
        *sp.add(func_pos) = metamethod;
        *sp.add(func_pos + 1) = arg1;
        *sp.add(func_pos + 2) = arg2;
        *sp.add(func_pos + 3) = arg3;
    }
    lua_state.set_top_raw(func_pos + 4);

    // Call with 0 results (nresults=0)
    if metamethod.is_lua_function() {
        let lua_func = unsafe { metamethod.as_lua_function_unchecked() };
        let chunk = lua_func.chunk();
        let upvalue_ptrs = lua_func.upvalues().as_ptr();

        let new_base = func_pos + 1;
        let caller_depth = lua_state.call_depth();

        if !(chunk.param_count == 3
            && lua_state.try_push_lua_frame_exact(
                new_base,
                0,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?)
        {
            lua_state.push_lua_frame(
                new_base,
                3,
                0,
                chunk.param_count,
                chunk.max_stack_size,
                chunk as *const _,
                upvalue_ptrs,
            )?;
        }
        lua_state.inc_n_ccalls()?;
        let r = lua_execute(lua_state, caller_depth);
        lua_state.dec_n_ccalls();
        r?;
    } else if metamethod.is_cfunction() {
        call::call_c_function(lua_state, func_pos, 3, 0)?;
    } else {
        return Err(crate::stdlib::debug::callerror(lua_state, &metamethod));
    }

    Ok(())
}

/// Try comparison metamethod (for Lt and Le)
/// Returns Some(bool) if metamethod was called, None if no metamethod
pub fn try_comp_tm(
    lua_state: &mut LuaState,
    p1: LuaValue,
    p2: LuaValue,
    tm_kind: TmKind,
) -> LuaResult<Option<bool>> {
    // Try trait-based comparison for userdata
    if p1.ttisfulluserdata()
        && let Some(ud1) = p1.as_userdata_mut()
        && let Some(ud2) = p2.as_userdata_mut()
    {
        let result = match tm_kind {
            TmKind::Lt => ud1.get_trait().lua_lt(ud2.get_trait()),
            TmKind::Le => ud1.get_trait().lua_le(ud2.get_trait()),
            _ => None,
        };
        if let Some(b) = result {
            return Ok(Some(b));
        }
    }

    // Try to get metamethod from p1, then p2
    let metamethod = get_binop_metamethod(lua_state, &p1, &p2, tm_kind);

    if let Some(mm) = metamethod {
        // Call metamethod and convert result to boolean
        let result = call_tm_res(lua_state, mm, p1, p2)?;
        // GC check is already done in luaT_callTMres
        Ok(Some(!result.is_falsy()))
    } else {
        Ok(None)
    }
}

#[inline(always)]
pub fn call_newindex_tm_fast(
    lua_state: &mut LuaState,
    ci: &mut CallInfo,
    obj: LuaValue,
    meta: TablePtr,
    key: LuaValue,
    value: LuaValue,
) -> LuaResult<bool> {
    let Some(tm) = get_metamethod_from_meta_ptr(lua_state, meta, TmKind::NewIndex) else {
        return Ok(false);
    };
    if !tm.is_function() {
        return Ok(false);
    }

    match call_tm(lua_state, tm, obj, key, value) {
        Ok(()) => Ok(true),
        Err(LuaError::Yield) => {
            ci.set_pending_finish_get(-2);
            ci.call_status |= CIST_PENDING_FINISH;
            Err(LuaError::Yield)
        }
        Err(e) => Err(e),
    }
}

/// Tag Method types (TMS from ltm.h)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TmKind {
    Index = 0,
    NewIndex = 1,
    Gc = 2,
    Mode = 3,
    Len = 4,
    Eq = 5,
    Add = 6,
    Sub = 7,
    Mul = 8,
    Mod = 9,
    Pow = 10,
    Div = 11,
    IDiv = 12,
    Band = 13,
    Bor = 14,
    Bxor = 15,
    Shl = 16,
    Shr = 17,
    Unm = 18,
    Bnot = 19,
    Lt = 20,
    Le = 21,
    Concat = 22,
    Call = 23,
    Close = 24,
    ToString = 25,
    N = 26, // number of tag methods
}

impl TmKind {
    /// Convert u8 to TmKind
    pub fn from_u8(value: u8) -> Option<Self> {
        if value <= TmKind::ToString as u8 {
            Some(unsafe { Self::from_u8_unchecked(value) })
        } else {
            None
        }
    }

    /// Convert u8 to TmKind without bounds checking.
    /// SAFETY: caller must ensure value <= TmKind::ToString (25)
    #[inline(always)]
    pub unsafe fn from_u8_unchecked(value: u8) -> Self {
        unsafe { std::mem::transmute(value) }
    }

    /// Get the metamethod name
    pub const fn name(self) -> &'static str {
        match self {
            TmKind::Index => "__index",
            TmKind::NewIndex => "__newindex",
            TmKind::Gc => "__gc",
            TmKind::Mode => "__mode",
            TmKind::Len => "__len",
            TmKind::Eq => "__eq",
            TmKind::Add => "__add",
            TmKind::Sub => "__sub",
            TmKind::Mul => "__mul",
            TmKind::Mod => "__mod",
            TmKind::Pow => "__pow",
            TmKind::Div => "__div",
            TmKind::IDiv => "__idiv",
            TmKind::Band => "__band",
            TmKind::Bor => "__bor",
            TmKind::Bxor => "__bxor",
            TmKind::Shl => "__shl",
            TmKind::Shr => "__shr",
            TmKind::Unm => "__unm",
            TmKind::Bnot => "__bnot",
            TmKind::Lt => "__lt",
            TmKind::Le => "__le",
            TmKind::Concat => "__concat",
            TmKind::Call => "__call",
            TmKind::Close => "__close",
            TmKind::ToString => "__tostring",
            TmKind::N => "__n", // Not a real metamethod
        }
    }
}

impl From<TmKind> for u8 {
    fn from(value: TmKind) -> Self {
        value as u8
    }
}
