/**
 * Modern Albufeira Prolog Interpreter
 *
 * Warranty & Liability
 * To the extent permitted by applicable law and unless explicitly
 * otherwise agreed upon, XLOG Technologies AG makes no warranties
 * regarding the provided information. XLOG Technologies AG assumes
 * no liability that any problems might be solved with the information
 * provided by XLOG Technologies AG.
 *
 * Rights & License
 * All industrial property rights regarding the information - copyright
 * and patent rights in particular - are the sole property of XLOG
 * Technologies AG. If the company was not the originator of some
 * excerpts, XLOG Technologies AG has at least obtained the right to
 * reproduce, change and translate the information.
 *
 * Reproduction is restricted to the whole unaltered document. Reproduction
 * of the information is only allowed for non-commercial uses. Selling,
 * giving away or letting of the execution of the library is prohibited.
 * The library can be distributed as part of your applications and libraries
 * for execution provided this comment remains unchanged.
 *
 * Restrictions
 * Only to be distributed with programs that add significant and primary
 * functionality to the library. Not to be distributed with additional
 * software intended to replace any components of the library.
 *
 * Trademarks
 * Jekejeke is a registered trademark of XLOG Technologies AG.
 */

import {exec_unify, exec_build, check_atom, is_variable, unify,
    check_sink, Source, Sink, add, make_check, char_count,
    exec_deref, is_pending
} from "../../nova/core.mjs";

/*********************************************************************/
/* open_input_atom_stream/2                                          */
/*********************************************************************/

/**
 * open_input_atom_stream(A, S): internal only
 * The built-in succeeds in S with a new input stream for the atom A.
 */
function test_open_input_atom_stream(args) {
    let text = exec_build(args[0]);
    check_atom(text);
    let src = new 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): internal only
 * The built-in succeeds in S with a new output stream.
 */
function test_open_output_atom_stream(args) {
    let dst = new Sink();
    dst.data = [];
    dst.send = mem_send;
    return exec_unify(args[0], dst);
}

function mem_send(data, buf) {
    data.push(buf);
    return data;
}

/**
 * sys_output_atom_stream_get(S, A): internal only
 * The built-in succeeds in A with the content of the output stream S.
 */
function test_sys_output_atom_stream_get(args) {
    let writer = exec_build(args[0]);
    check_sink(writer);
    let res = writer.data.join("");
    return exec_unify(args[1], res);
}

/*********************************************************************/
/* XML Escape                                                        */
/*********************************************************************/

let entity = {};
let entityrev = new Array(256);

function 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.
 */
function test_xml_escape(args) {
    let alpha = exec_deref(args[0]);
    if (is_variable(alpha) || is_pending(alpha)) {
        let 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);
    }
}

export function xml_escape(text) {
    let res = "";
    let back = 0;
    let i = 0;
    while (i < text.length) {
        let ch = text.codePointAt(i);
        let elem = get_entity_rev(ch);
        if (elem !== undefined) {
            res += text.slice(back, i);
            res += "&";
            res += elem;
            res += ";";
            back = i + char_count(ch);
        }
        i += char_count(ch);
    }
    if (back !== 0) {
        res += text.slice(back);
        text = res;
    }
    return text;
}

function get_entity_rev(ch) {
    return (ch <= 255 ? entityrev[ch] : undefined);
}

function xml_unescape(text) {
    let res = "";
    let back = 0;
    let i = 0;
    while (i < text.length) {
        let ch = text.codePointAt(i);
        if (ch === 38) {
            let k = i;
            i += char_count(ch);
            while (i < text.length && (ch = text.codePointAt(i)) !== 59) {
                i += char_count(ch);
            }
            if (i < text.length) {
                let val = get_entity(text.slice(k + 1, i));
                if (val !== undefined) {
                    res += text.slice(back, k);
                    res += String.fromCodePoint(val);
                    back = i + char_count(ch);
                }
                i += char_count(ch);
            }
        } else {
            i += char_count(ch);
        }
    }
    if (back !== 0) {
        res += text.slice(back);
        text = res;
    }
    return text;
}

function get_entity(text) {
    return entity[text];
}

/*********************************************************************/
/* Percent Encode                                                    */
/*********************************************************************/

/**
 * percent_encode(X, Y):
 * The predicate succeeds in Y with the percent encode of X.
 */
