/////////////////////////////////////////////////////////
//
//
// LUASTACK
//
// The standard lua C API asks you to work with a stack machine. You're supposed
// to manually push and pop values on the lua stack. I find this difficult, I
// find it hard to remember what stack position contains what value.
//
// To make it easier, I've created this module, "LuaStack."  This module
// creates the illusion that you're working with local variables that contain
// lua values.
//
// Of course, this is all using the lua stack under the covers.  Lua
// local variables are actually just lua stack addresses.  But that's
// all kept fairly well hidden.  When you use Lua local variables, and
// the accessors inside class LuaStack, it appears that you're
// manipulating data using local variables instead of using a stack.
// For people like me, that's easier to think about.
//
// Here's an example.
//
// let's say you have a function that takes two arguments
// ARG1 and ARG2, has a single return value RET1, and needs two local
// variables LOC1 and LOC2.  We would declare it like this:
//
// int myfunc(lua_State *L) {
//
//     LuaArg arg1, arg2;         // Declare local variables to hold the arguments.
//     LuaRet ret1;               // Declare local variables to hold the return values.
//     LuaVar loc1, loc2, loc3;   // Declare local variables for other purposes.
//
//     // Assign every local var a stack index.
//     LuaStack LS(L, arg1, arg2, ret1, loc1, loc2, loc3);
//
//     // manipulate the data in the lua local variables...
//     LS.rawget(loc1, arg1, arg2);
//     ... etc ...
// }
//
// Class LuaArg, LuaRet, and LuaVar are all lua local variables.
// The luastack constructor assigns each one of them a position on
// the lua stack.  It also makes sure that the arguments are in
// the LuaArg variables, and it makes sure that the LuaRet values
// are the only thing left on the stack at return time.
//
// Class LuaStack provides a complete catalog of accessors
// like 'rawget' - roughly speaking, it provides equivalents to
// every major accessor in the lua API.  However, the accessors
// provided by LuaStack take input and output from lua locals, not
// from the stack.  For example, consider this:
//
//    LS.rawget(value, tab, key);
//
// In the above, value, tab, and key should be lua local variables.
// This does a rawget on 'table', with the specified 'key', and
// stores the result in 'value'.  Nothing is added to or removed
// from the lua stack.  In general, none of the accessors in class
// LuaStack add anything to the stack, or pop anything from the
// stack.
//
// Class LuaStack can also do automatic type conversions.  For
// example, suppose you do this:
//
//    LS.rawget(value, tab, key);
//
// Nominally, you would expect value, tab, and key to be lua local
// variables. But if you pass a eng::string for key, then LuaStack will
// automatically convert it.  In general, class LuaStack can
// convert lua_Integer, lua_Number, eng::string, bool, and LuaNil.
//
// On output, LuaStack can convert lua_Integers, lua_Numbers, and
// eng::strings.  In this case, strict type checking is done.  If
// there is a type mismatch, a lua error is thrown.
//
// You can use the operator 'set' to assign a value to a lua local
// variable:
//
//   LS.set(val1, val2);
//
// This is actually a copy operation that copies from one lua local
// variable to another.  But using type conversions, it can also be
// used to assign arbitrary values to lua local variables, or to
// get values from lua local variables.
// 
// Passing LuaNewTable as an input will cause a new table to be
// created before calling the specified operation.
//
//
/////////////////////////////////////////////////////////
//
//
// LuaStack type checking
//
// LuaStack contains accessors for type checking.  These include:
//
//     bool LuaStack::isnumber(LuaSlot s)
//     bool LuaStack::isinteger(LuaSlot s)
//     bool LuaStack::isstring(LuaSlot s)
//     etc...
//
// And it also contains operations that throw errors:
//
//     void LuaStack::checknumber(LuaSlot s)
//     void LuaStack::checkinteger(LuaSlot s)
//     void LuaStack::checkstring(LuaSlot s)
//     etc...
//
// These are different from the lua builtins in that they are strict.
// For example, 'isnumber' only returns true if the value in the
// lua local variable is already a number.  No conversions are done.
//
// These functions do checking and also conversion at the same time:
//
//     lua_Integer  LuaStack::ckinteger(LuaSlot s)
//     lua_Number   LuaStack::cknumber(LuaSlot s)
//     eng::string  LuaStack::ckstring(LuaSlot s)
//     lua_State   *LuaStack::ckthread(LuaSlot s)
//
// Like the other operations, they are strict.
//
//
// LUADEFINE
//
// LuaDefine is a macro that defines a C function which is
// exposed to lua.  It creates a global registry of functions
// created with LuaDefine. You use it like so:
//
//    LuaDefine(function_name, "arguments", "documentation") {
//        ...
//    }
//
// This macroexpands into a function definition and a function
// registration.  The function definition looks like this:
//
//    int function_name(lua_State *L) {
//        ...
//    }
//
// The macro expansion generates this function definition, but it
// also generates a "registration object" whose constructor puts
// this function into a global registry of lua-callable C functions.
// This global registry is later used to inject these C functions
// into the lua intepreter.  The mode is a string that contain
// the following characters:
//
//    c - create a class, and put a function into it.
//    f - create a global function not inside a class.
//
//
/////////////////////////////////////////////////////////


