###
# 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 asyncio
import nova.store as store
from nova.store import (Provable, set, VAR_MASK_SEEN, is_goal,
     stack_peek, stack_pop, stack_push, is_skeleton, Goal, is_clause,
     MASK_PRED_TEST, MASK_PRED_ARITH, Variable, VAR_MASK_SERNO,
     VAR_MASK_STATE, is_compound, deref, copy_term, add_clause,
     is_variable, Compound, is_structure, MASK_PRED_SPECIAL,
     Skeleton, Place, Clause, check_nonvar)
import nova.machine as machine
from nova.machine import (make_error, exec_deref, is_pending,
     cont, bind, SYS_MASK_ASYNC_MODE, mark2_term, is_frozen,
     Choice, more, snap_setup, snap_cleanup, unify, melt_directive,
     is_integer, is_atom, is_number, is_float, solve, VOID_ARGS,
     solve_signal, SYS_MASK_ALLOW_YIELD, is_special, register_interrupt,
     exec_build, exec_unify, register_signal, Frozen)
from nova.unicode import code_category, code_numeric
import sys
import importlib
import math

MAX_ARITY = 2147483647


################################################################
# Special Predicate                                            #
################################################################

###
# Return a built-in for a special.
#
# @param func The special.
# @return Provable The built-in.
##
def make_special(func):
    peek = Provable()
    peek.flags |= MASK_PRED_SPECIAL
    peek.func = func
    return peek


###
# Return a built-in for a check.
#
# @param func The check.
# @return Provable The built-in.
##
def make_check(func):
    peek = Provable()
    peek.flags |= MASK_PRED_TEST
    peek.func = func
    return peek


###
# Return a built-in for an arithmetic.
#
# @param func The arithmetic.
# @return Provable The built-in.
##
def make_arithmetic(func):
    peek = Provable()
    peek.flags |= MASK_PRED_ARITH
    peek.func = func
    return peek


################################################################
# fail/0, '$CUT'/1 and '$MARK'/1                               #
################################################################

###
# fail: [ISO 7.8.2]
# The built-in fails.
##
def test_fail(args):
    return False


###
# '$CUT'(R): internal only
# The built-in removes the choice points up to R and succeeds.
##
def test_cut(args):
    choice = exec_build(args[0])
    more(choice)
    return True


###
# '$MARK'(R): Internal only
# The built-in binds R to the top choice point.
##
def test_mark(args):
    choice = machine.redo
    return exec_unify(args[0], choice)


################################################################
# '$SEQ'/2and '$ALT'/1                                         #
################################################################

###
# '$SEQ'(O, L): internal only
# The built-in first matches the option O with the top choice point.
# The built-in then sequentially adds the goals L to the continuation.
##
def special_seq(args):
    temp = deref(args[0])
    solve_mark(temp)
    temp = deref(args[1])
    solve_seq(temp)
    return True


###
# If temp is just(R) bind R the top choice point. If temp is
# nothing do nothing. Otherwise throw an exception.
#
# @param temp The Prolog term.
##
def solve_mark(temp):
    if temp == "nothing":
        pass
    elif (is_structure(temp) and
            temp.functor == 'just' and
            len(temp.args) == 1):
        temp = deref(temp.args[0])
        bind(machine.redo, temp)
    else:
        check_nonvar(temp)
        temp = copy_term(temp)
        raise make_error(Compound("type_error", ["maybe", temp]))


###
# Sequentially adds the literals L to the continuation.
#
# @param peek The literals.
##
def solve_seq(peek):
    back = None
    res = None
    while (is_structure(peek) and
            peek.functor == "." and
            len(peek.args) == 2):
        goal = deref(peek.args[0])
        goal = Compound(".", [goal, NotImplemented])
        if back is None:
            res = goal
        else:
            back.args[1] = goal
        back = goal
        peek = deref(peek.args[1])
    check_nil(peek)
    if back is None:
        res = machine.call.args[1]
    else:
        back.args[1] = machine.call.args[1]
    cont(res)


###
# '$ALT'(L): internal only
# The built-in alternatively adds the variants L to the
# continuation and succeeds.
##
def special_alt(args):
    goal = deref(args[0])
    return solve_alt(goal, -1, None)


###
# Alternatively adds the variants to the continuation.
#
# @param peek The variants.
# @param at This argument is ignored.
# @param choice The choice point for reuse or null.
# @return True if a variant could be added, otherwise false.
##
def solve_alt(peek, at, choice):
    if (is_structure(peek) and
            peek.functor == "." and
            len(peek.args) == 2):
        mark = machine.trail
        goal = deref(peek.args[0])
        solve_mark(deref(goal.args[0]))
        peek = deref(peek.args[1])
        if (is_structure(peek) and
                peek.functor == "." and
                len(peek.args) == 2):
            if choice is None:
                choice = Choice(solve_alt, peek, -1, mark)
            else:
                choice.data = peek
            more(choice)
        solve_seq(deref(goal.args[1]))
        return True
    else:
        return False


