#include "pxl8_script.h"

#include <limits.h>
#include <stdlib.h>
#include <string.h>

#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>

#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>

#include "pxl8_cart.h"
#include "pxl8_embed.h"
#include "pxl8_gui.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
#include "pxl8_script_ffi.h"

struct pxl8_script {
    lua_State* L;
    pxl8_gfx* gfx;
    pxl8_input_state* input;
    pxl8_sfx_mixer* mixer;
    char last_error[PXL8_MAX_ERROR_SIZE];
    char main_path[PXL8_MAX_PATH];
    char watch_dir[PXL8_MAX_PATH];
    time_t latest_mod_time;
    int repl_env_ref;
    bool repl_mode;
};

#define PXL8_MAX_REPL_COMMAND_SIZE 4096

static int pxl8_cart_loader(lua_State* L) {
    const char* found_path = lua_tostring(L, lua_upvalueindex(1));
    const char* code = lua_tostring(L, lua_upvalueindex(2));
    size_t code_len = lua_objlen(L, lua_upvalueindex(2));
    bool is_fennel = lua_toboolean(L, lua_upvalueindex(3));

    if (is_fennel) {
        lua_getglobal(L, "fennel");
        lua_getfield(L, -1, "eval");
        lua_pushlstring(L, code, code_len);
        lua_createtable(L, 0, 1);
        lua_pushstring(L, found_path);
        lua_setfield(L, -2, "filename");

        if (lua_pcall(L, 2, 1, 0) != 0) {
            lua_remove(L, -2);
            return lua_error(L);
        }
        lua_remove(L, -2);
    } else {
        if (luaL_loadbuffer(L, code, code_len, found_path) != 0) {
            return lua_error(L);
        }
        if (lua_pcall(L, 0, 1, 0) != 0) {
            return lua_error(L);
        }
    }

    return 1;
}

static int pxl8_cart_searcher(lua_State* L) {
    const char* modname = luaL_checkstring(L, 1);

    pxl8_cart* cart = pxl8_get_cart();
    if (!cart || !pxl8_cart_is_packed(cart)) {
        lua_pushstring(L, "\n\tno packed cart mounted");
        return 1;
    }

    char path[512];
    size_t len = strlen(modname);
    size_t j = 0;
    for (size_t i = 0; i < len && j < sizeof(path) - 5; i++) {
        if (modname[i] == '.') {
            path[j++] = '/';
        } else {
            path[j++] = modname[i];
        }
    }
    path[j] = '\0';

    const char* extensions[] = {".fnl", ".lua", "/init.fnl", "/init.lua"};
    u8* data = NULL;
    u32 size = 0;
    char found_path[512];
    bool is_fennel = false;

    for (int i = 0; i < 4; i++) {
        snprintf(found_path, sizeof(found_path), "%s%s", path, extensions[i]);
        if (pxl8_cart_read_file(cart, found_path, &data, &size) == PXL8_OK) {
            is_fennel = (i == 0 || i == 2);
            break;
        }
    }

    if (!data) {
        lua_pushfstring(L, "\n\tno file '%s' in cart", path);
        return 1;
    }

    lua_pushstring(L, found_path);
    lua_pushlstring(L, (const char*)data, size);
    lua_pushboolean(L, is_fennel);
    pxl8_cart_free_file(data);

    lua_pushcclosure(L, pxl8_cart_loader, 3);
    return 1;
}

static void pxl8_script_repl_promote_locals(const char* input, char* output, size_t output_size) {
    size_t i = 0;
    size_t j = 0;
    bool in_string = false;
    bool in_comment = false;

    while (input[i] && j < output_size - 1) {
        if (in_comment) {
            output[j++] = input[i];
            if (input[i] == '\n') {
                in_comment = false;
            }
            i++;
            continue;
        }

        if (input[i] == ';' && !in_string) {
            in_comment = true;
            output[j++] = input[i++];
            continue;
        }

        if (input[i] == '"' && (i == 0 || input[i-1] != '\\')) {
            in_string = !in_string;
            output[j++] = input[i++];
            continue;
        }

        if (!in_string && input[i] == '(' &&
            strncmp(&input[i], "(local ", 7) == 0) {
            memcpy(&output[j], "(global ", 8);
            j += 8;
            i += 7;
            continue;
        }

        output[j++] = input[i++];
    }
    output[j] = '\0';
}

void pxl8_lua_log(int level, const char* file, int line, const char* msg) {
    if (file && (file[0] == '?' || file[0] == '\0')) file = NULL;
    switch (level) {
        case PXL8_LOG_LEVEL_TRACE: pxl8_log_write_trace(file, line, "%s", msg); break;
        case PXL8_LOG_LEVEL_DEBUG: pxl8_log_write_debug(file, line, "%s", msg); break;
        case PXL8_LOG_LEVEL_INFO:  pxl8_log_write_info("%s", msg); break;
        case PXL8_LOG_LEVEL_WARN:  pxl8_log_write_warn(file, line, "%s", msg); break;
        case PXL8_LOG_LEVEL_ERROR: pxl8_log_write_error(file, line, "%s", msg); break;
    }
}

