/*
 * Copyright (c) 2017. Stephan Mahieu
 *
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE', which is part of this source code package.
 */

//import {DateUtil} from 'DateUtil.js';

class XmlUtil {

    /**
     * Deserialize an XML string containing formhistory data.
     *
     * @param  xmlString {String}
     *         the string source of the XML
     *
     * @return {Object}
     *         an object with arrays holding formhistory entries
     */
    static parseXMLdata(xmlString) {
        let parsedEntries = [];
        let parsedEditorfield = [];

        let now = DateUtil.getCurrentDate();

        let parser = new DOMParser();
        try {
            let doc = parser.parseFromString(xmlString, "text/xml");

            if ("formhistory" === doc.documentElement.nodeName) {

                // formhistory fields
                let fldElem = doc.getElementsByTagName("field");
                let nameElem, valElem;
                for(let ii=0; ii<fldElem.length; ii++) {
                    if (fldElem[ii].hasChildNodes()) {
                        nameElem = fldElem[ii].getElementsByTagName("name");
                        valElem = fldElem[ii].getElementsByTagName("value");

                        if (1 === valElem.length && 0 < valElem[0].textContent.length) {
                            parsedEntries.push({
                                id:        -1,
                                name:      decodeURIComponent(nameElem[0].textContent),
                                value:     decodeURIComponent(valElem[0].textContent),
                                used:      decodeURIComponent(this._getElementValue(fldElem[ii], "timesUsed", 0)),
                                first:                        this._getElemenDate(  fldElem[ii], "firstUsed", now),
                                last:                         this._getElemenDate(  fldElem[ii], "lastUsed",  now),
                                /* new since 2.0.0. */
                                type:      decodeURIComponent(this._getElementValue(fldElem[ii], "type", "")),
                                host:      decodeURIComponent(this._getElementValue(fldElem[ii], "host", "")),
                                url:       decodeURIComponent(this._getElementValue(fldElem[ii], "url", "")),
                                pagetitle: decodeURIComponent(this._getElementValue(fldElem[ii], "pagetitle", ""))
                            });
                        }
                    }
                }

                // multiline editor fields
                let editorfieldElem = doc.getElementsByTagName("editorField");
                let edFldElem;
                for(let nn=0; nn<editorfieldElem.length; nn++) {
                    if (editorfieldElem[nn].hasChildNodes()) {
                        edFldElem = editorfieldElem[nn];
                        parsedEditorfield.push({
                            id:         decodeURIComponent(this._getElementValue(edFldElem, "id", "")),
                            name:       decodeURIComponent(this._getElementValue(edFldElem, "name", "")),
                            type:       decodeURIComponent(this._getElementValue(edFldElem, "type", "")),
                            formid:     decodeURIComponent(this._getElementValue(edFldElem, "formid", "")),
                            content:    decodeURIComponent(this._getElementValue(edFldElem, "content", "")),
                            host:       decodeURIComponent(this._getElementValue(edFldElem, "host", "")),
                            url:        decodeURIComponent(this._getElementValue(edFldElem, "url", "")),
                            firstsaved:                    this._getElemenDate(edFldElem, "firstsaved", ""),
                            lastsaved:                     this._getElemenDate(edFldElem, "lastsaved", ""),
                            /* new since 2.0.0. */
                            used:                          this._getElementValue(edFldElem, "timesUsed", 1),
                            pagetitle:  decodeURIComponent(this._getElementValue(edFldElem, "pagetitle", ""))
                        });
                    }
                }
            }
        } catch(ex) {
            alert("XML parser exception: " + ex);
        }

        return {
            entries:   parsedEntries,
            multiline: parsedEditorfield
        };
    }

