// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @constructor
 */
WebInspector.InplaceEditor = function()
{
}

/**
 * @typedef {{cancel: function(), commit: function(), setWidth: function(number)}}
 */
WebInspector.InplaceEditor.Controller;

/**
 * @param {!Element} element
 * @param {!WebInspector.InplaceEditor.Config=} config
 * @return {?WebInspector.InplaceEditor.Controller}
 */
WebInspector.InplaceEditor.startEditing = function(element, config)
{
    if (!WebInspector.InplaceEditor._defaultInstance)
        WebInspector.InplaceEditor._defaultInstance = new WebInspector.InplaceEditor();
    return WebInspector.InplaceEditor._defaultInstance.startEditing(element, config);
}

/**
 * @param {!Element} element
 * @param {!WebInspector.InplaceEditor.Config=} config
 * @return {!Promise.<!WebInspector.InplaceEditor.Controller>}
 */
WebInspector.InplaceEditor.startMultilineEditing = function(element, config)
{
    return self.runtime.instancePromise(WebInspector.InplaceEditor).then(startEditing);

    /**
     * @param {!Object} inplaceEditor
     * @return {!WebInspector.InplaceEditor.Controller|!Promise.<!WebInspector.InplaceEditor.Controller>}
     */
    function startEditing(inplaceEditor)
    {
        var controller = /** @type {!WebInspector.InplaceEditor} */ (inplaceEditor).startEditing(element, config);
        if (!controller)
            return Promise.rejectWithError("Editing is already in progress");
        return controller;
    }
}

WebInspector.InplaceEditor.prototype = {
    /**
     * @return {string}
     */
    editorContent: function(editingContext) {
        var element = editingContext.element;
        if (element.tagName === "INPUT" && element.type === "text")
            return element.value;

        return element.textContent;
    },

    setUpEditor: function(editingContext)
    {
        var element = editingContext.element;
        element.classList.add("editing");

        var oldTabIndex = element.getAttribute("tabIndex");
        if (typeof oldTabIndex !== "number" || oldTabIndex < 0)
            element.tabIndex = 0;
        WebInspector.setCurrentFocusElement(element);
        editingContext.oldTabIndex = oldTabIndex;
    },

    closeEditor: function(editingContext)
    {
        var element = editingContext.element;
        element.classList.remove("editing");

        if (typeof editingContext.oldTabIndex !== "number")
            element.removeAttribute("tabIndex");
        else
            element.tabIndex = editingContext.oldTabIndex;
        element.scrollTop = 0;
        element.scrollLeft = 0;
    },

    cancelEditing: function(editingContext)
    {
        var element = editingContext.element;
        if (element.tagName === "INPUT" && element.type === "text")
            element.value = editingContext.oldText;
        else
            element.textContent = editingContext.oldText;
    },

    augmentEditingHandle: function(editingContext, handle)
    {
    },

    /**
     * @param {!Element} element
     * @param {!WebInspector.InplaceEditor.Config=} config
     * @return {?WebInspector.InplaceEditor.Controller}
     */
    startEditing: function(element, config)
    {
        if (!WebInspector.markBeingEdited(element, true))
            return null;

        config = config || new WebInspector.InplaceEditor.Config(function() {}, function() {});
        var editingContext = { element: element, config: config };
        var committedCallback = config.commitHandler;
        var cancelledCallback = config.cancelHandler;
        var pasteCallback = config.pasteHandler;
        var context = config.context;
        var isMultiline = config.multiline || false;
        var moveDirection = "";
        var self = this;

        /**
         * @param {!Event} e
         */
        function consumeCopy(e)
        {
            e.consume();
        }

        this.setUpEditor(editingContext);

        editingContext.oldText = isMultiline ? config.initialValue : this.editorContent(editingContext);

        /**
         * @param {!Event=} e
         */
        function blurEventListener(e) {
            if (config.blurHandler && !config.blurHandler(element, e))
                return;
            if (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element))
                editingCommitted.call(element);
        }

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

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

            WebInspector.restoreFocusFromElement(element);
            self.closeEditor(editingContext);
        }

        /** @this {Element} */
        function editingCancelled()
        {
            self.cancelEditing(editingContext);
            cleanUpAfterEditing();
            cancelledCallback(this, context);
        }

        /** @this {Element} */
        function editingCommitted()
        {
            cleanUpAfterEditing();

            committedCallback(this, self.editorContent(editingContext), editingContext.oldText, context, moveDirection);
        }

        function defaultFinishHandler(event)
        {
            var isMetaOrCtrl = WebInspector.isMac() ?
                event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
                event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
            if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl))
                return "commit";
            else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
                return "cancel";
            else if (!isMultiline && event.keyIdentifier === "U+0009") // Tab key
                return "move-" + (event.shiftKey ? "backward" : "forward");
        }

        function handleEditingResult(result, event)
        {
            if (result === "commit") {
                editingCommitted.call(element);
                event.consume(true);
            } else if (result === "cancel") {
                editingCancelled.call(element);
                event.consume(true);
            } else if (result && result.startsWith("move-")) {
                moveDirection = result.substring(5);
                if (event.keyIdentifier !== "U+0009")
                    blurEventListener();
            }
        }

        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, isMultiline);
        element.addEventListener("keydown", keyDownEventListener, true);
        if (pasteCallback)
            element.addEventListener("paste", pasteEventListener, true);

        var handle = {
            cancel: editingCancelled.bind(element),
            commit: editingCommitted.bind(element),
            setWidth: function() {}
        };
        this.augmentEditingHandle(editingContext, handle);
        return handle;
    }
}

/**
 * @constructor
 * @param {function(!Element,string,string,T,string)} commitHandler
 * @param {function(!Element,T)} cancelHandler
 * @param {T=} context
 * @param {function(!Element,!Event):boolean=} blurHandler
 * @template T
 */
WebInspector.InplaceEditor.Config = function(commitHandler, cancelHandler, context, blurHandler)
{
    this.commitHandler = commitHandler;
    this.cancelHandler = cancelHandler
    this.context = context;
    this.blurHandler = blurHandler;

    /**
     * Handles the "paste" event, return values are the same as those for customFinishHandler
     * @type {function(!Element)|undefined}
     */
    this.pasteHandler;

    /**
     * Whether the edited element is multiline
     * @type {boolean|undefined}
     */
    this.multiline;

    /**
     * Custom finish handler for the editing session (invoked on keydown)
     * @type {function(!Element,*)|undefined}
     */
    this.customFinishHandler;
}

WebInspector.InplaceEditor.Config.prototype = {
    setPasteHandler: function(pasteHandler)
    {
        this.pasteHandler = pasteHandler;
    },

    /**
     * @param {string} initialValue
     * @param {!Object} mode
     * @param {string} theme
     * @param {boolean=} lineWrapping
     * @param {boolean=} smartIndent
     */
    setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent)
    {
        this.multiline = true;
        this.initialValue = initialValue;
        this.mode = mode;
        this.theme = theme;
        this.lineWrapping = lineWrapping;
        this.smartIndent = smartIndent;
    },

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