static void pxl8_script_set_error(pxl8_script* script, const char* error) {
    if (!script) return;
    if (!error) {
        snprintf(script->last_error, sizeof(script->last_error), "Unknown error");
        return;
    }

    const char* src = error;
    char* dst = script->last_error;
    char* end = dst + sizeof(script->last_error) - 1;

    while (*src && dst < end) {
        if (strncmp(src, "[string \"", 9) == 0) {
            src += 9;
            const char* mod_start = src;
            while (*src && !(*src == '"' && *(src+1) == ']')) src++;
            size_t mod_len = src - mod_start;

            if (mod_len > 4 && strncmp(mod_start, "pxl8", 4) == 0) {
                const char* prefix = "src/lua/";
                while (*prefix && dst < end) *dst++ = *prefix++;
                for (size_t i = 0; i < mod_len && dst < end; i++) {
                    *dst++ = (mod_start[i] == '.') ? '/' : mod_start[i];
                }
                const char* suffix = ".lua";
                while (*suffix && dst < end) *dst++ = *suffix++;
            } else {
                for (size_t i = 0; i < mod_len && dst < end; i++) {
                    *dst++ = mod_start[i];
                }
            }

            if (*src == '"' && *(src+1) == ']') src += 2;
        } else {
            *dst++ = *src++;
        }
    }
    *dst = '\0';
}

const char* pxl8_script_get_last_error(pxl8_script* script) {
    return script ? script->last_error : "Invalid script context";
}

static int pxl8_embed_searcher(lua_State* L) {
    const char* name = luaL_checkstring(L, 1);
    const pxl8_embed* e = pxl8_embed_find(name);
    if (!e) {
        lua_pushfstring(L, "\n\tno embedded module '%s'", name);
        return 1;
    }
    if (luaL_loadbuffer(L, e->data, e->size, name) != 0) {
        return lua_error(L);
    }
    lua_pushstring(L, name);
    return 2;
}

static void pxl8_install_embed_searcher(lua_State* L) {
    lua_getglobal(L, "package");
    lua_getfield(L, -1, "searchers");
    if (lua_isnil(L, -1)) {
        lua_pop(L, 1);
        lua_getfield(L, -1, "loaders");
    }

    int len = (int)lua_objlen(L, -1);
    for (int i = len; i >= 2; i--) {
        lua_rawgeti(L, -1, i);
        lua_rawseti(L, -2, i + 1);
    }

    lua_pushcfunction(L, pxl8_embed_searcher);
    lua_rawseti(L, -2, 2);
    lua_pop(L, 2);
}

pxl8_script* pxl8_script_create(bool repl_mode) {
    pxl8_script* script = (pxl8_script*)calloc(1, sizeof(pxl8_script));
    if (!script) return NULL;
    script->repl_mode = repl_mode;

    script->L = luaL_newstate();
    if (!script->L) {
        free(script);
        return NULL;
    }

    luaL_openlibs(script->L);

    lua_getglobal(script->L, "require");
    lua_pushstring(script->L, "ffi");
    if (lua_pcall(script->L, 1, 1, 0) != 0) {
        pxl8_script_set_error(script, lua_tostring(script->L, -1));
        pxl8_script_destroy(script);
        return NULL;
    }

    lua_getfield(script->L, -1, "cdef");
    lua_pushstring(script->L, pxl8_ffi_cdefs);
    if (lua_pcall(script->L, 1, 0, 0) != 0) {
        pxl8_script_set_error(script, lua_tostring(script->L, -1));
        pxl8_script_destroy(script);
        return NULL;
    }

    lua_pop(script->L, 1);

    pxl8_install_embed_searcher(script->L);

    const pxl8_embed* fennel = pxl8_embed_find("fennel");
    if (fennel && luaL_loadbuffer(script->L, fennel->data, fennel->size, "fennel") == 0) {
        if (lua_pcall(script->L, 0, 1, 0) == 0) {
            lua_setglobal(script->L, "fennel");

            lua_getglobal(script->L, "fennel");
            lua_getfield(script->L, -1, "install");
            if (lua_isfunction(script->L, -1)) {
                lua_newtable(script->L);
                lua_pushboolean(script->L, 1);
                lua_setfield(script->L, -2, "correlate");
                lua_pushboolean(script->L, 1);
                lua_setfield(script->L, -2, "lambdaAsFn");
                if (script->repl_mode) {
                    lua_pushboolean(script->L, 1);
                    lua_setfield(script->L, -2, "useMetadata");
                    lua_pushboolean(script->L, 1);
                    lua_setfield(script->L, -2, "assertAsRepl");
                }
                if (lua_pcall(script->L, 1, 0, 0) != 0) {
                    pxl8_warn("Failed to install fennel searcher: %s", lua_tostring(script->L, -1));
                    lua_pop(script->L, 1);
                }

                lua_getglobal(script->L, "package");
                lua_getfield(script->L, -1, "loaders");
                if (lua_isnil(script->L, -1)) {
                    lua_pop(script->L, 1);
                    lua_getfield(script->L, -1, "searchers");
                }
                if (lua_istable(script->L, -1)) {
                    int n = (int)lua_objlen(script->L, -1);
                    for (int i = n; i >= 2; i--) {
                        lua_rawgeti(script->L, -1, i);
                        lua_rawseti(script->L, -2, i + 1);
                    }
                    lua_pushcfunction(script->L, pxl8_cart_searcher);
                    lua_rawseti(script->L, -2, 2);
                }
                lua_pop(script->L, 2);
            } else {
                lua_pop(script->L, 1);
            }
            lua_pop(script->L, 1);
        } else {
            pxl8_warn("Failed to execute fennel: %s", lua_tostring(script->L, -1));
            lua_pop(script->L, 1);
        }
    }

    script->last_error[0] = '\0';
    script->repl_env_ref = LUA_NOREF;

    if (script->repl_mode) {
        lua_newtable(script->L);
        lua_newtable(script->L);
        lua_getglobal(script->L, "_G");
        lua_setfield(script->L, -2, "__index");
        lua_setmetatable(script->L, -2);
        script->repl_env_ref = luaL_ref(script->L, LUA_REGISTRYINDEX);
    }

    return script;
}

