#ifdef WITH_RIVE_SCRIPTING
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/assets/script_asset.hpp"
#include "lualib.h"
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <queue>
#include <vector>
#include <algorithm>

using namespace rive;

int luaopen_rive_base(lua_State* L);
int luaopen_rive_math(lua_State* L);
int luaopen_rive_renderer_library(lua_State* L);
int luaopen_rive_properties(lua_State* L);
int luaopen_rive_artboards(lua_State* L);
int luaopen_rive_data_values(lua_State* L);
int luaopen_rive_input(lua_State* L);
int luaopen_rive_contex(lua_State* L);

std::unordered_map<std::string, int16_t> atoms = {
    {"length", (int16_t)LuaAtoms::length},
    {"lengthSquared", (int16_t)LuaAtoms::lengthSquared},
    {"normalized", (int16_t)LuaAtoms::normalized},
    {"distance", (int16_t)LuaAtoms::distance},
    {"distanceSquared", (int16_t)LuaAtoms::distanceSquared},
    {"dot", (int16_t)LuaAtoms::dot},
    {"lerp", (int16_t)LuaAtoms::lerp},
    {"moveTo", (int16_t)LuaAtoms::moveTo},
    {"lineTo", (int16_t)LuaAtoms::lineTo},
    {"quadTo", (int16_t)LuaAtoms::quadTo},
    {"cubicTo", (int16_t)LuaAtoms::cubicTo},
    {"close", (int16_t)LuaAtoms::close},
    {"type", (int16_t)LuaAtoms::type},
    {"reset", (int16_t)LuaAtoms::reset},
    {"add", (int16_t)LuaAtoms::add},
    {"contours", (int16_t)LuaAtoms::contours},
    {"measure", (int16_t)LuaAtoms::measure},
    {"invert", (int16_t)LuaAtoms::invert},
    {"isIdentity", (int16_t)LuaAtoms::isIdentity},
    {"width", (int16_t)LuaAtoms::width},
    {"height", (int16_t)LuaAtoms::height},
    {"clamp", (int16_t)LuaAtoms::clamp},
    {"repeat", (int16_t)LuaAtoms::repeat},
    {"mirror", (int16_t)LuaAtoms::mirror},
    {"bilinear", (int16_t)LuaAtoms::bilinear},
    {"nearest", (int16_t)LuaAtoms::nearest},
    {"style", (int16_t)LuaAtoms::style},
    {"join", (int16_t)LuaAtoms::join},
    {"cap", (int16_t)LuaAtoms::cap},
    {"thickness", (int16_t)LuaAtoms::thickness},
    {"blendMode", (int16_t)LuaAtoms::blendMode},
    {"feather", (int16_t)LuaAtoms::feather},
    {"gradient", (int16_t)LuaAtoms::gradient},
    {"color", (int16_t)LuaAtoms::color},
    {"stroke", (int16_t)LuaAtoms::stroke},
    {"fill", (int16_t)LuaAtoms::fill},
    {"miter", (int16_t)LuaAtoms::miter},
    {"round", (int16_t)LuaAtoms::round},
    {"bevel", (int16_t)LuaAtoms::bevel},
    {"butt", (int16_t)LuaAtoms::butt},
    {"square", (int16_t)LuaAtoms::square},
    {"srcOver", (int16_t)LuaAtoms::srcOver},
    {"screen", (int16_t)LuaAtoms::screen},
    {"overlay", (int16_t)LuaAtoms::overlay},
    {"darken", (int16_t)LuaAtoms::darken},
    {"lighten", (int16_t)LuaAtoms::lighten},
    {"colorDodge", (int16_t)LuaAtoms::colorDodge},
    {"colorBurn", (int16_t)LuaAtoms::colorBurn},
    {"hardLight", (int16_t)LuaAtoms::hardLight},
    {"softLight", (int16_t)LuaAtoms::softLight},
    {"difference", (int16_t)LuaAtoms::difference},
    {"exclusion", (int16_t)LuaAtoms::exclusion},
    {"multiply", (int16_t)LuaAtoms::multiply},
    {"hue", (int16_t)LuaAtoms::hue},
    {"saturation", (int16_t)LuaAtoms::saturation},
    {"luminosity", (int16_t)LuaAtoms::luminosity},
    {"copy", (int16_t)LuaAtoms::copy},
    {"drawPath", (int16_t)LuaAtoms::drawPath},
    {"drawImage", (int16_t)LuaAtoms::drawImage},
    {"drawImageMesh", (int16_t)LuaAtoms::drawImageMesh},
    {"clipPath", (int16_t)LuaAtoms::clipPath},
    {"save", (int16_t)LuaAtoms::save},
    {"restore", (int16_t)LuaAtoms::restore},
    {"transform", (int16_t)LuaAtoms::transform},
    {"value", (int16_t)LuaAtoms::value},
    {"red", (int16_t)LuaAtoms::red},
    {"green", (int16_t)LuaAtoms::green},
    {"blue", (int16_t)LuaAtoms::blue},
    {"alpha", (int16_t)LuaAtoms::alpha},
    {"getNumber", (int16_t)LuaAtoms::getNumber},
    {"getTrigger", (int16_t)LuaAtoms::getTrigger},
    {"getString", (int16_t)LuaAtoms::getString},
    {"getBoolean", (int16_t)LuaAtoms::getBoolean},
    {"getColor", (int16_t)LuaAtoms::getColor},
    {"getList", (int16_t)LuaAtoms::getList},
    {"addListener", (int16_t)LuaAtoms::addListener},
    {"removeListener", (int16_t)LuaAtoms::removeListener},
    {"fire", (int16_t)LuaAtoms::fire},
    {"push", (int16_t)LuaAtoms::push},
    {"insert", (int16_t)LuaAtoms::insert},
    {"pop", (int16_t)LuaAtoms::pop},
    {"swap", (int16_t)LuaAtoms::swap},
    {"shift", (int16_t)LuaAtoms::shift},
    {"draw", (int16_t)LuaAtoms::draw},
    {"advance", (int16_t)LuaAtoms::advance},
    {"frameOrigin", (int16_t)LuaAtoms::frameOrigin},
    {"data", (int16_t)LuaAtoms::data},
    {"instance", (int16_t)LuaAtoms::instance},
    {"new", (int16_t)LuaAtoms::newAtom},
    {"bounds", (int16_t)LuaAtoms::bounds},
    {"pointerDown", (int16_t)LuaAtoms::pointerDown},
    {"pointerUp", (int16_t)LuaAtoms::pointerUp},
    {"pointerMove", (int16_t)LuaAtoms::pointerMove},
    {"pointerExit", (int16_t)LuaAtoms::pointerExit},
    {"isNumber", (int16_t)LuaAtoms::isNumber},
    {"isString", (int16_t)LuaAtoms::isString},
    {"isBoolean", (int16_t)LuaAtoms::isBoolean},
    {"isColor", (int16_t)LuaAtoms::isColor},
    {"hit", (int16_t)LuaAtoms::hit},
    {"id", (int16_t)LuaAtoms::id},
    {"position", (int16_t)LuaAtoms::position},
    {"rotation", (int16_t)LuaAtoms::rotation},
    {"scale", (int16_t)LuaAtoms::scale},
    {"worldTransform", (int16_t)LuaAtoms::worldTransform},
    {"scaleX", (int16_t)LuaAtoms::scaleX},
    {"scaleY", (int16_t)LuaAtoms::scaleY},
    {"decompose", (int16_t)LuaAtoms::decompose},
    {"children", (int16_t)LuaAtoms::children},
    {"parent", (int16_t)LuaAtoms::parent},
    {"node", (int16_t)LuaAtoms::node},
    {"addToPath", (int16_t)LuaAtoms::addToPath},
    {"positionAndTangent", (int16_t)LuaAtoms::positionAndTangent},
    {"warp", (int16_t)LuaAtoms::warp},
    {"extract", (int16_t)LuaAtoms::extract},
    {"next", (int16_t)LuaAtoms::next},
    {"isClosed", (int16_t)LuaAtoms::isClosed},
    {"markNeedsUpdate", (int16_t)LuaAtoms::markNeedsUpdate},
    {"viewModel", (int16_t)LuaAtoms::viewModel},
};

