#include "luastack.hpp"
#include <cassert>
#include <cstdio>
#include <climits>
#include "wrap-string.hpp"
#include "wrap-set.hpp"
#include "wrap-sstream.hpp"
#include "util.hpp"

LuaSpecial LuaRegistry(LUA_REGISTRYINDEX);
LuaNilMarker LuaNil;
LuaNewTableMarker LuaNewTable;

LuaFunctionReg::LuaFunctionReg(const char *n, const char *a, const char *d, bool s, lua_CFunction f) {
    name_ = n;
    args_ = a;
    docs_ = d;
    func_ = f;
    sandbox_ = s;
    next_ = All;
    All = this;
}

LuaConstantReg::LuaConstantReg(const char *n, const char *d, LuaToken tokenvalue, lua_Number numbervalue) {
    name_ = n;
    docs_ = d;
    tokenvalue_ = tokenvalue;
    numbervalue_ = numbervalue;
    next_ = All;
    All = this;
}

const LuaFunctionReg *LuaFunctionReg::lookup(lua_CFunction fn) {
    for (const LuaFunctionReg *r = All; r != 0; r = r->next_) {
        if (r->func_ == fn) {
            return r;
        }
    }
    return nullptr;
}

LuaFunctionReg *LuaFunctionReg::All;
LuaConstantReg *LuaConstantReg::All;


static int panicf(lua_State *L) {
    const char *p = lua_tostring(L, -1);
    fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n", p);
    fflush(stderr);
    exit(1);
}

// An allocator for lua states that uses system malloc and system free.
static void *l_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
    if (nsize == 0) {
        free(ptr);
        return NULL;
    } else {
        return realloc(ptr, nsize);
    }
}

lua_State *LuaCoreStack::newstate (lua_Alloc allocf) {
    if (allocf == nullptr) allocf = l_alloc;
    lua_State *L = lua_newstate(allocf, NULL);
    assert(L != nullptr);
    lua_atpanic(L, &panicf);

    // We want all states to have a classes table, a tangibles table,
    // and persist/unpersist tables so that LS.makeclass, LS.maketan,
    // and eris work out-of-the-box.
    LuaVar globtab;
    LuaExtStack LS(L, globtab);
    LS.rawset(LuaRegistry, "classes", LuaNewTable);
    LS.rawset(LuaRegistry, "classnames", LuaNewTable);
    LS.rawset(LuaRegistry, "tangibles", LuaNewTable);
    LS.rawset(LuaRegistry, "persist", LuaNewTable);
    LS.rawset(LuaRegistry, "unpersist", LuaNewTable);
    LS.rawset(LuaRegistry, "funcnames", LuaNewTable);
    LS.rawset(LuaRegistry, "sourcedb", LuaNewTable);

    // Tag the registry and global environment with their tabletypes.
    LS.settabletype(LuaRegistry, LUA_TT_REGISTRY);
    LS.getglobaltable(globtab);
    LS.settabletype(globtab, LUA_TT_GLOBALENV);

    return L;
}

void LuaCoreStack::argerr(const char *nm, const char *tp) const {
    luaL_error(L_, "'%s' should be %s", nm, tp);
}

std::optional<bool> LuaCoreStack::tryboolean(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TBOOLEAN) {
        return lua_toboolean(L_, s);
    }
    return std::nullopt;
}

std::optional<lua_Integer> LuaCoreStack::tryinteger(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TNUMBER) {
        lua_Number result = lua_tonumber(L_, s);
        if (lua_Integer(result) == result) {
            return lua_Integer(result);
        }
    }
    return std::nullopt;
}

std::optional<int> LuaCoreStack::tryint(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TNUMBER) {
        lua_Number result = lua_tonumber(L_, s);
        if (int(result) == result) {
            return int(result);
        }
    }
    return std::nullopt;
}

std::optional<lua_Number> LuaCoreStack::trynumber(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TNUMBER) {
        return lua_tonumber(L_, s);
    }
    return std::nullopt;
}

std::optional<eng::string> LuaCoreStack::trystring(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TSTRING) {
        size_t len;
        const char *str = lua_tolstring(L_, s, &len);
        return eng::string(str, len);
    }
    return std::nullopt;
}