################################################################
# sys_raise/1, sys_trap/3                                      #
################################################################

###
# sys_raise(E): internal only
# The predicate raises the exception E.
##
def test_sys_raise(args):
    problem = exec_build(args[0])
    raise Exception(copy_term(problem))


###
# sys_trap(G, E, F): internal only
# The built-in succeeds whenever G succeeds. If
# there was an exception that unifies with E, the
# built-in further succeeds whenever F succeeds.
##
def special_sys_trap(args):
    goal = deref(args[0])
    snap = snap_setup()
    goal = Compound(".", [goal, "[]"])
    cont(goal)
    return solve_catch(snap, True, None)


###
# Call, redo or resume a goal.
# If there is an exception put the handler on the continuation.
#
# @param snap The surrounding choice point.
# @param found The call or redo flag.
# @param choice The choice point for reuse or null.
# @return True if goal succeeds, otherwise false.
##
def solve_catch(snap, found, choice):
    if choice is not None:
        choice.mark = None
        choice.cont = "[]"
        choice.tail = None
    try:
        found = solve(snap, found)
    except Exception as err:
        snap_cleanup(snap)
        goal = deref(machine.call.args[0])
        err = map_throwable(err)
        if not unify(goal.args[1], err.args[0]):
            raise err from None
        goal = deref(goal.args[2])
        goal = Compound(".", [goal, machine.call.args[1]])
        cont(goal)
        return True
    if found is False:
        return False
    if machine.redo is not snap:
        if choice is None:
            choice = Choice(solve_catch, snap, False, machine.trail)
        else:
            choice.mark = machine.trail
            choice.cont = machine.call
            choice.tail = machine.redo
        more(choice)
    else:
        more(snap.tail)
    if found is True:
        cont(snap.cont.args[1])
    return found


def map_throwable(err):
    if isinstance(err, RecursionError):
        err = make_error(Compound("system_error", ["stack_overflow"]))
    elif isinstance(err, MemoryError):
        err = make_error(Compound("system_error", ["out_of_memory"]))
    return err


################################################################
# os_sleep_promise/2 and os_import_promise/3                   #
################################################################

###
# os_sleep_promise(D, P):
# The predicate succeeds in P with a promise for a delay D.
##
def test_os_sleep_promise(args):
    delay = exec_build(args[0])
    check_integer(delay)
    if delay < 0:
        raise make_error(Compound("domain_error",
              ["not_less_than_zero", delay]))
    buf = machine.ctx
    return exec_unify(args[1], lambda: sleep_promise(buf, delay))


async def sleep_promise(buf, delay):
    register_interrupt(buf, asyncio.current_task().cancel)
    try:
        await asyncio.sleep(delay/1000.0)
    except asyncio.CancelledError:
        pass
    finally:
        register_interrupt(buf, lambda: None)


###
# os_import_promise(F, M, Q): internal only
# The predicate succeeds in Q with with a promise for the
# import M of the file F.
##
def test_os_import_promise(args):
    url = exec_build(args[0])
    check_atom(url)
    res = {}
    if not exec_unify(args[1], res):
        return False
    buf = machine.ctx
    return exec_unify(args[2], lambda: import_promise(buf, url, res))


async def import_promise(buf, url, res):
    name = find_name(url)
    if name is None:
        register_signal(buf,
            Compound("existence_error", ["module", url]))
    else:
        try:
            module = await asyncio.to_thread(blocking_import, name)
            res["module"] = module
        except SyntaxError:
            register_signal(buf,
                Compound("syntax_error", ["bad_module"]))
        except ModuleNotFoundError:
            register_signal(buf,
                Compound("existence_error", ["module", url]))


def blocking_import(name):
    return importlib.import_module(name)


###
# Find a Python module name from an url
#
# @param url The url
# @return The Python module name or None
##
def find_name(url):
    if not url.endswith(".py"):
        return None
    path = find_path(url)
    if path is None:
        return None
    url = url[len(path)+1:len(url)-3]
    url = url.replace("/", ".")
    url = url.replace("\\", ".")
    return url


###
# Find a filepath entry for an url
#
# @param url The url
# @return The filepath entry or None
##
def find_path(url):
    i = 0
    while i < len(sys.path):
        path = sys.path[i]
        if (url.startswith(path) and
                (url.startswith("/", len(path)) or
                 url.startswith("\\", len(path)))):
            return path
        i = i+1
    return None


###
# os_invoke_main(M): internal only
# Invoke the main method of the module M.
##
def test_os_invoke_main(args):
    module = exec_build(args[0])
    val = getattr(module, "main", NotImplemented)
    if val is NotImplemented:
        raise make_error(Compound("existence_error",
                    ["method", "main"]))
    elif not callable(val):
        raise make_error(Compound("permission_error",
                    ["invoke", "method", "main"]))
    else:
        val()
    return True


