#include "luastack.hpp"
#include <iostream>
#include <cassert>
#include <cstdio>

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;
}

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;

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 *LuaStack::newstate (lua_Alloc allocf) {
    if (allocf == nullptr) allocf = l_alloc;
    lua_State *L = lua_newstate(allocf, NULL);
    if (L) lua_atpanic(L, &panicf);
    return L;
}

bool LuaStack::ckboolean(LuaSlot s) const {
    luaL_checktype(L_, s, LUA_TBOOLEAN);
    return lua_toboolean(L_, s) ? true:false;
}

lua_Integer LuaStack::ckinteger(LuaSlot s) const {
    luaL_checktype(L_, s, LUA_TNUMBER);
    return lua_tointeger(L_, s);
}

int LuaStack::ckint(LuaSlot s) const {
    luaL_checktype(L_, s, LUA_TNUMBER);
    return (int)lua_tointeger(L_, s);
}

lua_Number LuaStack::cknumber(LuaSlot s) const {
    luaL_checktype(L_, s, LUA_TNUMBER);
    return lua_tonumber(L_, s);
}

eng::string LuaStack::ckstring(LuaSlot s) const {
    luaL_checktype(L_, s, LUA_TSTRING);
    size_t len;
    const char *str = lua_tolstring(L_, s, &len);
    return eng::string(str, len);
}

lua_State *LuaStack::ckthread(LuaSlot s) const {
    luaL_checktype(L_, s, LUA_TTHREAD);
    return lua_tothread(L_, s);
}

void LuaStack::count_slots_finalize(int narg, int nvar, int nret) {
    narg_ = narg;
    nret_ = nret;
    nvar_ = nvar;
    ngap_ = nret - nvar - narg;
    if (ngap_ < 0) ngap_ = 0;

    int argtop = lua_gettop(L_);
    argpos_ = argtop + 1 - narg_;
    gappos_ = argpos_ + narg_;
    varpos_ = gappos_ + ngap_;
    retpos_ = varpos_ + nvar_;

    rettop_ = retpos_ + nret_ - 1;
    finaltop_ = argpos_ + nret_ - 1;
}

void LuaStack::clear_frame() {
    lua_settop(L_, varpos_ - 1);
    for (int i = 0; i < nvar_ + nret_; i++) {
        lua_pushnil(L_);
    }
}

int LuaStack::result() {
    lua_settop(L_, rettop_);
    int i = finaltop_;
    for (int j = 0; j < nret_; j++) {
        lua_replace(L_, i);
        i -= 1;
    }
    lua_settop(L_, finaltop_);
    return nret_;
}

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

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

bool LuaStack::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;
    }
}

int LuaStack::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;
}

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

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

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

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

bool LuaStack::validclassname(std::string_view cname) {
    if (cname.empty()) return false;
    if (cname == "_G") return false;
    return true;
}

bool LuaStack::validclassname(LuaSlot slot) const {
    if (!isstring(slot)) return false;
    return validclassname(ckstring(slot));
}

eng::string LuaStack::classname(LuaSlot tab) const {
    eng::string result;
    if (istable(tab)) {
        lua_pushstring(L_, "__class");
        lua_rawget(L_, tab);
        if (lua_type(L_, -1) == LUA_TSTRING) {
            lua_pushglobaltable(L_); // cname table
            lua_pushvalue(L_, -2); // cname table cname
            lua_rawget(L_, -2); // cname table ctab
            if (lua_rawequal(L_, -1, tab)) {
                size_t len;
                const char *s = lua_tolstring(L_, -3, &len);
                result = eng::string(s, len);
                if (!validclassname(result)) {
                    result = "";
                }
            }
            lua_pop(L_, 3);
        } else {
            lua_pop(L_, 1);
        }
    }
    return result;
}

eng::string LuaStack::getclass(LuaSlot classtab, LuaSlot classname) const {
    lua_checkstack(L_, 20);
    LuaVar globtab, cname;
    LuaStack LS(L_, globtab, cname);
    LS.getglobaltable(globtab);

    if (LS.isstring(classname)) {
        if (!validclassname(LS.ckstring(classname))) {
            eng::string err = "invalid class name: " + LS.ckstring(classname);
            LS.result();
            return err;
        }
        LS.rawget(classtab, globtab, classname);
        if (!LS.istable(classtab)) {
            eng::string err = "not a class: " + LS.ckstring(classname);
            LS.result();
            return err;
        }
        LS.rawget(cname, classtab, "__class");
        if (!LS.rawequal(cname, classname)) {
            eng::string err = "not a valid class: " + LS.ckstring(classname);
            LS.result();
            return err;
        }
        LS.result();
        return "";
    } else if (LS.istable(classname)) {
        LS.rawget(cname, classname, "__class");
        if (!LS.isstring(cname)) {
            eng::string err = "table is not a class.";
            LS.result();
            return err;
        }
        if (!validclassname(LS.ckstring(cname))) {
            eng::string err = "invalid class name: " + LS.ckstring(cname);
            LS.result();
            return err;
        }
        LS.rawget(classtab, globtab, cname);
        if (!LS.rawequal(classtab, classname)) {
            eng::string err = "not a valid class: " + LS.ckstring(cname);
            LS.result();
            return err;
        }
        LS.result();
        return "";
    } else {
        eng::string err = "getclass expects a string or a classtab";
        LS.result();
        return err;
    }
}

eng::string LuaStack::getclass(LuaSlot tab, std::string_view name) const {
    push_any_value(name);
    LuaSpecial classname(lua_gettop(L_));
    eng::string err = getclass(tab, classname);
    lua_pop(L_, 1);
    return err;
}

void LuaStack::makeclass(LuaSlot classtab, LuaSlot classname) const {
    lua_checkstack(L_, 20);
    LuaVar globtab, cname;
    LuaStack LS(L_, globtab, cname);

    // Validate the class name.
    assert(LS.validclassname(classname));
    
    // Fetch the global environment from the registry.
    LS.getglobaltable(globtab);
    
    // Get the classtab from the global environment.
    LS.rawget(classtab, globtab, classname);

    // Make a new table if necessary.
    if (!LS.istable(classtab)) {
        LS.newtable(classtab);
        LS.rawset(globtab, classname, classtab);
    }
    
    // Repair the special fields.
    LS.settabletype(classtab, LUA_TT_CLASS);
    LS.rawset(classtab, "__class", classname);
    LS.rawset(classtab, "__index", classtab);

    // Put the stack back.
    LS.result();
}

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


int64_t LuaStack::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 LuaStack::issortablekey(LuaSlot s) const {
    int type = lua_type(L_, s);
    return (type == LUA_TBOOLEAN) || (type == LUA_TNUMBER) || (type == LUA_TSTRING);
}

void LuaStack::movesortablekey(LuaSlot key, LuaStack &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;
    }
    default:
        assert(false && "movesortablekey: not a sortable key");
    }
}

void LuaStack::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 LuaStack::rawlen(LuaSlot obj) const {
    return lua_rawlen(L_, obj.index());
}

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

void LuaStack::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 LuaStack::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 LuaStack::getvisited(LuaSlot tab) const {
    uint16_t bits = lua_getflagbits(L_, tab.index());
    return (bits & 0x0010);
}

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