    /**
     * Serialize data (formhistory, config, etc) into a XML string representation.
     *
     * @param  entries {Array}
     *         array containing text entries
     *
     * @param  multilines {Array}
     *         array containing multiline entries
     *
     * @return {String}
     *         a pretty printed XML string representation of the entries
     */
    static serializeToXMLString(entries, multilines) {
        // create a DOM tree
        let doc = document.implementation.createDocument("", "", null);
        let rootElem = doc.createElement("formhistory");
        doc.appendChild(rootElem);

        // create a header
        this._appendHeaderElement(doc, rootElem);

        // add formhistory fields
        if (entries && 0 < entries.length) {
            let fieldsElem = doc.createElement("fields");
            rootElem.appendChild(fieldsElem);

            let fieldElem;
            for(let ii=0; ii<entries.length; ii++) {
                fieldElem = this._createFormhistoryElement(doc, entries[ii]);
                fieldsElem.appendChild(fieldElem);
            }
        }

        // add multiline editorfields
        if (multilines && 0 < multilines.length) {
            let editorfieldsElem = doc.createElement("editorFields");
            rootElem.appendChild(editorfieldsElem);

            let editorfieldElem;
            for(let nn=0; nn<multilines.length; nn++) {
                editorfieldElem = this._createEditorfieldElement(doc, multilines[nn]);
                editorfieldsElem.appendChild(editorfieldElem);
            }
        }

        // serialize to string (pretty printed)
        let serializer = new XMLSerializer();

        //return serializer.serializeToString(doc);
        return(this._prettyPrintXML(serializer.serializeToString(doc), "\t"));
    }

    //----------------------------------------------------------------------------
    // Helper methods
    //----------------------------------------------------------------------------
    
    /**
     * Get the textcontent of a DOM Element from a parent by tagname. If no tag
     * is found, the default value is returned.
     *
     * @param  parentElem {Element}
     *         the DOM element containing the child element(s)
     *
     * @param  tagName {String}
     *         the name of the tag to search for inside the parent
     *
     * @param  defaultValue
     *         the value to return if no tag is found
     *
     * @return {String}
     *         the textcontent of the requested child element or the default
     *         value if tag is not found
     */
    static _getElementValue(parentElem, tagName, defaultValue) {
        let result = defaultValue;
        let childElem = parentElem.getElementsByTagName(tagName);
        if (1 === childElem.length && "" !== childElem[0].textContent) {
            result = childElem[0].textContent;
        }
        return result;
    }

    /**
     * Get the date content of a DOM Element from a parent by tagname. If no tag
     * is found, the default value is returned.
     *
     * @param  parentElem {Element}
     *         the DOM element containing the child element(s)
     *
     * @param  tagName {String}
     *         the name of the tag to search for inside the parent
     *
     * @param  defaultValue
     *         the value to return if no tag is found
     *
     * @return {Number}
     *         the date value in milliseconds of the requested child element or
     *         the default value if tag is not found
     */
    static _getElemenDate (parentElem, tagName, defaultValue) {
        let result = defaultValue;
        let childElem = parentElem.getElementsByTagName(tagName);
        if (1 === childElem.length) {
            if (childElem[0].firstElementChild !== null) {
                // found an old microseconds format, convert it to milliseconds
                result = this._microToMillis(this._getElementValue(childElem[0], "date", ""));
            }
            else {
                result = DateUtil.fromISOdateString(childElem[0].textContent);
            }
        }
        return result;
    }

    /**
     *  Convert microseconds to milliseconds.
     */
    static _microToMillis(uSeconds) {
        if (isNaN(uSeconds)) {
            return uSeconds;
        }
        return Math.round(uSeconds/1000);
    }

    /**
     * Encode special characters for use inside an XML document.
     *
     * @param  aString {String}
     *         string which may contain characters which are not allowed inside
     *         a XML document.
     *
     * @return {String}
     *         a string in which all invalid (international) characters are
     *         encoded so they can be safely used inside XML
     */
    static _encode(aString) {
        // use encodeURIComponent() which can handle all international chars but
        // keep it somewhat readable by converting back some safe (for XML) chars
        return encodeURIComponent(aString)
            .replace(/%20/g, " ")
            .replace(/^ /g, "%20") /* keep leading space  */
            .replace(/ $/g, "%20") /* keep trailing space */
            .replace(/%21/g, "!")
            .replace(/%22/g, '"')
            .replace(/%23/g, "#")
            .replace(/%24/g, "$")
            /* do not replace %25 (%) */
            .replace(/%26/g, "&")
            .replace(/%2B/g, "+")
            .replace(/%2C/g, ",")
            .replace(/%2F/g, "/")
            .replace(/%3A/g, ":")
            .replace(/%3B/g, ";")
            .replace(/%3D/g, "=")
            .replace(/%3F/g, "?")
            .replace(/%40/g, "@")
            .replace(/%5B/g, "[")
            .replace(/%5C/g, "\\")
            .replace(/%5D/g, "]")
            .replace(/%5E/g, "^")
            .replace(/%60/g, "`")
            .replace(/%7B/g, "{")
            .replace(/%7C/g, "|")
            .replace(/%7D/g, "}")
            .replace(/%7E/g, "~");
    }

