// https://github.com/kunitoki/LuaBridge3
// Copyright 2020, Lucio Asnaghi
// Copyright 2012, Vinnie Falco <vinnie.falco@gmail.com>
// Copyright 2007, Nathan Reed
// SPDX-License-Identifier: MIT

#pragma once

#include "Config.h"

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <type_traits>
#include <utility>

namespace luabridge {

/**
 * @brief Helper for unused vars.
 */
template <class... Args>
constexpr void unused(Args&&...)
{
}

// These functions and defines are for Luau.
#if LUABRIDGE_ON_LUAU
inline int luaL_ref(lua_State* L, int idx)
{
    LUABRIDGE_ASSERT(idx == LUA_REGISTRYINDEX);

    const int ref = lua_ref(L, -1);

    lua_pop(L, 1);

    return ref;
}

inline void luaL_unref(lua_State* L, int idx, int ref)
{
    unused(idx);

    lua_unref(L, ref);
}

template <class T>
inline void* lua_newuserdata_x(lua_State* L, size_t sz)
{
    return lua_newuserdatadtor(L, sz, [](void* x)
    {
        T* object = static_cast<T*>(x);
        object->~T();
    });
}

inline void lua_pushcfunction_x(lua_State *L, lua_CFunction fn, const char* debugname)
{
    lua_pushcfunction(L, fn, debugname);
}

inline void lua_pushcclosure_x(lua_State* L, lua_CFunction fn, const char* debugname, int n)
{
    lua_pushcclosure(L, fn, debugname, n);
}

inline int lua_error_x(lua_State* L)
{
    lua_error(L);
    return 0;
}

inline int lua_getstack_x(lua_State* L, int level, lua_Debug* ar)
{
    return lua_getinfo(L, level, "nlS", ar);
}

inline int lua_getstack_info_x(lua_State* L, int level, const char* what, lua_Debug* ar)
{
    return lua_getinfo(L, level, what, ar);
}

#else
using ::luaL_ref;
using ::luaL_unref;

template <class T>
inline void* lua_newuserdata_x(lua_State* L, size_t sz)
{
    return lua_newuserdata(L, sz);
}

inline void lua_pushcfunction_x(lua_State *L, lua_CFunction fn, const char* debugname)
{
    unused(debugname);

    lua_pushcfunction(L, fn);
}

inline void lua_pushcclosure_x(lua_State* L, lua_CFunction fn, const char* debugname, int n)
{
    unused(debugname);

    lua_pushcclosure(L, fn, n);
}

inline int lua_error_x(lua_State* L)
{
    return lua_error(L);
}

inline int lua_getstack_x(lua_State* L, int level, lua_Debug* ar)
{
    return lua_getstack(L, level, ar);
}

inline int lua_getstack_info_x(lua_State* L, int level, const char* what, lua_Debug* ar)
{
    lua_getstack(L, level, ar);
    return lua_getinfo(L, what, ar);
}

#endif // LUABRIDGE_ON_LUAU

// These are for Lua versions prior to 5.3.0.
#if LUA_VERSION_NUM < 503
inline lua_Number to_numberx(lua_State* L, int idx, int* isnum)
{
    lua_Number n = lua_tonumber(L, idx);

    if (isnum)
        *isnum = (n != 0 || lua_isnumber(L, idx));

    return n;
}

inline lua_Integer to_integerx(lua_State* L, int idx, int* isnum)
{
    int ok = 0;
    lua_Number n = to_numberx(L, idx, &ok);

    if (ok)
    {
        const auto int_n = static_cast<lua_Integer>(n);
        if (n == static_cast<lua_Number>(int_n))
        {
            if (isnum)
                *isnum = 1;

            return int_n;
        }
    }

    if (isnum)
        *isnum = 0;

    return 0;
}

#endif // LUA_VERSION_NUM < 503

// These are for Lua versions prior to 5.2.0.
#if LUA_VERSION_NUM < 502
using lua_Unsigned = std::make_unsigned_t<lua_Integer>;

#if ! LUABRIDGE_ON_LUAU
inline int lua_absindex(lua_State* L, int idx)
{
    if (idx > LUA_REGISTRYINDEX && idx < 0)
        return lua_gettop(L) + idx + 1;
    else
        return idx;
}
#endif

inline int lua_rawgetp(lua_State* L, int idx, const void* p)
{
    idx = lua_absindex(L, idx);
    luaL_checkstack(L, 1, "not enough stack slots");
    lua_pushlightuserdata(L, const_cast<void*>(p));
    lua_rawget(L, idx);
    return lua_type(L, -1);
}

inline void lua_rawsetp(lua_State* L, int idx, const void* p)
{
    idx = lua_absindex(L, idx);
    luaL_checkstack(L, 1, "not enough stack slots");
    lua_pushlightuserdata(L, const_cast<void*>(p));
    lua_insert(L, -2);
    lua_rawset(L, idx);
}

#define LUA_OPEQ 1
#define LUA_OPLT 2
#define LUA_OPLE 3

inline int lua_compare(lua_State* L, int idx1, int idx2, int op)
{
    switch (op)
    {
    case LUA_OPEQ:
        return lua_equal(L, idx1, idx2);

    case LUA_OPLT:
        return lua_lessthan(L, idx1, idx2);

    case LUA_OPLE:
        return lua_equal(L, idx1, idx2) || lua_lessthan(L, idx1, idx2);

    default:
        return 0;
    }
}

#if ! LUABRIDGE_ON_LUAJIT
inline void* luaL_testudata(lua_State* L, int ud, const char* tname)
{
    void* p = lua_touserdata(L, ud);
    if (p == nullptr)
        return nullptr;

    if (! lua_getmetatable(L, ud))
        return nullptr;

    luaL_getmetatable(L, tname);
    if (! lua_rawequal(L, -1, -2))
        p = nullptr;

    lua_pop(L, 2);
    return p;
}
#endif

inline int get_length(lua_State* L, int idx)
{
    return static_cast<int>(lua_objlen(L, idx));
}

#else // LUA_VERSION_NUM >= 502
inline int get_length(lua_State* L, int idx)
{
    lua_len(L, idx);
    const int len = static_cast<int>(luaL_checknumber(L, -1));
    lua_pop(L, 1);
    return len;
}

#endif // LUA_VERSION_NUM < 502

#ifndef LUA_OK
#define LUABRIDGE_LUA_OK 0
#else
#define LUABRIDGE_LUA_OK LUA_OK
#endif

/**
 * @brief Helper to throw or return an error code.
 */
template <class T, class ErrorType>
std::error_code throw_or_error_code(ErrorType error)
{
#if LUABRIDGE_HAS_EXCEPTIONS
    throw T(makeErrorCode(error).message().c_str());
#else
    return makeErrorCode(error);
#endif
}

template <class T, class ErrorType>
std::error_code throw_or_error_code(lua_State* L, ErrorType error)
{
#if LUABRIDGE_HAS_EXCEPTIONS
    throw T(L, makeErrorCode(error));
#else
    return unused(L), makeErrorCode(error);
#endif
}

/**
 * @brief Helper to throw or LUABRIDGE_ASSERT.
 */
template <class T, class... Args>
void throw_or_assert(Args&&... args)
{
#if LUABRIDGE_HAS_EXCEPTIONS
    throw T(std::forward<Args>(args)...);
#else
    unused(std::forward<Args>(args)...);
    LUABRIDGE_ASSERT(false);
#endif
}

/**
 * @brief Helper to set unsigned.
 */
template <class T>
void pushunsigned(lua_State* L, T value)
{
    static_assert(std::is_unsigned_v<T>);

    lua_pushinteger(L, static_cast<lua_Integer>(value));
}

/**
 * @brief Helper to convert to integer.
 */
inline lua_Number tonumber(lua_State* L, int idx, int* isnum)
{
#if ! LUABRIDGE_ON_LUAU && LUA_VERSION_NUM > 502
    return lua_tonumberx(L, idx, isnum);
#else
    return to_numberx(L, idx, isnum);
#endif
}

/**
 * @brief Helper to convert to integer.
 */
inline lua_Integer tointeger(lua_State* L, int idx, int* isnum)
{
#if ! LUABRIDGE_ON_LUAU && LUA_VERSION_NUM > 502
    return lua_tointegerx(L, idx, isnum);
#else
    return to_integerx(L, idx, isnum);
#endif
}

/**
 * @brief Register main thread, only supported on 5.1.
 */
inline constexpr char main_thread_name[] = "__luabridge_main_thread";

inline void register_main_thread(lua_State* threadL)
{
#if LUA_VERSION_NUM < 502
    if (threadL == nullptr)
        lua_pushnil(threadL);
    else
        lua_pushthread(threadL);

    lua_setglobal(threadL, main_thread_name);
#else
    unused(threadL);
#endif
}

/**
 * @brief Get main thread, not supported on 5.1.
 */
inline lua_State* main_thread(lua_State* threadL)
{
#if LUA_VERSION_NUM < 502
    lua_getglobal(threadL, main_thread_name);
    if (lua_isthread(threadL, -1))
    {
        auto L = lua_tothread(threadL, -1);
        lua_pop(threadL, 1);
        return L;
    }
    LUABRIDGE_ASSERT(false); // Have you forgot to call luabridge::registerMainThread ?
    lua_pop(threadL, 1);
    return threadL;
#else
    lua_rawgeti(threadL, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
    lua_State* L = lua_tothread(threadL, -1);
    lua_pop(threadL, 1);
    return L;
#endif
}

/**
 * @brief Get a table value, bypassing metamethods.
 */
inline int rawgetfield(lua_State* L, int index, const char* key)
{
    LUABRIDGE_ASSERT(lua_istable(L, index));
    index = lua_absindex(L, index);
    lua_pushstring(L, key);
#if LUA_VERSION_NUM <= 502
    lua_rawget(L, index);
    return lua_type(L, -1);
#else
    return lua_rawget(L, index);
#endif
}

/**
 * @brief Set a table value, bypassing metamethods.
 */
inline void rawsetfield(lua_State* L, int index, const char* key)
{
    LUABRIDGE_ASSERT(lua_istable(L, index));
    index = lua_absindex(L, index);
    lua_pushstring(L, key);
    lua_insert(L, -2);
    lua_rawset(L, index);
}

/**
 * @brief Returns true if the value is a full userdata (not light).
 */
[[nodiscard]] inline bool isfulluserdata(lua_State* L, int index)
{
    return lua_isuserdata(L, index) && !lua_islightuserdata(L, index);
}

/**
 * @brief Test lua_State objects for global equality.
 *
 * This can determine if two different lua_State objects really point
 * to the same global state, such as when using coroutines.
 *
 * @note This is used for assertions.
 */
[[nodiscard]] inline bool equalstates(lua_State* L1, lua_State* L2)
{
    return lua_topointer(L1, LUA_REGISTRYINDEX) == lua_topointer(L2, LUA_REGISTRYINDEX);
}

/**
 * @brief Return the size of lua table, even if not a sequence { 1=x, 2=y, 3=... }.
 */
[[nodiscard]] inline int table_length(lua_State* L, int index)
{
    LUABRIDGE_ASSERT(lua_istable(L, index));

    int items_count = 0;

    lua_pushnil(L);
    while (lua_next(L, index) != 0)
    {
        ++items_count;

        lua_pop(L, 1);
    }

    return items_count;
}

/**
 * @brief Return an aligned pointer of type T.
 */
template <class T>
[[nodiscard]] T* align(void* ptr) noexcept
{
    const auto address = reinterpret_cast<size_t>(ptr);

    const auto offset = address % alignof(T);
    const auto aligned_address = (offset == 0) ? address : (address + alignof(T) - offset);

    return reinterpret_cast<T*>(aligned_address);
}

/**
 * @brief Return if a pointer of type T is aligned.
 */
template <std::size_t Alignment, class T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
[[nodiscard]] bool is_aligned(T address) noexcept
{
    static_assert(Alignment > 0u);

    return (reinterpret_cast<std::uintptr_t>(address) & (Alignment - 1u)) == 0u;
}

/**
 * @brief Return the space needed to align the type T on an unaligned address.
 */
template <class T>
[[nodiscard]] constexpr size_t maximum_space_needed_to_align() noexcept
{
    return sizeof(T) + alignof(T) - 1;
}

/**
 * @brief Deallocate lua userdata taking into account alignment.
 */
template <class T>
int lua_deleteuserdata_aligned(lua_State* L)
{
    LUABRIDGE_ASSERT(isfulluserdata(L, 1));

    T* aligned = align<T>(lua_touserdata(L, 1));
    aligned->~T();

    return 0;
}

/**
 * @brief Allocate lua userdata taking into account alignment.
 *
 * Using this instead of lua_newuserdata directly prevents alignment warnings on 64bits platforms.
 */
template <class T, class... Args>
void* lua_newuserdata_aligned(lua_State* L, Args&&... args)
{
    using U = std::remove_reference_t<T>;

#if LUABRIDGE_ON_LUAU
    void* pointer = lua_newuserdatadtor(L, maximum_space_needed_to_align<U>(), [](void* x)
    {
        U* aligned = align<U>(x);
        aligned->~U();
    });
#else
    void* pointer = lua_newuserdata_x<U>(L, maximum_space_needed_to_align<U>());

    lua_newtable(L);
    lua_pushcfunction_x(L, &lua_deleteuserdata_aligned<U>, "");
    rawsetfield(L, -2, "__gc");
    lua_setmetatable(L, -2);
#endif

    U* aligned = align<U>(pointer);

    new (aligned) U(std::forward<Args>(args)...);

    return pointer;
}

/**
 * @brief Safe error able to walk backwards for error reporting correctly.
 */
inline int raise_lua_error(lua_State* L, const char* fmt, ...)
{
    va_list argp;
    va_start(argp, fmt);
    lua_pushvfstring(L, fmt, argp);
    va_end(argp);

    const char* message = lua_tostring(L, -1);
    if (message != nullptr)
    {
        if (auto str = std::string_view(message); !str.empty() && str[0] == '[')
            return lua_error_x(L);
    }

    bool pushed_error = false;
    for (int level = 1; level <= 2; ++level)
    {
        lua_Debug ar;

#if LUABRIDGE_ON_LUAU
        if (lua_getinfo(L, level, "sl", &ar) == 0)
            continue;
#else
        if (lua_getstack(L, level, &ar) == 0 || lua_getinfo(L, "Sl", &ar) == 0)
            continue;
#endif

        if (ar.currentline <= 0)
            continue;

        lua_pushfstring(L, "%s:%d: ", ar.short_src, ar.currentline);
        pushed_error = true;

        break;
    }

    if (! pushed_error)
        lua_pushliteral(L, "");

    lua_pushvalue(L, -2);
    lua_remove(L, -3);
    lua_concat(L, 2);

    return lua_error_x(L);
}

/**
 * @brief Checks if the value on the stack is a number type and can fit into the corresponding c++ integral type..
 */
template <class U = lua_Integer, class T>
constexpr bool is_integral_representable_by(T value)
{
    constexpr bool same_signedness = (std::is_unsigned_v<T> && std::is_unsigned_v<U>)
        || (!std::is_unsigned_v<T> && !std::is_unsigned_v<U>);

    if constexpr (sizeof(T) == sizeof(U))
    {
        if constexpr (same_signedness)
            return true;

        if constexpr (std::is_unsigned_v<T>)
            return value <= static_cast<T>((std::numeric_limits<U>::max)());

        return value >= static_cast<T>((std::numeric_limits<U>::min)())
            && static_cast<U>(value) <= (std::numeric_limits<U>::max)();
    }

    if constexpr (sizeof(T) < sizeof(U))
    {
        return static_cast<U>(value) >= (std::numeric_limits<U>::min)()
            && static_cast<U>(value) <= (std::numeric_limits<U>::max)();
    }

    if constexpr (std::is_unsigned_v<T>)
        return value <= static_cast<T>((std::numeric_limits<U>::max)());

    return value >= static_cast<T>((std::numeric_limits<U>::min)())
        && value <= static_cast<T>((std::numeric_limits<U>::max)());
}

template <class U = lua_Integer>
bool is_integral_representable_by(lua_State* L, int index)
{
    int isValid = 0;

    const auto value = tointeger(L, index, &isValid);

    return isValid ? is_integral_representable_by<U>(value) : false;
}

/**
 * @brief Checks if the value on the stack is a number type and can fit into the corresponding c++ numerical type..
 */
template <class U = lua_Number, class T>
constexpr bool is_floating_point_representable_by(T value)
{
    if constexpr (sizeof(T) == sizeof(U))
        return true;

    if constexpr (sizeof(T) < sizeof(U))
        return static_cast<U>(value) >= -(std::numeric_limits<U>::max)()
            && static_cast<U>(value) <= (std::numeric_limits<U>::max)();

    return value >= static_cast<T>(-(std::numeric_limits<U>::max)())
        && value <= static_cast<T>((std::numeric_limits<U>::max)());
}

template <class U = lua_Number>
bool is_floating_point_representable_by(lua_State* L, int index)
{
    int isValid = 0;

    const auto value = tonumber(L, index, &isValid);

    return isValid ? is_floating_point_representable_by<U>(value) : false;
}

} // namespace luabridge
