/**
 * Modern Albufeira Prolog Interpreter
 *
 * Warranty & Liability
 * To the extent permitted by applicable law and unless explicitly
 * otherwise agreed upon, XLOG Technologies AG makes no warranties
 * regarding the provided information. XLOG Technologies AG assumes
 * no liability that any problems might be solved with the information
 * provided by XLOG Technologies AG.
 *
 * Rights & License
 * All industrial property rights regarding the information - copyright
 * and patent rights in particular - are the sole property of XLOG
 * Technologies AG. If the company was not the originator of some
 * excerpts, XLOG Technologies AG has at least obtained the right to
 * reproduce, change and translate the information.
 *
 * Reproduction is restricted to the whole unaltered document. Reproduction
 * of the information is only allowed for non-commercial uses. Selling,
 * giving away or letting of the execution of the library is prohibited.
 * The library can be distributed as part of your applications and libraries
 * for execution provided this comment remains unchanged.
 *
 * Restrictions
 * Only to be distributed with programs that add significant and primary
 * functionality to the library. Not to be distributed with additional
 * software intended to replace any components of the library.
 *
 * Trademarks
 * Jekejeke is a registered trademark of XLOG Technologies AG.
 */

import {
    add, deref, bind, is_structure, Compound, make_special,
    is_number, make_error, VOID_ARGS, cont, get_cont,
    check_atomic, make_check, stack_pop, stack_push,
    stack_peek, Item, Variable, check_integer,
    walk_vars, is_compound,
    is_integer, exec_build, exec_unify, VAR_MASK_SEEN
} from "../nova/core.mjs";

/*********************************************************************/
/* numbervars/3                                                      */
/*********************************************************************/

/**
 * numbervars(X, N, M):
 * The predicate succeeds in M with the next index after
 * numbering all variables in X starting with index N.
 */
function test_numbervars(args) {
    let alpha = exec_build(args[0]);
    let beta = exec_build(args[1]);
    check_integer(beta);
    if (beta >= 0) {
        beta = numbervars("$VAR", alpha, beta);
    } else {
        beta = (-1) - beta;
        beta = numbervars("_", alpha, beta);
        beta = (-1) - beta;
    }
    return exec_unify(args[2], beta);
}

function numbervars(functor, alpha, beta) {

    function numbervars2(node) {
        bind(new Compound(functor, [beta]), node);
        beta++;
        return false;
    }

    walk_vars(alpha, node => false, VAR_MASK_SEEN);
    walk_vars(alpha, numbervars2, 0);
    return beta;
}

/**************************************************************/
/* Association Map                                            */
/**************************************************************/

function assoc_get(map, key) {
    if (map === null)
        return undefined;
    return map.get(key);
}

function assoc_set(map, key, value) {
    if (map === null)
        map = new Map();
    map.set(key, value);
    return map;
}

/******************************************************************/
/* unnumbervars/3                                                 */
/******************************************************************/

/**
 * unnumbervars(S, N, T):
 * The predicate succeeds in T with a copy of S with the
 * numbering greater or equal than N replaced by variables.
 */
function test_unnumbervars(args) {
    let alpha = exec_build(args[0]);
    let beta = exec_build(args[1]);
    check_integer(beta);
    let res;
    try {
        if (beta >= 0) {
            res = walk_replace("$VAR", beta, alpha);
        } else {
            res = (-1) - beta;
            res = walk_replace("_", res, alpha);
        }
    } finally {
        if (beta >= 0) {
            walk_unmax("$VAR", alpha);
        } else {
            walk_unmax("_", alpha);
        }
    }
    return exec_unify(args[2], res);
}

function walk_replace(functor, beta, first) {
    let stack = null;
    let map = null;
    for (; ; ) {
        first = deref(first);
        if (is_structure(first)) {
            if (functor === first.functor &&
                first.args.length === 1) {
                let peek = deref(first.args[0]);
                if (is_integer(peek) && beta <= peek) {
                    first = assoc_get(map, peek);
                    if (first === undefined) {
                        first = new Variable();
                        map = assoc_set(map, peek, first);
                    }
                }
            }
            if (is_structure(first)) {
                if (!is_compound(first.functor)) {
                    first.functor = new Compound(first.functor,
                        new Array(first.args.length));
                    let item = new Item(first, null, 0);
                    stack = stack_push(stack, item);
                    first = first.args[0];
                    continue;
                } else {
                    first = first.functor;
                }
            }
        }
        let item = stack_peek(stack);
        while (item !== null &&
                item.idx  === item.first.args.length - 1) {
            item.first.functor.args[item.idx] = first;
            first = item.first.functor;
            stack_pop(stack);
            item = stack_peek(stack);
        }
        if (item === null) {
            return first;
        } else {
            item.first.functor.args[item.idx] = first;
            item.idx++;
            first = item.first.args[item.idx];
        }
    }
}