std::optional<std::string_view> LuaCoreStack::trystringview(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TSTRING) {
        size_t len;
        const char *str = lua_tolstring(L_, s, &len);
        return std::string_view(str, len);
    }
    return std::nullopt;
}

std::optional<lua_State*> LuaCoreStack::trythread(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TTHREAD) {
        return lua_tothread(L_, s);
    }
    return std::nullopt;
}

std::optional<LuaToken> LuaCoreStack::trytoken(LuaSlot s) const {
    if (lua_type(L_, s) == LUA_TLIGHTUSERDATA) {
        return LuaToken(lua_touserdata(L_, s));
    }
    return std::nullopt;
}

std::optional<util::DXYZ> LuaCoreStack::tryxyz(LuaSlot s) const {
    if (lua_istable(L_, s) && (lua_nkeys(L_, s) == 3)) {
        int top = lua_gettop(L_);
        lua_rawgeti(L_, s, 3);
        lua_rawgeti(L_, s, 2);
        lua_rawgeti(L_, s, 1);
        if ((lua_type(L_, -1)==LUA_TNUMBER) && (lua_type(L_, -2)==LUA_TNUMBER) && (lua_type(L_, -3)==LUA_TNUMBER)) {
            util::DXYZ result;
            result.x = lua_tonumber(L_, -1);
            result.y = lua_tonumber(L_, -2);
            result.z = lua_tonumber(L_, -3);
            lua_settop(L_, top);
            return result;
        }
        lua_settop(L_, top);
    }
    return std::nullopt;
}

bool LuaCoreStack::trytable(LuaSlot s) const {
    return lua_istable(L_, s);
}

bool LuaCoreStack::trynil(LuaSlot s) const {
    return lua_isnil(L_, s);
}

bool LuaCoreStack::tryfunction(LuaSlot s) const {
    return lua_isfunction(L_, s);
}

bool LuaCoreStack::trycfunction(LuaSlot s) const {
    return lua_iscfunction(L_, s);
}

bool LuaCoreStack::trytangible(LuaSlot s) const {
    return (lua_istable(L_, s) && gettabletype(s) == LUA_TT_TANGIBLE);
}



bool LuaCoreStack::ckboolean(LuaSlot s, const char *argname) const {
    auto result = tryboolean(s);
    if (!result) argerr(argname, "boolean");
    return *result;
}

lua_Integer LuaCoreStack::ckinteger(LuaSlot s, const char *argname) const {
    auto result = tryinteger(s);
    if (!result) argerr(argname, "integer");
    return *result;
}

int LuaCoreStack::ckint(LuaSlot s, const char *argname) const {
    auto result = tryint(s);
    if (!result) argerr(argname, "int");
    return *result;
}

lua_Number LuaCoreStack::cknumber(LuaSlot s, const char *argname) const {
    auto result = trynumber(s);
    if (!result) argerr(argname, "number");
    return *result;
}

eng::string LuaCoreStack::ckstring(LuaSlot s, const char *argname) const {
    auto result = trystring(s);
    if (!result) argerr(argname, "string");
    return *result;
}

std::string_view LuaCoreStack::ckstringview(LuaSlot s, const char *argname) const {
    auto result = trystringview(s);
    if (!result) argerr(argname, "string");
    return *result;
}

lua_State * LuaCoreStack::ckthread(LuaSlot s, const char *argname) const {
    auto result = trythread(s);
    if (!result) argerr(argname, "thread");
    return *result;
}

LuaToken LuaCoreStack::cktoken(LuaSlot s, const char *argname) const {
    auto result = trytoken(s);
    if (!result) argerr(argname, "token");
    return *result;
}

util::DXYZ LuaCoreStack::ckxyz(LuaSlot s, const char *argname) const {
    auto result = tryxyz(s);
    if (!result) argerr(argname, "xyz");
    return *result;
}

void LuaCoreStack::cktable(LuaSlot s, const char *argname) const {
    if (!trytable(s)) argerr(argname, "table");
}

void LuaCoreStack::cknil(LuaSlot s, const char *argname) const {
    if (!trynil(s)) argerr(argname, "nil");
}