static const luaL_Reg lualibs[] = {
    {"", luaopen_base},
    {LUA_TABLIBNAME, luaopen_table},
    {LUA_MATHLIBNAME, luaopen_math},
    {"rive", luaopen_rive_base},
    {LUA_OSLIBNAME, luaopen_os},
    {LUA_STRLIBNAME, luaopen_string},
    {LUA_UTF8LIBNAME, luaopen_utf8},
    {LUA_BUFFERLIBNAME, luaopen_buffer},
    {LUA_BITLIBNAME, luaopen_bit32},
    {"math", luaopen_rive_math},
    {"renderer", luaopen_rive_renderer_library},
    {"properties", luaopen_rive_properties},
    {"artboard", luaopen_rive_artboards},
    {"dataValue", luaopen_rive_data_values},
    {"input", luaopen_rive_input},
    {"context", luaopen_rive_contex},
    {NULL, NULL},
};

namespace rive
{
int luaopen_rive(lua_State* L)
{
    lua_callbacks(L)->useratom = [](const char* s, size_t l) -> int16_t {
        auto itr = atoms.find(s);
        if (itr != atoms.end())
        {
            return itr->second;
        }

        return -1;
    };

    const luaL_Reg* lib = lualibs;
    for (; lib->func; lib++)
    {
        lua_pushcfunction(L, lib->func, NULL);
        lua_pushstring(L, lib->name);
        lua_call(L, 1, 0);
    }
    return 0;
}

int rive_luaErrorHandler(lua_State* L)
{
    ScriptingContext* context =
        static_cast<ScriptingContext*>(lua_getthreaddata(L));
    context->printError(L);

    // Optionally, you can push a new value onto the stack to be returned by
    // lua_pcall For example, push a specific error code or a more detailed
    // message
    const char* error = lua_tostring(L, -1);
    lua_pushstring(L, error);
    return 1; // Number of return values
    // return 0;
}

int rive_lua_pcall(lua_State* state, int nargs, int nresults)
{
    ScriptingContext* context =
        static_cast<ScriptingContext*>(lua_getthreaddata(state));

    return context->pCall(state, nargs, nresults);
}

int rive_lua_pushRef(lua_State* state, int ref)
{
    return lua_rawgeti(state, luaRegistryIndex, ref);
}

void rive_lua_pop(lua_State* state, int count)
{
    lua_settop(state, -count - 1);
}

static void* l_alloc(void* ud, void* ptr, size_t osize, size_t nsize)
{
    (void)ud;
    (void)osize;
    if (nsize == 0)
    {
        free(ptr);
        // delete[] (uint8_t*)ptr;
        return NULL;
    }
    else
    {
        // auto nptr = new uint8_t[nsize];
        // memcpy(nptr, ptr, std::min(nsize, osize));
        // delete[] (uint8_t*)ptr;
        // return nptr;
        return realloc(ptr, nsize);
    }
}

static const char* registeredCacheTableKey = "_MODULES";

static int checkRegisteredModules(lua_State* L, const char* path)
{
    luaL_findtable(L, LUA_REGISTRYINDEX, registeredCacheTableKey, 1);
    lua_getfield(L, -1, path);
    if (lua_isnil(L, -1))
    {
        lua_pop(L, 2);
        return 0;
    }

    lua_remove(L, -2);
    return 1;
}

static int lua_requireinternal(lua_State* L, const char* requirerChunkname)
{
    // Discard extra arguments, we only use path
    lua_settop(L, 1);
    const char* path = luaL_checkstring(L, 1);

    if (checkRegisteredModules(L, path) == 1)
    {
        return 1;
    }

    // Record missing dependency if we're registering a module
    if (requirerChunkname)
    {
        ScriptingContext* context =
            static_cast<ScriptingContext*>(lua_getthreaddata(L));
        if (context)
        {
            context->recordMissingDependency(requirerChunkname, path);
        }
    }

    luaL_error(L, "require could not find a script named %s", path);
    return 0;
}

static int lua_require(lua_State* L)
{
    lua_Debug ar;
    int level = 1;

    do
    {
        if (!lua_getinfo(L, level++, "s", &ar))
        {
            luaL_error(L, "require is not supported in this context");
        }
    } while (ar.what[0] == 'C');

    return lua_requireinternal(L, ar.source);
}

static int luaR_error(lua_State* L)
{
    int level = luaL_optinteger(L, 2, 1);
    lua_settop(L, 1);
    if (lua_isstring(L, 1) && level > 0)
    {
        luaL_where(L, level);
        lua_pushvalue(L, 1);
        lua_concat(L, 2);
    }
    lua_error(L);
}

static int lua_late(lua_State* L)
{
    lua_pushnil(L);
    return 1;
}

void ScriptingVM::init(lua_State* state, ScriptingContext* context)
{
    luaopen_rive(state);
    lua_setthreaddata(state, context);

    lua_pushcclosurek(state, lua_require, "require", 0, nullptr);
    lua_setglobal(state, "require");

    lua_pushcclosurek(state, luaR_error, "error", 0, nullptr);
    lua_setglobal(state, "error");

    lua_pushcclosurek(state, lua_late, "late", 0, nullptr);
    lua_setglobal(state, "late");

    luaL_sandbox(state);
    luaL_sandboxthread(state);
}

ScriptingVM::ScriptingVM(ScriptingContext* context) : m_context(context)
{
    m_state = lua_newstate(l_alloc, nullptr);
    init(m_state, m_context);
}

ScriptingVM::~ScriptingVM() { lua_close(m_state); }

static int register_module(lua_State* L)
{
    // This is only called internally where we ensure we're pushing the right
    // elements on the stack so we can assert these at debug time only.
    assert(lua_gettop(L) == 2 &&
           "expected 2 arguments: require script name and desired result");
    assert(luaL_checkstring(L, 1) &&
           "first argument must be the name of the script");

    luaL_findtable(L, LUA_REGISTRYINDEX, registeredCacheTableKey, 1);
    // (1) path, (2) result, (3) cache table

    lua_insert(L, 1);
    // (1) cache table, (2) path, (3) result

    lua_settable(L, 1);
    // (1) cache table

    lua_pop(L, 1);

    return 0;
}

static bool push_module(lua_State* L, const char* name, Span<uint8_t> bytecode)
{
    if (bytecode.empty())
    {
        return false;
    }
    // module needs to run in a new thread, isolated from the rest
    // note: we create ML on main thread so that it doesn't inherit environment
    // of L
    lua_State* GL = lua_mainthread(L);
    lua_State* ML = lua_newthread(GL);
    lua_xmove(GL, L, 1);
    // new thread needs to have the globals sandboxed
    luaL_sandboxthread(ML);
    lua_setthreaddata(ML, lua_getthreaddata(L));

    int status =
        luau_load(ML, name, (const char*)bytecode.data(), bytecode.size(), 0);

    if (status == 0)
    {
        int status = lua_resume(ML, L, 0);
        if (status == 0)
        {
            if (lua_gettop(ML) == 0)
            {
                lua_pushfstring(ML, "%s:1: module must return a value", name);
            }
            else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1))
            {
                lua_pushfstring(ML,
                                "%s:1: module must return a table or function",
                                name);
            }
        }
        else if (status == LUA_YIELD)
        {
            lua_pushfstring(ML, "%s:1: module can not yield", name);
        }
        else if (!lua_isstring(ML, -1))
        {
            lua_pushfstring(ML,
                            "%s:1: unknown error while running module",
                            name);
        }
    }

    // add ML result to L stack
    lua_xmove(ML, L, 1);
    // An error occurred if the top of the stack is a string.
    if (lua_isstring(L, -1))
    {
        ScriptingContext* context =
            static_cast<ScriptingContext*>(lua_getthreaddata(L));
        context->printError(L);
        lua_pop(L, 2);
        return false;
    }
    // remove ML thread from L stack
    lua_remove(L, -2);
    // added one value to L stack: module result
    return true;
}