void pxl8_script_destroy(pxl8_script* script) {
    if (!script) return;
    if (script->L) {
        if (script->repl_env_ref != LUA_NOREF) {
            luaL_unref(script->L, LUA_REGISTRYINDEX, script->repl_env_ref);
        }
        lua_close(script->L);
    }
    free(script);
}

void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx) {
    if (!script) return;
    script->gfx = gfx;
    if (script->L && gfx) {
        lua_pushlightuserdata(script->L, gfx);
        lua_setglobal(script->L, "pxl8_gfx");
    }
}

void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) {
    if (!script) return;
    script->input = input;
    if (script->L && input) {
        lua_pushlightuserdata(script->L, input);
        lua_setglobal(script->L, "pxl8_input");
    }
}

void pxl8_script_set_rng(pxl8_script* script, void* rng) {
    if (!script) return;
    if (script->L && rng) {
        lua_pushlightuserdata(script->L, rng);
        lua_setglobal(script->L, "pxl8_rng");
    }
}

void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) {
    if (!script) return;
    script->mixer = mixer;
    if (script->L && mixer) {
        lua_pushlightuserdata(script->L, mixer);
        lua_setglobal(script->L, "pxl8_sfx");
    }
}

void pxl8_script_set_sys(pxl8_script* script, void* sys) {
    if (!script) return;
    if (script->L && sys) {
        lua_pushlightuserdata(script->L, sys);
        lua_setglobal(script->L, "pxl8_sys");
    }
}

static pxl8_result pxl8_script_prepare_path(pxl8_script* script, const char* filename, char* out_basename, size_t basename_size) {
    char filename_copy[PATH_MAX];
    pxl8_strncpy(filename_copy, filename, sizeof(filename_copy));

    char* last_slash = strrchr(filename_copy, '/');

    if (last_slash) {
        *last_slash = '\0';
        char script_dir[PATH_MAX];
        char original_cwd[PATH_MAX];

        if (realpath(filename_copy, script_dir) && getcwd(original_cwd, sizeof(original_cwd))) {
            chdir(script_dir);
            pxl8_script_set_cart_path(script, script_dir, original_cwd);
            strncpy(out_basename, last_slash + 1, basename_size - 1);
            out_basename[basename_size - 1] = '\0';
        } else {
            strncpy(out_basename, filename, basename_size - 1);
            out_basename[basename_size - 1] = '\0';
        }
    } else {
        strncpy(out_basename, filename, basename_size - 1);
        out_basename[basename_size - 1] = '\0';
    }

    return PXL8_OK;
}

pxl8_result pxl8_script_run_file(pxl8_script* script, const char* filename) {
    if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;

    char basename[PATH_MAX];
    pxl8_script_prepare_path(script, filename, basename, sizeof(basename));

    pxl8_result result = PXL8_OK;
    if (luaL_dofile(script->L, basename) != 0) {
        pxl8_script_set_error(script, lua_tostring(script->L, -1));
        lua_pop(script->L, 1);
        result = PXL8_ERROR_SCRIPT_ERROR;
    } else {
        script->last_error[0] = '\0';
    }

    return result;
}

static time_t get_file_mod_time(const char* path) {
    struct stat file_stat;
    if (stat(path, &file_stat) == 0) {
        return file_stat.st_mtime;
    }
    return 0;
}

static time_t get_latest_script_mod_time(const char* dir_path) {
    DIR* dir = opendir(dir_path);
    if (!dir) return 0;

    time_t latest = 0;
    struct dirent* entry;

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;

        char full_path[512];
        snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);

        struct stat st;
        if (stat(full_path, &st) == 0) {
            if (S_ISDIR(st.st_mode)) {
                time_t subdir_time = get_latest_script_mod_time(full_path);
                if (subdir_time > latest) {
                    latest = subdir_time;
                }
            } else {
                size_t len = strlen(entry->d_name);
                bool is_script = (len > 4 && strcmp(entry->d_name + len - 4, ".fnl") == 0) ||
                                (len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0);

                if (is_script) {
                    time_t mod_time = get_file_mod_time(full_path);
                    if (mod_time > latest) {
                        latest = mod_time;
                    }
                }
            }
        }
    }

    closedir(dir);
    return latest;
}

