#include "twn_game_api.h"

/* TODO: actually move it back it its own file, it doesn't give any compilation benefits */
#include "minilua.h"

#include "state.h"
#include "luabind.c"

#include <SDL2/SDL.h>

#define UDATA_NESTING_LIMIT 128


/* generated by bindgen.py */
void bindgen_load_twn(lua_State *L);
void bindgen_unload_twn(lua_State *L);
void bindgen_build_context(lua_State *L);
void bindgen_upload_context(lua_State *L);


/* require will go through physicsfs exclusively so that scripts can be in the data dir */
/* TODO: allow for bytecode files */
/* TODO: support .lua suffixes files? */
static int physfs_loader(lua_State *L) {
    const char *name = luaL_checkstring(L, 1);

    static const char *name_breaker = NULL;
    if (name_breaker && SDL_strcmp(name, name_breaker) == 0) {
        log_string(name_breaker, "Recursive load on itself from lua module");
        return 0;
    } name_breaker = name;

    /* replace dots with path slashes */
    char *path_copy = SDL_strdup(name);
    char *ch = NULL;
    while ((ch = SDL_strchr(path_copy, '.')))
        *ch = '/';

    char *final_path = NULL;
    SDL_asprintf(&final_path, "/scripts/%s.lua", path_copy);
    SDL_free(path_copy);

    if (SDL_strcmp(file_read(final_path, ":exists").data, "yes") == 0) {
        char *error_message = NULL;
        SDL_asprintf(&error_message, "could not find module %s in filesystem", name);
        lua_pushstring(L, error_message);
        SDL_free(error_message);

        SDL_free(final_path);
        return 1;
    }

    char *final_path_binary = NULL;
    SDL_asprintf(&final_path_binary, "%s%s", final_path, ":binary");

    String file = file_read(final_path, ":binary");
    SDL_free(final_path);

    /* TODO: use reader interface for streaming instead */
    int const result = luaL_loadbuffer(L, file.data, (size_t)file.length, name);
    if (result != LUA_OK)
        log_string(lua_tostring(L, -1), NULL);

    return result == LUA_OK;
}


/* WARN! experimental and will probably be removed */
/* it is an attempt to ease memory usage problems posed by using lua, partially successful */
static void *custom_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
    (void)ud;

    /* small allocations are placed in slots, as there's a big chance they will not need to be resized */
    static char slots[1024][128];
    static int16_t free_slots[1024] = { [0] = -1 };
    static size_t free_slot_count = 1024;

    if (free_slots[0] == -1)
        for (int i = 0; i < 1024; i++)
            free_slots[i] = (int16_t)i;

    if (nsize == 0) {
        if (ptr && (char *)ptr >= &slots[0][0] && (char *)ptr <= &slots[1024-1][128-1])
            free_slots[free_slot_count++] = (int16_t)(((uintptr_t)ptr - (uintptr_t)slots) / 128);
        else if (osize)
            SDL_free(ptr);
        return NULL;
    } else {
        if (!ptr && nsize <= 128 && free_slot_count > 0) {
            /* use a slot */
            return slots[free_slots[--free_slot_count]];
        }

        if ((char *)ptr >= &slots[0][0] && (char *)ptr <= &slots[1024-1][128-1]) {
            /* still fits */
            if (nsize <= 128)
                return ptr;

            /* move from slot to dynamic memory */
            void *mem = SDL_malloc(nsize);
            SDL_memcpy(mem, ptr, osize);
            free_slots[free_slot_count++] = (int16_t)(((uintptr_t)ptr - (uintptr_t)slots) / 128);
            return mem;
        }

        return SDL_realloc(ptr, nsize);
    }
}


static void exchange_lua_states(lua_State *from, lua_State *to, int level, int index) {
    if (level >= UDATA_NESTING_LIMIT) {
        log_string("ctx.udata nesting limit is reached", NULL);
        return;
    }

    /* TODO: use arrays for optimized paths */
    /* TODO: preallocate table records */
    switch (lua_type(from, index)) {
    case LUA_TTABLE:
        lua_newtable(to);
        lua_pushnil(from); /* first key */
        while (lua_next(from, index - 1) != 0) {
            /* 'key' at index -2 and 'value' at index -1 */
            exchange_lua_states(from, to, level + 1, -2);
            exchange_lua_states(from, to, level + 1, -1);
            lua_settable(to, index - 2);
            /* removes 'value'; keeps 'key' for next iteration */
            lua_pop(from, 1);
        }
        break;
    case LUA_TNUMBER:
        lua_pushnumber(to, lua_tonumber(from, index));
        break;
    case LUA_TBOOLEAN:
        lua_pushboolean(to, lua_toboolean(from, index));
        break;
    case LUA_TSTRING:
        lua_pushstring(to, lua_tostring(from, index));
        break;
    case LUA_TNIL:
        lua_pushnil(to);
        break;
    default:
        /* TODO: provide a path and type of it for better diagnostic */
        log_string("Unserializable udata found and is ignored", NULL);
        break;
    }
}