static void dump_stack(lua_State* state)
{
    int i;
    int top = lua_gettop(state);
    for (i = 1; i <= top; i++)
    { /* repeat for each level */
        int t = lua_type(state, i);
        switch (t)
        {

            case LUA_TSTRING: /* strings */
                fprintf(stderr,
                        "  (%i)[STRING] %s\n",
                        i,
                        lua_tostring(state, i));
                break;

            case LUA_TBOOLEAN: /* booleans */
                fprintf(stderr,
                        "  (%i)[BOOLEAN] %s\n",
                        i,
                        lua_toboolean(state, i) ? "true" : "false");
                break;

            case LUA_TNUMBER: /* numbers */
                fprintf(stderr,
                        "  (%i)[NUMBER] %g\n",
                        i,
                        lua_tonumber(state, i));
                break;

            default: /* other values */
                fprintf(stderr, "  (%i)[%s]\n", i, lua_typename(state, t));
                break;
        }
    }
    fprintf(stderr, "\n"); /* end the listing */
}

void ScriptingVM::dumpStack(lua_State* state) { dump_stack(state); }

void ScriptingContext::addModule(ModuleDetails* moduleDetails)
{
    m_modulesToRegister.push_back(moduleDetails);
    m_moduleLookup[moduleDetails->moduleName()] = moduleDetails;
}