################################################################
# '$YIELD'/1, shield/1 and unshield/1                          #
################################################################

###
# '$YIELD'(R): Internal only
# The built-in stops the interpreter loop with return value R.
##
def special_yield(args):
    if (store.engine.flags & SYS_MASK_ALLOW_YIELD) == 0:
        raise make_error(Compound("system_error", ["illegal_yield"]))
    cont(machine.call.args[1])
    more(Choice(solve_signal, None, 0, machine.trail))
    return deref(args[0])


###
# shield(G):
# The predicate succeeds whenever the goal G succeeds.
# The goal is executed without auto-yield.
##
def special_shield(args):
    goal = deref(args[0])
    snap = snap_setup()
    goal = Compound(".", [goal, "[]"])
    cont(goal)
    return solve_shield(snap, True, None)


###
# Call, redo or resume a goal.
# If there is an exception put the handler on the continuation.
#
# @param snap The surrounding choice point.
# @param found The call or redo flag.
# @param choice The choice point for reuse or null.
# @return True if goal succeeds, otherwise false.
##
def solve_shield(snap, found, choice):
    if choice is not None:
        choice.mark = None
        choice.cont = "[]"
        choice.tail = None
    back = store.engine.flags & SYS_MASK_ASYNC_MODE
    store.engine.flags &= ~SYS_MASK_ASYNC_MODE
    try:
        found = solve(snap, found)
    except Exception as err:
        store.engine.flags &= ~SYS_MASK_ASYNC_MODE
        store.engine.flags |= back
        snap_cleanup(snap)
        raise err from None
    store.engine.flags &= ~SYS_MASK_ASYNC_MODE
    store.engine.flags |= back
    if found is False:
        return False
    if machine.redo is not snap:
        if choice is None:
            choice = Choice(solve_shield, snap, False, machine.trail)
        else:
            choice.mark = machine.trail
            choice.cont = machine.call
            choice.tail = machine.redo
        more(choice)
    else:
        more(snap.tail)
    if found is True:
        cont(snap.cont.args[1])
    return found


###
# unshield(G):
# The predicate succeeds whenever the goal G succeeds.
# The goal is executed with auto-yield.
##
def special_unshield(args):
    goal = deref(args[0])
    snap = snap_setup()
    goal = Compound(".", [goal, "[]"])
    cont(goal)
    return solve_unshield(snap, True, None)


###
# Call, redo or resume a goal.
# If there is an exception put the handler on the continuation.
#
# @param snap The surrounding choice point.
# @param found The call or redo flag.
# @param choice The choice point for reuse or null.
# @return True if goal succeeds, otherwise false.
##
def solve_unshield(snap, found, choice):
    if choice is not None:
        choice.mark = None
        choice.cont = "[]"
        choice.tail = None
    back = store.engine.flags & SYS_MASK_ASYNC_MODE
    store.engine.flags |= SYS_MASK_ASYNC_MODE
    try:
        found = solve(snap, found)
    except Exception as err:
        store.engine.flags &= ~SYS_MASK_ASYNC_MODE
        store.engine.flags |= back
        snap_cleanup(snap)
        raise err from None
    store.engine.flags &= ~SYS_MASK_ASYNC_MODE
    store.engine.flags |= back
    if found is False:
        return False
    if machine.redo is not snap:
        if choice is None:
            choice = Choice(solve_unshield, snap, False, machine.trail)
        else:
            choice.mark = machine.trail
            choice.cont = machine.call
            choice.tail = machine.redo
        more(choice)
    else:
        more(snap.tail)
    if found is True:
        cont(snap.cont.args[1])
    return found


###
# call(A): [ISO 7.8.3]
# The predicate succeeds whenever the goal A succeeds.
##
def special_call(args):
    goal = deref(args[0])
    goal = Compound(".", [goal, machine.call.args[1]])
    cont(goal)
    return True


################################################################
# Type Assertions                                              #
################################################################

###
# Assure that the object is an atom.
#
# @param beta The object.
##
def check_atom(beta):
    if not is_atom(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["atom", beta]))


###
# Assure that the object is a number.
#
# @param beta The object.
##
def check_number(beta):
    if not is_number(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["number", beta]))


###
# Assure that the object is an integer.
#
# @param beta The object.
##
def check_integer(beta):
    if not is_integer(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["integer", beta]))


###
# Assure that the object is atomic.
#
# @param beta The object.
##
def check_atomic(beta):
    if is_variable(beta) or is_structure(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["atomic", beta]))


###
# Assure that the object is nil.
#
# @param beta The object.
##
def check_nil(beta):
    if beta == "[]":
        return
    if (is_structure(beta) and beta.functor == "."
            and len(beta.args) == 2):
        raise make_error(Compound("representation_error",
                          ["max_arity"]))
    else:
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error",
                          ["list", beta]))