void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd) {
    if (!script || !script->L || !cart_path || !original_cwd) return;

    lua_getglobal(script->L, "package");
    lua_getfield(script->L, -1, "path");
    const char* current_path = lua_tostring(script->L, -1);

    char new_path[2048];
    snprintf(new_path, sizeof(new_path),
             "%s/?.lua;%s/?/init.lua;%s/src/?.lua;%s/src/?/init.lua;%s/src/lua/?.lua;%s",
             cart_path, cart_path, cart_path, cart_path, original_cwd,
             current_path ? current_path : "");

    lua_pushstring(script->L, new_path);
    lua_setfield(script->L, -3, "path");
    lua_pop(script->L, 2);

    lua_getglobal(script->L, "fennel");
    if (!lua_isnil(script->L, -1)) {
        lua_getfield(script->L, -1, "path");
        const char* fennel_path = lua_tostring(script->L, -1);

        snprintf(new_path, sizeof(new_path),
                 "%s/?.fnl;%s/?/init.fnl;%s/src/?.fnl;%s/src/?/init.fnl;%s",
                 cart_path, cart_path, cart_path, cart_path,
                 fennel_path ? fennel_path : "");

        lua_pushstring(script->L, new_path);
        lua_setfield(script->L, -3, "path");
        lua_pop(script->L, 1);
    }
    lua_pop(script->L, 1);

    pxl8_strncpy(script->watch_dir, cart_path, sizeof(script->watch_dir));
    script->latest_mod_time = get_latest_script_mod_time(script->watch_dir);
}

pxl8_result pxl8_script_run_fennel_file(pxl8_script* script, const char* filename) {
    if (!script || !script->L || !filename) return PXL8_ERROR_NULL_POINTER;

    char basename[PATH_MAX];
    pxl8_script_prepare_path(script, filename, basename, sizeof(basename));

    lua_getglobal(script->L, "fennel");
    if (lua_isnil(script->L, -1)) {
        pxl8_script_set_error(script, "Fennel not loaded");
        lua_pop(script->L, 1);
        return PXL8_ERROR_SCRIPT_ERROR;
    }

    pxl8_cart* cart = pxl8_get_cart();
    pxl8_result result = PXL8_OK;

    if (cart && pxl8_cart_is_packed(cart)) {
        u8* data = NULL;
        u32 size = 0;
        if (pxl8_cart_read_file(cart, basename, &data, &size) != PXL8_OK) {
            pxl8_script_set_error(script, "Failed to read script from cart");
            lua_pop(script->L, 1);
            return PXL8_ERROR_FILE_NOT_FOUND;
        }

        lua_getfield(script->L, -1, "eval");
        lua_pushlstring(script->L, (const char*)data, size);

        lua_createtable(script->L, 0, 1);
        lua_pushstring(script->L, basename);
        lua_setfield(script->L, -2, "filename");

        pxl8_cart_free_file(data);

        if (lua_pcall(script->L, 2, 0, 0) != 0) {
            pxl8_script_set_error(script, lua_tostring(script->L, -1));
            lua_pop(script->L, 1);
            result = PXL8_ERROR_SCRIPT_ERROR;
        } else {
            script->last_error[0] = '\0';
        }
    } else {
        lua_getfield(script->L, -1, "dofile");
        lua_pushstring(script->L, basename);

        if (lua_pcall(script->L, 1, 0, 0) != 0) {
            pxl8_script_set_error(script, lua_tostring(script->L, -1));
            lua_pop(script->L, 1);
            result = PXL8_ERROR_SCRIPT_ERROR;
        } else {
            script->last_error[0] = '\0';
        }
    }

    lua_pop(script->L, 1);

    return result;
}

static pxl8_result pxl8_script_eval_internal(pxl8_script* script, const char* code, bool repl_mode) {
    if (!script || !script->L || !code) return PXL8_ERROR_NULL_POINTER;

    char transformed_code[PXL8_MAX_REPL_COMMAND_SIZE];
    if (repl_mode) {
        pxl8_script_repl_promote_locals(code, transformed_code, sizeof(transformed_code));
        code = transformed_code;
    }

    lua_getglobal(script->L, "fennel");
    if (lua_isnil(script->L, -1)) {
        pxl8_script_set_error(script, "Fennel not loaded");
        lua_pop(script->L, 1);
        return PXL8_ERROR_SCRIPT_ERROR;
    }

    lua_getfield(script->L, -1, "eval");
    lua_pushstring(script->L, code);

    lua_newtable(script->L);
    lua_pushboolean(script->L, true);
    lua_setfield(script->L, -2, "useMetadata");

    if (repl_mode && script->repl_env_ref != LUA_NOREF) {
        lua_rawgeti(script->L, LUA_REGISTRYINDEX, script->repl_env_ref);
        lua_setfield(script->L, -2, "env");
        lua_pushboolean(script->L, false);
        lua_setfield(script->L, -2, "allowedGlobals");
    }

    if (lua_pcall(script->L, 2, 1, 0) != 0) {
        const char* error = lua_tostring(script->L, -1);
        if (error) {
            char cleaned_error[2048];
            size_t j = 0;
            for (size_t i = 0; error[i] && j < sizeof(cleaned_error) - 1; i++) {
                if (error[i] == '\t') {
                    cleaned_error[j++] = ' ';
                } else {
                    cleaned_error[j++] = error[i];
                }
            }
            cleaned_error[j] = '\0';
            pxl8_script_set_error(script, cleaned_error);
        } else {
            pxl8_script_set_error(script, "Unknown error");
        }
        lua_pop(script->L, 2);
        return PXL8_ERROR_SCRIPT_ERROR;
    }

    if (!lua_isnil(script->L, -1)) {
        lua_pushvalue(script->L, -1);
        lua_setglobal(script->L, "_");

        lua_getglobal(script->L, "tostring");
        lua_pushvalue(script->L, -2);
        if (lua_pcall(script->L, 1, 1, 0) == 0) {
            const char* result = lua_tostring(script->L, -1);
            if (result && strlen(result) > 0) {
                printf("=> %s\n", result);
                fflush(stdout);
            }
            lua_pop(script->L, 1);
        }
    }

    lua_pop(script->L, 2);
    script->last_error[0] = '\0';
    return PXL8_OK;
}

