###
# 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.
##

from nova.store import (set, is_variable, Compound, deref,
     stack_push, stack_pop, stack_peek, is_structure,
     VAR_MASK_SERNO, variable_serno, Item)
import nova.machine as machine
from nova.machine import (cont, exec_build, int32,
     exec_eval, exec_unify, Choice, more, is_compound,
     union_find, union_add, is_integer, is_frozen,
     make_error, unbind, union_undo, exec_deref, object_hash_code,
     is_pending, unify, object_equals, is_special)
from nova.special import (check_integer, MAX_ARITY, make_check,
     check_nil, check_atom, narrow_float, make_special,
     make_arithmetic, norm_float)
import math
import sys


################################################################
# is/2, (-)/2, abs/2 and sign/2                                #
################################################################

###
# X is Y: [ISO 8.6.1]
# The predicate succeeds in X with the evaluation of Y.
##
def test_eval(args):
    try:
        res = exec_eval(args[1])
        return exec_unify(args[0], res)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# -(A, B): [ISO 9.1.7]
# The predicate succeeds in B with the negation of A.
# This implements NaN propagation IEEE 754-2019 §6.2.3.
##
def arit_neg(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        return -alpha
    else:
        return 0 - alpha


###
# abs(A, B): [ISO 9.1.7]
# The predicate succeeds in B with the absolute value of A.
##
def arit_abs(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        return abs(alpha)
    else:
        return math.fabs(narrow_float(alpha))


###
# sign(A, B): [ISO 9.1.4]
# The predicate succeeds in B with the sign of A.
##
def arit_sign(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        if alpha < 0:
            return -1
        if alpha == 0:
            return 0
        return 1
    else:
        alpha = narrow_float(alpha)
        if alpha < 0.0:
            return -1.0
        if alpha == 0.0:
            return 0.0
        return 1.0


################################################################
# (+)/3, (-)/3, (*)/3, (/)/3, (//)/3 and (rem)/3.       #
################################################################

###
# +(A, B, C): [ISO 9.1.7]
# The predicate succeeds in C with the sum of A and B.
##
def arit_add(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    if is_integer(alpha) and is_integer(beta):
        return alpha + beta
    else:
        return norm_float(narrow_float(alpha) + narrow_float(beta))


###
# -(A, B, C):
# The predicate succeeds in C with A subtracted by B.
##
def arit_sub(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    if is_integer(alpha) and is_integer(beta):
        return alpha - beta
    else:
        return norm_float(narrow_float(alpha) - narrow_float(beta))


###
# *(A, B, C): [ISO 9.1.7]
# The predicate succeeds in C with the product of A and B.
##
def arit_mul(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    if is_integer(alpha) and is_integer(beta):
        return alpha * beta
    else:
        return norm_float(narrow_float(alpha) * narrow_float(beta))


###
# /(A, B, C): [ISO 9.1.7]
# The predicate succeeds in C with A float divided by B.
##
def arit_quot(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    beta = narrow_float(beta)
    if beta == 0.0:
        raise make_error(Compound("evaluation_error", ["zero_divisor"]))
    return norm_float(narrow_float(alpha) / beta)


###
# //(A, B, C): [ISO 9.1.7]
# The predicate succeeds in C with A truncate divided by B.
##
def arit_intquot(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    if beta == 0:
        raise make_error(Compound("evaluation_error", ["zero_divisor"]))
    res = abs(alpha) // abs(beta)
    if (alpha < 0) != (beta < 0):
        return -res
    else:
        return res


###
# rem(A, B, C): [ISO 9.1.7]
# The predicate succeeds in C with A remainder B.
##
def arit_rem(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    if beta == 0:
        raise make_error(Compound("evaluation_error", ["zero_divisor"]))
    res = abs(alpha) % abs(beta)
    if alpha < 0:
        return -res
    else:
        return res


################################################################
# float/2, ^/3, div/3 and mod/3                                #
################################################################

###
# float(A, B): [ISO 9.17]
# The predicate succeeds in B with the approximated A.
##
def arit_float(args):
    alpha = exec_eval(args[0])
    return narrow_float(alpha)


###
# ^(A, B, C): [TC2 9.3.10]
# The predicate succeeds in C with A int power by B.
##
def arit_intpow(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    if is_integer(alpha) and is_integer(beta):
        if beta < 0:
            raise make_error(Compound("domain_error",
                 ["not_less_than_zero", beta]))
        return alpha ** beta
    else:
        return norm_float(math.pow(narrow_float(alpha), narrow_float(beta)))


###
# div(A, B, C): [TC2 9.1.3]
# The predicate succeeds in C with A floor divided by B.
##
def arit_div(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    if beta == 0:
        raise make_error(Compound("evaluation_error", ["zero_divisor"]))
    return alpha // beta


###
# mod(A, B, C): [ISO 9.1.7]
# The predicate succeeds in C with A modulus B.
##
def arit_mod(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    if beta == 0:
        raise make_error(Compound("evaluation_error", ["zero_divisor"]))
    return alpha % beta


################################################################
# min/3, max/3, inf/1 and nan/1                                #
################################################################

###
# min(A, B, C): [TC2 9.3.9]
# The predicate succeeds in C with the minimum of A and B.
# This implements minimumNumber IEEE 754-2019 §9.6.
##
def arit_min(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    if is_special(alpha) or is_special(beta):
        if math.isnan(alpha):
            return beta
        if math.isnan(beta):
            return alpha
        if alpha == math.inf:
            return beta
        if beta == math.inf:
            return alpha
        if alpha == -math.inf:
            return alpha
        return beta
    elif is_integer(alpha) and is_integer(beta):
        return min(alpha, beta)
    else:
        return min(narrow_float(alpha), narrow_float(beta))


###
# max(A, B, C): [TC2 9.3.8]
# The predicate succeeds in C with the maximum of A and B.
# This implements maximumNumber IEEE 754-2019 §9.6.
##
def arit_max(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    if is_special(alpha) or is_special(beta):
        if math.isnan(alpha):
            return beta
        if math.isnan(beta):
            return alpha
        if alpha == -math.inf:
            return beta
        if beta == -math.inf:
            return alpha
        if alpha == math.inf:
            return alpha
        return beta
    elif is_integer(alpha) and is_integer(beta):
        return max(alpha, beta)
    else:
        return max(narrow_float(alpha), narrow_float(beta))


###
# inf(A):
# The predicate succeeds in A with positive infinity.
##
def arit_inf(args):
    return math.inf


###
# nan(A):
# The predicate succeeds in A with not a number.
##
def arit_nan(args):
    return math.nan


################################################################
# truncate/2, floor/2, ceiling/2 and round/2.                  #
################################################################

###
# truncate(A, B): [ISO 9.1.7]
# The predicate succeeds in B with the truncate of A.
##
def arit_truncate(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        return alpha
    else:
        return math.trunc(narrow_float(alpha))


###
# floor(A, B): [ISO 9.1.7]
# The predicate succeeds in B with the floor of A.
##
def arit_floor(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        return alpha
    else:
        return math.floor(narrow_float(alpha))


###
# ceiling(A, B): [ISO 9.1.7]
# The predicate succeeds in B with the ceiling of A.
##
def arit_ceiling(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        return alpha
    else:
        return math.ceil(narrow_float(alpha))


###
# round(A, B): [ISO 9.1.7]
# The predicate succeeds in B with the round of A.
##
def arit_round(args):
    alpha = exec_eval(args[0])
    if is_integer(alpha):
        return alpha
    else:
        return (math.floor(2*narrow_float(alpha))+1) >> 1


################################################################
# =:=/2, =\=/2, </2, >=/2, >/2 and =</2                        #
################################################################

###
# X =:= Y: [ISO 8.7.1]
# The predicate succeeds when X number equals Y, otherwise fails.
##
def test_numberequal(args):
    try:
        alpha = exec_eval(args[0])
        beta = exec_eval(args[1])
        return number_equal(alpha, beta)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# X =\= Y: [ISO 8.7.1]
# The predicate succeeds when X does not number equal Y, otherwise fails.
##
def test_numbernotequal(args):
    try:
        alpha = exec_eval(args[0])
        beta = exec_eval(args[1])
        return not number_equal(alpha, beta)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# Determine whether two Prolog numbers are equal.
# This implement compareQuietEqual IEEE 754-2019 §5.6.1.
#
# @param alpha The first Prolog number.
# @param beta The second Prolog number.
# @return True if the two Prolog numbers are equal.
##
def number_equal(alpha, beta):
    if is_special(alpha) or is_special(beta):
        if math.isnan(alpha):
            return False
        if math.isnan(beta):
            return False
        return alpha == beta
    elif is_integer(alpha) and is_integer(beta):
        return alpha == beta
    else:
        return narrow_float(alpha) == narrow_float(beta)


###
# X < Y: [ISO 8.7.1]
# The predicate succeeds when X is number less than Y, otherwise fails.
##
def test_numberless(args):
    try:
        alpha = exec_eval(args[0])
        beta = exec_eval(args[1])
        return number_less(alpha, beta)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# X >= Y: [ISO 8.7.1]
# The predicate succeeds when X is number greater or equal to Y, otherwise fails.
##
def test_numbergreaterequal(args):
    try:
        alpha = exec_eval(args[0])
        beta = exec_eval(args[1])
        return number_lessequal(beta, alpha)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# X > Y: [ISO 8.7.1]
# The predicate succeeds when X is number greater than Y, otherwise fails.
##
def test_numbergreater(args):
    try:
        alpha = exec_eval(args[0])
        beta = exec_eval(args[1])
        return number_less(beta, alpha)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# X =< Y: [ISO 8.7.1]
# The predicate succeeds when X is number less or equal to Y, otherwise fails.
##
def test_numberlessequal(args):
    try:
        alpha = exec_eval(args[0])
        beta = exec_eval(args[1])
        return number_lessequal(alpha, beta)
    except OverflowError:
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    except ValueError:
        raise make_error(Compound("evaluation_error", ["undefined"]))


###
# Determine whether a Prolog numbers is less than a Prolog number.
# This implement compareQuietLess IEEE 754-2019 §5.6.1.
#
# @param alpha The first Prolog number.
# @param beta The second Prolog number.
# @return True if the first is less than the second, otherwise false.
##
def number_less(alpha, beta):
    if is_special(alpha) or is_special(beta):
        if math.isnan(alpha):
            return False
        if math.isnan(beta):
            return False
        if alpha == beta:
            return False
        if beta == math.inf:
            return True
        return alpha == -math.inf
    if is_integer(alpha) and is_integer(beta):
        return alpha < beta
    else:
        return narrow_float(alpha) < narrow_float(beta)


###
# Determine whether a Prolog numbers is less or equal than a Prolog number.
# This implement compareQuietLessEqual IEEE 754-2019 §5.6.1.
#
# @param alpha The first Prolog number.
# @param beta The second Prolog number.
# @return True if the first is less or equal than the second, otherwise false.
##
def number_lessequal(alpha, beta):
    if is_special(alpha) or is_special(beta):
        if math.isnan(alpha):
            return False
        if math.isnan(beta):
            return False
        if alpha == beta:
            return True
        if beta == math.inf:
            return True
        return alpha == -math.inf
    if is_integer(alpha) and is_integer(beta):
        return alpha <= beta
    else:
        return narrow_float(alpha) <= narrow_float(beta)


################################################################
# sin/3, cos/2, tan/2, asin/2, acos/2, atan/2 and pi/1.        #
################################################################

###
# sin(A, B): [ISO 9.3.2]
# The predicate succeeds in B with the sine of A.
##
def arit_sin(args):
    alpha = exec_eval(args[0])
    return norm_float(math.sin(narrow_float(alpha)))


###
# cos(A, B): [ISO 9.3.3]
# The predicate succeeds in B with the cosine of A.
##
def arit_cos(args):
    alpha = exec_eval(args[0])
    return norm_float(math.cos(narrow_float(alpha)))


###
# tan(A, B): [TC2 9.3.14]
# The predicate succeeds in B with the tangent of A.
##
def arit_tan(args):
    alpha = exec_eval(args[0])
    return norm_float(math.tan(narrow_float(alpha)))


###
# asin(A, B): [TC2 9.3.11]
# The predicate succeeds in B with the arcus sine of A.
##
def arit_asin(args):
    alpha = exec_eval(args[0])
    return norm_float(math.asin(narrow_float(alpha)))


###
# acos(A, B): [TC2 9.3.12]
# The predicate succeeds in B with the arcus cosine of A.
##
def arit_acos(args):
    alpha = exec_eval(args[0])
    return norm_float(math.acos(narrow_float(alpha)))


###
# atan(A, B): [ISO 9.3.4]
# The predicate succeeds in B with the arcus tangent of A.
##
def arit_atan(args):
    alpha = exec_eval(args[0])
    return norm_float(math.atan(narrow_float(alpha)))


###
# pi(A): [TC2 9.3.15]
# The predicate succeeds in A with π.
##
def arit_pi(args):
    return math.pi


################################################################
# (**)/3, exp/2, log/2, sqrt/2, e/1, epsilon/1 and atan2/3     #
################################################################

###
# **(A, B, C): [ISO 9.3.1]
# The predicate succeeds in C with A float power by B.
##
def arit_pow(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    return norm_float(math.pow(narrow_float(alpha), narrow_float(beta)))


###
# exp(A, B): [ISO 9.3.5]
# The predicate succeeds in B with e power by A.
##
def arit_exp(args):
    alpha = exec_eval(args[0])
    return norm_float(math.exp(narrow_float(alpha)))


###
# log(A, B): [ISO 9.3.6]
# The predicate succeeds in B with the natural logarithm of A.
##
def arit_log(args):
    alpha = exec_eval(args[0])
    return norm_float(math.log(narrow_float(alpha)))


###
# sqrt(A, B): [ISO 9.3.7]
# The predicate succeeds in B with the square root of A.
##
def arit_sqrt(args):
    alpha = exec_eval(args[0])
    return norm_float(math.sqrt(narrow_float(alpha)))


###
# e(A): [N208 9.7.2]
# The predicate succeeds in A with the Euler number.
##
def arit_e(args):
    return math.e


###
# epsilon(A): [N208 9.7.3]
# The predicate succeeds in A with the machine epsilon.
##
def arit_epsilon(args):
    return sys.float_info.epsilon


###
# atan2(A, B, C): [TC2 9.3.13]
# The predicate succeeds in C with the arc tangent of A and B.
##
def arit_atan2(args):
    alpha = exec_eval(args[0])
    beta = exec_eval(args[1])
    alpha = narrow_float(alpha)
    beta = narrow_float(beta)
    if alpha == 0.0 and beta == 0.0:
        raise make_error(Compound("evaluation_error", ["undefined"]))
    else:
        return norm_float(math.atan2(alpha, beta))


################################################################
# (\)/2, (/\)/3, (\/)/3, (xor)/3, (>>)/3 and (<</3)            #
################################################################

###
# \(A, B): [ISO 9.4.5]
# The predicate succeeds in B with the bitwise not of A.
##
def arit_not(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    return ~alpha


###
# /\(A, B, C): [ISO 9.4.3]
# The predicate succeeds in C with the bitwise and of A and B.
##
def arit_and(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    return alpha & beta


###
# \/(A, B, C): [ISO 9.4.4]
# The predicate succeeds in C with the bitwise or of A and B.
##
def arit_or(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    return alpha | beta


###
# xor(A, B, C): [TC2 9.4.6]
# The predicate succeeds in C with the bitwise xor of A and B.
##
def arit_xor(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    return alpha ^ beta


###
# >>(A, B, C): [ISO 9.4.1]
# The predicate succeeds in C with A shift right by B.
##
def arit_shiftright(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    if beta >= 0:
        return alpha >> beta
    else:
        return alpha << (- beta)


###
# <<(A, B, C): [ISO 9.4.2]
# The predicate succeeds in C with A shift left by B.
##
def arit_shiftleft(args):
    alpha = exec_eval(args[0])
    check_integer(alpha)
    beta = exec_eval(args[1])
    check_integer(beta)
    if beta >= 0:
        return alpha << beta
    else:
        return alpha >> (- beta)


################################################################
# ==/2 and \==/2                                               #
################################################################

###
# S == T: [ISO 8.4.1]
# The built-in succeeds when S and T are syntactically equivalent
# Prolog terms, otherwise the built-in fails.
##
def test_equal(args):
    alpha = exec_build(args[0])
    beta = exec_build(args[1])
    return equal_term(alpha, beta)


###
# S \== T: [ISO 8.4.1]
# The built-in succeeds when S and T are not syntactically equivalent
# Prolog terms, otherwise the built-in fails.
##
def test_notequal(args):
    alpha = exec_build(args[0])
    beta = exec_build(args[1])
    return not equal_term(alpha, beta)


###
# Determine whether two Prolog terms are syntactically equivalent.
# Can handle cyclic terms and deep recursion.
#
# @param first The first Prolog term.
# @param second The second Prolog term.
# @return True if they are syntactically equivalent, otherwise false.
##
def equal_term(first, second):
    stack = None
    log = None
    try:
        while True:
            first = deref(first)
            second = deref(second)
            if not is_structure(first):
                if not object_equals(first, second):
                    break
            elif not is_structure(second):
                break
            elif len(first.args) != len(second.args):
                break
            else:
                first = union_find(first)
                second = union_find(second)
                if first is not second:
                    if (is_frozen(first) and is_frozen(second) and
                            first.hash != second.hash):
                        break
                    if first.functor != second.functor:
                        break
                    log = union_add(log, first, second)
                    if 0 != len(first.args) - 1:
                        item = Item(first, second, 0)
                        stack = stack_push(stack, item)
                    first = first.args[0]
                    second = second.args[0]
                    continue
            item = stack_peek(stack)
            if item is None:
                return True
            else:
                item.idx += 1
                first = item.first.args[item.idx]
                second = item.second.args[item.idx]
                if item.idx == len(item.first.args) - 1:
                    stack_pop(stack)
        return False
    finally:
        union_undo(log)


################################################################
# Visitor Triple                                               #
################################################################

###
# Create a visitor triple.
#
# @param backup The backup functor.
# @param accum The accumlated value.
# @param children The argument children.
##
class Triple:
    def __init__(self, backup, accum, children):
        self.backup = backup
        self.accum = accum
        self.children = children


###
# Check whether an object is a visitor triple.
#
# @param obj The object.
# @return True if the object is a triple, otherwise false.
##
def is_triple(obj):
    return isinstance(obj, Triple)


################################################################
# test_term_hash/2                                             #
################################################################

###
# term_hash(X, H):
# The predicate succeeds in H with the term hash of the term X.
##
def test_term_hash(args):
    alpha = exec_build(args[0])
    try:
        res = walk_compute(alpha, init_hash, union_hash)
    finally:
        walk_uncompute(alpha)
    return exec_unify(args[1], res)


def init_hash(first):
    if is_compound(first):
        return object_hash_code(first.functor)
    elif is_variable(first):
        return variable_serno(first)
    elif is_frozen(first):
        return first.hash
    else:
        return object_hash_code(first)


def union_hash(first, second):
    return int32(first * 31 + second)


def walk_compute(first, init, reduce):
    stack = None
    while True:
        first = deref(first)
        if is_compound(first):
            if not is_triple(first.functor):
                first.functor = Triple(first.functor, init(first), None)
                first.walk &= ~VAR_MASK_SERNO
                stack = stack_push(stack, first)
                first = first.args[0]
                continue
            else:
                first = first.functor.accum
        else:
            first = init(first)
        item = stack_peek(stack)
        while (item is not None and
               (item.walk & VAR_MASK_SERNO) == len(item.args) - 1):
            first = reduce(item.functor.accum, first)
            item.functor.accum = first
            stack_pop(stack)
            item = stack_peek(stack)
        if item is None:
            return first
        else:
            first = reduce(item.functor.accum, first)
            item.functor.accum = first
            item.walk += 1
            first = item.args[item.walk & VAR_MASK_SERNO]


def walk_uncompute(first):
    stack = None
    while True:
        first = deref(first)
        if is_compound(first):
            if is_triple(first.functor):
                first.functor = first.functor.backup
                if 0 != len(first.args) - 1:
                    first.walk &= ~VAR_MASK_SERNO
                    stack = stack_push(stack, first)
                first = first.args[0]
                continue
        item = stack_peek(stack)
        if item is None:
            return
        else:
            item.walk += 1
            first = item.args[item.walk & VAR_MASK_SERNO]
            if (item.walk & VAR_MASK_SERNO) == len(item.args) - 1:
                stack_pop(stack)


################################################################
# atom_codes/2 and char_code/2                                 #
################################################################

###
# atom_codes(A, L): [ISO 8.16.5]
# If A is a variable, the built-in succeeds in A with the atom
# for the Prolog list L. Otherwise the built-in succeeds in L
# with the Prolog list from the atom A.
##
def test_atom_codes(args):
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        res = exec_build(args[1])
        res = atom_codes_pack(res)
        return exec_unify(text, res)
    else:
        text = exec_build(text)
        check_atom(text)
        text = atom_codes_unpack(text)
        return exec_unify(args[1], text)


def atom_codes_pack(peek):
    temp = peek
    i = 0
    while (is_structure(temp) and
           temp.functor == "." and
           len(temp.args) == 2 and
           i < MAX_ARITY):
        ch = deref(temp.args[0])
        check_integer(ch)
        if ch < 0 or ch > 0x10FFFF:
            raise make_error(Compound("domain_error", ["character_code", ch]))
        i += 1
        temp = deref(temp.args[1])
    check_nil(temp)
    res = [NotImplemented] * i
    temp = peek
    i = 0
    while (is_structure(temp) and
           temp.functor == "." and
           len(temp.args) == 2):
        ch = deref(temp.args[0])
        res[i] = chr(ch)
        i += 1
        temp = deref(temp.args[1])
    return ''.join(res)


def atom_codes_unpack(text):
    back = None
    res = None
    i = 0
    while i < len(text):
        ch = ord(text[i])
        peek = Compound(".", [ch, NotImplemented])
        if back is not None:
            back.args[1] = peek
        else:
            res = peek
        back = peek
        i += 1
    if back is not None:
        back.args[1] = "[]"
    else:
        res = "[]"
    return res


###
# char_code(C, N): [ISO 8.16.6]
# If C is a variable, the built-in succeeds in C with the
# character for the code N. Otherwise the built-in succeeds
# in N with the code from character C.
##
def test_char_code(args):
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        ch = exec_build(args[1])
        check_integer(ch)
        if ch < 0 or ch > 0x10FFFF:
            raise make_error(Compound("domain_error", ["character_code", ch]))
        return exec_unify(text, chr(ch))
    else:
        text = exec_build(text)
        check_atom(text)
        if len(text) != 1:
             raise make_error(Compound("type_error",
                   ["character", text]))
        return exec_unify(args[1], ord(text[0]))


#######################################################################
# atom_length/2 and atom_join/2                                       #
#######################################################################

###
# atom_length(X, Y): [ISO 8.16.1]
# The predicate succeeds in Y with the length of the atom X.
##
def test_atom_length(args):
    text = exec_build(args[0])
    check_atom(text)
    return exec_unify(args[1], len(text))


###
# atom_join(L, A):
# The built-in succeeds in A with the join of the atoms L.
##
def test_atom_join(args):
    alpha = exec_build(args[0])
    res = atom_join(alpha)
    return exec_unify(args[1], res)


def atom_join(res):
    peek = res
    i = 0
    while is_structure(peek) and \
            peek.functor == "." and \
            len(peek.args) == 2 and \
            i < MAX_ARITY:
        i += 1
        peek = deref(peek.args[1])
    check_nil(peek)
    elems = [NotImplemented] * i
    peek = res
    i = 0
    while is_structure(peek) and \
            peek.functor == "." and \
            len(peek.args) == 2:
        val = deref(peek.args[0])
        check_atom(val)
        elems[i] = val
        i += 1
        peek = deref(peek.args[1])
    return "".join(elems)


#######################################################################
# atom_concat/3                                                       #
#######################################################################

###
# atom_concat(X, Y, Z): [ISO 8.16.2]
# The built-in succeeds when Z is the concatenation of X and Y.
##
def special_atom_concat(args):
    first = deref(args[0])
    second = deref(args[1])
    third = deref(args[2])
    if is_variable(second):
        if is_variable(first):
            check_atom(third)
            return solve2_concat(args, None, 0, None)
        else:
            check_atom(first)
            check_atom(third)
            if not third.startswith(first):
                return False
            if not unify(second, third[len(first):]):
                return False
    elif is_variable(first):
        check_atom(second)
        check_atom(third)
        if not third.endswith(second):
            return False
        if not unify(first, third[:len(third)-len(second)]):
            return False
    else:
        check_atom(first)
        check_atom(second)
        if not unify(third, first+second):
            return False
        cont(machine.call.args[1])
    return True


def solve_concat(rope, at, choice):
    goal = deref(machine.call.args[0])
    return solve2_concat(goal.args, rope, at, choice)


def solve2_concat(args, rope, at, choice):
    text = deref(args[2])
    mark = machine.trail
    while at <= len(text):
        if unify(args[0], text[0:at]) and \
                unify(args[1], text[at:]):
            at += 1
            if at <= len(text):
                if choice is None:
                    choice = Choice(solve_concat, None, at, mark)
                else:
                    choice.at = at
                more(choice)
            cont(machine.call.args[1])
            return True
        unbind(mark)
        at += 1
    return False


#######################################################################
# sys_atom_match/3 and sys_atom_part/4                                #
#######################################################################

###
# sys_atom_match(X, Y, Z):
# The built-in succeeds if X has substring Y at Z.
##
def special_sys_atom_match(args):
    text = deref(args[0])
    check_atom(text)
    part = deref(args[1])
    check_atom(part)
    alpha = deref(args[2])
    if is_variable(alpha):
        return solve2_match(args, None, 0, None)
    else:
        check_integer(alpha)
        if alpha < 0 or alpha > len(text):
            return False
        if not text.startswith(part, alpha):
            return False
    cont(machine.call.args[1])
    return True


def solve_match(rope, at, choice):
    goal = deref(machine.call.args[0])
    return solve2_match(goal.args, rope, at, choice)


def solve2_match(args, rope, at, choice):
    text = deref(args[0])
    part = deref(args[1])
    mark = machine.trail
    while at + len(part) <= len(text):
        at = text.find(part, at)
        if at < 0:
            return False
        if unify(args[2], at):
            at += 1
            if at + len(part) <= len(text):
                if choice is None:
                    choice = Choice(solve_match, None, at, mark)
                else:
                    choice.at = at
                more(choice)
            cont(machine.call.args[1])
            return True
        unbind(mark)
        at += 1
    return False


###
# sys_atom_part(X, Y, Z, T):
# The built-in succeeds in T with the substring at
# offset Y and with length Z from X.
##
def special_sys_atom_part(args):
    text = deref(args[0])
    check_atom(text)
    alpha = deref(args[1])
    if is_variable(alpha):
        beta = deref(args[2])
        check_integer(beta)
        if beta < 0 or beta > len(text):
            return False
        return solve2_part(args, 0, beta, None)
    else:
        check_integer(alpha)
        beta = deref(args[2])
        check_integer(beta)
        if alpha < 0 or alpha > len(text):
            return False
        if beta < 0 or alpha+beta > len(text):
            return False
        if not unify(args[3], text[alpha:alpha+beta]):
            return False
    cont(machine.call.args[1])
    return True


def solve_part(rope, at, choice):
    goal = deref(machine.call.args[0])
    return solve2_part(goal.args, rope, at, choice)


def solve2_part(args, alpha, beta, choice):
    text = deref(args[0])
    mark = machine.trail
    while beta <= len(text):
        if unify(args[1], alpha) and \
                unify(args[3], text[alpha:beta]):
            alpha += 1
            beta += 1
            if beta <= len(text):
                if choice is None:
                    choice = Choice(solve_part, alpha, beta, mark)
                else:
                    choice.data = alpha
                    choice.at = beta
                more(choice)
            cont(machine.call.args[1])
            return True
        unbind(mark)
        alpha += 1
        beta += 1
    return False


#######################################################################
# sys_last_atom_match/3 and sys_last_atom_part/4                      #
#######################################################################

###
# sys_last_atom_match(X, Y, Z):
# The built-in succeeds if X has substring Y at Z.
##
def special_sys_last_atom_match(args):
    text = deref(args[0])
    check_atom(text)
    part = deref(args[1])
    check_atom(part)
    alpha = deref(args[2])
    if is_variable(alpha):
        return solve2_last_match(args, None, len(text)-len(part), None)
    else:
        check_integer(alpha)
        if alpha < 0 or alpha > len(text):
            return False
        if not text.startswith(part, alpha):
            return False
    cont(machine.call.args[1])
    return True


def solve_last_match(rope, at, choice):
    goal = deref(machine.call.args[0])
    return solve2_last_match(goal.args, rope, at, choice)


def solve2_last_match(args, rope, at, choice):
    text = deref(args[0])
    part = deref(args[1])
    mark = machine.trail
    while at >= 0:
        at = text.rfind(part, 0, at+len(part))
        if at < 0:
            return False
        if unify(args[2], at):
            at -= 1
            if at >= 0:
                if choice is None:
                    choice = Choice(solve_last_match, None, at, mark)
                else:
                    choice.at = at
                more(choice)
            cont(machine.call.args[1])
            return True
        unbind(mark)
        at -= 1
    return False


###
# sys_last_atom_part(X, Y, Z, T):
# The built-in succeeds in T with the substring at
# offset Y and with length Z from X.
##
def special_sys_last_atom_part(args):
    text = deref(args[0])
    check_atom(text)
    alpha = deref(args[1])
    if is_variable(alpha):
        beta = deref(args[2])
        check_integer(beta)
        if beta < 0 or beta > len(text):
            return False
        return solve2_last_part(args, len(text)-beta, len(text), None)
    else:
        check_integer(alpha)
        beta = deref(args[2])
        check_integer(beta)
        if alpha < 0 or alpha > len(text):
            return False
        if beta < 0 or alpha+beta > len(text):
            return False
        if not unify(args[3], text[alpha:alpha+beta]):
            return False
    cont(machine.call.args[1])
    return True


def solve_last_part(rope, at, choice):
    goal = deref(machine.call.args[0])
    return solve2_last_part(goal.args, rope, at, choice)


def solve2_last_part(args, alpha, beta, choice):
    text = deref(args[0])
    mark = machine.trail
    while alpha >= 0:
        if unify(args[1], alpha) and \
                unify(args[3], text[alpha:beta]):
            alpha -= 1
            beta -= 1
            if alpha >= 0:
                if choice is None:
                    choice = Choice(solve_last_part, alpha, beta, mark)
                else:
                    choice.data = alpha
                    choice.at = beta
                more(choice)
            cont(machine.call.args[1])
            return True
        unbind(mark)
        alpha -= 1
        beta -= 1
    return False


#######################################################################
# Special Init                                                        #
#######################################################################

# number specials, basic predicates
set("is", 2, make_check(test_eval))
set("-", 2, make_arithmetic(arit_neg))
set("abs", 2, make_arithmetic(arit_abs))
set("sign", 2, make_arithmetic(arit_sign))

# number specials, basic operations
set("+", 3, make_arithmetic(arit_add))
set("-", 3, make_arithmetic(arit_sub))
set("*", 3, make_arithmetic(arit_mul))
set("/", 3, make_arithmetic(arit_quot))
set("//", 3, make_arithmetic(arit_intquot))
set("rem", 3, make_arithmetic(arit_rem))

# number specials, more operations
set("float", 2, make_arithmetic(arit_float))
set("^", 3, make_arithmetic(arit_intpow))
set("div", 3, make_arithmetic(arit_div))
set("mod", 3, make_arithmetic(arit_mod))

# number specials, magnitude operations
set("min", 3, make_arithmetic(arit_min))
set("max", 3, make_arithmetic(arit_max))
set("inf", 1, make_arithmetic(arit_inf))
set("nan", 1, make_arithmetic(arit_nan))

# number specials, rounding operations
set("truncate", 2, make_arithmetic(arit_truncate))
set("floor", 2, make_arithmetic(arit_floor))
set("ceiling", 2, make_arithmetic(arit_ceiling))
set("round", 2, make_arithmetic(arit_round))

# number specials, magnitude predicates
set("=:=", 2, make_check(test_numberequal))
set("=\\=", 2, make_check(test_numbernotequal))
set("<", 2, make_check(test_numberless))
set(">=", 2, make_check(test_numbergreaterequal))
set(">", 2, make_check(test_numbergreater))
set("=<", 2, make_check(test_numberlessequal))

# number specials, trigonometric operations
set("sin", 2, make_arithmetic(arit_sin))
set("cos", 2, make_arithmetic(arit_cos))
set("tan", 2, make_arithmetic(arit_tan))
set("asin", 2, make_arithmetic(arit_asin))
set("acos", 2, make_arithmetic(arit_acos))
set("atan", 2, make_arithmetic(arit_atan))
set("pi", 1, make_arithmetic(arit_pi))

# number specials, exponential operations
set("**", 3, make_arithmetic(arit_pow))
set("exp", 2, make_arithmetic(arit_exp))
set("log", 2, make_arithmetic(arit_log))
set("sqrt", 2, make_arithmetic(arit_sqrt))
set("e", 1, make_arithmetic(arit_e))
set("epsilon", 1, make_arithmetic(arit_epsilon))
set("atan2", 3, make_arithmetic(arit_atan2))

# number specials, bitwise operations
set("\\", 2, make_arithmetic(arit_not))
set("/\\", 3, make_arithmetic(arit_and))
set("\\/", 3, make_arithmetic(arit_or))
set("xor", 3, make_arithmetic(arit_xor))
set(">>", 3, make_arithmetic(arit_shiftright))
set("<<", 3, make_arithmetic(arit_shiftleft))

# term specials, miscellaneous equality
set("==", 2, make_check(test_equal))
set("\\==", 2, make_check(test_notequal))
set("term_hash", 2, make_check(test_term_hash))

# atom specials, deterministic
set("atom_codes", 2, make_check(test_atom_codes))
set("char_code", 2, make_check(test_char_code))
set("atom_length", 2, make_check(test_atom_length))
set("atom_join", 2, make_check(test_atom_join))

# atom specials, non-deterministic
set("atom_concat", 3, make_special(special_atom_concat))
set("sys_atom_match", 3, make_special(special_sys_atom_match))
set("sys_atom_part", 4, make_special(special_sys_atom_part))
set("sys_last_atom_match", 3, make_special(special_sys_last_atom_match))
set("sys_last_atom_part", 4, make_special(special_sys_last_atom_part))