/******************************************************************/
/* maxvars/2                                                      */
/******************************************************************/

/**
 * maxvars(X, M):
 * The predicate succeeds in M with the max numbering of the term X.
 */
function test_maxvars(args) {
    let alpha = exec_build(args[0]);
    let res;
    try {
        res = walk_max("$VAR", alpha);
    } finally {
        walk_unmax("$VAR", alpha);
    }
    return exec_unify(args[1], res);
}

function walk_max(functor, first) {
    let stack = null;
    let res = -1;
    for (; ; ) {
        first = deref(first);
        if (is_structure(first)) {
            if (functor === first.functor &&
                first.args.length === 1) {
                let peek = deref(first.args[0]);
                if (is_integer(peek)) {
                    if (res < peek)
                        res = peek;
                    first = null;
                }
            }
            if (first !== null) {
                if (!is_compound(first.functor)) {
                    first.functor = new Compound(
                                first.functor, VOID_ARGS);
                    if (0 !== first.args.length - 1) {
                        let item = new Item(first, null, 0);
                        stack = stack_push(stack, item);
                    }
                    first = first.args[0];
                    continue;
                }
            }
        }
        let item = stack_peek(stack);
        if (item == null) {
            return res;
        } else {
            item.idx++;
            first = item.first.args[item.idx];
            if (item.idx === item.first.args.length - 1)
                stack_pop(stack);
        }
    }
}

function walk_unmax(functor, first) {
    let stack = null;
    for (; ; ) {
        first = deref(first);
        if (is_structure(first)) {
            if (functor === first.functor &&
                first.args.length === 1) {
                let peek = deref(first.args[0]);
                if (is_integer(peek)) {
                    first = null;
                }
            }
            if (first !== null) {
                if (is_compound(first.functor)) {
                    first.functor = first.functor.functor;
                    if (0 !== first.args.length - 1) {
                        let item = new Item(first, null, 0);
                        stack = stack_push(stack, item);
                    }
                    first = first.args[0];
                    continue;
                }
            }
        }
        let item = stack_peek(stack);
        if (item == null) {
            return;
        } else {
            item.idx++;
            first = item.first.args[item.idx];
            if (item.idx === item.first.args.length - 1)
                stack_pop(stack);
        }
    }
}

/*********************************************************************/
/* call/2-4                                                          */
/*********************************************************************/

/**
 * call(F, A1, .., An): [Corr.2 8.15.4.4]
 * The predicate succeeds in calling the goal which results from
 * appending the arguments A1, .., An to the callable F.
 */
function special_calln(args) {
    let alpha = deref(args[0]);
    let functor;
    let oldargs;
    if (is_structure(alpha)) {
        functor = alpha.functor;
        oldargs = alpha.args;
    } else {
        functor = alpha;
        check_atomic(functor);
        oldargs = VOID_ARGS;
    }
    let arity = oldargs.length + args.length - 1;
    let goal;
    if (arity === 0) {
        goal = functor;
    } else {
        if (is_number(functor))
            throw make_error(new Compound("type_error", ["atom", functor]));
        let newargs = new Array(arity);
        for (let i = 0; i < oldargs.length; i++)
            newargs[i] = deref(oldargs[i]);
        for (let i = 0; i < args.length - 1; i++)
            newargs[i + oldargs.length] = deref(args[i + 1]);
        goal = new Compound(functor, newargs);
    }
    goal = new Compound(".", [goal, get_cont().args[1]]);
    cont(goal);
    return true;
}

/*********************************************************************/
/* Asm Lib Init                                                     */
/*********************************************************************/

export function main() {
    add("numbervars", 3, make_check(test_numbervars));
    add("unnumbervars", 3, make_check(test_unnumbervars));
    add("maxvars", 2, make_check(test_maxvars));
    add("call", 2, make_special(special_calln));
    add("call", 3, make_special(special_calln));
    add("call", 4, make_special(special_calln));
    add("call", 5, make_special(special_calln));
    add("call", 6, make_special(special_calln));
    add("call", 7, make_special(special_calln));
    add("call", 8, make_special(special_calln));
}