pxl8_result pxl8_script_eval(pxl8_script* script, const char* code) {
    return pxl8_script_eval_internal(script, code, false);
}

pxl8_result pxl8_script_eval_repl(pxl8_script* script, const char* code) {
    return pxl8_script_eval_internal(script, code, true);
}

pxl8_result pxl8_script_load_module(pxl8_script* script, const char* module_name) {
    if (!script || !script->L || !module_name) return PXL8_ERROR_NULL_POINTER;

    lua_getglobal(script->L, "require");
    lua_pushstring(script->L, module_name);
    if (lua_pcall(script->L, 1, 1, 0) != 0) {
        pxl8_script_set_error(script, lua_tostring(script->L, -1));
        lua_pop(script->L, 1);
        return PXL8_ERROR_SCRIPT_ERROR;
    }

    lua_setglobal(script->L, module_name);
    script->last_error[0] = '\0';
    return PXL8_OK;
}

pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name) {
    if (!script || !script->L || !name) return PXL8_ERROR_NULL_POINTER;

    lua_getglobal(script->L, name);
    if (!lua_isfunction(script->L, -1)) {
        lua_pop(script->L, 1);
        return PXL8_OK;
    }

    if (lua_pcall(script->L, 0, 0, 0) != 0) {
        pxl8_script_set_error(script, lua_tostring(script->L, -1));
        lua_pop(script->L, 1);
        return PXL8_ERROR_SCRIPT_ERROR;
    }

    script->last_error[0] = '\0';
    return PXL8_OK;
}

pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, f32 arg) {
    if (!script || !script->L || !name) return PXL8_ERROR_NULL_POINTER;

    lua_getglobal(script->L, name);
    if (!lua_isfunction(script->L, -1)) {
        lua_pop(script->L, 1);
        return PXL8_OK;
    }

    lua_pushnumber(script->L, arg);
    if (lua_pcall(script->L, 1, 0, 0) != 0) {
        pxl8_script_set_error(script, lua_tostring(script->L, -1));
        lua_pop(script->L, 1);
        return PXL8_ERROR_SCRIPT_ERROR;
    }

    script->last_error[0] = '\0';
    return PXL8_OK;
}

static bool pxl8_script_is_builtin_global(const char* name) {
    static const char* builtins[] = {
        "_G", "_VERSION", "arg", "assert", "bit", "collectgarbage",
        "coroutine", "debug", "dofile", "error", "fennel", "ffi",
        "gcinfo", "getfenv", "getmetatable", "init", "io", "ipairs",
        "jit", "load", "loadfile", "loadstring", "math", "module",
        "newproxy", "next", "os", "package", "pairs", "pcall",
        "print", "pxl8", "rawequal", "rawget", "rawset", "require",
        "select", "setfenv", "setmetatable", "string", "table",
        "tonumber", "tostring", "type", "unpack", "update", "frame",
        "xpcall", "pxl8_gfx", "pxl8_input", "pxl8_rng", "pxl8_sfx", "pxl8_sys",
        NULL
    };
    for (int i = 0; builtins[i]; i++) {
        if (strcmp(name, builtins[i]) == 0) return true;
    }
    return name[0] == '_' && name[1] == '_';
}

static void pxl8_script_cleanup(pxl8_script* script) {
    if (script->gfx) {
        pxl8_gfx_clear_textures(script->gfx);
    }

    if (script->mixer) {
        pxl8_sfx_mixer_clear(script->mixer);
    }
}

static void pxl8_script_save_upvalues(lua_State* L, const char* func_name, int saved_table) {
    lua_getglobal(L, func_name);
    if (!lua_isfunction(L, -1)) {
        lua_pop(L, 1);
        return;
    }

    int func_idx = lua_gettop(L);
    for (int i = 1; ; i++) {
        const char* name = lua_getupvalue(L, func_idx, i);
        if (!name) break;

        int vtype = lua_type(L, -1);
        if (name[0] != '(' &&
            vtype != LUA_TFUNCTION &&
            vtype != LUA_TUSERDATA &&
            vtype != LUA_TLIGHTUSERDATA &&
            vtype != LUA_TTHREAD &&
            vtype != LUA_TTABLE) {
            lua_pushstring(L, name);
            lua_pushvalue(L, -2);
            lua_settable(L, saved_table);
        }
        lua_pop(L, 1);
    }
    lua_pop(L, 1);
}

static void pxl8_script_restore_upvalues(lua_State* L, const char* func_name, int saved_table) {
    lua_getglobal(L, func_name);
    if (!lua_isfunction(L, -1)) {
        lua_pop(L, 1);
        return;
    }

    int func_idx = lua_gettop(L);
    for (int i = 1; ; i++) {
        const char* name = lua_getupvalue(L, func_idx, i);
        if (!name) break;
        lua_pop(L, 1);

        lua_getfield(L, saved_table, name);
        if (!lua_isnil(L, -1)) {
            lua_setupvalue(L, func_idx, i);
        } else {
            lua_pop(L, 1);
        }
    }
    lua_pop(L, 1);
}