################################################################
# =/2 and copy_term/2                                          #
################################################################

###
# S = T: [ISO 8.2.1]
# The built-in succeeds when the Prolog terms S and T unify,
# otherwise the built-in fails.
##
def test_unify(args):
    alpha = exec_build(args[0])
    return exec_unify(args[1], alpha)


###
# copy_term(S, T): [ISO 8.5.4]
# The built-in succeeds in T with a copy of S.
##
def test_copy_term(args):
    alpha = exec_build(args[0])
    alpha = copy_term(alpha)
    return exec_unify(args[1], alpha)


################################################################
# =../2, functor/3 and arg/3                                   #
################################################################

###
# T =.. [F|L]: [ISO 8.5.3]
# If T is a variable, the built-in succeeds in T with the Prolog term
# from the functor F and arguments L. Otherwise the built-in succeeds in
# F and L with the functor and arguments of the Prolog term T.
##
def test_univ(args):
    alpha = exec_deref(args[0])
    if is_variable(alpha) or is_pending(alpha):
        beta = exec_build(args[1])
        beta = special_univ_pack(beta)
        return exec_unify(alpha, beta)
    else:
        alpha = exec_build(alpha)
        alpha = special_univ_unpack(alpha)
        return exec_unify(args[1], alpha)


def special_univ_pack(beta):
    if (is_structure(beta) and
            "." == beta.functor and
            len(beta.args) == 2):
        pass
    else:
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["list", beta]))
    functor = deref(beta.args[0])
    check_atomic(functor)
    beta = deref(beta.args[1])
    args = list_objects(beta)
    if len(args) == 0:
        pass
    else:
        if is_number(functor):
            raise make_error(Compound("type_error", ["atom", functor]))
        functor = Compound(functor, args)
    return functor


def list_objects(obj):
    peek = obj
    arity = 0
    while (is_structure(peek) and
            "." == peek.functor and
            len(peek.args) == 2 and
            arity < MAX_ARITY):
        arity += 1
        peek = deref(peek.args[1])
    check_nil(peek)
    if arity == 0:
        return VOID_ARGS
    else:
        args = [NotImplemented] * arity
        peek = obj
        arity = 0
        while (is_structure(peek) and
                "." == peek.functor and
                len(peek.args) == 2):
            args[arity] = deref(peek.args[0])
            arity += 1
            peek = deref(peek.args[1])
        return args


def special_univ_unpack(alpha):
    if is_structure(alpha):
        temp = alpha.args
        alpha = alpha.functor
        return Compound(".", [alpha,
            objects_list(temp, 0, len(temp))])
    else:
        return Compound(".", [alpha, "[]"])


def objects_list(args, off, count):
    res = "[]"
    i = off + count - 1
    while i >= off:
        res = Compound(".", [args[i], res])
        i -= 1
    return res


###
# functor(T, F, A): [ISO 8.5.1]
# If T is a variable, the built-in succeeds in T with a new Prolog term
# from the functor F and the arity A. Otherwise the built-in succeeds in
# F and L with the functor and arguments of the Prolog term T.
##
def test_functor(args):
    alpha = exec_deref(args[0])
    if is_variable(alpha) or is_pending(alpha):
        functor = exec_build(args[1])
        check_atomic(functor)
        arity = exec_build(args[2])
        check_integer(arity)
        if arity < 0:
            raise make_error(Compound("domain_error",
                     ["not_less_than_zero", arity]))
        if arity > MAX_ARITY:
            raise make_error(Compound("representation_error",
                     ["max_arity"]))
        if arity == 0:
            pass
        else:
            if is_number(functor):
                raise make_error(Compound("type_error", ["atom", functor]))
            temp = [NotImplemented] * arity
            i = 0
            while i < arity:
                temp[i] = Variable()
                i += 1
            functor = Compound(functor, temp)
        return exec_unify(alpha, functor)
    else:
        alpha = exec_build(alpha)
        if is_structure(alpha):
            functor = alpha.functor
            arity = len(alpha.args)
        else:
            functor = alpha
            arity = 0
        if not exec_unify(args[1], functor):
            return False
        return exec_unify(args[2], arity)


###
# arg(K, X, Y): [ISO 8.5.2]
# The predicate succeeds in Y with the K-th argument of X.
##
def test_arg(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    beta = exec_build(args[1])
    check_callable(beta)
    if is_structure(beta):
        arity = len(beta.args)
    else:
        arity = 0
    if alpha < 1 or arity < alpha:
        return False
    beta = beta.args[alpha - 1]
    return exec_unify(args[2], beta)


###
# Assure that the object is a callable.
#
# @param beta The object.
##
def check_callable(beta):
    if is_variable(beta) or is_number(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["callable", beta]))


################################################################
# change_arg/3                                                 #
################################################################