#ifndef LUASTACK_HPP
#define LUASTACK_HPP

#include "wrap-string.hpp"

extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include "eris.h"
}

class LuaSlot : public eng::nevernew {
protected:
    int index_;
private:
    inline operator int() const {
        return index_;
    }
public:
    LuaSlot() {
        index_ = 0;
    }

    int index() const {
        return index_;
    }
    
    friend class LuaStack;
};

class LuaArg : public LuaSlot {};
class LuaRet : public LuaSlot {};
class LuaVar : public LuaSlot {};

class LuaSpecial : public LuaSlot {
public:
    LuaSpecial(int n) {
        index_ = n;
    }
};

extern LuaSpecial LuaRegistry;

class LuaUpvalue : public LuaSlot {
    public:
        LuaUpvalue(int n) {
            index_ = lua_upvalueindex(n);
        }
};

class LuaNilMarker {};
extern LuaNilMarker LuaNil;

class LuaNewTableMarker {};
extern LuaNewTableMarker LuaNewTable;

using LuaDeleterFn = void (*)(void *);

using LuaTypeTag = lua_CFunction;
template<typename T>
int LuaTypeTagValue(lua_State *L) { return 0; }

// Lua table types.  These deliberately do not overlap
// with lua type values.
//
#define LUA_TT_GENERAL       16
#define LUA_TT_REGISTRY      17
#define LUA_TT_GLOBALENV     18
#define LUA_TT_TANGIBLE      19
#define LUA_TT_TANGIBLEMETA  20
#define LUA_TT_DEADTANGIBLE  21
#define LUA_TT_GLOBALDB      22
#define LUA_TT_CLASS         23

class LuaStack : public eng::nevernew {
private:
    int narg_;
    int ngap_;
    int nvar_;
    int nret_;

    int argpos_;
    int gappos_;
    int varpos_;
    int retpos_;

    int rettop_;
    int finaltop_;
    
    lua_State *L_;

    template<int NARG, int NVAR, int NRET, class... SS>
    void count_slots(LuaArg &v, SS & ... stackslots)
    {
        count_slots<NARG+1, NRET, NVAR>(stackslots...);
    }

    template<int NARG, int NVAR, int NRET, class... SS>
    void count_slots(LuaVar &v, SS & ... stackslots)
    {
        count_slots<NARG, NVAR+1, NRET>(stackslots...);
    }
    template<int NARG, int NVAR, int NRET, class... SS>
    void count_slots(LuaRet &v, SS & ... stackslots)
    {
        count_slots<NARG, NVAR, NRET+1>(stackslots...);
    }

    template<int NARG, int NVAR, int NRET>
    void count_slots() {
        count_slots_finalize(NARG, NVAR, NRET);
    }

    void count_slots_finalize(int narg, int nvar, int nret);

    template<class... SS>
    void assign_slots(int argp, int varp, int retp, LuaArg &v, SS & ... stackslots) {
        v.index_ = argp;
        assign_slots(argp + 1, varp, retp, stackslots...);
    }

    template<class... SS>
    void assign_slots(int argp, int varp, int retp, LuaVar &v, SS & ... stackslots) {
        v.index_ = varp;
        assign_slots(argp, varp+1, retp, stackslots...);
    }
    
    template<class... SS>
    void assign_slots(int argp, int varp, int retp, LuaRet &v, SS & ... stackslots) {
        v.index_ = retp;
        assign_slots(argp, varp, retp+1, stackslots...);
    }

    void assign_slots(int argp, int varp, int retp) {}

