package doglet.text;

import nova.Machine;
import nova.Store;
import nova.runtime;
import nova.special;

import java.util.HashMap;
import java.util.Map;

/**
 * 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.
 */
public final class domlib {

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

    /**
     * ir_is_sink(O):
     * The predicate succeeds if O is a sink.
     */
    private static boolean test_ir_is_sink(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        return (obj instanceof runtime.Sink);
    }

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

    /**
     * ir_is_element(O):
     * The predicate succeeds if O is a DOM element.
     */
    private static boolean test_ir_is_element(Object[] args) {
        return false;
    }

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

    /**
     * dom_output_new(S, W):
     * The predicate succeeds in W with a new markup writer to the stream S.
     */
    private static boolean test_dom_output_new(Object[] args) {
        Object obj = Machine.exec_build(args[0]);
        runtime.check_sink(obj);
        runtime.Sink dst = new runtime.Sink();
        dst.data = obj;
        dst.send = domlib::xml_send;
        dst.notify = runtime::flush;
        dst.release = runtime::close;
        dst.buf = null;
        return Machine.exec_unify(args[1], dst);
    }

    private static Object xml_send(Object data, String buf) {
        String text = xml_escape(buf);
        runtime.put_atom((runtime.Sink) data, text);
        return data;
    }

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

    private static final Map entity = new HashMap();
    private static final String[] entityrev = new String[256];

    private static void addEntity(String text, int i) {
        Integer ival = Integer.valueOf(i);
        entity.put(text, ival);
        entityrev[i] = text;
    }

    static {
        addEntity("quot", '\"');
        addEntity("apos", '\'');
        addEntity("lt", '<');
        addEntity("gt", '>');
        addEntity("amp", '&');
        addEntity("nbsp", 0xA0);
    }

    /**
     * xml_escape(X, Y):
     * The predicate succeeds in Y with the XML escape of X.
     */
    private static boolean test_xml_escape(Object[] args) {
        Object alpha = Machine.exec_deref(args[0]);
        if (Store.is_variable(alpha) || Machine.is_pending(alpha)) {
            Object beta = Machine.exec_build(args[1]);
            special.check_atom(beta);
            beta = xml_unescape((String) beta);
            return Machine.exec_unify(alpha, beta);
        } else {
            alpha = Machine.exec_build(alpha);
            special.check_atom(alpha);
            alpha = xml_escape((String) alpha);
            return Machine.exec_unify(args[1], alpha);
        }
    }

    public static String xml_escape(String text) {
        StringBuilder res = null;
        int back = 0;
        int i = 0;
        while (i < text.length()) {
            int ch = text.codePointAt(i);
            String elem = getEntityRev(ch);
            if (elem != null) {
                if (res == null)
                    res = new StringBuilder();
                res.append(text, back, i);
                res.appendCodePoint('&');
                res.append(elem);
                res.appendCodePoint(';');
                back = i + Character.charCount(ch);
            }
            i += Character.charCount(ch);
        }
        if (back != 0) {
            res.append(text, back, text.length());
            text = res.toString();
        }
        return text;
    }

    private static String getEntityRev(int ch) {
        return (ch <= 255 ? entityrev[ch] : null);
    }

    private static String xml_unescape(String text) {
        StringBuilder res = null;
        int back = 0;
        int i = 0;
        while (i < text.length()) {
            int ch = text.codePointAt(i);
            if (ch == '&') {
                int k = i;
                i += Character.charCount(ch);
                while (i < text.length() && (ch = text.codePointAt(i)) != ';') {
                    i += Character.charCount(ch);
                }
                if (i < text.length()) {
                    int val = getEntity(text.substring(k + 1, i));
                    if (val != -1) {
                        if (res == null)
                            res = new StringBuilder();
                        res.append(text, back, k);
                        res.appendCodePoint(val);
                        back = i + Character.charCount(ch);
                    }
                    i += Character.charCount(ch);
                }
            } else {
                i += Character.charCount(ch);
            }
        }
        if (back != 0) {
            res.append(text, back, text.length());
            text = res.toString();
        }
        return text;
    }

    private static int getEntity(String text) {
        Integer ival = (Integer)entity.get(text);
        return (ival != null ? ival.intValue() : -1);
    }

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

