/*
 * Copyright 2016 Software Freedom Conservancy Inc.
 * Copyright 2016 Michael Gratton <mike@vee.net>
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later). See the COPYING file in this distribution.
 */

/**
 * Application logic for ConversationWebView.
 */
let ConversationPageState = function() {
    this.init.apply(this, arguments);
};

ConversationPageState.QUOTE_CONTAINER_CLASS = "geary-quote-container";
ConversationPageState.QUOTE_HIDE_CLASS = "geary-hide";

// Keep these in sync with ConversationWebView
ConversationPageState.NOT_DECEPTIVE = 0;
ConversationPageState.DECEPTIVE_HREF = 1;
ConversationPageState.DECEPTIVE_DOMAIN = 2;

ConversationPageState.prototype = {
    __proto__: PageState.prototype,
    init: function() {
        PageState.prototype.init.apply(this, []);

        let state = this;
        document.addEventListener("click", function(e) {
            if (e.target.tagName == "A" &&
                state.linkClicked(e.target)) {
                e.preventDefault();
            }
        }, true);
    },
    /**
     * Add email headers for printing
     */
    addPrintHeaders: function(headers) {
        let headerTable = document.getElementById('geary-message-headers');
        if (headerTable) headerTable.parentNode.removeChild(headerTable);

        headerTable = document.createElement('table');
        headerTable.id = 'geary-message-headers';
        for (var header in headers) {
            let row = headerTable.appendChild(document.createElement('tr'));
            let name = row.appendChild(document.createElement('th'));
            let value = row.appendChild(document.createElement('td'));
            name.textContent = header;
            value.textContent = headers[header];
        }

        document.body.insertBefore(headerTable, document.body.firstChild);
    },
    loaded: function() {
        this.updateDirection();
        this.createControllableQuotes();
        this.wrapSignature();
        PageState.prototype.loaded.apply(this, []);
    },
    /**
     * Set dir="auto" if not already set.
     *
     * This should provide a slightly better RTL experience.
     */
    updateDirection: function() {
        let dir = document.documentElement.dir;
        if (dir == null || dir.trim() == "") {
            document.documentElement.dir = "auto";
        }
    },
    /**
     * Add top level blockquotes to hide/show container.
     */
    createControllableQuotes: function() {
        let blockquoteList = document.documentElement.querySelectorAll("blockquote");
        for (let i = 0; i < blockquoteList.length; ++i) {
            let blockquote = blockquoteList.item(i);
            let nextSibling = blockquote.nextSibling;
            let parent = blockquote.parentNode;

            // Only insert into a quote container if the element is a
            // top level blockquote
            if (!ConversationPageState.isDescendantOf(blockquote, "BLOCKQUOTE")) {
                let quoteHeight = blockquote.offsetHeight;

                // Only make the quote it controllable if it is tall enough
                let isControllable = (quoteHeight > 120);

                let quoteContainer = document.createElement("DIV");
                quoteContainer.classList.add(
                    ConversationPageState.QUOTE_CONTAINER_CLASS
                );
                if (isControllable) {
                    quoteContainer.classList.add("geary-controllable");
                    quoteContainer.classList.add(
                        ConversationPageState.QUOTE_HIDE_CLASS
                    );
                }

                let quoteDiv = document.createElement("DIV");
                quoteDiv.classList.add("geary-quote");

                quoteDiv.appendChild(blockquote);
                quoteContainer.appendChild(quoteDiv);
                parent.insertBefore(quoteContainer, nextSibling);

                let containerHeight = quoteDiv.offsetHeight;

                let state = this;
                function newControllerButton(styleClass, text) {
                    let button = document.createElement("BUTTON");
                    button.classList.add("geary-button");
                    button.type = "button";
                    button.onclick = function() {
                        let hide = ConversationPageState.QUOTE_HIDE_CLASS;
                        quoteContainer.classList.toggle(hide);

                        // Update the preferred height. We calculate
                        // what the difference should be rather than
                        // getting it directly, since WK won't ever
                        // shrink the height of the HTML element.
                        let height = quoteHeight - containerHeight;
                        if (quoteContainer.classList.contains(hide)) {
                            height = state.lastPreferredHeight - height;
                        } else {
                            height = state.lastPreferredHeight + height;
                        }
                        state.updatePreferredHeight(height);
                    };
                    button.appendChild(document.createTextNode(text));

                    let container = document.createElement("DIV");
                    container.classList.add(styleClass);
                    container.appendChild(button);

                    return container;
                }

                if (isControllable) {
                    quoteContainer.appendChild(newControllerButton(
                        "geary-shower", "▼        ▼        ▼"
                    ));
                    quoteContainer.appendChild(newControllerButton(
                        "geary-hider", "▲        ▲        ▲"
                    ));
                }
            }
        }
    },
    /**
     * Look for and wrap a signature.
     *
     * Most HTML signatures fall into one
     * of these designs which are handled by this method:
     *
     * 1. GMail:            <div>-- </div>$SIGNATURE
     * 2. GMail Alternate:  <div><span>-- </span></div>$SIGNATURE
     * 3. Thunderbird:      <div>-- <br>$SIGNATURE</div>
     *
     */
    wrapSignature: function() {
        let possibleSigs = document.documentElement.querySelectorAll("div,span,p");
        let i = 0;
        let sigRegex = new RegExp("^--\\s*$");
        let alternateSigRegex = new RegExp("^--\\s*(?:<br|\\R)");
        for (; i < possibleSigs.length; ++i) {
            // Get the div and check that it starts a signature block
            // and is not inside a quote.
            let div = possibleSigs.item(i);
            let innerHTML = div.innerHTML;
            if ((sigRegex.test(innerHTML) || alternateSigRegex.test(innerHTML)) &&
                !ConversationPageState.isDescendantOf(div, "BLOCKQUOTE")) {
                break;
            }
        }
        // If we have a signature, move it and all of its following
        // siblings that are not quotes inside a signature div.
        if (i < possibleSigs.length) {
            let elem = possibleSigs.item(i);
            let parent = elem.parentNode;
            let signatureContainer = document.createElement("DIV");
            signatureContainer.classList.add("geary-signature");
            do {
                // Get its sibling _before_ we move it into the signature div.
                let sibling = elem.nextSibling;
                signatureContainer.appendChild(elem);
                elem = sibling;
            } while (elem != null);
            parent.appendChild(signatureContainer);
        }
    },
    getSelectionForQuoting: function() {
        let quote = null;
        let selection = window.getSelection();
        if (!selection.isCollapsed) {
            let range = selection.getRangeAt(0);
            let ancestor = range.commonAncestorContainer;
            if (ancestor.nodeType != Node.ELEMENT_NODE) {
                ancestor = ancestor.parentNode;
            }

            // If the selection is part of a plain text message, we
            // have to stick it in an appropriately styled div, so
            // that new lines are preserved. Do a non-strict ancestor
            // check since the common ancestor may well be the plain
            // text DIV itself
            let dummy = document.createElement("DIV");
            let includeDummy = false;
            if (ConversationPageState.isDescendantOf(
                ancestor, "DIV", "plaintext", false)) {
                dummy.classList.add("plaintext");
                dummy.setAttribute("style", "white-space: pre-wrap;");
                includeDummy = true;
            }
            dummy.appendChild(range.cloneContents());

            // Remove the chrome we put around quotes, leaving
            // only the blockquote element.
            let quotes = dummy.querySelectorAll(
                "." + ConversationPageState.QUOTE_CONTAINER_CLASS
            );
            for (let i = 0; i < quotes.length; i++) {
                let div = quotes.item(i);
                let blockquote = div.querySelector("blockquote");
                div.parentNode.replaceChild(blockquote, div);
            }

            quote = includeDummy ? dummy.outerHTML : dummy.innerHTML;
        }
        return quote;
    },
    getSelectionForFind: function() {
        let value = null;
        let selection = window.getSelection();

        if (selection.rangeCount > 0) {
            let range = selection.getRangeAt(0);
            value = range.toString().trim();
            if (value == "") {
                value = null;
            }
        }
        return value;
    },
    getAnchorTargetY: function(anchor) {
        let target = document.getElementById(anchor);
        if (target == null) {
            target = document.getElementsByName(anchor);
            if (target.length > 0) {
                target = target[0];
            }
        }
        if (target != null) {
            return target.getBoundingClientRect().top +
                document.documentElement.scrollTop;
        } else {
            return -1;
        }
    },
    linkClicked: function(link) {
        let cancelClick = false;
        let href = link.href;
        if (!href.startsWith("mailto:")) {
            let text = link.innerText;
            let reason = ConversationPageState.isDeceptiveText(text, href);
            if (reason != ConversationPageState.NOT_DECEPTIVE) {
                cancelClick = true;
                window.webkit.messageHandlers.deceptiveLinkClicked.postMessage({
                    reason: reason,
                    text: text,
                    href: href,
                    location: ConversationPageState.getNodeBounds(link)
                });
            }
        }

        return cancelClick;
    }
};

