/**
 * 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 {
    exec_build, is_variable, exec_unify, make_check,
    add, set_to_list, widen_bigint, exec_eval,
    norm_bigint, VAR_MASK_SEEN, make_arithmetic,
    check_integer, walk_compute, walk_uncompute,
    walk_vars, Compound, is_bigint, make_error
} from "../nova/core.mjs";

/*********************************************************************/
/* term_singletons2/2                                                */
/*********************************************************************/

/**
 * term_singletons(T, L):
 * The built-in succeeds in L with the singleton variables of T.
 */
function test_term_singletons(args) {
    let alpha = exec_build(args[0]);
    let res;
    try {
        res = walk_compute(alpha, init_map, union_map);
    } finally {
        walk_uncompute(alpha);
    }
    for (let [key, val] of res) {
        if (val)
            res.delete(key);
    }
    res = set_to_list(res.keys());
    return exec_unify(args[1], res);
}

let EMPTY_MAP = new Map();

function init_map(first) {
    if (is_variable(first)) {
        return new Map([[first, false]]);
    } else {
        return EMPTY_MAP;
    }
}

function union_map(first, second) {
    if (first.size === 0)
        return second;
    if (second.size === 0)
        return first;
    first = new Map(first);
    for (let [key, val] of second) {
        let value = first.get(key);
        if (value !== undefined) {
            value = true;
        } else {
            value = val;
        }
        first.set(key, value);
    }
    return first;
}

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

/**
 * ground(T): [TC2 8.3.10]
 * The built-in succceeds if T is ground.
 */
function test_ground(args) {
    let alpha = exec_build(args[0]);
    let res = walk_vars(alpha, node => true, VAR_MASK_SEEN);
    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.
 */
function test_nonground(args) {
    let alpha = exec_build(args[0]);
    let hit = undefined;

    function nonground2(node) {
        hit = node;
        return true;
    }

    let res = walk_vars(alpha, nonground2, VAR_MASK_SEEN);
    walk_vars(alpha, node => true, 0);
    return res && exec_unify(args[1], hit);
}

/*********************************************************************/
/* 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.
 */
function test_divmod(args) {
    let alpha = exec_build(args[0]);
    check_integer(alpha);
    let beta = exec_build(args[1]);
    check_integer(beta);
    let resdiv;
    let resmod;
    if (!is_bigint(alpha) && !is_bigint(beta)) {
        if (beta === 0)
            throw make_error(new Compound("evaluation_error",["zero_divisor"]));
        resdiv = Math.floor(alpha / beta);
        resmod = alpha - resdiv*beta;
    } else {
        alpha = widen_bigint(alpha);
        beta = widen_bigint(beta);
        if (beta === 0n)
            throw make_error(new Compound("evaluation_error",["zero_divisor"]));
        resdiv = bigint_div(alpha, beta);
        resmod = alpha - resdiv*beta;
        resdiv = norm_bigint(resdiv);
        resmod = norm_bigint(resmod);
    }
    if (!exec_unify(args[2], resdiv))
        return false;
    return exec_unify(args[3], resmod);
}

function bigint_div(alpha, beta) {
    let temp = alpha / beta;
    if ((alpha < 0n) !== (beta < 0n)) {
        let res = alpha % beta;
        if (res !== 0n)
            temp--;
    }
    return temp;
}

/**
 * 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.
 */
function arit_gcd(args) {
   let alpha = exec_eval(args[0]);
   check_integer(alpha);
   let beta = exec_eval(args[1]);
   check_integer(beta);
   if (!is_bigint(alpha) && !is_bigint(beta)) {
       return intGcd(alpha, beta);
   } else {
       return norm_bigint(
                bigIntGcd(widen_bigint(alpha),
                            widen_bigint(beta)));
   }
}

function intGcd(a, b) {
    a = Math.abs(a);
    b = Math.abs(b);
    while (b !== 0) {
        let h = a % b;
        a = b;
        b = h;
    }
    return a;
}

function bigIntGcd(a, b) {
    a = (a < 0n ? -a : a);
    b = (b < 0n ? -b : b);
    while (b !== 0n) {
        let h = a % b;
        a = b;
        b = h;
    }
    return a;
}

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

export function main() {
    add("term_singletons", 2, make_check(test_term_singletons));
    add("ground", 1, make_check(test_ground));
    add("nonground", 2, make_check(test_nonground));
    add("divmod", 4, make_check(test_divmod));
    add("gcd", 3, make_arithmetic(arit_gcd));
}