    void clear_frame();
    
private:
    // Push any value on the stack, by type.
    void push_any_value(LuaNewTableMarker s) const { lua_newtable(L_); }
    void push_any_value(LuaNilMarker s) const { lua_pushnil(L_); }
    void push_any_value(LuaSlot s) const { lua_pushvalue(L_, s); }
    void push_any_value(const eng::string &s) const { lua_pushlstring(L_, s.c_str(), s.size()); }
    void push_any_value(std::string_view s) const { lua_pushlstring(L_, s.data(), s.size()); }
    void push_any_value(const char *s) const { lua_pushstring(L_, s); }
    void push_any_value(float s) const { lua_pushnumber(L_, s); }
    void push_any_value(double s) const { lua_pushnumber(L_, s); }
    void push_any_value(int s) const { lua_pushinteger(L_, s); }
    void push_any_value(lua_Integer s) const { lua_pushinteger(L_, s); }
    void push_any_value(lua_CFunction s) const { lua_pushcfunction(L_, s); }
    void push_any_value(bool b) const { lua_pushboolean(L_, b ? 1:0); }

    // Push multiple values on the stack, in order, by type.
    template<typename T0, typename... T>
    void push_any_values(T0 arg0, T... args) {
        push_any_value(arg0);
        push_any_values(args...);
    }
    void push_any_values() {
    }

    template<typename T>
    static void delete_pointer(void *p) { delete (T*)p; }
    static void do_not_delete(void *p) { }

public:
    template<class... SS>
    LuaStack(lua_State *L, SS & ... stackslots) {
        L_ = L;
        count_slots<0, 0, 0>(stackslots...);
        if (lua_gettop(L) < narg_) {
            luaL_error(L, "not enough arguments to function");
        }
        assign_slots(argpos_, varpos_, retpos_, stackslots...);
        clear_frame();
    }

    ~LuaStack() {};

    int result();

public:
    static lua_State *newstate (lua_Alloc allocf);

    lua_State *state() const { return L_; }

    int type(LuaSlot s) const { return lua_type(L_, s); }
    void checktype(LuaSlot s, int type) const { luaL_checktype(L_, s, type); }

    bool istable(LuaSlot s) const { return lua_type(L_, s) == LUA_TTABLE; }
    bool isstring(LuaSlot s) const { return lua_type(L_, s) == LUA_TSTRING; }
    bool isnumber(LuaSlot s) const { return lua_type(L_, s) == LUA_TNUMBER; }
    bool isthread(LuaSlot s) const { return lua_type(L_, s) == LUA_TTHREAD; }
    bool isfunction(LuaSlot s) const { return lua_type(L_, s) == LUA_TFUNCTION; }
    bool isboolean(LuaSlot s) const { return lua_type(L_, s) == LUA_TBOOLEAN; }
    bool isnil(LuaSlot s) const { return lua_type(L_, s) == LUA_TNIL; }
    bool iscfunction(LuaSlot s) const { return lua_iscfunction(L_, s) != 0; }

    void checktable(LuaSlot index) const { checktype(index, LUA_TTABLE); }
    void checkstring(LuaSlot index) const { checktype(index, LUA_TSTRING); }
    void checknumber(LuaSlot index) const { checktype(index, LUA_TNUMBER); }
    void checkthread(LuaSlot index) const { checktype(index, LUA_TTHREAD); }
    void checkfunction(LuaSlot index) const { checktype(index, LUA_TFUNCTION); }
    void checkboolean(LuaSlot index) const { checktype(index, LUA_TBOOLEAN); }
    void checknil(LuaSlot index) const { checktype(index, LUA_TNIL); }

    bool ckboolean(LuaSlot s) const;
    lua_Integer ckinteger(LuaSlot s) const;
    int ckint(LuaSlot s) const;
    lua_Number cknumber(LuaSlot s) const;
    eng::string ckstring(LuaSlot s) const;
    lua_State *ckthread(LuaSlot s) const;
    
    void clearmetatable(LuaSlot tab) const;
    void setmetatable(LuaSlot tab, LuaSlot mt) const;
    bool getmetatable(LuaSlot mt, LuaSlot tab) const;

    void newtable(LuaSlot target) const;
    void createtable(LuaSlot target, int narr, int nrec) const;
    lua_State *newthread(LuaSlot target) const;
    void getglobaltable(LuaSlot gltab) const;
    void cleartable(LuaSlot tab, bool clearmeta) const;

    int rawlen(LuaSlot val) const;
    
    int next(LuaSlot tab, LuaSlot key, LuaSlot value) const;

    // Return true if the classname is legal.
    bool validclassname(LuaSlot value) const;
    static bool validclassname(std::string_view cname);

    // Return the class name if x is a valid classtab.
    // Otherwise, returns empty string.  If nonempty, the
    // result is guaranteed to be a validclassname.
    // This can also function as an "isclass" operator.
    eng::string classname(LuaSlot x) const;

    // Look up a class.
    // If there is a problem, returns an error message.
    // There are lots of error conditions, including such things
    // as no such class, corrupted class, classname invalid, etc.
    eng::string getclass(LuaSlot tab, LuaSlot name) const;
    eng::string getclass(LuaSlot tab, std::string_view name) const;