void game_tick(void) {
    if (ctx.initialization_needed) {
        if (!ctx.udata)
            ctx.udata = SDL_calloc(1, sizeof (State));

        State *state = ctx.udata;

        /* let's init lua */
        lua_State *new_state = lua_newstate(custom_alloc, NULL);
        lua_setallocf(new_state, custom_alloc, NULL);

        /* state existed already, copy its udata over */
        if (state->L != NULL) {
            lua_getglobal(state->L, "ctx");
            lua_getfield(state->L, -1, "udata");
            SDL_assert(!lua_isnoneornil(state->L, -1));
            SDL_assert(!lua_isnoneornil(state->L, -2));
            if (!lua_isnoneornil(state->L, -1)) {
                lua_newtable(new_state);
                exchange_lua_states(state->L, new_state, 0, -1);
                lua_setfield(new_state, -2, "udata");
                lua_setglobal(new_state, "ctx");
            }

            /* bye :) */
            lua_close(state->L);
        }

        state->L = new_state;

        /* fakey version of luaL_openlibs() that excludes file i/o and os stuff */
        {
            static const luaL_Reg loaded_libs[] = {
               { LUA_GNAME, luaopen_base },
               { LUA_LOADLIBNAME, luaopen_package },
               { LUA_COLIBNAME, luaopen_coroutine },
               { LUA_TABLIBNAME, luaopen_table },
               { LUA_STRLIBNAME, luaopen_string },
               { LUA_MATHLIBNAME, luaopen_math },
               { LUA_UTF8LIBNAME, luaopen_utf8 },
               { LUA_DBLIBNAME, luaopen_debug },
               { NULL, NULL },
            };

            for (const luaL_Reg *lib = loaded_libs; lib->func; ++lib) {
                luaL_requiref(state->L, lib->name, lib->func, true);
                lua_pop(state->L, 1); /* already stored, don't need the copy */
            }
        }

        /* package.searchers = { physfs_loader } */
        lua_getglobal(state->L, "package");
        lua_createtable(state->L, 0, 1);
        lua_setfield(state->L, -2, "searchers");

        lua_getfield(state->L, -1, "searchers");
        lua_pushcfunction(state->L, physfs_loader);
        lua_seti(state->L, -2, 1);

        /* pop package, package.searchers */
        lua_pop(state->L, 2);

        /* binding */
        bindgen_load_twn(state->L);

        /* now finally get to running the code */
        String file = file_read("/scripts/game.lua", ":binary");
        /* TODO: use reader interface for streaming instead */
        if (luaL_loadbuffer(state->L, file.data, (size_t)file.length, "game.lua") == LUA_OK) {
            if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
                log_string(luaL_tolstring(state->L, -1, NULL), "Error executing /scripts/game.lua entry");
                lua_pop(state->L, 1);
            } else
                state->loaded_successfully = true;
        } else {
            /* got some sort of error, it should be pushed on top of the stack */
            SDL_assert(lua_isstring(state->L, -1));
            log_string(luaL_tolstring(state->L, -1, NULL), "Error loading /scripts/game.lua entry");
            lua_pop(state->L, 1);
        }

        /* from this point we have access to everything defined in lua */
    }

    State *state = ctx.udata;

    if (state->loaded_successfully) {
        bindgen_build_context(state->L);
        lua_getglobal(state->L, "ctx");
        if (!lua_isnoneornil(state->L, -1)) {
            lua_getfield(state->L, -1, "udata");
            lua_setfield(state->L, -3, "udata");
        }
        lua_pop(state->L, 1);
        lua_setglobal(state->L, "ctx");

        lua_getglobal(state->L, "game_tick");
        if (lua_pcall(state->L, 0, 0, 0) != LUA_OK) {
            log_string(luaL_tolstring(state->L, -1, NULL), "Error executing game_tick()");
            lua_pop(state->L, 1);
        }

        lua_getglobal(state->L, "ctx");
        bindgen_upload_context(state->L);
    }
}


void game_end(void) {
    State *state = ctx.udata;
    bindgen_unload_twn(state->L);
    lua_close(state->L);
    SDL_free(state);
}