/*jshint esversion: 6 */
"use strict";

const assert               = require('assert');

const lua                  = require('./lua.js');
const lobject              = require('./lobject.js');
const ldo                  = require('./ldo.js');
const lapi                 = require('./lapi.js');
const luaT_init            = require('./ltm.js').luaT_init;
const CT                   = lua.constant_types;
const LUA_MULTRET          = lua.LUA_MULTRET;
const TS                   = lua.thread_status;
const LUA_NUMTAGS          = lua.LUA_NUMTAGS;

const BASIC_STACK_SIZE = 2 * lua.LUA_MINSTACK;

class CallInfo {

    constructor(funcOff, func, top, base, previous, next) {
        this.func = func;
        this.funcOff = funcOff;
        this.top = top;
        this.previous = previous;
        this.next = next;
        this.pcOff = 0;
        this.u = {
            l: {  /* only for Lua functions */
                base: base,  /* base for this function */
                savedpc: []
            },
            c: {  /* only for JS functions */
                k: null,  /* continuation in case of yields */
                old_errfunc: null,
                ctx: null  /* context info. in case of yields */
            }
        };
        this.nresults = 0;
        this.callstatus = 0;
    }

}

class lua_State extends lobject.TValue {

    constructor() {
        super(CT.LUA_TTHREAD, null);
        this.base_ci = new CallInfo(); // Will be populated later
        this.top = 0;
        this.ci = null;
        this.ciOff = null;
        this.stack = [];
        this.openupval = [];
        this.status = TS.LUA_OK;
        this.next = null;
        this.twups = [this];
        this.errorJmp = null;
        this.nny = 1;
        this.errfunc = 0;

        this.value = this;
    }

}

class global_State {

    constructor(L) {
        this.mainthread = L;
        this.strt = new Map();
        this.l_registry = new lobject.TValue(CT.LUA_TNIL, null);
        this.panic = null;
        this.version = null;
        this.twups = [];
        this.mt = new Array(LUA_NUMTAGS);
    }

    intern(stringArray) {
        // TODO: when weak value maps are available
        // let key = stringArray.map(e => `${e}|`).join('');

        // if (!this.strt.has(key)) 
        //     this.strt.set(key, new lobject.TValue(CT.LUA_TLNGSTR, stringArray));

        // return this.strt.get(key);
        
        return new lobject.TValue(CT.LUA_TLNGSTR, stringArray);
    }

}


const stack_init = function(L1, L) {
    L1.stack = new Array(BASIC_STACK_SIZE); // TODO: for now we don't care about the stack size
    L1.top = 0;
    let ci = L1.base_ci;
    ci.next = ci.previous = null;
    ci.callstatus = 0;
    ci.func = L1.stack[L1.top];
    ci.funcOff = L1.top;
    L1.stack[L1.top++] = new lobject.TValue(CT.LUA_TNIL, null);
    ci.top = L1.top + lua.LUA_MINSTACK;
    L1.ci = ci;
};

/*
** Create registry table and its predefined values
*/
const init_registry = function(L, g) {
    let registry = new lobject.Table();
    g.l_registry = registry;
    registry.value.set(lua.LUA_RIDX_MAINTHREAD - 1, L);
    registry.value.set(lua.LUA_RIDX_GLOBALS - 1, new lobject.Table());
};

/*
** open parts of the state that may cause memory-allocation errors.
** ('g->version' !== NULL flags that the state was completely build)
*/
const f_luaopen = function(L) {
    let g = L.l_G;
    stack_init(L, L);
    init_registry(L, g);
    luaT_init(L);
    g.version = lapi.lua_version(null);
};

const preinit_thread = function(L, g) {
    L.l_G = g;
    L.stack = [];
    L.ci = null;
    L.nci = 0;
    L.twups = [L];  /* thread has no upvalues */
    L.errorJmp = null;
    L.nCcalls = 0;
    L.hook = null;
    L.hookmask = 0;
    L.basehookcount = 0;
    L.allowhook = 1;
    L.hookcount = L.basehookcount;
    L.openupval = [];
    L.nny = 1;
    L.status = TS.LUA_OK;
    L.errfunc = 0;
};

const lua_newthread = function(L) {
    let g = L.l_G;
    let L1 = new lua_State();
    L.stack[L.top++] = L1;
    assert(L.top <= L.ci.top, "stack overflow");
    preinit_thread(L1, g);
    L1.hookmask = L.hookmask;
    L1.basehookcount = L.basehookcount;
    L1.hook = L.hook;
    L1.hookcount = L1.basehookcount;
    stack_init(L1, L);
    return L1;
};

const lua_newstate = function() {
    let L = new lua_State();
    let g = new global_State(L);

    preinit_thread(L, g);

    if (ldo.luaD_rawrunprotected(L, f_luaopen, null) !== TS.LUA_OK) {
        L = null;
    }

    return L;
};

module.exports.lua_State      = lua_State;
module.exports.CallInfo       = CallInfo;
module.exports.CIST_OAH       = (1<<0);  /* original value of 'allowhook' */
module.exports.CIST_LUA       = (1<<1);  /* call is running a Lua function */
module.exports.CIST_HOOKED    = (1<<2);  /* call is running a debug hook */
module.exports.CIST_FRESH     = (1<<3);  /* call is running on a fresh invocation of luaV_execute */
module.exports.CIST_YPCALL    = (1<<4);  /* call is a yieldable protected call */
module.exports.CIST_TAIL      = (1<<5);  /* call was tail called */
module.exports.CIST_HOOKYIELD = (1<<6);  /* last hook called yielded */
module.exports.CIST_LEQ       = (1<<7);  /* using __lt for __le */
module.exports.CIST_FIN       = (1<<8);   /* call is running a finalizer */
module.exports.lua_newstate   = lua_newstate;
module.exports.lua_newthread  = lua_newthread;