bool ScriptingContext::tryRegisterModule(lua_State* state,
                                         ModuleDetails* moduleDetails)
{
    if (!moduleDetails->verified())
    {
        return false;
    }
    const std::string& name = moduleDetails->moduleName();
    bool registerSuccess = false;
    int functionRef = 0;
    if (moduleDetails->isProtocolScript())
    {
        if (ScriptingVM::registerScript(state,
                                        name.c_str(),
                                        moduleDetails->moduleBytecode()))
        {
            // registerScript leaves the function on the stack
            if (static_cast<lua_Type>(lua_type(state, -1)) == LUA_TFUNCTION)
            {
                functionRef = lua_ref(state, -1);
            }
            lua_pop(state, 1);
            registerSuccess = true;
        }
    }
    else
    {
        if (ScriptingVM::registerModule(state,
                                        name.c_str(),
                                        moduleDetails->moduleBytecode()))
        {
            registerSuccess = true;
        }
    }
    if (registerSuccess)
    {
        moduleDetails->registrationComplete(functionRef);
        onModuleRegistered(moduleDetails);
        return true;
    }

    return false;
}

void ScriptingContext::performRegistration(lua_State* state)
{
    // Loop over all of the modules once. We need do a tryRegister
    // pass on each module in order to determine if it has any
    // required dependencies
    for (ModuleDetails* moduleDetails : m_modulesToRegister)
    {
        // Skip if already registered
        if (checkRegisteredModules(state,
                                   moduleDetails->moduleName().c_str()) == 1)
        {
            continue;
        }
        tryRegisterModule(state, moduleDetails);
    }

    // If any modules had dependencies, resolve their registration order
    // and try registering again
    if (!m_pendingModules.empty())
    {
        std::vector<ModuleDetails*> pendingModules;
        for (auto module : m_pendingModules)
        {
            pendingModules.push_back(module);
        }
        std::vector<ModuleDetails*> sortedModules;
        std::unordered_set<ModuleDetails*> visitedModules;

        ModuleDetails* module = pendingModules.back();
        pendingModules.pop_back();
        sortNextModule(module,
                       &pendingModules,
                       &sortedModules,
                       &visitedModules);

        // Register modules in sorted order
        for (ModuleDetails* moduleDetails : sortedModules)
        {
            tryRegisterModule(state, moduleDetails);
        }
    }

    m_modulesToRegister.clear();
    m_pendingModules.clear();
}