void LuaCoreStack::ckfunction(LuaSlot s, const char *argname) const {
    if (!tryfunction(s)) argerr(argname, "function");
}

void LuaCoreStack::ckcfunction(LuaSlot s, const char *argname) const {
    if (!trycfunction(s)) argerr(argname, "c-function");
}

void LuaCoreStack::cktangible(LuaSlot s, const char *argname) const {
    if (!trytangible(s)) argerr(argname, "tangible");
}


void LuaCoreStack::clearmetatable(LuaSlot tab) const {
    lua_pushnil(L_);
    lua_setmetatable(L_, tab);
}

void LuaCoreStack::setmetatable(LuaSlot tab, LuaSlot mt) const {
    lua_pushvalue(L_, mt);
    lua_setmetatable(L_, tab);
}

bool LuaCoreStack::getmetatable(LuaSlot mt, LuaSlot tab) const {
    if (lua_getmetatable(L_, tab)) {
        lua_replace(L_, mt);
        return true;
    } else {
        lua_pushnil(L_);
        lua_replace(L_, mt);
        return false;
    }
}

bool LuaCoreStack::next(LuaSlot tab, LuaSlot key, LuaSlot value) const {
    lua_pushvalue(L_, key);
    int ret = lua_next(L_, tab);
    if (ret != 0) {
        lua_replace(L_, value);
        lua_replace(L_, key);
    }
    return (ret != 0);
}

eng::string LuaCoreStack::load(LuaSlot result, std::string_view code, std::string_view context)
{
    // We interpret "=x" as syntactic sugar for "return x"
    eng::string expanded;
    if (sv::has_prefix(code, "="))
    {
        expanded = eng::string("return ") + eng::string(code.substr(1));
        code = expanded;
    }

    if (sv::has_prefix(code, "/"))
    {
        set(result, "slash command");
        return "slash command";
    }
    if (sv::is_whitespace(code))
    {
        set(result, "white space");
        return "white space";
    }

    eng::string fullcontext = eng::string("=") + eng::string(context);
    luaL_loadbuffer(L_, code.data(), code.size(), fullcontext.c_str());
    int type = lua_type(L_, -1);
    if (type == LUA_TFUNCTION) {
        // compiler returned a closure.
        lua_replace(L_, result.index());
        return "";
    } else if (type == LUA_TSTRING) {
        // compiler returned an error message.
        size_t len;
        const char *str = lua_tolstring(L_, -1, &len);
        eng::string message(str, len);
        lua_pop(L_, 1);
        if (sv::has_suffix(message, "near <eof>") && sv::is_possible_long_lua_expression(code))
        {
            message = "truncated lua";
        }
        if (message.empty()) message = "unknown compiler error";
        set(result, message);
        return message;
    } else {
        assert(false && "lua compiler didn't return a closure, but didn't return an error message either");
    }
}

void LuaCoreStack::getglobaltable(LuaSlot target) const {
    lua_pushglobaltable(L_);
    lua_replace(L_, target);
}

void LuaCoreStack::newtable(LuaSlot target) const {
    lua_newtable(L_);
    lua_replace(L_, target);
}

void LuaCoreStack::createtable(LuaSlot target, int narr, int nrec) const {
    lua_createtable(L_, narr, nrec);
    lua_replace(L_, target);
}

lua_State *LuaCoreStack::newthread(LuaSlot target) const {
    lua_State *result = lua_newthread(L_);
    lua_replace(L_, target);
    return result;
}

