package doglet;

import nova.*;

import java.math.BigInteger;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * 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 fastlib {

    /******************************************************************/
    /* term_singletons/2                                              */
    /******************************************************************/

    /**
     * term_singletons(T, L):
     * The built-in succeeds in L with the singleton variables of T.
     */
    private static boolean test_term_singletons(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object res;
        try {
            res = eval.walk_compute(alpha, fastlib::init_map, fastlib::union_map);
        } finally {
            eval.walk_uncompute(alpha);
        }
        Iterator it = ((Map)res).entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry)it.next();
            if (((Boolean)entry.getValue()).booleanValue())
                it.remove();
        }
        res = theatre.set_to_list(((Map)res).keySet());
        return Machine.exec_unify(args[1], res);
    }

    private static final Map EMPTY_MAP = new LinkedHashMap();

    private static Map init_map(Object first) {
        if (Store.is_variable(first)) {
            return Collections.singletonMap(first, Boolean.FALSE);
        } else {
            return EMPTY_MAP;
        }
    }

    private static Map union_map(Object alpha, Object beta) {
        Map first = (Map)alpha;
        Map second = (Map)beta;
        if (first.size() == 0)
            return second;
        if (second.size() == 0)
            return first;
        first = new LinkedHashMap(first);
        Iterator it = second.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry)it.next();
            Object value = first.get(entry.getKey());
            if (value != null) {
                value = Boolean.TRUE;
            } else {
                value = entry.getValue();
            }
            first.put(entry.getKey(), value);
        }
        return first;
    }

    /******************************************************************/
    /* ground/1 and nonground/2                                       */
    /******************************************************************/

    /**
     * ground(T): [TC2 8.3.10]
     * The built-in succceeds if T is ground.
     */
    private static boolean test_ground(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        boolean res = special.walk_vars(alpha, node -> true, Store.VAR_MASK_SEEN);
        special.walk_vars(alpha, node -> true, 0);
        return !res;
    }

    /**
     * nonground(T, V):
     * The built-in succeeds if T is non-ground and V is the first variable.
     */
    private static boolean test_nonground(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        nonground2 h = new nonground2();
        boolean res = special.walk_vars(alpha, h::run, Store.VAR_MASK_SEEN);
        special.walk_vars(alpha, node -> true, 0);
        return res && Machine.exec_unify(args[1], h.hit);
    }

    private static class nonground2 {
        private Object hit = null;

        public boolean run(Object node) {
            hit = node;
            return true;
        }

    }

    /******************************************************************/
    /* divmod/4 and gcd/3                                             */
    /******************************************************************/

    /**
     * divmod(X, Y, Z, T):
     * If X and Y are both integers then the predicate succeeds in
     * Z with the division of X by Y, and in T with the modulo of X by Y.
     */
    private static boolean test_divmod(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        special.check_integer(alpha);
        Object beta = Machine.exec_build(args[1]);
        special.check_integer(beta);
        Number resdiv;
        Number resmod;
        if (alpha instanceof Integer && beta instanceof Integer) {
            int u = ((Integer) beta).intValue();
            if (u == 0)
                throw Machine.make_error(new Store.Compound("evaluation_error",
                        new Object[]{"zero_divisor"}));
            int v = ((Integer) alpha).intValue();
            if (v == Integer.MIN_VALUE && u == -1) {
                resdiv = eval.NEG_MIN_INTEGER;
                resmod = Integer.valueOf(0);
            } else {
                int[] res2 = intDivMod(v, u);
                resdiv = Integer.valueOf(res2[0]);
                resmod = Integer.valueOf(res2[1]);
            }
        } else {
            BigInteger p = special.widen_bigint((Number) beta);
            if (p.signum() == 0)
                throw Machine.make_error(new Store.Compound("evaluation_error",
                        new Object[]{"zero_divisor"}));
            BigInteger[] res2 = bigDivMod(special.widen_bigint((Number) alpha), p);
            resdiv = special.norm_bigint(res2[0]);
            resmod = special.norm_bigint(res2[1]);
        }
        if (!Machine.exec_unify(args[2],resdiv))
            return false;
        return Machine.exec_unify(args[3],resmod);
    }

    private static int[] intDivMod(int v, int u) {
        int[] res = new int[2];
        res[0] = v / u;
        res[1] = v % u;
        if ((v < 0) != (u < 0)) {
            if (res[1] != 0) {
                res[0]--;
                res[1] += u;
            }
        }
        return res;
    }

    private static BigInteger[] bigDivMod(BigInteger v, BigInteger u) {
        BigInteger[] res = v.divideAndRemainder(u);
        if ((v.signum() < 0) != (u.signum() < 0)) {
            if (res[1].signum() != 0) {
                res[0] = res[0].subtract(BigInteger.ONE);
                res[1] = res[1].add(u);
            }
        }
        return res;
    }

    /**
     * gcd(X, Y, Z):
     * If X and Y are integers then the predicate succeeds in Z
     * with the greatest common divisor of X and Y.
     */
    private static Number arit_gcd(Object[] args) {
        Number alpha = Machine.exec_eval(args[0]);
        special.check_integer(alpha);
        Number beta = Machine.exec_eval(args[1]);
        special.check_integer(beta);
        if (alpha instanceof Integer && beta instanceof Integer) {
             return Integer.valueOf(intGcd(alpha.intValue(),
                    beta.intValue()));
        } else {
            return special.norm_bigint(
                    special.widen_bigint(alpha).gcd(
                            special.widen_bigint(beta)));
        }
    }

    private static int intGcd(int a, int b) {
        a = Math.abs(a);
        b = Math.abs(b);
        while (b != 0) {
            int h = Math.abs(a % b);
            a = b;
            b = h;
        }
        return a;
    }

    /******************************************************************/
    /* Fast Lib Init                                                  */
    /******************************************************************/

    public static void main() {
        Store.set("term_singletons", 2, special.make_check(fastlib::test_term_singletons));
        Store.set("ground", 1, special.make_check(fastlib::test_ground));
        Store.set("nonground", 2, special.make_check(fastlib::test_nonground));
        Store.set("divmod", 4, special.make_check(fastlib::test_divmod));
        Store.set("gcd", 3, special.make_arithmetic(fastlib::arit_gcd));
    }

}