###
# change_arg(K, X, Y):
# The predicate succeeds. As a side-effect the K-th argument of X is set to Y.
##
def test_change_arg(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    beta = exec_build(args[1])
    check_callable(beta)
    gamma = exec_build(args[2])
    if is_structure(beta):
        arity = len(beta.args)
    else:
        arity = 0
    if alpha < 1 or arity < alpha:
        return False
    linkarg(gamma, beta, alpha)
    return True


def linkarg(source, term, pos):
    if is_frozen(term):
        raise make_error(Compound("permission_error",
                  ["modify", "compound", term]))
    if (term.walk & VAR_MASK_STATE) == machine.gc_mask:
        mark2_term(source)
    term.args[pos-1] = source


################################################################
# term_variables/3                                             #
################################################################

###
# term_variables(T, L, E): [TC2 8.5.5]
# The built-in succeeds in L with a list with end E and
# with the variables of T as its list elements.
##
def test_term_variables(args):
    alpha = exec_build(args[0])
    res = None
    back = None

    def term_variables2(node):
        nonlocal res
        nonlocal back
        peek = Compound(".", [node, NotImplemented])
        if back is None:
            res = peek
        else:
            back.args[1] = peek
        back = peek
        return False

    walk_vars(alpha, term_variables2, VAR_MASK_SEEN)
    if back is None:
        res = "[]"
    else:
        back.args[1] = "[]"
    walk_vars(alpha, lambda node: False, 0)
    return exec_unify(args[1], res)


def walk_vars(first, acceptor, state):
    stack = None
    while True:
        first = deref(first)
        if is_variable(first):
            if (first.flags & VAR_MASK_SEEN) != state:
                if acceptor(first):
                    return True
                first.flags = (first.flags & ~VAR_MASK_SEEN) | state
        elif is_compound(first):
            if (first.walk & VAR_MASK_SEEN) != state:
                first.walk = (first.walk & ~VAR_MASK_SEEN) | state
                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 False
        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)


################################################################
# var/1, compound/1 and nonvar/1                               #
################################################################

###
# var(V): [ISO 8.3.1]
# The built-in succeeds if V is a Prolog variable. Otherwise, it fails.
##
def test_var(args):
    alpha = exec_build(args[0])
    return is_variable(alpha)


###
# compound(C): [ISO 8.3.6]
# The built-in succeeds if C is a Prolog compound. Otherwise, it fails.
##
def test_compound(args):
    alpha = exec_build(args[0])
    return is_structure(alpha)


###
# nonvar(V): [ISO 8.3.7]
# The built-in succeeds if V is not a Prolog variable. Otherwise, it fails.
##
def test_nonvar(args):
    alpha = exec_build(args[0])
    return not is_variable(alpha)


################################################################
# atomic/1, number/1 and float/1                               #
################################################################

###
# atomic(A): [ISO 8.3.5]
# The built-in succeeds if A is a Prolog symbol or number. Otherwise, it fails.
##
def test_atomic(args):
    alpha = exec_build(args[0])
    if is_structure(alpha) or is_variable(alpha):
        return False
    return True


###
# number(A): [ISO 8.3.8]
# The built-in succeeds if A is a Prolog number. Otherwise, it fails.
##
def test_number(args):
    alpha = exec_build(args[0])
    return is_number(alpha)


###
# float(A): [ISO 8.3.4]
# The built-in succeeds if A is a Prolog float. Otherwise, it fails.
##
def test_float(args):
    alpha = exec_build(args[0])
    return is_float(alpha) and not is_special(alpha)


################################################################
# integer/1, atom/1, callable/1 and must_be/2                  #
################################################################

###
# integer(A): [ISO 8.3.3]
# The built-in succeeds if A is a Prolog integer. Otherwise, it fails.
##
def test_integer(args):
    alpha = exec_build(args[0])
    return is_integer(alpha)


###
# atom(A): [ISO 8.3.2]
# The built-in succeeds if A is a Prolog atom. Otherwise, it fails.
##
def test_atom(args):
    alpha = exec_build(args[0])
    return is_atom(alpha)


###
# callable(C): [TC2 8.3.9]
# The built-in succeeds if C is a Prolog compound or symbol. Otherwise, it fails.
##
def test_callable(args):
    alpha = exec_build(args[0])
    if is_variable(alpha) or is_number(alpha):
        return False
    return True


###
# must_be(T, A):
# The built-in succeeds if A is of type T. Otherwise, an exception is thrown.
##
def test_must_be(args):
    alpha = exec_build(args[0])
    check_atom(alpha)
    beta = exec_build(args[1])
    if "integer" == alpha:
        check_integer(beta)
    elif "atom" == alpha:
        check_atom(beta)
    elif "callable" == alpha:
        check_callable(beta)
    else:
        raise make_error(Compound("type_error",
                ["type", alpha]))
    return True


################################################################
# Number Utility                                               #
################################################################