void LuaCoreStack::getclassinfo(LuaSlot classtab,
    eng::string &classname, eng::string &error, LuaSlot input) const {
    LuaVar lookup, cname;
    LuaExtStack LS(L_, lookup, cname);

    // Step 1: Resolve input to a class table.
    int xt = xtype(input);
    if (xt == LUA_TSTRING) {
        LS.rawget(lookup, LuaRegistry, "classes");
        LS.rawget(classtab, lookup, input);
    } else if (xt == LUA_TT_TANGIBLE) {
        if (LS.getmetatable(lookup, input)) {
            LS.rawget(classtab, lookup, "__index");
        } else {
            LS.set(classtab, LuaNil);
        }
    } else if (xt == LUA_TT_CLASS) {
        LS.set(classtab, input);
    } else if (xt == LUA_TT_GENERAL) {
        LS.getmetatable(classtab, input);
    } else {
        LS.set(classtab, LuaNil);
    }

    // Step 2: Validate classtab and get classname, or generate error.
    // A class table is only valid if it's registered in classnames.
    if (xtype(classtab) == LUA_TT_CLASS) {
        LS.rawget(lookup, LuaRegistry, "classnames");
        LS.rawget(cname, lookup, classtab);
        auto name = LS.trystring(cname);
        if (name) {
            classname = *name;
            error = "";
            return;
        }
    }

    // Step 3: We didn't find a valid classtab and classname.
    // Clear the classtab and classname return-values, then
    // figure out what went wrong and generate an error message.
    LS.set(classtab, LuaNil);
    classname = "";
    if (xt == LUA_TSTRING) {
        eng::string s = LS.ckstring(input);
        if (!sv::is_lua_classname(s)) {
            error = "invalid class name: " + s;
        } else {
            error = "class does not exist: " + s;
        }
    } else if (xt == LUA_TT_TANGIBLE) {
        error = "tangible has no valid class";
    } else if (xt == LUA_TT_GENERAL) {
        error = "table has no valid class";
    } else if (xt == LUA_TT_CLASS) {
        error = "class is no longer valid";
    } else {
        error = "expected a string, class, tangible, or table";
    }
}

eng::string LuaCoreStack::classname(LuaSlot input) const {
    LuaVar tab;
    LuaExtStack LS(L_, tab);
    eng::string name, error;
    getclassinfo(tab, name, error, input);
    return name;
}

eng::string LuaCoreStack::getclass(LuaSlot classtab, LuaSlot input) const {
    eng::string name, error;
    getclassinfo(classtab, name, error, input);
    return error;
}

eng::string LuaCoreStack::getclass(LuaSlot classtab, std::string_view classname) const {
    LuaVar input;
    LuaExtStack LS(L_, input);
    LS.set(input, classname);
    eng::string name, error;
    getclassinfo(classtab, name, error, input);
    return error;
}

void LuaCoreStack::makeclass(LuaSlot classtab, LuaSlot classname) const {
    LuaVar classes, classnames, globtab, cname;
    LuaExtStack LS(L_, classes, classnames, globtab, cname);

    // Validate the class name.
    assert(LS.isstring(classname));
    assert(sv::is_lua_classname(LS.ckstring(classname)));
    
    // Fetch the classes table from the registry.
    LS.rawget(classes, LuaRegistry, "classes");
    assert(LS.istable(classes));

    // Fetch the classnames table from the registry.
    LS.rawget(classnames, LuaRegistry, "classnames");
    assert(LS.istable(classnames));

    // Fetch the global environment from the registry.
    LS.getglobaltable(globtab);
    
    // Get the classtab from the classes table.
    LS.rawget(classtab, classes, classname);

    // Make a new table if necessary.
    if (!LS.istable(classtab)) {
        LS.newtable(classtab);
        LS.rawset(classes, classname, classtab);
        LS.rawset(classnames, classtab, classname);
    }
    
    // Put the table into the global environment.
    LS.rawset(globtab, classname, classtab);

    // Repair the special fields.
    LS.settabletype(classtab, LUA_TT_CLASS);
    LS.rawset(classtab, "__index", classtab);
}

void LuaCoreStack::makeclass(LuaSlot tab, std::string_view name) const {
    push_any_value(name);
    LuaSpecial classname(lua_gettop(L_));
    makeclass(tab, classname);
    lua_pop(L_, 1);
}

void LuaCoreStack::maketan(LuaSlot tab, int64_t id) const {
    assert(validpositiveinteger(id));

    LuaVar tangibles, metatab;
    LuaExtStack LS(L_, tangibles, metatab);
    
    // Try to get the existing tangible.
    LS.rawget(tangibles, LuaRegistry, "tangibles");
    LS.rawget(tab, tangibles, id);

    // If we succeeded, return it.
    if (LS.istable(tab)) {
        return;
    }

    // Create the tangible's database and metatable.
    LS.set(tab, LuaNewTable);
    LS.set(metatab, LuaNewTable);
    LS.setmetatable(tab, metatab);

    // Mark the tangible using the tabletype field.
    LS.settabletype(tab, LUA_TT_TANGIBLE);
    LS.settabletype(metatab, LUA_TT_TANGIBLEMETA);

    // Store the tangible ID and lock the metatable.
    LS.rawset(metatab, "id", id);
    LS.rawset(metatab, "__metatable", false);

    // Store the database into the tangibles table.
    LS.rawset(tangibles, id, tab);
}


