###
# 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 (exec_unify, exec_build,
     is_variable, check_atom, check_sink, Source,
     Sink, add, make_check, exec_deref, is_pending)


#######################################################################
# open_input_atom_stream/2                                            #
#######################################################################

###
# open_input_atom_stream(A, S):
# The built-in succeeds in S with a new input stream for the atom A.
##
def test_open_input_atom_stream(args):
    text = exec_build(args[0])
    check_atom(text)
    src = Source()
    src.buf = text
    return exec_unify(args[1], src)


#######################################################################
# open_output_atom_stream/1 and sys_output_atom_stream_get/2          #
#######################################################################

###
# open_output_atom_stream(S):
# The built-in succeeds in S with a new output stream.
##
def test_open_output_atom_stream(args):
    dst = Sink()
    dst.data = []
    dst.send = mem_send
    return exec_unify(args[0], dst)


def mem_send(data, buf):
    data.append(buf)
    return data


###
# sys_output_atom_stream_get(S, A):
# The built-in succeeds in A with the content of the output stream S.
##
def test_sys_output_atom_stream_get(args):
    writer = exec_build(args[0])
    check_sink(writer)
    res = "".join(writer.data)
    return exec_unify(args[1], res)


#######################################################################
# XML Escape                                                          #
#######################################################################

entity = {}
entityrev = [NotImplemented] * 256


def add_entity(text, i):
    entity[text] = i
    entityrev[i] = text


add_entity("quot", 34)
add_entity("apos", 39)
add_entity("lt", 60)
add_entity("gt", 62)
add_entity("amp", 38)
add_entity("nbsp", 0xA0)


###
# xml_escape(X, Y):
# The predicate succeeds in Y with the XML escape of X.
##
def test_xml_escape(args):
    alpha = exec_deref(args[0])
    if is_variable(alpha) or is_pending(alpha):
        beta = exec_build(args[1])
        check_atom(beta)
        beta = xml_unescape(beta)
        return exec_unify(alpha, beta)
    else:
        alpha = exec_build(alpha)
        check_atom(alpha)
        alpha = xml_escape(alpha)
        return exec_unify(args[1], alpha)


def xml_escape(text):
    res = ""
    back = 0
    i = 0
    while i < len(text):
        ch = ord(text[i])
        elem = get_entity_rev(ch)
        if elem is not NotImplemented:
            res += text[back:i]
            res += "&"
            res += elem
            res += ";"
            back = i + 1
        i += 1
    if back != 0:
        res += text[back:]
        text = res
    return text


def get_entity_rev(ch):
    return entityrev[ch] if ch <= 255 else NotImplemented


def xml_unescape(text):
    res = ""
    back = 0
    i = 0
    while i < len(text):
        ch = ord(text[i])
        if ch == 38:
            k = i
            i += 1
            while i < len(text) and (ch := ord(text[i])) != 59:
                i += 1
            if i < len(text):
                val = get_entity(text[k+1:i])
                if val is not NotImplemented:
                    res += text[back:k]
                    res += chr(val)
                    back = i + 1
                i += 1
        else:
            i += 1
    if back != 0:
        res += text[back:]
        text = res
    return text


def get_entity(text):
    return entity.get(text, NotImplemented)


#######################################################################
# Percent Encode                                                      #
#######################################################################

###
# percent_encode(X, Y):
# The predicate succeeds in Y with the percent encode of X.
##
def test_percent_encode(args):
    alpha = exec_deref(args[0])
    if is_variable(alpha) or is_pending(alpha):
        beta = exec_build(args[1])
        check_atom(beta)
        beta = percent_decode(beta)
        return exec_unify(alpha, beta)
    else:
        alpha = exec_build(alpha)
        check_atom(alpha)
        alpha = percent_encode(alpha)
        return exec_unify(args[1], alpha)


def percent_encode(text):
    res = ""
    back = 0
    i = 0
    while i < len(text):
        ch = ord(text[i])
        i += 1
        if ch == 32:  # space
            elem = "+"
        elif ch == 35:  # #
            elem = "%23"
        elif ch == 37:  # %
            elem = "%25"
        elif ch == 38:  # &
            elem = "%26"
        elif ch == 43:  # +
            elem = "%2B"
        elif ch == 61:  # =
            elem = "%3D"
        elif ch == 63:  # ?
            elem = "%3F"
        elif ch == 92:  # \
            elem = "%5C"
        else:
            elem = None
        if elem is not None:
            res += text[back:i - 1]
            res += elem
            back = i
    if back != 0:
        res += text[back:]
        text = res
    return text


def percent_decode(text):
    res = ""
    back = 0
    i = 0
    while i < len(text):
        ch = ord(text[i])
        i += 1
        if ch == 37:
            octet = octet_hex(text, i)
            if 0 <= octet < 128:
                res += text[back:i - 1]
                res += chr(octet)
                i += 2
                back = i
            elif 192 <= octet < 224:
                octet2 = percent_hex(text, i + 2)
                if 128 <= octet2 < 192:
                    res += text[back:i - 1]
                    res += chr((octet - 192) * 64 + (octet2 - 128))
                    i += 5
                    back = i
            elif 224 <= octet < 240:
                octet2 = percent_hex(text, i + 2)
                if 128 <= octet2 < 192:
                    octet = (octet - 224) * 64 + (octet2 - 128)
                    octet2 = percent_hex(text, i + 5)
                    if 128 <= octet2 < 192:
                        res += text[back:i - 1]
                        res += chr(octet * 64 + (octet2 - 128))
                        i += 8
                        back = i
            elif 240 <= octet < 248:
                octet2 = percent_hex(text, i + 2)
                if 128 <= octet2 < 192:
                    octet = (octet - 240) * 64 + (octet2 - 128)
                    octet2 = percent_hex(text, i + 5)
                    if 128 <= octet2 < 192:
                        octet = octet * 64 + (octet2 - 128)
                        octet2 = percent_hex(text, i + 8)
                        if 128 <= octet2 < 192:
                            res += text[back:i - 1]
                            res += chr(octet * 64 + (octet2 - 128))
                            i += 11
                            back = i
        elif ch == 43:
            res += text[back:i - 1]
            res += chr(32)  # space
            back = i
    if back != 0:
        res += text[back:]
        text = res
    return text


def percent_hex(text, i):
    ch = (ord(text[i]) if i < len(text) else -1)
    if ch == 37:
        i += 1
        return octet_hex(text, i)
    return -1


def octet_hex(text, i):
    val = digit_hex(ord(text[i]) if i < len(text) else -1)
    if val != -1:
        i += 1
        val2 = digit_hex(ord(text[i]) if i < len(text) else -1)
        if val2 != -1:
            return val * 16 + val2
    return -1


def digit_hex(ch):
    if 48 <= ch <= 57:
        return ch - 48
    if 65 <= ch <= 70:
        return ch - 65 + 10
    if 97 <= ch <= 102:
        return ch - 97 + 10
    return -1


#######################################################################
# Aux Lib Init                                                        #
#######################################################################

def main():
    add("open_input_atom_stream", 2, make_check(test_open_input_atom_stream))
    add("open_output_atom_stream", 1, make_check(test_open_output_atom_stream))
    add("sys_output_atom_stream_get", 2, make_check(test_sys_output_atom_stream_get))
    add("xml_escape", 2, make_check(test_xml_escape))
    add("percent_encode", 2, make_check(test_percent_encode))
