/**
 * 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 {
    check_nonvar, Compound,
    copy_term, fs, html_send, make_error,
    stream_flush, put_atom, stream_close, fiddle_out,
    exec_unify, exec_build, check_atom, is_variable,
    Sink, set, make_check, char_count,
    exec_deref, is_pending
} from "../../nova/core.mjs";

/*****************************************************************/
/* Sink API                                                      */
/*****************************************************************/

/**
 * ir_is_sink(O):
 * The predicate succeeds if O is a sink.
 */
function test_ir_is_sink(args) {
    let obj = exec_build(args[0]);
    return (obj instanceof Sink);
}

/*****************************************************************/
/* DOM Machine                                                   */
/*****************************************************************/

/**
 * ir_is_element(O):
 * The predicate succeeds if O is a DOM element.
 */
function test_ir_is_element(args) {
    let obj = exec_build(args[0]);
    if (fs !== undefined) {
        return false;
    } else {
        return (obj instanceof Element);
    }
}

/*****************************************************************/
/* Markup Streams                                                */
/*****************************************************************/

/**
 * dom_output_new(S, W):
 * The predicate succeeds in W with a new markup writer to the stream S.
 */
function test_dom_output_new(args) {
    let obj = exec_build(args[0]);
    let dst;
    if (obj instanceof Sink) {
        dst = new Sink();
        dst.data = obj;
        dst.send = xml_send;
        dst.notify = stream_flush;
        dst.release = stream_close;
        dst.buf = null;
    } else {
        check_element(obj);
        let dom = new Sink();
        dom.data = obj;
        dom.send = fiddle_out;

        dst = new Sink();
        dst.data = dom;
        dst.send = html_send;
        dst.notify = stream_flush;
        dst.release = stream_close;
        dst.buf = null;
    }
    return exec_unify(args[1], dst);
}

export function check_element(beta) {
    if (!(beta instanceof Element)) {
        check_nonvar(beta);
        beta = copy_term(beta);
        throw make_error(new Compound("type_error", ["element", beta]));
    }
}

function xml_send(data, buf) {
    let text = xml_escape(buf);
    put_atom(data, text);
    return data;
}

/*********************************************************************/
/* 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;
}

/*********************************************************************/
/* Dom Lib Init                                                      */
/*********************************************************************/

export function main() {
    set("ir_is_sink", 1, make_check(test_ir_is_sink));
    set("ir_is_element", 1, make_check(test_ir_is_element));
    set("dom_output_new", 2, make_check(test_dom_output_new));
    set("xml_escape", 2, make_check(test_xml_escape));
    set("percent_encode", 2, make_check(test_percent_encode));
}