    /**
     *  Create a DOM element for a date in native format (milliseconds) and append
     *  it to the parentElemen. Also add a comment inside the date tag containing
     *  the date in human readable form.
     *
     *  @param  parentElem {Element}
     *          the DOM element in which to add the data child element
     *
     *  @param  dateElem {Element}
     *          the DOM element representing the child date element
     *
     *  @param  milliseconds {Number}
     *          the date in milliseconds, the content of this element
     */
    static _appendDateElement(parentElem, dateElem, milliseconds) {
        if (milliseconds !== undefined) {
            // ISO date format
            dateElem.textContent = DateUtil.toISOdateString(milliseconds);
            parentElem.appendChild(dateElem);
        }
    }

    /**
     *  Create a DOM element holding a string value and append it to the
     *  parentElem.
     *
     *  @param  parentElem {Element}
     *          the DOM element in which to add the data child element
     *
     *  @param  childElem {Element}
       *          the DOM element representing the child element
       *
     *  @param  aValue {String}
     *          the text value
     */
    static _appendElement(parentElem, childElem, aValue) {
        childElem.textContent = aValue;
        parentElem.appendChild(childElem);
    }

    /**
     *  Create a Header element inside the given parent containing application and
     *  version info elements.
     *
     *  @param doc {Document}
     *         the document object
     *
     *  @param parentElem {Element}
     *         the DOM element in which to add the new child elements
     */
    static _appendHeaderElement(doc, parentElem) {
        let manifest = browser.runtime.getManifest();

        let headerElem = doc.createElement("header");
        parentElem.appendChild(headerElem);

        let appinfoElem = doc.createElement("application");
        appinfoElem.textContent = manifest.name;
        let versionElem = doc.createElement("version");
        versionElem.textContent = manifest.version;
        let dateElem = doc.createElement("exportDate");
        dateElem.textContent = DateUtil.getCurrentISOdateString();

        headerElem.appendChild(appinfoElem);
        headerElem.appendChild(versionElem);
        headerElem.appendChild(dateElem);
    }

    /**
     * Create a formfield element.
     * (cdata for value would be nice but is removed by XML.toXMLString())
     *
     * @param doc {Document}
     *        the document containing DOM-elements
     *
     * @param entry {Object}
     *        the formhistory object
     *
     * @return {Element}
     *         the formhistory element
     */
    static _createFormhistoryElement(doc, entry) {
        let fieldElem = doc.createElement("field");

        this._appendElement(fieldElem, doc.createElement("name"), entry.name);
        this._appendElement(fieldElem, doc.createElement("value"), this._encode(entry.value));
        this._appendElement(fieldElem, doc.createElement("timesUsed"), entry.used);
        this._appendDateElement(fieldElem, doc.createElement("firstUsed"), entry.first);
        this._appendDateElement(fieldElem, doc.createElement("lastUsed"),  entry.last);
        /* Extra since 2.0.0 */
        this._appendElement(fieldElem, doc.createElement("type"), entry.type);
        this._appendElement(fieldElem, doc.createElement("host"), entry.host);
        this._appendElement(fieldElem, doc.createElement("url"), entry.url);
        this._appendElement(fieldElem, doc.createElement("pagetitle"), entry.pagetitle);

        return fieldElem;
    }

