#include <AssetManager/AssetManager.hpp>
#include <Log/Log.hpp>
#include <ScriptEngine/ScriptEngine.hpp>
#include <Side/Side.hpp>
#include <Utilities/Utilities.hpp>

int ScriptEngine::luaCallback(lua_State *L)
{
    AbstractCallWrapper *wrapper = reinterpret_cast<AbstractCallWrapper*>(lua_touserdata(L, lua_upvalueindex(1)));
    AbstractCallWrapper::SavedCallFrame *context = nullptr;
    int result = wrapper->implementCall(context);
    if (result == -1)
    {
        return lua_yieldk(L, 0, (lua_KContext) context, luaContCallback);
    }

    delete context;
    return result;
}

int ScriptEngine::luaContCallback(lua_State *L, int status, lua_KContext ctx)
{
    AbstractCallWrapper::SavedCallFrame *frame = reinterpret_cast<AbstractCallWrapper::SavedCallFrame*>((void*) ctx);
    int result = frame->resume();
    if (result == -1)
    {
        return lua_yieldk(L, 0, ctx, luaContCallback);
    }

    delete frame;
    return result;
}

ScriptEngine::ScriptEngine()
{
    L = luaL_newstate();
    co = lua_newthread(L);
    coref = luaL_ref(L, LUA_REGISTRYINDEX);

    luaL_requiref(L, "_G", luaopen_base, 1);
    lua_pop(L, 1);

    luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1);
    lua_pop(L, 1);

    luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1);
    lua_pop(L, 1);

    luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1);
    lua_pop(L, 1);

    push(VoxelSprite::Color{});
    setGlobal("TRANSPARENT");

    push(VoxelSprite::Color{{0xFF, 0xFF, 0xFF, 0xFF}});
    setGlobal("WHITE");

    push(VoxelSprite::Color{{0x00, 0x00, 0x00, 0xFF}});
    setGlobal("BLACK");

    addFunc("color_from_rgba", [](int r, int g, int b, int a) -> VoxelSprite::Color
    {
        VoxelSprite::Color color;
        color.red = r;
        color.green = g;
        color.blue = b;
        color.alpha = a;
        return color;
    });

    addFunc("color_from_rgb", [](int r, int g, int b) -> VoxelSprite::Color
    {
        VoxelSprite::Color color;
        color.red = r;
        color.green = g;
        color.blue = b;
        color.alpha = 0xFF;
        return color;
    });
    
    addFunc("use_script", [this](std::string name)
    {
        std::string fullpath = assets.resolve(formatString("scripts/$+", name));
        if (!execFromFile(fullpath))
        {
            std::string error = pop<std::string>();
            Log::error(formatString("Failed to execute '$+': $+", fullpath, error));
        }
    });

    addFunc("get_side_name", [](int num)
    {
        if (num < 0 || num >= 6)
        {
            Log::error(formatString("get_side_name() called with invalid side number '$+'.", num));
            return "<invalid>";
        }

        return Side(num).name();
    });
}

ScriptEngine::~ScriptEngine()
{
    lua_close(L);
}

void ScriptEngine::push(std::string str)
{
    lua_pushstring(co, str.c_str());
}

void ScriptEngine::push(std::nullptr_t)
{
    lua_pushnil(co);
}

void ScriptEngine::push(int value)
{
    lua_pushinteger(co, value);
}

void ScriptEngine::push(VoxelSprite::Color color)
{
    push(color.asInt);
}

bool ScriptEngine::read(std::string &str, int index)
{
    if (!lua_isstring(co, index))
    {
        return false;
    }

    str = lua_tostring(co, index);
    return true;
}

bool ScriptEngine::read(int &value, int index)
{
    if (!lua_isinteger(co, index))
    {
        return false;
    }

    value = lua_tointeger(co, index);
    return true;
}

bool ScriptEngine::read(VoxelSprite::Color &color, int index)
{
    return read(color.asInt, index);
}

bool ScriptEngine::execFromString(std::string luaCode)
{
    if (!loadFromString(luaCode))
    {
        return false;
    }

    return exec();
}

bool ScriptEngine::execFromFile(std::string luaScriptPath)
{
    if (!loadFromFile(luaScriptPath))
    {
        return false;
    }

    return exec();
}

bool ScriptEngine::loadFromString(std::string luaCode)
{
    return luaL_loadstring(co, luaCode.c_str()) == 0;
}

bool ScriptEngine::loadFromFile(std::string luaScriptPath)
{
    return luaL_loadfile(co, luaScriptPath.c_str()) == 0;
}

ScriptEngine::Async<bool> ScriptEngine::resume()
{
#if LUA_VERSION_NUM >= 504
    int results;
    int status = lua_resume(co, nullptr, 0, &results);
#else
    int status = lua_resume(co, nullptr, 0);
#endif
    if (status == LUA_YIELD)
    {
        return Async<bool>::yield();
    }

    return Async<bool>(status == LUA_OK);
}

bool ScriptEngine::exec()
{
    return lua_pcall(co, 0, LUA_MULTRET, 0) == 0;
}

void ScriptEngine::setGlobal(std::string name)
{
    lua_setglobal(co, name.c_str());
}