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

const CT    = require('./lua.js').constant_types;
const UpVal = require('./lfunc.js').UpVal;

class TValue {

    constructor(type, value) {
        this.type = type;
        this.value = value;
        this.metatable = null;
    }

    /* type tag of a TValue (bits 0-3 for tags + variant bits 4-5) */
    ttype() {
        return this.type & 0x3F;
    }

    /* type tag of a TValue with no variants (bits 0-3) */
    ttnov() {
        return this.type & 0x0F;
    }

    checktag(t) {
        return this.type === t;
    }
    
    checktype(t) {
        return this.ttnov() === t;
    }
    
    ttisnumber() {
        return this.checktype(CT.LUA_TNUMBER);
    }
    
    ttisfloat() {
        return this.checktag(CT.LUA_TNUMFLT);
    }
    
    ttisinteger() {
        return this.checktag(CT.LUA_TNUMINT);
    }
    
    ttisnil() {
        return this.checktag(CT.LUA_TNIL);
    }
    
    ttisboolean() {
        return this.checktag(CT.LUA_TBOOLEAN);
    }
    
    ttislightuserdata() {
        return this.checktag(CT.LUA_TLIGHTUSERDATA);
    }
    
    ttisstring() {
        return this.checktype(CT.LUA_TSTRING);
    }
    
    ttisshrstring() {
        return this.checktag(CT.LUA_TSHRSTR);
    }
    
    ttislngstring() {
        return this.checktag(CT.LUA_TLNGSTR);
    }
    
    ttistable() {
        return this.checktag(CT.LUA_TTABLE);
    }
    
    ttisfunction() {
        return this.checktype(CT.LUA_TFUNCTION);
    }
    
    ttisclosure() {
        return (this.type & 0x1F) === CT.LUA_TFUNCTION;
    }
    
    ttisCclosure() {
        return this.checktag(CT.LUA_TCCL);
    }
    
    ttisLclosure() {
        return this.checktag(CT.LUA_TLCL);
    }
    
    ttislcf() {
        return this.checktag(CT.LUA_TLCF);
    }
    
    ttisfulluserdata() {
        return this.checktag(CT.LUA_TUSERDATA);
    }
    
    ttisthread() {
        return this.checktag(CT.LUA_TTHREAD);
    }
    
    ttisdeadkey() {
        return this.checktag(CT.LUA_TDEADKEY);
    }

    l_isfalse() {
        return this.ttisnil() || (this.ttisboolean() && this.value === false);
    }

}

const nil   = new TValue(CT.LUA_TNIL, null);

class Table extends TValue {

    constructor(array, hash) {
        super(CT.LUA_TTABLE, {
            array: array !== undefined ? array : [],
            hash: new Map(hash)
        });

        this.metatable = null;
    }

    static keyValue(key) {
        // Those lua values are used by value, others by reference
        if (key instanceof TValue
            && [CT.LUA_TNUMBER,
                CT.LUA_TSTRING,
                CT.LUA_TSHRSTR,
                CT.LUA_TLNGSTR,
                CT.LUA_TNUMFLT,
                CT.LUA_TNUMINT].indexOf(key.type) > -1) {
            key = key.value;
        }

        return key;
    }

    __newindex(table, key, value) {
        key = Table.keyValue(key);

        if (typeof key === 'number' && key > 0) {
            table.value.array[key - 1] = value; // Lua array starts at 1
        } else {
            table.value.hash.set(key, value);
        }
    }

    __index(table, key) {
        key = Table.keyValue(key);

        let v = nil;
        if (typeof key === 'number' && key > 0) {
            v = table.value.array[key - 1]; // Lua array starts at 1
        } else {
            v = table.value.hash.get(key);
        }

        return v ? v : nil;
    }

    __len(table) {
        return this.luaH_getn();
    }

}

class LClosure extends TValue {

    constructor(n) {
        super(CT.LUA_TLCL, null);

        this.p = null;
        this.nupvalues = n;

        let _ENV = new UpVal();
        _ENV.refcount = 0;
        _ENV.v = null;
        _ENV.u.open.next = null;
        _ENV.u.open.touched = true;
        _ENV.u.value = new Table();

        this.upvals = [
            _ENV
        ];

        this.value = this;
    }

}

class CClosure extends TValue {

    constructor(f, n) {
        super(CT.LUA_TCCL, null);

        this.f = f;
        this.nupvalues = n;
        this.upvalue = new Array(n);

        this.value = this;
    }

}

const RETS = "...";
const PRE  = "[string \"";
const POS  = "\"]";

const luaO_chunkid = function(source, bufflen) {
    let l = source.length;
    let out = "";
    if (source[0] === '=') {  /* 'literal' source */
        if (l < bufflen)  /* small enough? */
            out = `${source.slice(1)}`;
        else {  /* truncate it */
            out += `${source.slice(1, bufflen)}`;
        }
    } else if (source[0] === '@') {  /* file name */
        if (l <= bufflen)  /* small enough? */
            out = `${source.slice(1)}`;
        else {  /* add '...' before rest of name */
            bufflen -= RETS.length;
            out = `${RETS}${source.slice(1, l - bufflen)}`;
        }
    } else {  /* string; format as [string "source"] */
        let nli = source.indexOf('\n');  /* find first new line (if any) */
        let nl = nli ? source.slice(nli) : null;
        out = `${PRE}`;  /* add prefix */
        bufflen -= PRE.length - RETS.length; - POS.length + 1;  /* save space for prefix+suffix+'\0' */
        if (l < bufflen && nl === null) {  /* small one-line source? */
            out += `${source}`;  /* keep it */
        } else {
            if (nl !== null) l = nl.length - source.length;  /* stop at first newline */
            if (l > bufflen) l = bufflen;
            out += `${source}${RETS}`;
        }
        out += POS;
    }

    return out;
};

module.exports.LClosure     = LClosure;
module.exports.CClosure     = CClosure;
module.exports.TValue       = TValue;
module.exports.Table        = Table;
module.exports.luaO_chunkid = luaO_chunkid;