static void pxl8_script_save_globals(pxl8_script* script) {
    lua_State* L = script->L;

    lua_newtable(L);
    int saved_table = lua_gettop(L);

    lua_pushvalue(L, LUA_GLOBALSINDEX);
    lua_pushnil(L);
    while (lua_next(L, -2) != 0) {
        if (lua_type(L, -2) == LUA_TSTRING) {
            const char* name = lua_tostring(L, -2);
            int vtype = lua_type(L, -1);

            if (!pxl8_script_is_builtin_global(name) &&
                vtype != LUA_TFUNCTION &&
                vtype != LUA_TUSERDATA &&
                vtype != LUA_TLIGHTUSERDATA &&
                vtype != LUA_TTHREAD &&
                vtype != 10) {
                lua_pushvalue(L, -2);
                lua_pushvalue(L, -2);
                lua_settable(L, saved_table);
            }
        }
        lua_pop(L, 1);
    }
    lua_pop(L, 1);

    pxl8_script_save_upvalues(L, "update", saved_table);
    pxl8_script_save_upvalues(L, "frame", saved_table);

    lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state");
    pxl8_debug("Hot reload state saved");
}

static void pxl8_script_restore_globals(pxl8_script* script) {
    lua_State* L = script->L;

    lua_getfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state");
    if (lua_isnil(L, -1)) {
        lua_pop(L, 1);
        return;
    }

    int saved_table = lua_gettop(L);

    lua_pushnil(L);
    while (lua_next(L, saved_table) != 0) {
        lua_pushvalue(L, -2);
        lua_pushvalue(L, -2);
        lua_settable(L, LUA_GLOBALSINDEX);
        lua_pop(L, 1);
    }

    pxl8_script_restore_upvalues(L, "update", saved_table);
    pxl8_script_restore_upvalues(L, "frame", saved_table);

    lua_pop(L, 1);

    lua_pushnil(L);
    lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state");
    pxl8_debug("Hot reload state restored");
}

#define SER_NIL    0
#define SER_BOOL   1
#define SER_NUMBER 2
#define SER_STRING 3
#define SER_TABLE  4

typedef struct {
    u8* data;
    u32 size;
    u32 capacity;
} ser_buffer;

static void ser_buffer_init(ser_buffer* buf) {
    buf->capacity = 1024;
    buf->data = malloc(buf->capacity);
    buf->size = 0;
}

static void ser_buffer_grow(ser_buffer* buf, u32 needed) {
    if (buf->size + needed > buf->capacity) {
        while (buf->size + needed > buf->capacity) {
            buf->capacity *= 2;
        }
        buf->data = realloc(buf->data, buf->capacity);
    }
}

static void ser_write_u8(ser_buffer* buf, u8 v) {
    ser_buffer_grow(buf, 1);
    buf->data[buf->size++] = v;
}

static void ser_write_u32(ser_buffer* buf, u32 v) {
    ser_buffer_grow(buf, 4);
    buf->data[buf->size++] = v & 0xFF;
    buf->data[buf->size++] = (v >> 8) & 0xFF;
    buf->data[buf->size++] = (v >> 16) & 0xFF;
    buf->data[buf->size++] = (v >> 24) & 0xFF;
}

static void ser_write_f64(ser_buffer* buf, f64 v) {
    ser_buffer_grow(buf, 8);
    memcpy(buf->data + buf->size, &v, 8);
    buf->size += 8;
}

static void ser_write_bytes(ser_buffer* buf, const void* data, u32 len) {
    ser_buffer_grow(buf, len);
    memcpy(buf->data + buf->size, data, len);
    buf->size += len;
}

static void ser_write_value(ser_buffer* buf, lua_State* L, int idx, int depth) {
    int t = lua_type(L, idx);
    switch (t) {
        case LUA_TNIL:
            ser_write_u8(buf, SER_NIL);
            break;
        case LUA_TBOOLEAN:
            ser_write_u8(buf, SER_BOOL);
            ser_write_u8(buf, lua_toboolean(L, idx) ? 1 : 0);
            break;
        case LUA_TNUMBER:
            ser_write_u8(buf, SER_NUMBER);
            ser_write_f64(buf, lua_tonumber(L, idx));
            break;
        case LUA_TSTRING: {
            size_t len;
            const char* str = lua_tolstring(L, idx, &len);
            ser_write_u8(buf, SER_STRING);
            ser_write_u32(buf, (u32)len);
            ser_write_bytes(buf, str, (u32)len);
            break;
        }
        case LUA_TTABLE: {
            if (depth > 16) { ser_write_u8(buf, SER_NIL); break; }
            ser_write_u8(buf, SER_TABLE);
            lua_pushvalue(L, idx);
            int tbl = lua_gettop(L);
            lua_pushnil(L);
            while (lua_next(L, tbl) != 0) {
                ser_write_value(buf, L, -2, depth + 1);
                ser_write_value(buf, L, -1, depth + 1);
                lua_pop(L, 1);
            }
            lua_pop(L, 1);
            ser_write_u8(buf, SER_NIL);
            break;
        }
        default:
            ser_write_u8(buf, SER_NIL);
            break;
    }
}

