package doglet;

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

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

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

    /***************************************************************/
    /* @</2, @=</2, @>/2, @>=/2 and compare/3                      */
    /***************************************************************/

    /**
     * X @< Y: [ISO 8.4.1]
     * The predicate succeeds when X is syntactically less than Y, otherwise fails.
     */
    private static boolean test_less(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object beta = Machine.exec_build(args[1]);
        return compare_term(alpha, beta) < 0;
    }

    /**
     * X @=< Y: [ISO 8.4.1]
     * The predicate succeeds when X is syntactically less or equal to Y, otherwise fails.
     */
    private static boolean test_lessequal(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object beta = Machine.exec_build(args[1]);
        return compare_term(alpha, beta) <= 0;
    }

    /**
     * X @>= Y: [ISO 8.4.1]
     * The predicate succeeds when X is syntactically greater or equal to Y, otherwise fails.
     */
    private static boolean test_greaterequal(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object beta = Machine.exec_build(args[1]);
        return compare_term(alpha, beta) >= 0;
    }

    /**
     * X @> Y: [ISO 8.4.1]
     * The predicate succeeds when X is syntactically greater than Y, otherwise fails.
     */
    private static boolean test_greater(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object beta = Machine.exec_build(args[1]);
        return compare_term(alpha, beta) > 0;
    }

    /**
     * compare(C, X, Y): [TC2 8.4.2]
     * The predicate succeeds when C unifies with the result of comparing
     * X to Y. The result is one of the following atoms <, = or >.
     */
    private static boolean test_compare(Object[] args) {
        Object beta = Machine.exec_build(args[1]);
        Object gamma = Machine.exec_build(args[2]);
        int k = compare_term(beta, gamma);
        if (k < 0) {
            beta = "<";
        } else if (k == 0) {
            beta = "=";
        } else {
            beta = ">";
        }
        return Machine.exec_unify(args[0], beta);
    }

    /**
     * Determine the syntactic relationship between two Prolog terms.
     * Can handle cyclic terms and deep recursion.
     * Has left to right anomaly.
     *
     * @param first  The first Prolog term.
     * @param second The second Prolog term.
     * @return <0 for less, =0 for equal and >0 for greater
     */
    public static int compare_term(Object first, Object second) {
        List stack = null;
        List log = null;
        try {
            for (; ; ) {
                first = Store.deref(first);
                second = Store.deref(second);
                if (!Store.is_structure(first)) {
                    if (!Objects.equals(first, second))
                        break;
                } else if (!Store.is_structure(second)) {
                    break;
                } else if (((Store.Structure) first).args.length !=
                        ((Store.Structure) second).args.length) {
                    break;
                } else {
                    first = Machine.union_find((Store.Structure) first);
                    second = Machine.union_find((Store.Structure) second);
                    if (first != second) {
                        if (!((Store.Structure) first).functor.equals(
                                ((Store.Structure) second).functor))
                            break;
                        log = Machine.union_add(log, (Store.Structure) first,
                                (Store.Structure) second);
                        if (0 != ((Store.Structure) first).args.length - 1) {
                            Store.Item item = new Store.Item((Store.Structure)first, second, 0);
                            stack = Store.stack_push(stack, item);
                        }
                        first = ((Store.Structure) first).args[0];
                        second = ((Store.Structure) second).args[0];
                        continue;
                    }
                }
                Store.Item item = (Store.Item)Store.stack_peek(stack);
                if (item == null) {
                    return 0;
                } else {
                    item.idx++;
                    first = item.first.args[item.idx];
                    second = ((Store.Structure)item.second).args[item.idx];
                    if (item.idx == item.first.args.length - 1)
                        Store.stack_pop(stack);
                }
            }
            return compare_truncated(first, second);
        } finally {
            Machine.union_undo(log);
        }
    }

    /**
     * Determine the syntactic relationship between truncated Prolog terms.
     * Prolog compounds are truncated to their arity and functor.
     *
     * @param first  The first Prolog term.
     * @param second The second Prolog term.
     * @return <0 for less, =0 for equal and >0 for greater
     */
    private static int compare_truncated(Object first, Object second) {
        int i = compare_type(first);
        int k = i - compare_type(second);
        if (k != 0)
            return k;
        switch (i) {
            case 0:
                return Store.variable_serno((Store.Variable)first) -
                        Store.variable_serno((Store.Variable)second);
            case 2:
                return ((Double) first).compareTo((Double) second);
            case 3:
                return eval.integer_compare((Number) first, (Number) second);
            case 9:
                return ((String) first).compareTo((String) second);
            case 10:
                k = ((Store.Structure) first).args.length -
                        ((Store.Structure) second).args.length;
                if (k != 0)
                    return k;
                return ((String) ((Store.Structure) first).functor).compareTo(
                        (String) ((Store.Structure) second).functor);
            default:
                return 0;
        }
    }

    private static int compare_type(Object first) {
        if (Store.is_variable(first)) {
            return 0;
        } else if (Store.is_structure(first)) {
            return 10;
        } else if (Machine.is_atom(first)) {
            return 9;
        } else if (Machine.is_number(first)) {
            if (Machine.is_integer(first)) {
                return 3;
            } else if (Machine.is_special(first)) {
                if (eval.DOUBLE_NINF.equals(first)) {
                    return 1;
                } else if (eval.DOUBLE_PINF.equals(first)) {
                    return 4;
                } else if (eval.DOUBLE_NAN.equals(first)) {
                    return 5;
                }
            } else if (Machine.is_float(first)) {
                return 2;
            }
        } else if (first == Boolean.FALSE) {
            return 7;
        } else if (first == Boolean.TRUE) {
            return 8;
        } else if (first == null) {
            return 6;
        }
        throw Machine.make_error(new Store.Compound("resource_error",
                new Object[]{"not_supported"}));
    }

    /******************************************************************/
    /* sort/2 and keysort/2                                           */
    /******************************************************************/

    /**
     * sort(L, R): [TC2 8.4.3]
     * The predicate succeeds in R with the sorted list L.
     */
    private static boolean test_sort(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object[] res = special.list_objects(alpha);
        Arrays.sort(res, isolib::compare_term);
        int count = objects_dedup(res);
        return Machine.exec_unify(args[1], special.objects_list(res, 0, count));
    }

    private static int objects_dedup(Object[] res) {
        int j = 0;
        int i = 0;
        while (i < res.length) {
            Object alpha = res[i++];
            while (i < res.length && eval.equal_term(alpha, res[i]))
                i++;
            res[j++] = alpha;
        }
        return j;
    }

    /**
     * keysort(L, R): [TC2 8.4.4]
     * The predicate succeeds in R with the key sorted list L.
     */
    private static boolean test_keysort(Object[] args) {
        Object alpha = Machine.exec_build(args[0]);
        Object[] res = special.list_objects(alpha);
        objects_pairs(res);
        Arrays.sort(res, (first, second) -> compare_term(get_key(first), get_key(second)));
        return Machine.exec_unify(args[1], special.objects_list(res, 0, res.length));
    }

    private static void objects_pairs(Object[] res) {
        for (int i = 0; i < res.length; i++) {
            Object alpha = res[i];
            if (Store.is_structure(alpha) &&
                    "-".equals(((Store.Structure) alpha).functor) &&
                    ((Store.Structure) alpha).args.length == 2) {
                /* */
            } else {
                Store.check_nonvar(alpha);
                alpha = Store.copy_term(alpha);
                throw Machine.make_error(new Store.Compound("type_error",
                        new Object[]{"pair", alpha}));
            }
        }
    }

    private static Object get_key(Object peek) {
        return ((Store.Structure) peek).args[0];
    }

    /******************************************************************/
    /* Iso Lib Init                                                   */
    /******************************************************************/

    public static void main() {
        // term specials, syntactic comparison
        Store.set("@<", 2, special.make_check(isolib::test_less));
        Store.set("@=<", 2, special.make_check(isolib::test_lessequal));
        Store.set("@>=", 2, special.make_check(isolib::test_greaterequal));
        Store.set("@>", 2, special.make_check(isolib::test_greater));
        Store.set("compare", 3, special.make_check(isolib::test_compare));

        // list specials, miscellaneous sorting
        Store.set("sort", 2, special.make_check(isolib::test_sort));
        Store.set("keysort", 2, special.make_check(isolib::test_keysort));
    }

}