    // Create a class, or look up an existing class.
    // WARNING: this routine assert-fails if the parameter is not
    // a valid classname.  Check the classname before calling this!
    void makeclass(LuaSlot tab, LuaSlot name) const;
    void makeclass(LuaSlot tab, std::string_view name) const;

    // Get the ID of a tangible.  It's a little weird to put this in
    // this module.
    int64_t tanid(LuaSlot tab) const;

    // Return true if the value is a sortable key (string, number, or boolean).
    bool issortablekey(LuaSlot s) const;

    // Move a sortable key (string, number, or boolean) from one lua
    // environment to another lua environment.  WARNING: this assert-fails
    // if the value is not a sortable key.
    void movesortablekey(LuaSlot val, LuaStack &other, LuaSlot otherslot);

    bool rawequal(LuaSlot v1, LuaSlot v2) const {
        return lua_rawequal(L_, v1, v2);
    }

    bool rawequal(LuaSlot v1, const char *name) const {
        push_any_value(name);
        bool result = lua_rawequal(L_, v1, -1);
        lua_pop(L_, 1);
        return result;
    }
    
    template<typename VT>
    void set(LuaSlot target, VT value) const {
        push_any_value(value);
        lua_replace(L_, target);
    }

    template<typename KT>
    void rawget(LuaSlot target, LuaSlot tab, KT key) const {
        push_any_value(key);
        lua_rawget(L_, tab);
        lua_replace(L_, target);
    }

    void rawget(LuaSlot target, LuaSlot tab, int key) const {
         lua_rawgeti(L_, tab, key);
         lua_replace(L_, target);
    }
    
    template<typename KT, typename VT>
    void rawset(LuaSlot tab, KT key, VT value) const {
        push_any_value(key);
        push_any_value(value);
        lua_rawset(L_, tab);
    }
    
    template<typename VT>
    void rawset(LuaSlot tab, int key, VT value) const {
        push_any_value(value);
        lua_rawseti(L_, tab, key);
    }
    
    // Lua flagbits manipulation: Table types.
    int gettabletype(LuaSlot tab) const;
    void settabletype(LuaSlot tab, int t) const;

    // If slot is a table, returns the LUA_TT_XXX table type.
    // If slot is not a table, returns the LUA_TXXX general type.
    int xtype(LuaSlot slot) const;

    // Lua flagbits manipulation: visited bit.
    bool getvisited(LuaSlot tab) const;
    void setvisited(LuaSlot tab, bool visited) const;
};


class LuaFunctionReg : public eng::nevernew {
private:
    const char *name_;
    const char *args_;
    const char *docs_;
    bool sandbox_;
    lua_CFunction func_;
    LuaFunctionReg *next_;

public:
    static LuaFunctionReg *All;
    LuaFunctionReg(const char *name, const char *args, const char *docs, bool sand, lua_CFunction f);
    static const LuaFunctionReg *lookup(lua_CFunction fn);

    const char *get_name() const { return name_; }
    const char *get_args() const { return args_; }
    const char *get_docs() const { return docs_; }
    lua_CFunction get_func() const { return func_; }
    bool get_sandbox() const { return sandbox_; }
    LuaFunctionReg *next() const { return next_; }
    void set_func(lua_CFunction fn) { func_ = fn; }
};


#define LuaDefine(name, args, docs) \
    int lfn_##name(lua_State *L); \
    LuaFunctionReg reg_##name(#name, args, docs, false, lfn_##name); \
    int lfn_##name(lua_State *L)

#define LuaSandbox(name, args, docs) \
    int lfn_##name(lua_State *L); \
    LuaFunctionReg reg_##name(#name, args, docs, true, lfn_##name); \
    int lfn_##name(lua_State *L)

#define LuaDefineBuiltin(name, args, docs) \
    LuaFunctionReg reg_##name(#name, args, docs, false, nullptr);

#define LuaSandboxBuiltin(name, args, docs) \
    LuaFunctionReg reg_##name(#name, args, docs, true, nullptr);

#define LuaStringify(x) #x
#define LuaAssert(L, x) if (!(x)) { luaL_error((L), "Assert failed: %s (file %s line %d)", LuaStringify(x), __FILE__, __LINE__); }
#define LuaAssertStrEq(L, x, y) { eng::string _s1_(x); eng::string _s2_(y); if (_s1_ != _s2_) luaL_error((L), "Assert failed: value=%s (file %s line %d)", _s1_.c_str(), __FILE__, __LINE__); }
#endif // LUASTACK_HPP
