#ifndef LUA_WRAPPER_H
#define LUA_WRAPPER_H


#include <string>
#include <unordered_map>
#include "luaHead.h"

using namespace std;


struct lua_State;

typedef int luaref;
typedef int tableref;
typedef int funcref;


/*
TODO :
The use of lua ref stops the GB to free referenced variable
Maybe refs in cpp should be treated like CPP shared pointers?
*/
//TODO: keep track of all refs
//Use upvalues instead of LUA_REGISTRYINDEX?
//Warning ! referenced object must be unreferenced if we want to gargbage collect them

/* Remember to use :
for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
}
lua_pop(L, 1);
*/

class lua_wrapper
{
    public:
        //New default
        lua_wrapper(string filename);
        virtual ~lua_wrapper();


        //// Miscellaneous ////
        void stackDump();
        template<typename... T> void check_type_error(int type, T... err);
        string type_to_string(int type);


        //// Loding Single Values ////
        //Global
        template<typename T> T get_global(string name);
        //From open table
        template<typename T> T get_field(string name);
        template<typename T> T get_field(int index);
        //From reftable (opens & close the table)
        template<typename T> T get_from(tableref refIndex, string name);
        template<typename T> T get_from(tableref refIndex, int index);

        template<typename Value,  typename... Values>
        int get_top(Value &value, Values&... values);
        bool get_top(int& value);
        bool get_top(unsigned int& value);
        bool get_top(double& value);
        bool get_top(string& value);
        bool get_top(bool& value);

        //Push
        template<typename Head, typename... Tail>
        int push(Head value, Tail... values);
        int push(int value);
        int push(double value);
        int push(bool value);
        int push(const string &value);
        int push(const char * value);
        int push();


        //// Tables open/close ////
        //Global
        void open_global_table(string name);
        //From open table
        void open_field_table(string name);
        void open_field_table(int index);
        //From reference
        /*todo*/void open_table_from(tableref refIndex, string name);
        /*todo*/void open_table_from(tableref refIndex, int index);
        //At reference
        void open_table(tableref index);
        void close_table();
        //Table length
        size_t table_len_top();



        //// Multiples values (same value, continuous arrays) ////
        //Loads open table (on top)
        template<typename T> void load_array(vector<T>& rv);
        template<typename T> void load_2d_array(vector<vector<T>>& rv);
        //Load table at ref, stack remains clean
        template<typename T> void load_array(vector<T>& rv, tableref index);
        template<typename T> void load_2d_array(vector<vector<T>>& rv, tableref index);



        //// Name - Values pairs ////
        template<typename T> unordered_map<string, T> load_unordered_table_top();
        //template<typename T> map<string, T> load_ordered_table();
        void get_table_keys(vector<string>& rv, tableref index);

        //// Functions ////
        //Register
        funcref func_ref(string name);
        funcref func_ref();
        //Call
        template<typename Rv, typename... Args> Rv call_func(funcref index, Args... args);


        //// References ////
        //Global tables
        tableref table_ref_global(string name);
        //Tables from open table (just a shortcut to open & ref table on top)
        tableref table_ref_field(int index);
        tableref table_ref_field(string name);
        //Table on top
        tableref table_ref_top();
        //Free ref
        /*TODO*/ void free_ref(tableref refIndex);


        //// Table iterator? Using next? and ref?

    protected:
    private:
        lua_State* m_L;
        string m_file;



        template<typename T> void showMessage(T &&t);
        template<typename Head, typename... Tail> void showMessage(Head &&head, Tail&&... tail);



};


template<typename T>
unordered_map<string, T> lua_wrapper::load_unordered_table_top()
{
    unordered_map<string, T> rv;
    string key;
    T value;

    check_type_error(LUA_TTABLE, "load unordered: no table on top");
    lua_pushnil(m_L);
    while (lua_next(m_L, -2) != 0)
    {
        get_top(value);
        lua_pop(m_L, 1);
        get_top(key);
        rv[key] = value;
    }
    //No need to pop anything : lua_next pushes nothing when at the end
    return rv;
}


/*
template<typename T>
bool lua_wrapper::get_top(T &value)
{
    get_top(value);
}
*/

template<typename Value, typename... Values>
int lua_wrapper::get_top(Value &value, Values&... values)
{
    int nCalls = 0;
    nCalls += get_top(value);
    lua_pop(m_L, 1);
    nCalls += get_top(values...);
    lua_pop(m_L, 1);
    return nCalls;
}


template<typename Head, typename... Tail>
int lua_wrapper::push(Head value, Tail... values)
{
    int nCalls = 0;
    nCalls += push(value);
    nCalls += push(values...);
    return nCalls;
}


template<typename Rv, typename... Args>
Rv lua_wrapper::call_func(funcref index, Args... args)
{
    //Get function from ref
    lua_rawgeti(m_L, LUA_REGISTRYINDEX, index);
    check_type_error(LUA_TFUNCTION, "call_func, wrong type");

    int nVar = push(args...);
    lua_pcall(m_L, nVar, 1, 0);

    Rv value;
    get_top(value);
    lua_pop(m_L, 1);
    return value;

}