function test_percent_encode(args) {
    let alpha = exec_deref(args[0]);
    if (is_variable(alpha) || is_pending(alpha)) {
        let 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);
    }
}

function percent_encode(text) {
    let res = "";
    let back = 0;
    let i = 0;
    while (i < text.length) {
        let ch = text.codePointAt(i);
        i += char_count(ch);
        let elem;
        switch (ch) {
            case 32: // space
                elem = "+";
                break;
            case 35: // #
                elem = "%23";
                break;
            case 37: // %
                elem = "%25";
                break;
            case 38: // &
                elem = "%26";
                break;
            case 43: // +
                elem = "%2B";
                break;
            case 61: // =
                elem = "%3D";
                break;
            case 63: // ?
                elem = "%3F";
                break;
            case 92: // \
                elem = "%5C";
                break;
            default:
                elem = null;
                break;
        }
        if (elem !== null) {
            res += text.slice(back, i - 1);
            res += elem;
            back = i;
        }
    }
    if (back !== 0) {
        res += text.slice(back);
        text = res;
    }
    return text;
}

function percent_decode(text) {
    let res = "";
    let back = 0;
    let i = 0;
    while (i < text.length) {
        let ch = text.codePointAt(i);
        i += char_count(ch);
        if (ch === 37) { // %
            let octet = octet_hex(text, i);
            if (0 <= octet && octet < 128) {
                res += text.slice(back, i - 1);
                res += String.fromCodePoint(octet);
                i += 2;
                back = i;
            } else if (192 <= octet && octet < 224) {
                let octet2 = percent_hex(text, i + 2);
                if (128 <= octet2 && octet2 < 192) {
                    res += text.slice(back, i - 1);
                    res += String.fromCodePoint((octet - 192) * 64 + (octet2 - 128));
                    i += 5;
                    back = i;
                }
            } else if (224 <= octet && octet < 240) {
                let octet2 = percent_hex(text, i + 2);
                if (128 <= octet2 && octet2 < 192) {
                    octet = (octet - 224) * 64 + (octet2 - 128);
                    octet2 = percent_hex(text, i + 5);
                    if (128 <= octet2 && octet2 < 192) {
                        res += text.slice(back, i - 1);
                        res += String.fromCodePoint(octet * 64 + (octet2 - 128));
                        i += 8;
                        back = i;
                    }
                }
            } else if (240 <= octet && octet < 248) {
                let octet2 = percent_hex(text, i + 2);
                if (128 <= octet2 && octet2 < 192) {
                    octet = (octet - 240) * 64 + (octet2 - 128);
                    octet2 = percent_hex(text, i + 5);
                    if (128 <= octet2 && octet2 < 192) {
                        octet = octet * 64 + (octet2 - 128);
                        octet2 = percent_hex(text, i + 8);
                        if (128 <= octet2 && octet2 < 192) {
                            res += text.slice(back, i - 1);
                            res += String.fromCodePoint(octet * 64 + (octet2 - 128));
                            i += 11;
                            back = i;
                        }
                    }
                }
            }
        } else if (ch === 43) { // +
            res += text.slice(back, i - 1);
            res += String.fromCodePoint(32); // space
            back = i;
        }
    }
    if (back !== 0) {
        res += text.slice(back);
        text = res;
    }
    return text;
}

function percent_hex(text, i) {
    let ch = (i < text.length ? text.charCodeAt(i) : -1);
    if (ch === 37) {
        i++;
        return octet_hex(text, i);
    }
    return -1;
}

function octet_hex(text, i) {
    let val = digit_hex(i < text.length ? text.charCodeAt(i) : -1);
    if (val !== -1) {
        i++;
        let val2 = digit_hex(i < text.length ? text.charCodeAt(i) : -1);
        if (val2 !== -1) {
            return val * 16 + val2;
        }
    }
    return -1;
}

function digit_hex(ch) {
    if (48 <= ch && ch <= 57) return ch - 48;
    if (65 <= ch && ch <= 70) return ch - 65 + 10;
    if (97 <= ch && ch <= 102) return ch - 97 + 10;
    return -1;
}

/*********************************************************************/
/* Aux Lib Init                                                      */
/*********************************************************************/

export function main() {
    // system specials, input/output, internal only
    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));
}