u32 pxl8_script_serialize_globals(pxl8_script* script, u8** out_data) {
    if (!script || !out_data) return 0;
    lua_State* L = script->L;

    ser_buffer buf;
    ser_buffer_init(&buf);

    lua_pushvalue(L, LUA_GLOBALSINDEX);
    lua_pushnil(L);
    while (lua_next(L, -2) != 0) {
        if (lua_type(L, -2) == LUA_TSTRING) {
            const char* name = lua_tostring(L, -2);
            int vtype = lua_type(L, -1);

            if (!pxl8_script_is_builtin_global(name) &&
                vtype != LUA_TFUNCTION &&
                vtype != LUA_TUSERDATA &&
                vtype != LUA_TLIGHTUSERDATA &&
                vtype != LUA_TTHREAD &&
                vtype != 10) {
                ser_write_value(&buf, L, -2, 0);
                ser_write_value(&buf, L, -1, 0);
            }
        }
        lua_pop(L, 1);
    }
    lua_pop(L, 1);

    ser_write_u8(&buf, SER_NIL);

    *out_data = buf.data;
    return buf.size;
}

typedef struct {
    const u8* data;
    u32 size;
    u32 pos;
} deser_buffer;

static u8 deser_read_u8(deser_buffer* buf) {
    if (buf->pos >= buf->size) return 0;
    return buf->data[buf->pos++];
}

static u32 deser_read_u32(deser_buffer* buf) {
    if (buf->pos + 4 > buf->size) return 0;
    u32 v = buf->data[buf->pos] | (buf->data[buf->pos+1] << 8) |
            (buf->data[buf->pos+2] << 16) | (buf->data[buf->pos+3] << 24);
    buf->pos += 4;
    return v;
}

static f64 deser_read_f64(deser_buffer* buf) {
    if (buf->pos + 8 > buf->size) return 0;
    f64 v;
    memcpy(&v, buf->data + buf->pos, 8);
    buf->pos += 8;
    return v;
}

static void deser_read_value(deser_buffer* buf, lua_State* L, int depth) {
    u8 type = deser_read_u8(buf);
    switch (type) {
        case SER_NIL:
            lua_pushnil(L);
            break;
        case SER_BOOL:
            lua_pushboolean(L, deser_read_u8(buf));
            break;
        case SER_NUMBER:
            lua_pushnumber(L, deser_read_f64(buf));
            break;
        case SER_STRING: {
            u32 len = deser_read_u32(buf);
            if (buf->pos + len <= buf->size) {
                lua_pushlstring(L, (const char*)(buf->data + buf->pos), len);
                buf->pos += len;
            } else {
                lua_pushnil(L);
            }
            break;
        }
        case SER_TABLE: {
            if (depth > 16) { lua_pushnil(L); break; }
            lua_newtable(L);
            while (buf->pos < buf->size) {
                u8 key_type = deser_read_u8(buf);
                if (key_type == SER_NIL) break;
                buf->pos--;
                deser_read_value(buf, L, depth + 1);
                deser_read_value(buf, L, depth + 1);
                lua_settable(L, -3);
            }
            break;
        }
        default:
            lua_pushnil(L);
            break;
    }
}

void pxl8_script_deserialize_globals(pxl8_script* script, const u8* data, u32 size) {
    if (!script || !data || size == 0) return;
    lua_State* L = script->L;

    deser_buffer buf = { .data = data, .size = size, .pos = 0 };

    while (buf.pos < buf.size) {
        u8 key_type = deser_read_u8(&buf);
        if (key_type == SER_NIL) break;
        buf.pos--;

        deser_read_value(&buf, L, 0);
        deser_read_value(&buf, L, 0);
        lua_settable(L, LUA_GLOBALSINDEX);
    }

    pxl8_debug("Deserialized globals from %u bytes", size);
}

void pxl8_script_free_serialized(u8* data) {
    free(data);
}

pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) {
    if (!script || !path) return PXL8_ERROR_NULL_POINTER;

    pxl8_strncpy(script->main_path, path, sizeof(script->main_path));

    script->watch_dir[0] = '.';
    script->watch_dir[1] = '\0';

    script->latest_mod_time = get_latest_script_mod_time(script->watch_dir);

    const char* ext = strrchr(path, '.');
    pxl8_result result = PXL8_ERROR_INVALID_FORMAT;

    if (ext && strcmp(ext, ".fnl") == 0) {
        result = pxl8_script_run_fennel_file(script, path);
    } else if (ext && strcmp(ext, ".lua") == 0) {
        result = pxl8_script_run_file(script, path);
    } else {
        pxl8_script_set_error(script, "Unknown script type (expected .fnl or .lua)");
        return PXL8_ERROR_INVALID_FORMAT;
    }

    if (result == PXL8_OK) {
        pxl8_info("Loaded script: %s", path);
    } else {
        pxl8_error("Failed to load script: %s", script->last_error);
    }

    return result;
}

static void pxl8_script_clear_cart_modules(pxl8_script* script) {
    lua_State* L = script->L;

    lua_getglobal(L, "package");
    lua_getfield(L, -1, "loaded");

    lua_pushnil(L);
    while (lua_next(L, -2) != 0) {
        lua_pop(L, 1);
        const char* name = lua_tostring(L, -1);
        if (name && strncmp(name, "pxl8", 4) != 0 &&
            strcmp(name, "fennel") != 0 &&
            strcmp(name, "ffi") != 0 &&
            strcmp(name, "bit") != 0 &&
            strcmp(name, "jit") != 0 &&
            strcmp(name, "string") != 0 &&
            strcmp(name, "table") != 0 &&
            strcmp(name, "math") != 0 &&
            strcmp(name, "io") != 0 &&
            strcmp(name, "os") != 0 &&
            strcmp(name, "debug") != 0 &&
            strcmp(name, "coroutine") != 0 &&
            strcmp(name, "package") != 0) {
            lua_pushvalue(L, -1);
            lua_pushnil(L);
            lua_settable(L, -4);
        }
    }

    lua_pop(L, 2);
}