bool LuaCoreStack::tanblank(LuaSlot tab) const {
    bool result = true;
    if (istable(tab) && gettabletype(tab) == LUA_TT_TANGIBLE) {
        if (lua_getmetatable(L_, tab.index())) {
            lua_pushstring(L_, "threads");
            lua_rawget(L_, -2);
            if (lua_type(L_, -1) == LUA_TTABLE) {
                result = false;
            }
            lua_pop(L_, 2);
        }
    }
    return result;
}

int64_t LuaCoreStack::tanid(LuaSlot tab) const {
    int64_t result = 0;
    if (istable(tab) && gettabletype(tab) == LUA_TT_TANGIBLE) {
        if (lua_getmetatable(L_, tab.index())) {
            lua_pushstring(L_, "id");
            lua_rawget(L_, -2);
            if (lua_type(L_, -1) == LUA_TNUMBER) {
                result = int64_t(lua_tonumber(L_, -1));
            }
            lua_pop(L_, 2);
        }
    }
    return result;
}

bool LuaCoreStack::tangetclass(LuaSlot classobj, LuaSlot tab) {
    if (istable(tab) && (gettabletype(tab) == LUA_TT_TANGIBLE) && lua_getmetatable(L_, tab.index())) {
        lua_pushstring(L_, "__index");
        lua_rawget(L_, -2);
        lua_replace(L_, classobj);
        lua_pop(L_, 1);
        if (istable(classobj) && (gettabletype(classobj) == LUA_TT_CLASS)) {
            return true;
        }
    }
    set(classobj, LuaNil);
    return false;
}

bool LuaCoreStack::issortablekey(LuaSlot s) const {
    int type = lua_type(L_, s);
    return (type == LUA_TBOOLEAN) || (type == LUA_TNUMBER) || (type == LUA_TSTRING) || (type == LUA_TLIGHTUSERDATA);
}

void LuaCoreStack::movesortablekey(LuaSlot key, LuaCoreStack &otherstack, LuaSlot otherslot) {
    int type = lua_type(L_, key);
    switch (type) {
    case LUA_TBOOLEAN:
        lua_pushboolean(otherstack.L_, lua_toboolean(L_, key));
        lua_replace(otherstack.L_, otherslot);
        break;
    case LUA_TNUMBER:
        lua_pushnumber(otherstack.L_, lua_tonumber(L_, key));
        lua_replace(otherstack.L_, otherslot);
        break;
    case LUA_TSTRING: {
        size_t len;
        const char *str = lua_tolstring(L_, key, &len);
        lua_pushlstring(otherstack.L_, str, len);
        lua_replace(otherstack.L_, otherslot);
        break;
    }
    case LUA_TLIGHTUSERDATA:
        lua_pushlightuserdata(otherstack.L_, lua_touserdata(L_, key));
        lua_replace(otherstack.L_, otherslot);
        break;
    default:
        assert(false && "movesortablekey: not a sortable key");
    }
}

void LuaCoreStack::cleartable(LuaSlot tab, bool clearmeta) const {
    assert(istable(tab));
    lua_pushnil(L_);
    while (lua_next(L_, tab.index()) != 0) {
        lua_pop(L_, 1); // Pop the old value.
        lua_pushvalue(L_, -1); // Clone the key
        lua_pushnil(L_); // Push the new value.
        lua_rawset(L_, tab.index());
    }
    if (clearmeta) {
        lua_pushnil(L_);
        lua_setmetatable(L_, tab.index());
    }
}

int LuaCoreStack::rawlen(LuaSlot obj) const {
    return lua_rawlen(L_, obj.index());
}

int LuaCoreStack::nkeys(LuaSlot obj) const {
    return lua_nkeys(L_, obj.index());
}

