package doglet;

import nova.Machine;
import nova.Store;
import nova.eval;
import nova.special;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 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.
 */
public final class asmlib {

    /******************************************************************/
    /* 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.
     */
    private static boolean test_numbervars(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object beta = Machine.exec_build(args[1]);
        special.check_integer(beta);
        if (eval.integer_signum((Number)beta) >= 0) {
            beta = numbervars("$VAR", alpha, (Number) beta);
        } else {
            beta = eval.subtract(Integer.valueOf(-1), (Number) beta);
            beta = numbervars("_", alpha, (Number) beta);
            beta = eval.subtract(Integer.valueOf(-1), (Number) beta);
        }
        return Machine.exec_unify(args[2], beta);
    }

    private static Number numbervars(String functor, Object alpha, Number beta) {
        numbervars2 h = new numbervars2(functor, beta);
        special.walk_vars(alpha, node -> false, Store.VAR_MASK_SEEN);
        special.walk_vars(alpha, h::run, 0);
        return h.beta;
    }

    private static class numbervars2 {
        private final String functor;
        private Number beta;

        public numbervars2(String functor, Number beta)  {
            this.functor = functor;
            this.beta = beta;
        }

        public boolean run(Object node) {
            Machine.bind(new Store.Compound(functor, new Object[]{beta}),
                    (Store.Variable)node);
            beta = eval.add(beta, Integer.valueOf(1));
            return false;
        }

    }

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

    private static Object assoc_get(Map map, Object key) {
        if (map == null)
            return null;
        return map.get(key);
    }

