/*
 * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.isBeingEdited = function(element)
{
    while (element) {
        if (element.__editing)
            return true;
        element = element.parentNode;
    }

    return false;
};

WI.markBeingEdited = function(element, value)
{
    if (value) {
        if (element.__editing)
            return false;
        element.__editing = true;
        WI.__editingCount = (WI.__editingCount || 0) + 1;
    } else {
        if (!element.__editing)
            return false;
        delete element.__editing;
        --WI.__editingCount;
    }
    return true;
};

WI.isEditingAnyField = function()
{
    return !!WI.__editingCount;
};

WI.isEventTargetAnEditableField = function(event)
{
    let textInputTypes = new Set(["text", "search", "number", "tel", "url", "email", "password"]);
    if (event.target instanceof HTMLInputElement)
        return textInputTypes.has(event.target.type);

    let codeMirrorEditorElement = event.target.enclosingNodeOrSelfWithClass("CodeMirror");
    if (codeMirrorEditorElement && codeMirrorEditorElement.CodeMirror)
        return !codeMirrorEditorElement.CodeMirror.getOption("readOnly");

    if (event.target instanceof HTMLTextAreaElement)
        return true;

    if (event.target.isContentEditable)
        return true;

    if (event.target.enclosingNodeOrSelfWithClass("text-prompt"))
        return true;

    if (WI.isBeingEdited(event.target))
        return true;

    return false;
};

WI.EditingConfig = class EditingConfig
{
    constructor(commitHandler, cancelHandler, context)
    {
        this.commitHandler = commitHandler;
        this.cancelHandler = cancelHandler;
        this.context = context;
        this.spellcheck = false;
    }

    setPasteHandler(pasteHandler)
    {
        this.pasteHandler = pasteHandler;
    }

    setMultiline(multiline)
    {
        this.multiline = multiline;
    }

    setCustomFinishHandler(customFinishHandler)
    {
        this.customFinishHandler = customFinishHandler;
    }

    setNumberCommitHandler(numberCommitHandler)
    {
        this.numberCommitHandler = numberCommitHandler;
    }
};

WI.startEditing = function(element, config)
{
    if (!WI.markBeingEdited(element, true))
        return null;

    config = config || new WI.EditingConfig(function() {}, function() {});
    var committedCallback = config.commitHandler;
    var cancelledCallback = config.cancelHandler;
    var pasteCallback = config.pasteHandler;
    var context = config.context;
    var oldText = getContent(element);
    var moveDirection = "";

    element.classList.add("editing");
    element.contentEditable = "plaintext-only";

    var oldSpellCheck = element.hasAttribute("spellcheck") ? element.spellcheck : undefined;
    element.spellcheck = config.spellcheck;

    if (config.multiline)
        element.classList.add("multiline");

    var oldTabIndex = element.tabIndex;
    if (element.tabIndex < 0)
        element.tabIndex = 0;

    function blurEventListener() {
        editingCommitted.call(element);
    }

    function getContent(element) {
        if (element.tagName === "INPUT" && element.type === "text")
            return element.value;
        else
            return element.textContent;
    }

    function cleanUpAfterEditing()
    {
        WI.markBeingEdited(element, false);

        this.classList.remove("editing");
        this.contentEditable = false;

        this.scrollTop = 0;
        this.scrollLeft = 0;

        if (oldSpellCheck === undefined)
            element.removeAttribute("spellcheck");
        else
            element.spellcheck = oldSpellCheck;

        if (oldTabIndex === -1)
            this.removeAttribute("tabindex");
        else
            this.tabIndex = oldTabIndex;

        element.removeEventListener("blur", blurEventListener, false);
        element.removeEventListener("keydown", keyDownEventListener, true);
        if (pasteCallback)
            element.removeEventListener("paste", pasteEventListener, true);

        WI.restoreFocusFromElement(element);
    }

    function editingCancelled()
    {
        if (this.tagName === "INPUT" && this.type === "text")
            this.value = oldText;
        else
            this.textContent = oldText;

        cleanUpAfterEditing.call(this);

        cancelledCallback(this, context);
    }

    function editingCommitted()
    {
        cleanUpAfterEditing.call(this);

        committedCallback(this, getContent(this), oldText, context, moveDirection);
    }

    function defaultFinishHandler(event)
    {
        var hasOnlyMetaModifierKey = event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey;
        if (isEnterKey(event) && (!config.multiline || hasOnlyMetaModifierKey))
            return "commit";
        else if (event.keyCode === WI.KeyboardShortcut.Key.Escape.keyCode || event.keyIdentifier === "U+001B")
            return "cancel";
        else if (event.keyIdentifier === "U+0009") // Tab key
            return "move-" + (event.shiftKey ? "backward" : "forward");
        else if (event.altKey) {
            if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down")
                return "modify-" + (event.keyIdentifier === "Up" ? "up" : "down");
            if (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown")
                return "modify-" + (event.keyIdentifier === "PageUp" ? "up-big" : "down-big");
        }
    }

    function handleEditingResult(result, event)
    {
        if (result === "commit") {
            editingCommitted.call(element);
            event.preventDefault();
            event.stopPropagation();
        } else if (result === "cancel") {
            editingCancelled.call(element);
            event.preventDefault();
            event.stopPropagation();
        } else if (result && result.startsWith("move-")) {
            moveDirection = result.substring(5);
            if (event.keyIdentifier !== "U+0009")
                blurEventListener();
        } else if (result && result.startsWith("modify-")) {
            let direction = result.substring(7);
            let delta = direction.startsWith("up") ? 1 : -1;
            if (direction.endsWith("big"))
                delta *= 10;

            if (event.shiftKey)
                delta *= 10;
            else if (event.ctrlKey)
                delta /= 10;

            let modified = WI.incrementElementValue(element, delta);
            if (!modified)
                return;

            if (typeof config.numberCommitHandler === "function")
                config.numberCommitHandler(element, getContent(element), oldText, context, moveDirection);

            event.preventDefault();
        }
    }

    function pasteEventListener(event)
    {
        var result = pasteCallback(event);
        handleEditingResult(result, event);
    }

    function keyDownEventListener(event)
    {
        var handler = config.customFinishHandler || defaultFinishHandler;
        var result = handler(event);
        handleEditingResult(result, event);
    }

    element.addEventListener("blur", blurEventListener, false);
    element.addEventListener("keydown", keyDownEventListener, true);
    if (pasteCallback)
        element.addEventListener("paste", pasteEventListener, true);

    element.focus();

    return {
        cancel: editingCancelled.bind(element),
        commit: editingCommitted.bind(element)
    };
};

WI.incrementElementValue = function(element, delta)
{
    let selection = element.ownerDocument.defaultView.getSelection();
    if (!selection.rangeCount)
        return false;

    let range = selection.getRangeAt(0);
    if (!element.contains(range.commonAncestorContainer))
        return false;

    let wordRange = range.startContainer.rangeOfWord(range.startOffset, WI.EditingSupport.StyleValueDelimiters, element);
    let word = wordRange.toString();
    let wordPrefix = "";
    let wordSuffix = "";
    let nonNumberInWord = /[^\d-\.]+/.exec(word);
    if (nonNumberInWord) {
        let nonNumberEndOffset = nonNumberInWord.index + nonNumberInWord[0].length;
        if (range.startOffset > wordRange.startOffset + nonNumberInWord.index && nonNumberEndOffset < word.length && range.startOffset !== wordRange.startOffset) {
            wordPrefix = word.substring(0, nonNumberEndOffset);
            word = word.substring(nonNumberEndOffset);
        } else {
            wordSuffix = word.substring(nonNumberInWord.index);
            word = word.substring(0, nonNumberInWord.index);
        }
    }

    let matches = WI.EditingSupport.CSSNumberRegex.exec(word);
    if (!matches || matches.length !== 4)
        return false;

    let replacement = matches[1] + (Math.round((parseFloat(matches[2]) + delta) * 100) / 100) + matches[3];

    selection.removeAllRanges();
    selection.addRange(wordRange);
    document.execCommand("insertText", false, wordPrefix + replacement + wordSuffix);

    let container = range.commonAncestorContainer;
    let startOffset = range.startOffset;
    // This check is for the situation when the cursor is in the space between the
    // opening quote of the attribute and the first character. In that spot, the
    // commonAncestorContainer is actually the entire attribute node since `="` is
    // added as a simple text node. Since the opening quote is immediately before
    // the attribute, the node for that attribute must be the next sibling and the
    // text of the attribute's value must be the first child of that sibling.
    if (container.parentNode.classList.contains("editing") && container.nextSibling) {
        container = container.nextSibling.firstChild;
        startOffset = 0;
    }
    startOffset += wordPrefix.length;

    if (!container)
        return false;

    let replacementSelectionRange = document.createRange();
    replacementSelectionRange.setStart(container, startOffset);
    replacementSelectionRange.setEnd(container, startOffset + replacement.length);

    selection.removeAllRanges();
    selection.addRange(replacementSelectionRange);

    return true;
};

WI.EditingSupport = {
    StyleValueDelimiters: " \xA0\t\n\"':;,/()",
    CSSNumberRegex: /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/,
    NumberRegex: /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/
};