/**
 * Returns an [x, y, width, height] array of a node's bounds.
 */
ConversationPageState.getNodeBounds = function(node) {
    let x = 0;
    let y = 0;
    let parent = node;
    while (parent != null) {
        x += parent.offsetLeft;
        y += parent.offsetTop;
        parent = parent.offsetParent;
    }
    return {
        x: x,
        y: y,
        width: node.offsetWidth,
        height: node.offsetHeight
    };
};

/**
 * Test for URL-like `text` that leads somewhere other than `href`.
 */
ConversationPageState.isDeceptiveText = function(text, href) {
    // First, does text look like a URI? 
    let domain = new RegExp("^"
                          + "([a-z]*://)?"                             // Optional scheme
                          + "([^\\s:/#%&*@()]+\\.[^\\s:/#%&*@()\\.]+)" // Domain
                          + "(/[^\\s]*)?"                              // Optional path
                          + "$");             
    let textParts = text.match(domain);
    if (textParts == null) {
        return ConversationPageState.NOT_DECEPTIVE;
    }
    let hrefParts = href.match(domain);
    if (hrefParts == null) {
        // If href doesn't look like a URL, something is fishy, so
        // warn the user
        return ConversationPageState.DECEPTIVE_HREF;
    }

    // Second, do the top levels of the two domains match?  We
    // compare the top n levels, where n is the minimum of the
    // number of levels of the two domains.
    let textDomain = textParts[2].toLowerCase().split(".").reverse();
    let hrefDomain = hrefParts[2].toLowerCase().split(".").reverse();
    let segmentCount = Math.min(textDomain.length, hrefDomain.length);
    if (segmentCount == 0) {
        return ConversationPageState.DECEPTIVE_DOMAIN;
    }
    for (let i = 0; i < segmentCount; i++) {
        if (textDomain[i] != hrefDomain[i]) {
            return ConversationPageState.DECEPTIVE_DOMAIN;
        }
    }

    return ConversationPageState.NOT_DECEPTIVE;
};

/**
 * See if this element has an ancestor with the given tag and class.
 *
 * The value of ancestorTag must be all uppercase.
 *
 * If ancestorClass is null, no class checking is done.
 * If strict is is true, the given node will not be checked.
 */
ConversationPageState.isDescendantOf = function(node,
                                                ancestorTag,
                                                ancestorClass = null,
                                                strict = true) {
    let ancestor = strict ? node.parentNode : node;
    while (ancestor != null) {
        if (ancestor.nodeName.toUpperCase() == ancestorTag &&
            (ancestorClass == null ||
             ancestor.classList.contains(ancestorClass))) {
            return true;
        }
        ancestor = ancestor.parentNode;
    }
    return false;
};

var geary = new ConversationPageState();