int LuaCoreStack::gettabletype(LuaSlot tab) const {
    uint16_t bits = lua_getflagbits(L_, tab.index());
    return LUA_TT_GENERAL + (bits & 0x000F);
}

void LuaCoreStack::settabletype(LuaSlot tab, int t) const {
    assert((t >= LUA_TT_GENERAL) && (t <= LUA_TT_CLASS));
    int offset = (t - LUA_TT_GENERAL);
    lua_modflagbits(L_, tab.index(), 0x000F, offset);
}

int LuaCoreStack::xtype(LuaSlot slot) const {
    int t = lua_type(L_, slot);
    if (t != LUA_TTABLE) return t;
    uint16_t bits = lua_getflagbits(L_, slot);
    return LUA_TT_GENERAL + (bits & 0x000F);
}

bool LuaCoreStack::getvisited(LuaSlot tab) const {
    uint16_t bits = lua_getflagbits(L_, tab.index());
    return (bits & 0x0010);
}

void LuaCoreStack::setvisited(LuaSlot tab, bool visited) const {
    lua_modflagbits(L_, tab.index(), 0x0010, visited ? 0x0010 : 0);
}


static int tailcall_continuation(lua_State *L)
{
    int base;
    lua_getctx(L, &base);
    return lua_gettop(L) - base;
}

int LuaDefStack::tailcall_internal(bool passup, int base, int nargs) {
    lua_callk(L_, nargs, passup ? LUA_MULTRET : 0, base, tailcall_continuation);
    return lua_gettop(L_) - base;
}

LuaDefine(unittests_token, "", "Unit tests for LuaToken encoding") {
    // Test round-trip encoding for various strings.
    LuaAssertStrEq(L, LuaToken("a").str(), "a");
    LuaAssertStrEq(L, LuaToken("z").str(), "z");
    LuaAssertStrEq(L, LuaToken("0").str(), "0");
    LuaAssertStrEq(L, LuaToken("9").str(), "9");
    LuaAssertStrEq(L, LuaToken("null").str(), "null");
    LuaAssertStrEq(L, LuaToken("hello").str(), "hello");
    LuaAssertStrEq(L, LuaToken("zzzzzzzzzzzz").str(), "zzzzzzzzzzzz");
    LuaAssertStrEq(L, LuaToken("a0").str(), "a0");
    LuaAssertStrEq(L, LuaToken("0a").str(), "0a");
    LuaAssertStrEq(L, LuaToken("000000000000").str(), "000000000000");
    LuaAssertStrEq(L, LuaToken("foo_bar").str(), "foo_bar");
    LuaAssertStrEq(L, LuaToken("_").str(), "_");
    LuaAssertStrEq(L, LuaToken("a_b").str(), "a_b");

    // Test that empty/invalid strings produce the empty token.
    LuaAssert(L, LuaToken(std::string_view("")).empty());
    LuaAssert(L, LuaToken(std::string_view("hello world")).empty());
    LuaAssert(L, LuaToken(std::string_view("aaaaaaaaaaaaa")).empty()); // 13 chars

    // Test that numeric ordering matches lexicographic ordering.
    LuaAssert(L, LuaToken("a").value > LuaToken("0").value);
    LuaAssert(L, LuaToken("b").value > LuaToken("a").value);
    LuaAssert(L, LuaToken("aa").value > LuaToken("a").value);
    LuaAssert(L, LuaToken("ab").value > LuaToken("aa").value);
    LuaAssert(L, LuaToken("b").value > LuaToken("az").value);
    LuaAssert(L, LuaToken("ba").value > LuaToken("az").value);
    LuaAssert(L, LuaToken("hello").value > LuaToken("hell").value);
    LuaAssert(L, LuaToken("a0").value > LuaToken("a").value);
    LuaAssert(L, LuaToken("a").value != LuaToken("a0").value);
    LuaAssert(L, LuaToken("0").value > LuaToken("_").value);
    LuaAssert(L, LuaToken("a").value > LuaToken("9").value);
    LuaAssert(L, LuaToken("a_b").value > LuaToken("a_a").value);
    LuaAssert(L, LuaToken("foo_bar").value != LuaToken("foobar").value);

    return 0;
}

