/*
EVAL.js
-------

reads exprs from AST and executes them

types of elem:
all have type
all have val

TYPE        VAL
"closure"   {scope:{}, args:['x', 'y'], defn:<function-like>}

<function-like>:
{type:"ins", ins:[]}
{type:"builtin", fn:(scope) => {stack}}

exported functionality:
defnOp(name, function) - define a built-in operator
defn(name, ins, scope) - extend scope with AST
makeOp(name, args, fn) - lift a function to an operator
evalRPN(scope, ast)
*/

let builtins = {};

export const defnOp = (name, data) => {
    builtins[name] = data;
}

export const defn = (name, ins, scope) => {
    let defscope = Object.create(scope);
    let fnForm = {type:"closure", val:{scope:defscope, args:[], defn:{type:"ins", ins:ins}}};
    defscope[name] = [fnForm];
    let out = execRPN(defscope, ins);
    defscope[name] = out.stacks[0];
    return defscope;
}

export const makeOp = (args, fn) => {
    return [{type:"closure", val:{scope:{}, args:args, defn:{type:"builtin", fn:fn}}}];
}

const lookup = (name, scope) => {
    let n = scope[name];
    if (n) {
        return n;
    }
    n = builtins[name];
    if (n) {
        return n;
    }
    console.log(scope);
    throw '"' + name + '" not in scope'
}

const extend = (scope, name, elems) => {
    let o = Object.create(scope);
    o[name] = elems;
    return o;
}

const runFn = (defn, state) => {
    if (defn.type === "ins") {
        state.calls.push(defn.ins);
        state.stacks.push([]);
    } else if (defn.type === "builtin") {
        let scope = state.scopes[state.scopes.length-1];
        let out = defn.fn(scope);
        state.calls.push([]);
        state.stacks.push(out);
    }
}

const giveArg = (arg, elem) => {
    let argN = elem.val.args[elem.val.args.length-1];
    let newscope = extend(elem.val.scope, argN, [arg]);
    return {type:elem.type, val:{scope:newscope, args:elem.val.args.slice(0,-1), defn:elem.val.defn}};
}

const apply = (elem, state) => {
    if (elem.type === "closure") {
        if (elem.val.args.length === 0) {
            state.scopes.push(elem.val.scope);
            runFn(elem.val.defn, state);
        } else if (state.stacks[state.stacks.length-1].length > 0) {
            apply(giveArg(state.stacks[state.stacks.length-1].pop(), elem), state);
        } else {
            state.stacks[state.stacks.length-1].push(elem);
        }
    } else {
        state.stacks[state.stacks.length-1].push(elem);
    }
}

const applyMany = (elems, state) => {
    for (let i = 0; i < elems.length; i++) {
        apply(elems[i], state);
    }
}

const makeStackElems = (ins, state, handler) => {
    if (ins.type === "push") {
        throw 'nested push error'
    } else if (ins.type === "ident") {
        return lookup(ins.val, state.scopes[state.scopes.length-1]);
    } else if (ins.type === "func") {
        return [{type:"closure", val:{scope:state.scopes[state.scopes.length-1], args:ins.args, defn:{type:"ins", ins:ins.body}}}];
    } else {
        return handler(ins);
    }
}

const doIns = (ins, state, handler) => {
    if (ins.type === "push") {
        state.stacks[state.stacks.length-1] = state.stacks[state.stacks.length-1].concat(makeStackElems(ins.elem, state, handler));
    } else if (ins.type === "defn") {
        state.scopes[state.scopes.length-1] = defn(ins.ident, ins.defn, state.scopes[state.scopes.length-1]);
    } else {
        applyMany(makeStackElems(ins, state, handler), state);
    }
}

let hasDoneAchievement = false

export const step = (state, handler, showIns, maxdepth) => {
    if (!hasDoneAchievement && state.stacks.length > 100) {
        if ("points" in window) {
            points.then(points => points.unlockAchievement("rpnv4recursion"))
        }
        hasDoneAchievement = true
    }

    if (state.stacks.length > maxdepth) {
        throw 'max recursion depth exceeded'
    }
    if (state.calls[state.calls.length-1].length === 0) {
        if (state.calls.length === 1) {
            throw 'finished execution'
        }
        if (state.stacks.length < 2) {
            throw 'nothing to return'
        }
        state.calls.pop();
        state.scopes.pop();
        let out = state.stacks.pop();
        applyMany(out, state);
        return {start:0, end:0};
    } else {
        let ins = state.calls[state.calls.length-1][0];
        state.calls[state.calls.length-1] = state.calls[state.calls.length-1].slice(1);
        try {
            doIns(ins, state, handler);
        } catch (error) {
            throw error + ' while executing "' + showIns(ins) + '"'
        }
        return ins.pos;
    }
}

export const execRPN = (scope, ins, handler=(x)=>[x], showIns=(x)=>x.name, maxdepth=16384) => {
    let state = {scopes:[scope], stacks:[[]], calls:[ins]};
    while (state.calls[0].length > 0 || state.calls.length > 1) {
        step(state, handler, showIns, maxdepth);
    }
    return state;
}