    /**
     * Create an editorfield element for a multiline field.
     *
     * @param doc {Document}
     *        the document containing DOM-elements
     *
     * @param editorField {Object}
     *        the multiline object
     *
     * @return {Element}
     *         the editorField element
     */
    static _createEditorfieldElement(doc, editorField) {
        let editorElem = doc.createElement("editorField");

        this._appendElement(    editorElem, doc.createElement("id"), this._encode(editorField.id));
        this._appendElement(    editorElem, doc.createElement("name"), this._encode(editorField.name));
        this._appendElement(    editorElem, doc.createElement("type"), this._encode(editorField.type));
        this._appendElement(    editorElem, doc.createElement("formid"), this._encode(editorField.formid));
        this._appendElement(    editorElem, doc.createElement("host"), this._encode(editorField.host));
        this._appendElement(    editorElem, doc.createElement("url"), this._encode(editorField.url));
        this._appendDateElement(editorElem, doc.createElement("firstsaved"), editorField.first);
        this._appendDateElement(editorElem, doc.createElement("lastsaved"), editorField.last);
        this._appendElement(    editorElem, doc.createElement("content"), this._encode(editorField.content));
        /* Extra since 2.0.0 */
        this._appendElement(    editorElem, doc.createElement("timesUsed"), editorField.used);
        this._appendElement(    editorElem, doc.createElement("pagetitle"), editorField.pagetitle);

        return editorElem;
    }

    /**
     * Pretty print XML.
     * Adapted from vkBeautify by Vadim Kiryukhin (http://www.eslinstructor.net/vkbeautify/).
     *
     * @param text {String}
     *        the XML text
     *
     * @param indent {String}
     *        the text to use for indentation
     *
     * @return {String}
     *         pretty printed XML
     */
    static _prettyPrintXML(text, indent) {
        let shift = ['\n'];
        let maxNestingLevel = 6;
        for (let i=0; i<maxNestingLevel; i++) {
            shift.push(shift[i]+indent);
        }

        let ar = text.replace(/>\s*</g,"><")
                .replace(/</g,"~::~<")
                .replace(/\s*xmlns:/g,"~::~xmlns:")
                .replace(/\s*xmlns=/g,"~::~xmlns=")
                .split('~::~'),
            len = ar.length,
            inComment = false,
            deep = 0,
            str = '';

        for(let i=0;i<len;i++) {
            // start comment or <![CDATA[...]]> or <!DOCTYPE //
            if(ar[i].search(/<!/) > -1) {
                str += shift[deep]+ar[i];
                inComment = true;
                // end comment  or <![CDATA[...]]> //
                if(ar[i].search(/-->/) > -1 || ar[i].search(/]>/) > -1 || ar[i].search(/!DOCTYPE/) > -1 ) {
                    inComment = false;
                }
            } else
            // end comment  or <![CDATA[...]]> //
            if(ar[i].search(/-->/) > -1 || ar[i].search(/]>/) > -1) {
                str += ar[i];
                inComment = false;
            } else
            // <elm></elm> //
            if( /^<\w/.exec(ar[i-1]) && /^<\/\w/.exec(ar[i]) &&
                /^<[\w:\-.,]+/.exec(ar[i-1]) == /^<\/[\w:\-.,]+/.exec(ar[i])[0].replace('/','')) {
                str += ar[i];
                if(!inComment) deep--;
            } else
            // <elm> //
            if(ar[i].search(/<\w/) > -1 && ar[i].search(/<\//) === -1 && ar[i].search(/\/>/) === -1 ) {
                str = !inComment ? str += shift[deep++]+ar[i] : str += ar[i];
            } else
            // <elm>...</elm> //
            if(ar[i].search(/<\w/) > -1 && ar[i].search(/<\//) > -1) {
                str = !inComment ? str += shift[deep]+ar[i] : str += ar[i];
            } else
            // </elm> //
            if(ar[i].search(/<\//) > -1) {
                str = !inComment ? str += shift[--deep]+ar[i] : str += ar[i];
            } else
            // <elm/> //
            if(ar[i].search(/\/>/) > -1 ) {
                str = !inComment ? str += shift[deep]+ar[i] : str += ar[i];
            } else
            // <? xml ... ?> //
            if(ar[i].search(/<\?/) > -1) {
                str += shift[deep]+ar[i];
            } else
            // xmlns //
            if( ar[i].search(/xmlns:/) > -1 || ar[i].search(/xmlns=/) > -1) {
                str += shift[deep]+ar[i];
            }
            else {
                str += ar[i];
            }
        }
        return  (str[0] === '\n') ? str.slice(1) : str;
    }
}