void ScriptingContext::sortNextModule(
    ModuleDetails* module,
    std::vector<ModuleDetails*>* pendingModules,
    std::vector<ModuleDetails*>* sortedModules,
    std::unordered_set<ModuleDetails*>* visitedModules)
{
    // If already visited, skip
    if (visitedModules->find(module) != visitedModules->end())
    {
        return;
    }

    auto dependencies = module->missingDependencies();
    for (const auto& dependencyName : dependencies)
    {
        auto lookupIt = m_moduleLookup.find(dependencyName);
        if (lookupIt != m_moduleLookup.end())
        {
            ModuleDetails* dependencyModule = lookupIt->second;
            // Recursively process the dependency
            sortNextModule(dependencyModule,
                           pendingModules,
                           sortedModules,
                           visitedModules);
        }
    }

    if (std::find(sortedModules->begin(), sortedModules->end(), module) ==
        sortedModules->end())
    {
        sortedModules->push_back(module);
    }
    visitedModules->insert(module);
    if (!pendingModules->empty())
    {
        ModuleDetails* nextModule = pendingModules->back();
        pendingModules->pop_back();
        sortNextModule(nextModule,
                       pendingModules,
                       sortedModules,
                       visitedModules);
    }
}

void ScriptingContext::recordMissingDependency(
    const std::string& requiringModule,
    const std::string& missingModule)
{
    if (!requiringModule.empty())
    {
        ModuleDetails* moduleDetails = m_moduleLookup[requiringModule];
        if (moduleDetails != nullptr)
        {
            moduleDetails->addMissingDependency(missingModule);
            m_pendingModules.insert(moduleDetails);
        }
    }
}