bool pxl8_script_check_reload(pxl8_script* script) {
    if (!script || script->main_path[0] == '\0') {
        return false;
    }

    time_t current_mod_time = get_latest_script_mod_time(script->watch_dir);
    if (current_mod_time > script->latest_mod_time && current_mod_time != 0) {
        pxl8_info("Script files modified, reloading: %s", script->main_path);
        script->latest_mod_time = current_mod_time;

        pxl8_script_cleanup(script);
        pxl8_script_save_globals(script);
        pxl8_script_clear_cart_modules(script);

        const char* ext = strrchr(script->main_path, '.');
        bool reloaded = false;

        if (ext && strcmp(ext, ".fnl") == 0) {
            if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) {
                reloaded = true;
            }
        } else if (ext && strcmp(ext, ".lua") == 0) {
            if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) {
                reloaded = true;
            }
        }

        if (reloaded) {
            pxl8_script_restore_globals(script);
            pxl8_script_call_function(script, "init");
        }

        return reloaded;
    }

    return false;
}

static bool pxl8_script_is_incomplete_error(const char* error) {
    if (!error) return false;
    return strstr(error, "expected closing delimiter") != NULL ||
           strstr(error, "unexpected end of source") != NULL ||
           strstr(error, "expected whitespace before") != NULL ||
           strstr(error, "unexpected end") != NULL;
}

bool pxl8_script_is_incomplete_input(pxl8_script* script) {
    if (!script) return false;
    return pxl8_script_is_incomplete_error(script->last_error);
}

static pxl8_resolution parse_resolution(const char* str) {
    if (strcmp(str, "240x160") == 0) return PXL8_RESOLUTION_240x160;
    if (strcmp(str, "320x180") == 0) return PXL8_RESOLUTION_320x180;
    if (strcmp(str, "320x240") == 0) return PXL8_RESOLUTION_320x240;
    if (strcmp(str, "640x360") == 0) return PXL8_RESOLUTION_640x360;
    if (strcmp(str, "640x480") == 0) return PXL8_RESOLUTION_640x480;
    if (strcmp(str, "800x600") == 0) return PXL8_RESOLUTION_800x600;
    if (strcmp(str, "960x540") == 0) return PXL8_RESOLUTION_960x540;
    return PXL8_RESOLUTION_640x360;
}

static pxl8_pixel_mode parse_pixel_mode(const char* str) {
    if (strcmp(str, "indexed") == 0) return PXL8_PIXEL_INDEXED;
    if (strcmp(str, "hicolor") == 0) return PXL8_PIXEL_HICOLOR;
    return PXL8_PIXEL_INDEXED;
}

pxl8_result pxl8_script_load_cart_manifest(pxl8_script* script, pxl8_cart* cart) {
    if (!script || !script->L || !cart) return PXL8_ERROR_NULL_POINTER;

    if (!pxl8_cart_file_exists(cart, "cart.fnl")) {
        return PXL8_OK;
    }

    u8* data = NULL;
    u32 size = 0;
    if (pxl8_cart_read_file(cart, "cart.fnl", &data, &size) != PXL8_OK) {
        return PXL8_OK;
    }

    lua_getglobal(script->L, "fennel");
    if (lua_isnil(script->L, -1)) {
        lua_pop(script->L, 1);
        pxl8_cart_free_file(data);
        return PXL8_OK;
    }

    lua_getfield(script->L, -1, "eval");
    lua_pushlstring(script->L, (const char*)data, size);
    lua_createtable(script->L, 0, 1);
    lua_pushstring(script->L, "cart.fnl");
    lua_setfield(script->L, -2, "filename");

    pxl8_cart_free_file(data);

    if (lua_pcall(script->L, 2, 1, 0) != 0) {
        pxl8_warn("Failed to load cart.fnl: %s", lua_tostring(script->L, -1));
        lua_pop(script->L, 2);
        return PXL8_OK;
    }

    if (lua_istable(script->L, -1)) {
        lua_getfield(script->L, -1, "title");
        if (lua_isstring(script->L, -1)) {
            pxl8_cart_set_title(cart, lua_tostring(script->L, -1));
        }
        lua_pop(script->L, 1);

        lua_getfield(script->L, -1, "resolution");
        if (lua_isstring(script->L, -1)) {
            pxl8_cart_set_resolution(cart, parse_resolution(lua_tostring(script->L, -1)));
        }
        lua_pop(script->L, 1);

        lua_getfield(script->L, -1, "pixel-mode");
        if (lua_isstring(script->L, -1)) {
            pxl8_cart_set_pixel_mode(cart, parse_pixel_mode(lua_tostring(script->L, -1)));
        }
        lua_pop(script->L, 1);

        lua_getfield(script->L, -1, "window-size");
        if (lua_istable(script->L, -1)) {
            lua_rawgeti(script->L, -1, 1);
            lua_rawgeti(script->L, -2, 2);
            if (lua_isnumber(script->L, -2) && lua_isnumber(script->L, -1)) {
                pxl8_size size = {(i32)lua_tonumber(script->L, -2), (i32)lua_tonumber(script->L, -1)};
                pxl8_cart_set_window_size(cart, size);
            }
            lua_pop(script->L, 2);
        }
        lua_pop(script->L, 1);
    }

    lua_pop(script->L, 2);
    return PXL8_OK;
}