    /**
     * percent_encode(X, Y):
     * The predicate succeeds in Y with the percent encode of X.
     */
    private static boolean test_percent_encode(Object[] args) {
        Object alpha = Machine.exec_deref(args[0]);
        if (Store.is_variable(alpha) || Machine.is_pending(alpha)) {
            Object beta = Machine.exec_build(args[1]);
            special.check_atom(beta);
            beta = percent_decode((String) beta);
            return Machine.unify(alpha, beta);
        } else {
            alpha = Machine.exec_build(alpha);
            special.check_atom(alpha);
            alpha = percent_encode((String) alpha);
            return Machine.exec_unify(args[1], alpha);
        }
    }

    public static String percent_encode(String text) {
        StringBuilder res = null;
        int back = 0;
        int i = 0;
        while (i < text.length()) {
            int ch = text.codePointAt(i);
            i += Character.charCount(ch);
            String 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) {
                if (res == null)
                    res = new StringBuilder();
                res.append(text, back, i - 1);
                res.append(elem);
                back = i;
            }
        }
        if (back != 0) {
            res.append(text, back, text.length());
            text = res.toString();
        }
        return text;
    }

    public static String percent_decode(String text) {
        StringBuilder res = null;
        int back = 0;
        int i = 0;
        while (i < text.length()) {
            int ch = text.codePointAt(i);
            i += Character.charCount(ch);
            if (ch == 37) { // %
                int octet = octet_hex(text, i);
                if (0 <= octet && octet < 128) {
                    if (res == null)
                        res = new StringBuilder();
                    res.append(text, back, i - 1);
                    res.appendCodePoint(octet);
                    i += 2;
                    back = i;
                } else if (192 <= octet && octet < 224) {
                    int octet2 = percent_hex(text, i + 2);
                    if (128 <= octet2 && octet2 < 192) {
                        if (res == null)
                            res = new StringBuilder();
                        res.append(text, back, i - 1);
                        res.appendCodePoint((octet - 192) * 64 + (octet2 - 128));
                        i += 5;
                        back = i;
                    }
                } else if (224 <= octet && octet < 240) {
                    int 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) {
                            if (res == null)
                                res = new StringBuilder();
                            res.append(text, back, i - 1);
                            res.appendCodePoint(octet * 64 + (octet2 - 128));
                            i += 8;
                            back = i;
                        }
                    }
                } else if (240 <= octet && octet < 248) {
                    int 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) {
                                if (res == null)
                                    res = new StringBuilder();
                                res.append(text, back, i - 1);
                                res.appendCodePoint(octet * 64 + (octet2 - 128));
                                i += 11;
                                back = i;
                            }
                        }
                    }
                }
            } else if (ch == 43) { // +
                if (res == null)
                    res = new StringBuilder();
                res.append(text, back, i - 1);
                res.appendCodePoint(32); // space
                back = i;
            }
        }
        if (back != 0) {
            res.append(text, back, text.length());
            text = res.toString();
        }
        return text;
    }

    private static int percent_hex(String text, int i) {
        int ch = (i < text.length() ? text.charAt(i) : -1);
        if (ch == 37) {
            i++;
            return octet_hex(text, i);
        }
        return -1;
    }

    private static int octet_hex(String text, int i) {
        int val = digit_hex(i < text.length() ? text.charAt(i) : -1);
        if (val != -1) {
            i++;
            int val2 = digit_hex(i < text.length() ? text.charAt(i) : -1);
            if (val2 != -1) {
                return val * 16 + val2;
            }
        }
        return -1;
    }

    private static int digit_hex(int 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                                                   */
    /******************************************************************/

    public static void main() {
        Store.set("ir_is_sink", 1, special.make_check(domlib::test_ir_is_sink));
        Store.set("ir_is_element", 1, special.make_check(domlib::test_ir_is_element));
        Store.set("dom_output_new", 2, special.make_check(domlib::test_dom_output_new));
        Store.set("xml_escape", 2, special.make_check(domlib::test_xml_escape));
        Store.set("percent_encode", 2, special.make_check(domlib::test_percent_encode));
    }

}