void ScriptingContext::onModuleRegistered(ModuleDetails* moduleDetails)
{
    for (ModuleDetails* module : m_modulesToRegister)
    {
        if (!module->missingDependencies().empty())
        {
            moduleDetails->clearMissingDependency(moduleDetails->moduleName());
        }
    }
    auto it = m_pendingModules.find(moduleDetails);
    if (it != m_pendingModules.end())
    {
        m_pendingModules.erase(it);
    }
}

bool ScriptingVM::registerScript(lua_State* state,
                                 const char* name,
                                 Span<uint8_t> bytecode)
{
    // Check if already registered
    if (checkRegisteredModules(state, name) == 1)
    {
        return true;
    }

    if (!push_module(state, name, bytecode))
    {
        return false;
    }

    return true;
}

bool ScriptingVM::registerModule(lua_State* state,
                                 const char* name,
                                 Span<uint8_t> bytecode)
{
    // Check if already registered
    if (checkRegisteredModules(state, name) == 1)
    {
        return true;
    }

    lua_pushcfunction(state, register_module, nullptr);
    lua_pushstring(state, name);
    if (!push_module(state, name, bytecode))
    {
        lua_pop(state, 2);
        return false;
    }

    lua_call(state, 2, 0);
    return true;
}

void ScriptingVM::unregisterModule(lua_State* state, const char* name)
{
    luaL_findtable(state, LUA_REGISTRYINDEX, registeredCacheTableKey, 1);
    lua_pushstring(state, name);
    lua_pushnil(state);
    lua_settable(state, -3);
    lua_pop(state, 1);
}

void ScriptingVM::unregisterModule(const char* name)
{
    return unregisterModule(m_state, name);
}

bool ScriptingVM::registerModule(const char* name, Span<uint8_t> bytecode)
{
    return registerModule(m_state, name, bytecode);
}

bool ScriptingVM::registerScript(const char* name, Span<uint8_t> bytecode)
{
    return registerScript(m_state, name, bytecode);
}

int CPPRuntimeScriptingContext::pCall(lua_State* state, int nargs, int nresults)
{
    // calculate stack position for message handler
    int hpos = lua_gettop(state) - nargs;
    lua_pushcfunction(state, rive_luaErrorHandler, "riveErrorHandler");
    lua_insert(state, hpos);

    startTimedExecution(state);
    int ret = lua_pcall(state, nargs, nresults, hpos);
    endTimedExecution(state);
    lua_remove(state, hpos);
    return ret;
}

} // namespace rive
#endif