    private static Map assoc_set(Map map, Object key, Object value) {
        if (map == null)
            map = new HashMap();
        map.put(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.
     */
    private static boolean test_unnumbervars(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object beta = Machine.exec_build(args[1]);
        special.check_integer(beta);
        Object res;
        try {
            if (eval.integer_signum((Number) beta) >= 0) {
                res = walk_replace("$VAR", (Number) beta, alpha);
            } else {
                res = eval.subtract(Integer.valueOf(-1), (Number) beta);
                res = walk_replace("_", (Number) res, alpha);
            }
        } finally {
            if (eval.integer_signum((Number) beta) >= 0) {
                walk_unmax("$VAR", alpha);
            } else {
                walk_unmax("_", alpha);
            }
        }
        return Machine.exec_unify(args[2], res);
    }

    private static Object walk_replace(String functor, Number beta, Object first) {
        List stack = null;
        Map map = null;
        for (; ; ) {
            first = Store.deref(first);
            if (Store.is_structure(first)) {
                if (functor.equals(((Store.Structure) first).functor) &&
                        ((Store.Structure) first).args.length == 1) {
                    Object peek = Store.deref(((Store.Structure) first).args[0]);
                    if (Machine.is_integer(peek) &&
                            eval.integer_compare(beta, (Number) peek) <= 0) {
                        first = assoc_get(map, peek);
                        if (first == null) {
                            first = new Store.Variable();
                            map = assoc_set(map, peek, first);
                        }
                    }
                }
                if (Store.is_structure(first)) {
                    if (!Store.is_compound(((Store.Structure) first).functor)) {
                        ((Store.Structure) first).functor = new Store.Compound(((Store.Structure) first).functor,
                                new Object[((Store.Structure) first).args.length]);
                        Store.Item item = new Store.Item((Store.Structure)first, null, 0);
                        stack = Store.stack_push(stack, item);
                        first = ((Store.Structure) first).args[0];
                        continue;
                    } else {
                        first = ((Store.Structure) first).functor;
                    }
                }
            }
            Store.Item item = (Store.Item) Store.stack_peek(stack);
            while (item != null &&
                    item.idx  == item.first.args.length - 1) {
                ((Store.Structure)item.first.functor).args[item.idx] = first;
                first = item.first.functor;
                Store.stack_pop(stack);
                item = (Store.Item) Store.stack_peek(stack);
            }
            if (item == null) {
                return first;
            } else {
                ((Store.Structure)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.
     */
    private static boolean test_maxvars(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object res;
        try {
            res = walk_max("$VAR", alpha);
        } finally {
            walk_unmax("$VAR", alpha);
        }
        return Machine.exec_unify(args[1], res);
    }

    private static Number walk_max(String functor, Object first) {
        List stack = null;
        Number res = Integer.valueOf(-1);
        for (; ; ) {
            first = Store.deref(first);
            if (Store.is_structure(first)) {
                if (functor.equals(((Store.Structure) first).functor) &&
                        ((Store.Structure) first).args.length == 1) {
                    Object peek = Store.deref(((Store.Structure) first).args[0]);
                    if (Machine.is_integer(peek)) {
                        if (eval.integer_compare(res, (Number) peek) < 0)
                            res = (Number) peek;
                        first = null;
                    }
                }
                if (first != null) {
                    if (!Store.is_compound(((Store.Structure) first).functor)) {
                        ((Store.Structure) first).functor = new Store.Compound(
                                ((Store.Structure) first).functor, Machine.VOID_ARGS);
                        if (0 != ((Store.Structure) first).args.length - 1) {
                            Store.Item item = new Store.Item((Store.Structure) first, null, 0);
                            stack = Store.stack_push(stack, item);
                        }
                        first = ((Store.Structure) first).args[0];
                        continue;
                    }
                }
            }
            Store.Item item = (Store.Item) Store.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)
                    Store.stack_pop(stack);
            }
        }
    }

    private static void walk_unmax(String functor, Object first) {
        List stack = null;
        for (; ; ) {
            first = Store.deref(first);
            if (Store.is_structure(first)) {
                if (functor.equals(((Store.Structure) first).functor) &&
                        ((Store.Structure) first).args.length == 1) {
                    Object peek = Store.deref(((Store.Structure) first).args[0]);
                    if (Machine.is_integer(peek)) {
                        first = null;
                    }
                }
                if (first != null) {
                    if (Store.is_compound(((Store.Structure) first).functor)) {
                        ((Store.Structure) first).functor = ((Store.Structure) ((Store.Structure) first).functor).functor;
                        if (0 != ((Store.Structure) first).args.length - 1) {
                            Store.Item item = new Store.Item((Store.Structure) first, null, 0);
                            stack = Store.stack_push(stack, item);
                        }
                        first = ((Store.Structure) first).args[0];
                        continue;
                    }
                }
            }
            Store.Item item = (Store.Item) Store.stack_peek(stack);
            if (item == null) {
                return;
            } else {
                item.idx++;
                first = item.first.args[item.idx];
                if (item.idx == item.first.args.length - 1)
                    Store.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.
     */
    private static Object special_calln(Object[] args) {
        Object alpha = Store.deref(args[0]);
        Object functor;
        Object[] oldargs;
        if (Store.is_structure(alpha)) {
            functor = ((Store.Structure) alpha).functor;
            oldargs = ((Store.Structure) alpha).args;
        } else {
            functor = alpha;
            special.check_atomic(alpha);
            oldargs = Machine.VOID_ARGS;
        }
        int arity = oldargs.length + args.length - 1;
        Object goal;
        if (arity == 0) {
            goal = functor;
        } else {
            if (Machine.is_number(functor))
                throw Machine.make_error(new Store.Compound("type_error",
                        new Object[]{"atom", functor}));
            Object[] newargs = new Object[arity];
            for (int i = 0; i < oldargs.length; i++)
                newargs[i] = Store.deref(oldargs[i]);
            for (int i = 0; i < args.length - 1; i++)
                newargs[i + oldargs.length] = Store.deref(args[i + 1]);
            goal = new Store.Compound(functor, newargs);
        }
        goal = new Store.Compound(".", new Object[]{goal,
                ((Store.Structure) Machine.call).args[1]});
        Machine.cont(goal);
        return Boolean.TRUE;
    }

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

    public static void main() {
        Store.add("numbervars", 3, special.make_check(asmlib::test_numbervars));
        Store.add("unnumbervars", 3, special.make_check(asmlib::test_unnumbervars));
        Store.add("maxvars", 2, special.make_check(asmlib::test_maxvars));
        Store.add("call", 2, special.make_special(asmlib::special_calln));
        Store.add("call", 3, special.make_special(asmlib::special_calln));
        Store.add("call", 4, special.make_special(asmlib::special_calln));
        Store.add("call", 5, special.make_special(asmlib::special_calln));
        Store.add("call", 6, special.make_special(asmlib::special_calln));
        Store.add("call", 7, special.make_special(asmlib::special_calln));
        Store.add("call", 8, special.make_special(asmlib::special_calln));
    }

}
