###
# 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.core import (
    is_number, VOID_ARGS, Compound, is_structure, check_integer,
    add, get_cont, check_atomic, exec_build, exec_unify,
    VAR_MASK_SEEN, deref, make_special, cont, make_error,
    Variable, Item, stack_push, stack_peek, is_integer,
    stack_pop, walk_vars, make_check, bind, is_compound)

####################################################################
# numbervars/3                                                     #
####################################################################

###
# numbervars(X, N, M):
# The predicate succeeds in M with the next index after
# numbering all variables in X starting with index N.
##
def test_numbervars(args):
    alpha = exec_build(args[0])
    beta = exec_build(args[1])
    check_integer(beta)
    if beta >= 0:
        beta = numbervars("$VAR", alpha, beta)
    else:
        beta = (-1) - beta
        beta = numbervars("_", alpha, beta)
        beta = (-1) - beta
    return exec_unify(args[2], beta)


def numbervars(functor, alpha, beta):

    def numbervars2(node):
        nonlocal beta
        bind(Compound(functor, [beta]), node)
        beta += 1
        return False

    walk_vars(alpha, lambda node: False, VAR_MASK_SEEN)
    walk_vars(alpha, numbervars2, 0)
    return beta


################################################################
# Association Map                                              #
################################################################

def assoc_get(vmap, key):
    if vmap is None:
        return NotImplemented
    return vmap.get(key, NotImplemented)


def assoc_set(vmap, key, value):
    if vmap is None:
        vmap = {}
    vmap[key] = value
    return vmap


################################################################
# Unary Work                                                   #
################################################################

###
# Create an unary work item.
#
# @param first The Prolog term.
# @param idx The index.
##
class Shuttle:
    def __init__(self, first, idx):
        self.first = first
        self.idx = idx


################################################################
# unnumbervars/3                                               #
################################################################

###
# unnumbervars(S, N, T):
# The predicate succeeds in T with a copy of S with the
# numbering greater or equal than N replaced by variables.
##
def test_unnumbervars(args):
    alpha = exec_build(args[0])
    beta = exec_build(args[1])
    check_integer(beta)
    try:
        if beta >= 0:
            res = walk_replace("$VAR", beta, alpha)
        else:
            res = (-1) - beta
            res = walk_replace("_", res, alpha)
    finally:
        if beta >= 0:
            walk_unmax("$VAR", alpha)
        else:
            walk_unmax("_", alpha)
    return exec_unify(args[2], res)


def walk_replace(functor, beta, first):
    stack = None
    vmap = None
    while True:
        first = deref(first)
        if is_structure(first):
            if (functor == first.functor and
                    len(first.args) == 1):
                peek = deref(first.args[0])
                if is_integer(peek) and beta <= peek:
                    first = assoc_get(vmap, peek)
                    if first is NotImplemented:
                        first = Variable()
                        vmap = assoc_set(vmap, peek, first)
            if is_structure(first):
                if not is_compound(first.functor):
                    first.functor = Compound(first.functor,
                        [NotImplemented] * len(first.args))
                    item = Item(first, None, 0)
                    stack = stack_push(stack, item)
                    first = first.args[0]
                    continue
                else:
                    first = first.functor
        item = stack_peek(stack)
        while (item is not None and
               item.idx == len(item.first.args) - 1):
            item.first.functor.args[item.idx] = first
            first = item.first.functor
            stack_pop(stack)
            item = stack_peek(stack)
        if item is None:
            return first
        else:
            item.first.functor.args[item.idx] = first
            item.idx += 1
            first = item.first.args[item.idx]


####################################################################
# maxvars/2                                                        #
####################################################################

###
# maxvars(X, M):
# The predicate succeeds in M with the max numbering of the term X.
##
def test_maxvars(args):
    alpha = exec_build(args[0])
    try:
        res = walk_max("$VAR", alpha)
    finally:
        walk_unmax("$VAR", alpha)
    return exec_unify(args[1], res)


def walk_max(functor, first):
    stack = None
    res = -1
    while True:
        first = deref(first)
        if is_structure(first):
            if (functor == first.functor and
                    len(first.args) == 1):
                peek = deref(first.args[0])
                if is_integer(peek):
                    if res < peek:
                        res = peek
                    first = None
            if first is not None:
                if not is_compound(first.functor):
                    first.functor = Compound(
                                first.functor, VOID_ARGS)
                    if 0 != len(first.args) - 1:
                        item = Item(first, None, 0)
                        stack = stack_push(stack, item)
                    first = first.args[0]
                    continue
        item = stack_peek(stack)
        if item is None:
            return res
        else:
            item.idx += 1
            first = item.first.args[item.idx]
            if item.idx == len(item.first.args) - 1:
                stack_pop(stack)


def walk_unmax(functor, first):
    stack = None
    while True:
        first = deref(first)
        if is_structure(first):
            if (functor == first.functor and
                    len(first.args) == 1):
                peek = deref(first.args[0])
                if is_integer(peek):
                    first = None
            if first is not None:
                if is_compound(first.functor):
                    first.functor = first.functor.functor
                    if 0 != len(first.args) - 1:
                        item = Item(first, None, 0)
                        stack = stack_push(stack, item)
                    first = first.args[0]
                    continue
        item = stack_peek(stack)
        if item is None:
            return
        else:
            item.idx += 1
            first = item.first.args[item.idx]
            if item.idx == len(item.first.args) - 1:
                stack_pop(stack)


####################################################################
# call/2-4                                                         #
####################################################################

###
# call(F, A1, .., An): [Corr.2 8.15.4.4]
# The predicate succeeds in calling the goal which results from
# appending the arguments A1, .., An to the callable F.
##
def special_calln(args):
    alpha = deref(args[0])
    if is_structure(alpha):
        functor = alpha.functor
        oldargs = alpha.args
    else:
        functor = alpha
        check_atomic(functor)
        oldargs = VOID_ARGS
    arity = len(oldargs) + len(args) - 1
    if arity == 0:
        goal = functor
    else:
        if is_number(functor):
            raise make_error(Compound("type_error", ["atom", functor]))
        newargs = [NotImplemented] * arity
        i = 0
        while i < len(oldargs):
            newargs[i] = deref(oldargs[i])
            i += 1
        i = 0
        while i < len(args) - 1:
            newargs[i + len(oldargs)] = deref(args[i + 1])
            i += 1
        goal = Compound(functor, newargs)
    goal = Compound(".", [goal, get_cont().args[1]])
    cont(goal)
    return True


####################################################################
# Asm Lib Init                                                     #
####################################################################

def main():
    add("numbervars", 3, make_check(test_numbervars))
    add("unnumbervars", 3, make_check(test_unnumbervars))
    add("maxvars", 2, make_check(test_maxvars))
    add("call", 2, make_special(special_calln))
    add("call", 3, make_special(special_calln))
    add("call", 4, make_special(special_calln))
    add("call", 5, make_special(special_calln))
    add("call", 6, make_special(special_calln))
    add("call", 7, make_special(special_calln))
    add("call", 8, make_special(special_calln))

