#pragma once

extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

#include <string>
#include <functional>
#include <vector>
#include <stdexcept>
#include <sstream>
#include <iostream>

#include "Logging.h"
#ifdef _WIN32
#include <windows.h>
#endif

namespace NovaLua
{

    class State;
    class Table;

    class Context
    {
    public:
        Context(State &state, lua_State *L) : state(state), L(L) {}

        template <typename T>
        T getArg(int index);

        void push(int val) { lua_pushinteger(L, val); }
        void push(double val) { lua_pushnumber(L, val); }
        void push(const std::string &val) { lua_pushstring(L, val.c_str()); }
        void push(bool val) { lua_pushboolean(L, val); }

    private:
        State &state;
        lua_State *L;

        friend class State;
    };

    using Function = std::function<int(Context &)>;

    class Table
    {
    public:
        Table(State &state, std::string name, Table *parent = nullptr)
            : state(state), name(std::move(name)), parent(parent) {}

        const std::string &getName() const { return name; }
        Table *getParent() const { return parent; }

    private:
        State &state;
        std::string name;
        Table *parent;

        friend class State;
    };

    inline void handleError(const std::string &message)
    {
        std::ostringstream oss;
        oss << "[NovaLua ERROR] " << message << "\n";

#ifdef _WIN32
        MessageBoxA(nullptr, oss.str().c_str(), "NovaLua Error", MB_ICONERROR | MB_OK);
        std::cerr << oss.str();

#else
        std::cerr << oss.str();
#endif
    }

    class State
    {
    public:
        State()
        {
            L = luaL_newstate();
            luaL_openlibs(L);
            *reinterpret_cast<State **>(lua_getextraspace(L)) = this;
        }

        ~State()
        {
            if (L)
                lua_close(L);
        }

        static State Create()
        {
            return State();
        }

        void doFile(const std::string &path)
        {
            if (luaL_dofile(L, path.c_str()))
                handleError(lua_tostring(L, -1));
        }

        void doString(const std::string &code)
        {
            if (luaL_dostring(L, code.c_str()))
                handleError(lua_tostring(L, -1));
        }

        template <typename Ret = void, typename... Args>
        Ret call(const std::string &func, Args... args)
        {
            lua_getglobal(L, func.c_str());

            if (!lua_isfunction(L, -1))
            {
                lua_pop(L, 1);
                throw std::bad_function_call();
            }

            (push(args), ...);

            constexpr int numArgs = sizeof...(Args);
            constexpr int numReturns = std::is_same_v<Ret, void> ? 0 : 1;

            if (lua_pcall(L, numArgs, numReturns, 0))
            {
                std::string err = lua_tostring(L, -1);
                lua_pop(L, 1);
                throw std::runtime_error(err);
            }

            if constexpr (!std::is_same_v<Ret, void>)
            {
                return get<Ret>();
            }
        }

        template <typename T>
        void setGlobal(const std::string &name, T val)
        {
            push(val);
            lua_setglobal(L, name.c_str());
        }

        Table createTable(const std::string &name, Table *parent = nullptr)
        {
            lua_newtable(L); // push new table

            if (parent)
            {
                pushTable(*parent);
                lua_pushvalue(L, -2);              // duplicate table
                lua_setfield(L, -2, name.c_str()); // parent[name] = table
                lua_pop(L, 1);                     // pop parent
            }
            else
            {
                lua_setglobal(L, name.c_str());
            }

            return Table(*this, name, parent);
        }

        void registerFunction(const std::string &name, Function fn, Table *parent = nullptr)
        {
            functionMap.push_back(std::move(fn));
            lua_pushlightuserdata(L, &functionMap.back());
            lua_pushcclosure(L, [](lua_State *L) -> int
                             {
            Function* fnPtr = static_cast<Function*>(lua_touserdata(L, lua_upvalueindex(1)));
            auto* self = *reinterpret_cast<State**>(lua_getextraspace(L));
            Context ctx(*self, L);
            return (*fnPtr)(ctx); }, 1);

            if (parent)
            {
                pushTable(*parent);
                lua_pushvalue(L, -2); // copy fn
                lua_setfield(L, -2, name.c_str());
                lua_pop(L, 1); // pop table
            }
            else
            {
                lua_setglobal(L, name.c_str());
            }

            lua_pop(L, 1); // pop fn
        }

        void printGlobals()
        {
            lua_getglobal(L, "_G");

            if (!lua_istable(L, -1))
            {
                Logger::LogError("[Lua] _G is not a table!");
                lua_pop(L, 1);
                return;
            }

            Logger::LogDebug("[Lua] Global Variables and Functions:");

            lua_pushnil(L); // first key
            while (lua_next(L, -2))
            {
                const char *key = lua_tostring(L, -2);
                const char *type = luaL_typename(L, -1);

                Logger::LogDebug("  %s: %s", key ? key : "<non-string>", type);

                lua_pop(L, 1); // remove value, keep key for next iteration
            }

            lua_pop(L, 1); // pop _G
        }

        lua_State *raw() { return L; }

        template <typename T>
        T get(); // Forward declarations, defined below

    private:
        lua_State *L;
        std::vector<Function> functionMap;

        void pushTable(const Table &table)
        {
            if (table.parent)
            {
                pushTable(*table.parent);
                lua_getfield(L, -1, table.name.c_str());
                lua_remove(L, -2); // remove parent
            }
            else
            {
                lua_getglobal(L, table.name.c_str());
            }
        }

        void push(int val) { lua_pushinteger(L, val); }
        void push(double val) { lua_pushnumber(L, val); }
        void push(const std::string &val) { lua_pushstring(L, val.c_str()); }
        void push(const char *val) { lua_pushstring(L, val); }
        void push(bool val) { lua_pushboolean(L, val); }

        friend class Context;
    };

    // ------------------- Specializations -------------------

    template <>
    inline int State::get<int>()
    {
        int val = lua_tointeger(L, -1);
        lua_pop(L, 1);
        return val;
    }

    template <>
    inline double State::get<double>()
    {
        double val = lua_tonumber(L, -1);
        lua_pop(L, 1);
        return val;
    }

    template <>
    inline std::string State::get<std::string>()
    {
        std::string val = lua_tostring(L, -1);
        lua_pop(L, 1);
        return val;
    }

    template <>
    inline bool State::get<bool>()
    {
        bool val = lua_toboolean(L, -1);
        lua_pop(L, 1);
        return val;
    }

    // ---------------- Context Argument Specializations ----------------

    template <>
    inline int Context::getArg<int>(int i) { return lua_tointeger(L, i); }

    template <>
    inline double Context::getArg<double>(int i) { return lua_tonumber(L, i); }

    template <>
    inline std::string Context::getArg<std::string>(int i) { return lua_tostring(L, i); }

    template <>
    inline bool Context::getArg<bool>(int i) { return lua_toboolean(L, i); }

} // namespace NovaLua