template<typename T>
void lua_wrapper::showMessage(T &&t) {
    cout << t << endl;
}



template<typename Head, typename... Tail>
void lua_wrapper::showMessage(Head &&head, Tail&&... tail) {
    std::cout << head;
    showMessage(std::forward<Tail>(tail)...);
}



template<typename... T>
void lua_wrapper::check_type_error(int type, T... err)
{
    if( type == lua_type (m_L, -1) ) return;
    cout << "Error: Wrong type on the stack." << endl
         << "Required type \"" << type_to_string(type) << "\"." << endl
         << "Got \"" << type_to_string(lua_type(m_L, -1)) << "\"." << endl
         << "Message: " << endl;

    showMessage(err...);
    cout << "Program will now close." << endl;
    exit(EXIT_FAILURE);
}



//Todo : Overload with a reference to a table
template<typename T>
void lua_wrapper::load_array(vector<T>& rv)
{
    //Check if table open
    check_type_error(LUA_TTABLE, "load_array_top : no table on top");

    size_t tabLen = luaL_len(m_L,-1);
    rv.clear();
    rv.reserve(tabLen);
    T temp;

    for(size_t i = 1; i <= tabLen; i++)
    {
        lua_rawgeti(m_L, -1, i);
        if(!get_top(temp))
        {
            cout << "Field \"" << i << "\" : wrong type." << endl;
            if(lua_isnil(m_L, -1)) cout << "(Returned nil)" << endl;
            exit(EXIT_FAILURE);
        }
        lua_pop(m_L, 1);
        rv.push_back(temp);
    }
}



template<typename T>
void lua_wrapper::load_array(vector<T>& rv, tableref index)
{
    open_table(index);
    load_array(rv);
    close_table();
}



template<typename T>
void lua_wrapper::load_2d_array(vector<vector<T>>& rv)
{
    //Check if table open
    check_type_error(LUA_TTABLE, "load_2d_array_top : not table on top");

    size_t matLen = luaL_len(m_L,-1);
    rv.clear();
    rv.reserve(matLen);

    for(size_t i = 1; i <= matLen; i++)
    {
        lua_rawgeti(m_L, -1, i);
        rv.push_back(vector<T>());
        load_array(rv.back());
        lua_pop(m_L, 1);
    }
}



template<typename T>
void lua_wrapper::load_2d_array(vector<vector<T>>& rv, tableref index)
{
    open_table(index);
    load_2d_array(rv);
    close_table();
}



//Implementation in the header because of template (alternative is explicit declaration of each supported type)
template<typename T>
T lua_wrapper::get_global(string name)
{
    T value;
    lua_getglobal(m_L, name.c_str());

    if(!get_top(value))
    {
        cout << "Global value \"" << name << "\" : wrong type." << endl;
        if(lua_isnil(m_L, -1)) cout << "(Returned nil)" << endl;
        exit(EXIT_FAILURE);
    }
    lua_pop(m_L, 1);
    return value;
}



//Implementation in the header because of template (alternative is explicit declaration of each supported type)
template<typename T>
T lua_wrapper::get_field(string name)
{
    //Check if table open
    if(!lua_istable(m_L, -1))
    {
        cout << "No open table on top !" << endl;
        exit(EXIT_FAILURE);
    }

    //Declare returned value & pushes field on top
    T value;
    lua_getfield(m_L, -1, name.c_str());

    //Conversion based on overloaded "get_top"; Checking for failure (wrong type)
    if(!get_top(value))
    {
        cout << "Field \"" << name << "\" : wrong type." << endl;
        if(lua_isnil(m_L, -1)) cout << "(Returned nil)" << endl;
        exit(EXIT_FAILURE);
    }
    lua_pop(m_L, 1);
    return value;
}



template<typename T>
T lua_wrapper::get_field(int index)
{
    //Check if table open
    if(!lua_istable(m_L, -1))
    {
        cout << "No open table on top !" << endl;
        exit(EXIT_FAILURE);
    }

    //Declare return && get table[index] on top
    T value;
    lua_rawgeti(m_L, -1, index);

    //Conversion based on overloaded "get_top"; Checking for failure (wrong type)
    if(!get_top(value))
    {
        cout << "Field \"" << index << "\" : wrong type." << endl;
        if(lua_isnil(m_L, -1)) cout << "(Returned nil)" << endl;
        exit(EXIT_FAILURE);
    }
    lua_pop(m_L, 1);
    return value;
}



template<typename T>
T lua_wrapper::get_from(tableref refIndex, string name)
{
    open_table(refIndex);
    T temp = get_field<T>(name);
    close_table();
    return temp;
}



template<typename T>
T lua_wrapper::get_from(tableref refIndex, int index)
{
    open_table(refIndex);
    T temp = get_field<T>(index);
    close_table();
    return temp;
}



#endif // LUA_WRAPPER_H

//Load : global, table field, top, array, matrix