###
# Narrow a Prolog number to a Python float.
#
# @param alpha The Prolog number.
# @return number The Python float.
##
def narrow_float(alpha):
    if is_float(alpha):
        if is_special(alpha):
            raise make_error(Compound("evaluation_error", ["undefined"]))
        return alpha
    else:
        try:
            return float(alpha)
        except OverflowError:
            raise make_error(Compound("evaluation_error", ["float_overflow"]))


###
# Norm a Python float to a Prolog float.
#
# @param alpha The Python float.
# @return number The Prolog float.
##
def norm_float(alpha):
    if math.isnan(alpha):
        raise make_error(Compound("evaluation_error", ["undefined"]))
    if math.isinf(alpha):
        raise make_error(Compound("evaluation_error", ["float_overflow"]))
    if alpha == 0.0:
        return 0.0
    return alpha


################################################################
# code_category/2 and code_numeric/2                           #
################################################################

###
# code_category(C, T):
# The predicate succeeds in T with the Unicode general category of C.
# Otherwise, the predicate succeeds in T with 0.
##
def test_code_category(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    if alpha < 0 or alpha > 0x10FFFF:
        alpha = 0  # UNASSIGNED
    else:
        alpha = code_category(alpha)
    return exec_unify(args[1], alpha)


###
# code_numeric(C, V):
# The predicate succeeds in V with the Unicode numeric value of C,
# in case it is integer and between 0 and 35. Otherwise, the predicate
# succeeds in V with -1.
##
def test_code_numeric(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    if alpha < 0 or alpha > 0x10FFFF:
        alpha = -1  # UNASSIGNED
    else:
        alpha = code_numeric(alpha)
    return exec_unify(args[1], alpha)


################################################################
# atom_integer/3                                               #
################################################################

###
# atom_integer(A, R, N):
# If A is a variable, then the built-in succeeds in A with the
# atom for the Prolog integer N in radix R. Otherwise the
# built-in succeeds in N with the Prolog number from the
# atom A in radix R.
##
def test_atom_integer(args):
    radix = exec_build(args[1])
    check_integer(radix)
    if radix < 2 or radix > 36:
        raise make_error(Compound("domain_error", ["radix", radix]))
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        beta = exec_build(args[2])
        check_integer(beta)
        beta = atom_integer_encode(beta, radix)
        return exec_unify(text, beta)
    else:
        text = exec_build(text)
        check_atom(text)
        text = atom_integer_decode(text, radix)
        return exec_unify(args[2], text)


###
# Encode a Prolog integer to a string.
#
# @param num The Prolog integer.
# @param radix The radix.
# @return The string.
##
def atom_integer_encode(num, radix):
    if radix == 10:
        return str(num)
    elif num < 0:
        return "-" + atom_integer_encode(- num, radix)
    elif radix == 2:
        return bin(num)[2:]
    elif radix == 8:
        return oct(num)[2:]
    elif radix == 16:
        return hex(num)[2:]
    else:
        return str_encode(num, radix)


def str_encode(num, radix):
    blen = num.bit_length()
    if blen < 48:
        return str_integer(num, radix)
    if blen < 512:
        blen = 48 // (radix - 1).bit_length()
        base = radix ** blen
        count = 0
        res = str_integer(num % base, radix)
        num = num // base
        while num != 0:
            count += blen
            res = res.rjust(count, "0")
            res = str_integer(num % base, radix) + res
            num = num // base
        return res
    blen = (blen // (radix - 1).bit_length() + 1) // 2
    base = radix ** blen
    return (str_encode(num // base, radix) +
            str_encode(num % base, radix).rjust(blen, "0"))


def str_integer(num, radix):
    res = chr_digit(num % radix)
    num = num // radix
    while num != 0:
        res = chr_digit(num % radix) + res
        num = num // radix
    return res


def chr_digit(num):
    if num < 10:
        return chr(num + 48)
    else:
        return chr(num + 87)


###
# Decode a Prolog integer from a string.
#
# @param text The string
# @param radix The radix.
# @param signed The signed flag.
# @return The Prolog integer.
##
def atom_integer_decode(text, radix):
    text = ascii_replace(text, radix, False)
    try:
        return int(text, radix)
    except ValueError:
        raise make_error(Compound("syntax_error", ["illegal_number"]))


###
# Convert and validate Unicode number values
# into capital ASCII number value.
#
# @param text The string.
# @param radix The radix.
# @param expo The float flag
# @return The The new string.
##
def ascii_replace(text, radix, expo):
    buf = ""
    last = 0
    pos = 0
    while pos < len(text):
        ch = ord(text[pos])
        val = code_numeric(ch)
        if val >= 0 and val < radix:
            if ch <= 127:  # ASCII
                pass
            else:
                if val < 10:
                    val += 48  # '0'
                else:
                    val += 55  # 'A'-10
                buf += text[last:pos] + chr(val)
                last = pos + 1
        elif (ch == 43 or ch == 45) and (pos == 0 or
                                         (expo and (ord(text[pos-1]) == 101 or ord(text[pos-1]) == 69))):
            pass
        elif expo and (ch == 46 or ch == 101 or ch == 69):
            pass
        else:
            raise make_error(Compound("syntax_error", ["illegal_number"]))
        pos += 1
    if last != 0:
        buf += text[last:]
        return buf
    else:
        return text


################################################################
# atom_number/2                                                #
################################################################

###
# atom_number(A, N):
# If A is a variable, then the built-in succeeds in A with the
# atom for the Prolog number N. Otherwise the built-in succeeds in N
# with the Prolog number from the atom A.
##
def test_atom_number(args):
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        beta = exec_build(args[1])
        check_number(beta)
        beta = atom_number_encode(beta)
        return exec_unify(text, beta)
    else:
        text = exec_build(text)
        check_atom(text)
        text = atom_number_decode(text)
        return exec_unify(args[1], text)


###
# Encode a Prolog number to a string.
#
# @param num The Prolog number.
# @return The string.
##
def atom_number_encode(num):
    if is_integer(num):
        return str(num)
    else:
        res = "%.16g" % num
        if float(res) != num:
            res = "%.17g" % num
        return shape_number(res)


###
# Shape the number string so that it has always a period,
# no exponent positive sign, no exponent leading zeros and
# lower case exponent.
#
# @param res The ascii number string.
# @return The shaped number string.
##
def shape_number(res):
    peek = res.find("e")
    if peek != -1:
        res = shape_number_mantissa(res[0:peek]) + \
              "e" + shape_number_exponent(res[peek+1:])
    else:
        res = shape_number_mantissa(res)
    return res


def shape_number_mantissa(res):
    if "." not in res:
        res += ".0"
    return res


def shape_number_exponent(res):
    if res.startswith("+"):
        res = res[1:]
    if res.startswith("0") and 1 < len(res):
        res = res[1:]
    elif res.startswith("-0") and 2 < len(res):
        res = "-" + res[2:]
    return res


###
# Decode a Prolog number from a string. Unlike the ISO
# core standard and numbers without a period but with
# an exponent are accepted as float.
#
# @param str The string
# @return The Prolog number.
##
def atom_number_decode(text):
    text = ascii_replace(text, 10, True)
    if "." in text or "e" in text or "E" in text:
        try:
            return float(text)
        except ValueError:
            raise make_error(Compound("syntax_error", ["illegal_number"]))
    else:
        try:
            return int(text)
        except ValueError:
            raise make_error(Compound("syntax_error", ["illegal_number"]))


################################################################
# sys_goal_ossify/3 and sys_clause_ossify/6                      #
################################################################

###
# sys_goal_ossify(B, R, G): internal only
# The built-in succeeds in G with a Python object representing
# a goal with body instructions B and the cut option R.
##
def test_sys_goal_ossify(args):
    alpha = exec_build(args[0])
    body = list_objects(alpha)
    beta = exec_build(args[1])
    size = 0
    try:
        size = number_term(beta, size)
        size = number_objects(body, size)
        cutvar = ossify_cutvar(beta)
        ossify_objects(body)
    finally:
        unossify_term(beta)
        unossify_objects(alpha)
    return exec_unify(args[2],
        Goal(size, body, cutvar))


###
# sys_clause_ossify(H, B, R, W, C): internal only
# The built-in succeeds in C with a Python object representing
# a clause with head instructions H, body instructions B,
# cut option R and head functor W.
##
def test_sys_clause_ossify(args):
    alpha = exec_build(args[0])
    head = list_objects(alpha)
    beta = exec_build(args[1])
    body = list_objects(beta)
    gamma = exec_build(args[2])
    mue = exec_build(args[3])
    size = 0
    try:
        size = number_term(gamma, size)
        size = number_objects(head, size)
        size = number_objects(body, size)
        cutvar = ossify_cutvar(gamma)
        ossify_objects(head)
        ossify_objects(body)
    finally:
        unossify_term(gamma)
        unossify_objects(alpha)
        unossify_objects(beta)
    return exec_unify(args[4],
          Clause(size, mue, head, body, cutvar))


################################################################
# Object Lists                                                 #
################################################################

def number_objects(peek, size):
    i = 0
    while i < len(peek):
       size = number_term(peek[i], size)
       i += 1
    return size


def ossify_objects(peek):
    i = 0
    while i < len(peek):
        peek[i] = ossify_term(peek[i])
        i += 1


def unossify_objects(peek):
    while (is_structure(peek) and
            "." == peek.functor and
            len(peek.args) == 2):
        unossify_term(peek.args[0])
        peek = deref(peek.args[1])


################################################################
# Cut Variable                                                 #
################################################################

def ossify_cutvar(peek):
    if (is_structure(peek) and
        "just" == peek.functor and
        len(peek.args) == 1):
        res = ossify_term(peek.args[0])
        return res.index
    else:
        return -1


################################################################
# Term Ossify                                                  #
################################################################

def number_term(alpha, size):

    def number_term2(first):
        nonlocal size
        while True:
            first = deref(first)
            if is_variable(first):
                if first.tail is None:
                    first.tail = NotImplemented
                elif first.tail is NotImplemented:
                    first.tail = Place(size)
                    size += 1
            elif is_compound(first):
                i = 0
                while i < len(first.args) - 1:
                    number_term2(first.args[i])
                    i += 1
                first = first.args[i]
                continue
            break

    number_term2(alpha)
    return size


def ossify_term(first):
    back = None
    while True:
        first = deref(first)
        if is_variable(first):
            first = first.tail
        elif is_compound(first):
            args = [NotImplemented] * len(first.args)
            i = 0
            while i < len(first.args)-1:
                args[i] = ossify_term(first.args[i])
                i += 1
            args[i] = back
            back = Skeleton(first.functor, args)
            first = first.args[i]
            continue
        while back is not None:
            peek = back.args[len(back.args)-1]
            back.args[len(back.args)-1] = first
            first = try_freeze(back)
            back = peek
        return first


def try_freeze(alpha):
    i = 0
    while i < len(alpha.args):
        temp = alpha.args[i]
        if is_pending(temp):
            return alpha
        if is_skeleton(temp):
            return alpha
        i += 1
    return Frozen(alpha.functor, alpha.args)


def unossify_term(first):
    while True:
        first = deref(first)
        if is_variable(first):
            if first.tail is not None:
                first.tail = None
        elif is_compound(first):
            i = 0
            while i < len(first.args)-1:
                unossify_term(first.args[i])
                i += 1
            first = first.args[i]
            continue
        break


################################################################
# sys_clause_add/2 and sys_goal_call/1                           #
################################################################

###
# sys_clause_add(C, O): internal only
# The built-in succeeds. As a side effect the Python object clause C
# is added according to options O to the knowledge base.
##
def test_sys_clause_add(args):
    gamma = exec_build(args[0])
    check_clause(gamma)
    flags = exec_build(args[1])
    check_integer(flags)
    add_clause(gamma, flags)
    return True


def check_clause(beta):
    if not is_clause(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["clause", beta]))


###
# sys_goal_call(A):
# The predicate succeeds whenever the compiled goal A succeeds.
##
def special_sys_goal_call(args):
    goal = deref(args[0])
    check_goal(goal)
    goal = melt_directive(goal)
    solve_seq(goal)
    return True


def check_goal(beta):
    if not is_goal(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["goal", beta]))


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

# Albufeira compiler, deterministic
set("fail", 0, make_check(test_fail))
set("$CUT", 1, make_check(test_cut))
set("$MARK", 1, make_check(test_mark))
set("$SEQ", 2, make_special(special_seq))
set("$ALT", 1, make_special(special_alt))
set("sys_raise", 1, make_check(test_sys_raise))
set("sys_trap", 3, make_special(special_sys_trap))

# Albufeira compiler, for call/1
set("os_sleep_promise", 2, make_check(test_os_sleep_promise))
set("os_import_promise", 3, make_check(test_os_import_promise))
set("os_invoke_main", 1, make_check(test_os_invoke_main))
set("$YIELD", 1, make_special(special_yield))
set("shield", 1, make_special(special_shield))
set("unshield", 1, make_special(special_unshield))
set("call", 1, make_special(special_call))

# term specials
set("=", 2, make_check(test_unify))
set("copy_term", 2, make_check(test_copy_term))
set("=..", 2, make_check(test_univ))
set("functor", 3, make_check(test_functor))
set("arg", 3, make_check(test_arg))
set("change_arg", 3, make_check(test_change_arg))
set("term_variables", 2, make_check(test_term_variables))

# type specials
set("var", 1, make_check(test_var))
set("compound", 1, make_check(test_compound))
set("nonvar", 1, make_check(test_nonvar))
set("atomic", 1, make_check(test_atomic))
set("number", 1, make_check(test_number))
set("float", 1, make_check(test_float))

# must specials
set("integer", 1, make_check(test_integer))
set("atom", 1, make_check(test_atom))
set("callable", 1, make_check(test_callable))
set("must_be", 2, make_check(test_must_be))

# atom specials
set("code_category", 2, make_check(test_code_category))
set("code_numeric", 2, make_check(test_code_numeric))
set("atom_integer", 3, make_check(test_atom_integer))
set("atom_number", 2, make_check(test_atom_number))

# clause specials
set("sys_goal_ossify", 3, make_check(test_sys_goal_ossify))
set("sys_clause_ossify", 5, make_check(test_sys_clause_ossify))
set("sys_clause_add", 2, make_check(test_sys_clause_add))
set("sys_goal_call", 1, make_special(special_sys_goal_call))