/*
 * Copyright (C) 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2011 Google 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.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE 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 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.
 */

/**
 * @constructor
 * @extends {WebInspector.Object}
 * @implements {WebInspector.SuggestBoxDelegate}
 * @param {function(!Element, string, number, !Range, boolean, function(!Array.<string>, number=))} completions
 * @param {string=} stopCharacters
 */
WebInspector.TextPrompt = function(completions, stopCharacters)
{
    /**
     * @type {!Element|undefined}
     */
    this._proxyElement;
    this._proxyElementDisplay = "inline-block";
    this._loadCompletions = completions;
    this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
    this._autocompletionTimeout = WebInspector.TextPrompt.DefaultAutocompletionTimeout;
    this._title = "";
}

WebInspector.TextPrompt.DefaultAutocompletionTimeout = 250;

WebInspector.TextPrompt.Events = {
    ItemApplied: "text-prompt-item-applied",
    ItemAccepted: "text-prompt-item-accepted"
};

WebInspector.TextPrompt.prototype = {
    /**
     * @param {number} timeout
     */
    setAutocompletionTimeout: function(timeout)
    {
        this._autocompletionTimeout = timeout;
    },

    /**
     * @param {boolean} suggestBoxEnabled
     */
    setSuggestBoxEnabled: function(suggestBoxEnabled)
    {
        this._suggestBoxEnabled = suggestBoxEnabled;
    },

    renderAsBlock: function()
    {
        this._proxyElementDisplay = "block";
    },

    /**
     * Clients should never attach any event listeners to the |element|. Instead,
     * they should use the result of this method to attach listeners for bubbling events.
     *
     * @param {!Element} element
     * @return {!Element}
     */
    attach: function(element)
    {
        return this._attachInternal(element);
    },

    /**
     * Clients should never attach any event listeners to the |element|. Instead,
     * they should use the result of this method to attach listeners for bubbling events
     * or the |blurListener| parameter to register a "blur" event listener on the |element|
     * (since the "blur" event does not bubble.)
     *
     * @param {!Element} element
     * @param {function(!Event)} blurListener
     * @return {!Element}
     */
    attachAndStartEditing: function(element, blurListener)
    {
        var proxyElement = this._attachInternal(element);
        this._startEditing(blurListener);
        return proxyElement;
    },

    /**
     * @param {!Element} element
     * @return {!Element}
     */
    _attachInternal: function(element)
    {
        if (this._proxyElement)
            throw "Cannot attach an attached TextPrompt";
        this._element = element;

        this._boundOnKeyDown = this.onKeyDown.bind(this);
        this._boundOnInput = this.onInput.bind(this);
        this._boundOnMouseWheel = this.onMouseWheel.bind(this);
        this._boundSelectStart = this._selectStart.bind(this);
        this._boundRemoveSuggestionAids = this._removeSuggestionAids.bind(this);
        this._proxyElement = element.ownerDocument.createElement("span");
        var shadowRoot = WebInspector.createShadowRootWithCoreStyles(this._proxyElement, "ui/textPrompt.css");
        this._contentElement = shadowRoot.createChild("div");
        this._contentElement.createChild("content");
        this._proxyElement.style.display = this._proxyElementDisplay;
        element.parentElement.insertBefore(this._proxyElement, element);
        this._proxyElement.appendChild(element);
        this._element.classList.add("text-prompt");
        this._element.addEventListener("keydown", this._boundOnKeyDown, false);
        this._element.addEventListener("input", this._boundOnInput, false);
        this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false);
        this._element.addEventListener("selectstart", this._boundSelectStart, false);
        this._element.addEventListener("blur", this._boundRemoveSuggestionAids, false);
        this._element.ownerDocument.defaultView.addEventListener("resize", this._boundRemoveSuggestionAids, false);

        if (this._suggestBoxEnabled)
            this._suggestBox = new WebInspector.SuggestBox(this);

        if (this._title)
            this._proxyElement.title = this._title;

        return this._proxyElement;
    },

    detach: function()
    {
        this._removeFromElement();
        this._proxyElement.parentElement.insertBefore(this._element, this._proxyElement);
        this._proxyElement.remove();
        delete this._proxyElement;
        this._element.classList.remove("text-prompt");
        WebInspector.restoreFocusFromElement(this._element);
    },

    /**
     * @return {string}
     */
    text: function()
    {
        return this._element.textContent;
    },

    /**
     * @return {string}
     */
    userEnteredText: function()
    {
        var text = this.text();
        if (this.autoCompleteElement) {
            var addition = this.autoCompleteElement.textContent;
            text = text.substring(0, text.length - addition.length);
        }
        return text;
    },

    /**
     * @param {string} x
     */
    setText: function(x)
    {
        this._removeSuggestionAids();
        if (!x) {
            // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
            this._element.removeChildren();
            this._element.createChild("br");
        } else {
            this._element.textContent = x;
        }

        this.moveCaretToEndOfPrompt();
        this._element.scrollIntoView();
    },

    /**
     * @return {string}
     */
    title: function()
    {
        return this._title;
    },

    /**
     * @param {string} title
     */
    setTitle: function(title)
    {
        this._title = title;
        if (this._proxyElement)
            this._proxyElement.title = title;
    },

    _removeFromElement: function()
    {
        this.clearAutoComplete(true);
        this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
        this._element.removeEventListener("input", this._boundOnInput, false);
        this._element.removeEventListener("selectstart", this._boundSelectStart, false);
        this._element.removeEventListener("blur", this._boundRemoveSuggestionAids, false);
        this._element.ownerDocument.defaultView.removeEventListener("resize", this._boundRemoveSuggestionAids, false);
        if (this._isEditing)
            this._stopEditing();
        if (this._suggestBox)
            this._suggestBox.removeFromElement();
    },

    /**
     * @param {function(!Event)=} blurListener
     */
    _startEditing: function(blurListener)
    {
        this._isEditing = true;
        this._contentElement.classList.add("text-prompt-editing");
        if (blurListener) {
            this._blurListener = blurListener;
            this._element.addEventListener("blur", this._blurListener, false);
        }
        this._oldTabIndex = this._element.tabIndex;
        if (this._element.tabIndex < 0)
            this._element.tabIndex = 0;
        WebInspector.setCurrentFocusElement(this._element);
        if (!this.text())
            this._updateAutoComplete();
    },

    _stopEditing: function()
    {
        this._element.tabIndex = this._oldTabIndex;
        if (this._blurListener)
            this._element.removeEventListener("blur", this._blurListener, false);
        this._contentElement.classList.remove("text-prompt-editing");
        delete this._isEditing;
    },

    _removeSuggestionAids: function()
    {
        this.clearAutoComplete();
        this.hideSuggestBox();
    },

    _selectStart: function()
    {
        if (this._selectionTimeout)
            clearTimeout(this._selectionTimeout);

        this._removeSuggestionAids();

        /**
         * @this {WebInspector.TextPrompt}
         */
        function moveBackIfOutside()
        {
            delete this._selectionTimeout;
            if (!this.isCaretInsidePrompt() && this._element.isComponentSelectionCollapsed()) {
                this.moveCaretToEndOfPrompt();
                this.autoCompleteSoon();
            }
        }

        this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
    },

    /**
     * @param {boolean=} force
     */
    _updateAutoComplete: function(force)
    {
        this.clearAutoComplete();
        this.autoCompleteSoon(force);
    },

    /**
     * @param {!Event} event
     */
    onMouseWheel: function(event)
    {
        // Subclasses can implement.
    },

    /**
     * @param {!Event} event
     */
    onKeyDown: function(event)
    {
        if (isEnterKey(event))
            return;

        var handled = false;
        delete this._needUpdateAutocomplete;

        switch (event.key) {
        case "Tab":
            handled = this.tabKeyPressed(event);
            break;
        case "ArrowLeft":
        case "Home":
            this._removeSuggestionAids();
            break;
        case "ArrowRight":
        case "End":
            if (this.isCaretAtEndOfPrompt())
                handled = this.acceptAutoComplete();
            else
                this._removeSuggestionAids();
            break;
        case "Escape":
            if (this.isSuggestBoxVisible()) {
                this._removeSuggestionAids();
                handled = true;
            }
            break;
        case " ": // Space
            if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
                this._updateAutoComplete(true);
                handled = true;
            }
            break;
        case "Alt":
        case "Meta":
        case "Shift":
        case "Control":
            break;
        }

        if (!handled && this.isSuggestBoxVisible())
            handled = this._suggestBox.keyPressed(event);

        if (!handled)
            this._needUpdateAutocomplete = true;

        if (handled)
            event.consume(true);
    },

    /**
     * @param {!Event} event
     */
    onInput: function(event)
    {
        if (this._needUpdateAutocomplete)
            this._updateAutoComplete();
    },

    /**
     * @return {boolean}
     */
    acceptAutoComplete: function()
    {
        var result = false;
        if (this.isSuggestBoxVisible())
            result = this._suggestBox.acceptSuggestion();
        if (!result)
            result = this._acceptSuggestionInternal();

        return result;
    },

    /**
     * @param {boolean=} includeTimeout
     */
    clearAutoComplete: function(includeTimeout)
    {
        if (includeTimeout && this._completeTimeout) {
            clearTimeout(this._completeTimeout);
            delete this._completeTimeout;
        }
        delete this._waitingForCompletions;

        if (!this.autoCompleteElement)
            return;

        this.autoCompleteElement.remove();
        delete this.autoCompleteElement;
        delete this._userEnteredRange;
        delete this._userEnteredText;
    },

    /**
     * @param {boolean=} force
     */
    autoCompleteSoon: function(force)
    {
        var immediately = this.isSuggestBoxVisible() || force;
        if (!this._completeTimeout)
            this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : this._autocompletionTimeout);
    },

    /**
     * @param {boolean=} force
     * @param {boolean=} reverse
     */
    complete: function(force, reverse)
    {
        this.clearAutoComplete(true);
        var selection = this._element.getComponentSelection();
        var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
        if (!selectionRange)
            return;

        var shouldExit;

        if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
            shouldExit = true;
        else if (!selection.isCollapsed)
            shouldExit = true;
        else if (!force) {
            // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
            var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
            if (wordSuffixRange.toString().length)
                shouldExit = true;
        }
        if (shouldExit) {
            this.hideSuggestBox();
            return;
        }

        var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
        this._waitingForCompletions = true;
        this._loadCompletions(/** @type {!Element} */ (this._proxyElement), this.text(), selectionRange.startOffset, wordPrefixRange, force || false, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse, !!force));
    },

    disableDefaultSuggestionForEmptyInput: function()
    {
        this._disableDefaultSuggestionForEmptyInput = true;
    },

    /**
     * @param {!Selection} selection
     * @param {!Range} textRange
     */
    _boxForAnchorAtStart: function(selection, textRange)
    {
        var rangeCopy = selection.getRangeAt(0).cloneRange();
        var anchorElement = createElement("span");
        anchorElement.textContent = "\u200B";
        textRange.insertNode(anchorElement);
        var box = anchorElement.boxInWindow(window);
        anchorElement.remove();
        selection.removeAllRanges();
        selection.addRange(rangeCopy);
        return box;
    },

    /**
     * @param {!Array.<string>} completions
     * @param {number} wordPrefixLength
     */
    _buildCommonPrefix: function(completions, wordPrefixLength)
    {
        var commonPrefix = completions[0];
        for (var i = 0; i < completions.length; ++i) {
            var completion = completions[i];
            var lastIndex = Math.min(commonPrefix.length, completion.length);
            for (var j = wordPrefixLength; j < lastIndex; ++j) {
                if (commonPrefix[j] !== completion[j]) {
                    commonPrefix = commonPrefix.substr(0, j);
                    break;
                }
            }
        }
        return commonPrefix;
    },

    /**
     * @return {?Range}
     * @suppressGlobalPropertiesCheck
     */
    _createRange: function()
    {
        return document.createRange();
    },

    /**
     * @param {string} prefix
     * @return {!WebInspector.SuggestBox.Suggestions}
     */
    additionalCompletions: function(prefix)
    {
        return [];
    },

    /**
     * @param {!Selection} selection
     * @param {!Range} originalWordPrefixRange
     * @param {boolean} reverse
     * @param {boolean} force
     * @param {!Array.<string>} completions
     * @param {number=} selectedIndex
     */
    _completionsReady: function(selection, originalWordPrefixRange, reverse, force, completions, selectedIndex)
    {
        var prefix = originalWordPrefixRange.toString();

        // Filter out dupes.
        var store = new Set();
        completions = completions.filter(item => !store.has(item) && !!store.add(item));
        var annotatedCompletions = completions.map(item => ({title: item}));

        if (prefix || force) {
            if (prefix)
                annotatedCompletions = annotatedCompletions.concat(this.additionalCompletions(prefix));
            else
                annotatedCompletions = this.additionalCompletions(prefix).concat(annotatedCompletions);
        }

        if (!this._waitingForCompletions || !annotatedCompletions.length) {
            this.hideSuggestBox();
            return;
        }

        delete this._waitingForCompletions;

        var selectionRange = selection.getRangeAt(0);

        var fullWordRange = this._createRange();
        fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
        fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);

        if (prefix + selectionRange.toString() !== fullWordRange.toString())
            return;

        selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text()) ? -1 : (selectedIndex || 0);

        this._userEnteredRange = fullWordRange;
        this._userEnteredText = fullWordRange.toString();

        if (this._suggestBox)
            this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), annotatedCompletions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);

        if (selectedIndex === -1)
            return;

        var wordPrefixLength = originalWordPrefixRange.toString().length;
        this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);

        if (this.isCaretAtEndOfPrompt()) {
            var completionText = annotatedCompletions[selectedIndex].title;
            var prefixText = this._userEnteredRange.toString();
            var suffixText = completionText.substring(wordPrefixLength);
            this._userEnteredRange.deleteContents();
            this._element.normalize();
            var finalSelectionRange = this._createRange();

            var prefixTextNode = createTextNode(prefixText);
            fullWordRange.insertNode(prefixTextNode);

            this.autoCompleteElement = createElementWithClass("span", "auto-complete-text");
            this.autoCompleteElement.textContent = suffixText;

            prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);

            finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
            finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
            selection.removeAllRanges();
            selection.addRange(finalSelectionRange);
            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied);
        }
    },

    _completeCommonPrefix: function()
    {
        if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
            return;

        if (!this.isSuggestBoxVisible()) {
            this.acceptAutoComplete();
            return;
        }

        this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
        this._acceptSuggestionInternal(true);
    },

    /**
     * @override
     * @param {string} completionText
     * @param {boolean=} isIntermediateSuggestion
     */
    applySuggestion: function(completionText, isIntermediateSuggestion)
    {
        this._applySuggestion(completionText, isIntermediateSuggestion);
    },

    /**
     * @param {string} completionText
     * @param {boolean=} isIntermediateSuggestion
     */
    _applySuggestion: function(completionText, isIntermediateSuggestion)
    {
        if (!this._userEnteredRange) {
            // We could have already cleared autocompletion range by the time this is called. (crbug.com/587683)
            return;
        }

        var wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;

        this._userEnteredRange.deleteContents();
        this._element.normalize();
        var finalSelectionRange = this._createRange();
        var completionTextNode = createTextNode(completionText);
        this._userEnteredRange.insertNode(completionTextNode);
        if (this.autoCompleteElement) {
            this.autoCompleteElement.remove();
            delete this.autoCompleteElement;
        }

        if (isIntermediateSuggestion)
            finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
        else
            finalSelectionRange.setStart(completionTextNode, completionText.length);

        finalSelectionRange.setEnd(completionTextNode, completionText.length);

        var selection = this._element.getComponentSelection();
        selection.removeAllRanges();
        selection.addRange(finalSelectionRange);
        if (isIntermediateSuggestion)
            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
    },

    /**
     * @override
     */
    acceptSuggestion: function()
    {
        this._acceptSuggestionInternal();
    },

    /**
     * @param {boolean=} prefixAccepted
     * @return {boolean}
     */
    _acceptSuggestionInternal: function(prefixAccepted)
    {
        if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
            return false;

        var text = this.autoCompleteElement.textContent;
        var textNode = createTextNode(text);
        this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
        delete this.autoCompleteElement;

        var finalSelectionRange = this._createRange();
        finalSelectionRange.setStart(textNode, text.length);
        finalSelectionRange.setEnd(textNode, text.length);

        var selection = this._element.getComponentSelection();
        selection.removeAllRanges();
        selection.addRange(finalSelectionRange);

        if (!prefixAccepted) {
            this.hideSuggestBox();
            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
        } else
            this.autoCompleteSoon(true);

        return true;
    },

    hideSuggestBox: function()
    {
        if (this.isSuggestBoxVisible())
            this._suggestBox.hide();
    },

    /**
     * @return {boolean}
     */
    isSuggestBoxVisible: function()
    {
        return this._suggestBox && this._suggestBox.visible();
    },

    /**
     * @return {boolean}
     */
    isCaretInsidePrompt: function()
    {
        return this._element.isInsertionCaretInside();
    },

    /**
     * @return {boolean}
     */
    isCaretAtEndOfPrompt: function()
    {
        var selection = this._element.getComponentSelection();
        var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
        if (!selectionRange || !selection.isCollapsed)
            return false;

        var node = selectionRange.startContainer;
        if (!node.isSelfOrDescendant(this._element))
            return false;

        if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
            return false;

        var foundNextText = false;
        while (node) {
            if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
                if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
                    return false;
                foundNextText = true;
            }

            node = node.traverseNextNode(this._element);
        }

        return true;
    },

    /**
     * @return {boolean}
     */
    isCaretOnFirstLine: function()
    {
        var selection = this._element.getComponentSelection();
        var focusNode = selection.focusNode;
        if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
            return true;

        if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
            return false;
        focusNode = focusNode.previousSibling;

        while (focusNode) {
            if (focusNode.nodeType !== Node.TEXT_NODE)
                return true;
            if (focusNode.textContent.indexOf("\n") !== -1)
                return false;
            focusNode = focusNode.previousSibling;
        }

        return true;
    },

    /**
     * @return {boolean}
     */
    isCaretOnLastLine: function()
    {
        var selection = this._element.getComponentSelection();
        var focusNode = selection.focusNode;
        if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
            return true;

        if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
            return false;
        focusNode = focusNode.nextSibling;

        while (focusNode) {
            if (focusNode.nodeType !== Node.TEXT_NODE)
                return true;
            if (focusNode.textContent.indexOf("\n") !== -1)
                return false;
            focusNode = focusNode.nextSibling;
        }

        return true;
    },

    moveCaretToEndOfPrompt: function()
    {
        var selection = this._element.getComponentSelection();
        var selectionRange = this._createRange();

        var container = this._element;
        while (container.childNodes.length)
            container = container.lastChild;
        var offset = container.nodeType === Node.TEXT_NODE ? container.textContent.length : 0;
        selectionRange.setStart(container, offset);
        selectionRange.setEnd(container, offset);

        selection.removeAllRanges();
        selection.addRange(selectionRange);
    },

    /**
     * @param {!Event} event
     * @return {boolean}
     */
    tabKeyPressed: function(event)
    {
        this.acceptAutoComplete();

        // Consume the key.
        return true;
    },

    /**
     * @return {?Element}
     */
    proxyElementForTests: function()
    {
        return this._proxyElement || null;
    },

    __proto__: WebInspector.Object.prototype
}


/**
 * @constructor
 * @extends {WebInspector.TextPrompt}
 * @param {function(!Element, string, number, !Range, boolean, function(!Array.<string>, number=))} completions
 * @param {string=} stopCharacters
 */
WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
{
    WebInspector.TextPrompt.call(this, completions, stopCharacters);

    /**
     * @type {!Array.<string>}
     */
    this._data = [];

    /**
     * 1-based entry in the history stack.
     * @type {number}
     */
    this._historyOffset = 1;

    this._addCompletionsFromHistory = true;
}

WebInspector.TextPromptWithHistory.prototype = {
    /**
     * @return {!Array.<string>}
     */
    historyData: function()
    {
        // FIXME: do we need to copy this?
        return this._data;
    },

    /**
     * @override
     * @param {string} prefix
     * @return {!WebInspector.SuggestBox.Suggestions}
     */
    additionalCompletions: function(prefix)
    {
        if (!this._addCompletionsFromHistory || !this.isCaretAtEndOfPrompt())
            return [];
        var result = [];
        var text = this.text();
        var set = new Set();
        for (var i = this._data.length - 1; i >= 0 && result.length < 50; --i) {
            var item = this._data[i];
            if (!item.startsWith(text))
                continue;
            if (set.has(item))
                continue;
            set.add(item);
            result.push({title: item.substring(text.length - prefix.length), className: "additional"});
        }
        return result;
    },

    /**
     * @param {!Array.<string>} data
     */
    setHistoryData: function(data)
    {
        this._data = [].concat(data);
        this._historyOffset = 1;
    },

    /**
     * @param {boolean} value
     */
    setAddCompletionsFromHistory: function(value)
    {
        this._addCompletionsFromHistory = value;
    },

    /**
     * Pushes a committed text into the history.
     * @param {string} text
     */
    pushHistoryItem: function(text)
    {
        if (this._uncommittedIsTop) {
            this._data.pop();
            delete this._uncommittedIsTop;
        }

        this._historyOffset = 1;
        if (text === this._currentHistoryItem())
            return;
        this._data.push(text);
    },

    /**
     * Pushes the current (uncommitted) text into the history.
     */
    _pushCurrentText: function()
    {
        if (this._uncommittedIsTop)
            this._data.pop(); // Throw away obsolete uncommitted text.
        this._uncommittedIsTop = true;
        this.clearAutoComplete(true);
        this._data.push(this.text());
    },

    /**
     * @return {string|undefined}
     */
    _previous: function()
    {
        if (this._historyOffset > this._data.length)
            return undefined;
        if (this._historyOffset === 1)
            this._pushCurrentText();
        ++this._historyOffset;
        return this._currentHistoryItem();
    },

    /**
     * @return {string|undefined}
     */
    _next: function()
    {
        if (this._historyOffset === 1)
            return undefined;
        --this._historyOffset;
        return this._currentHistoryItem();
    },

    /**
     * @return {string|undefined}
     */
    _currentHistoryItem: function()
    {
        return this._data[this._data.length - this._historyOffset];
    },

    /**
     * @override
     */
    onKeyDown: function(event)
    {
        var newText;
        var isPrevious;

        switch (event.key) {
        case "ArrowUp":
            if (!this.isCaretOnFirstLine() || this.isSuggestBoxVisible())
                break;
            newText = this._previous();
            isPrevious = true;
            break;
        case "ArrowDown":
            if (!this.isCaretOnLastLine() || this.isSuggestBoxVisible())
                break;
            newText = this._next();
            break;
        case "P": // Ctrl+P = Previous
        case "p":
            if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
                newText = this._previous();
                isPrevious = true;
            }
            break;
        case "N": // Ctrl+N = Next
        case "n":
            if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
                newText = this._next();
            break;
        }

        if (newText !== undefined) {
            event.consume(true);
            this.setText(newText);

            if (isPrevious) {
                var firstNewlineIndex = this.text().indexOf("\n");
                if (firstNewlineIndex === -1)
                    this.moveCaretToEndOfPrompt();
                else {
                    var selection = this._element.getComponentSelection();
                    var selectionRange = this._createRange();

                    selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
                    selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);

                    selection.removeAllRanges();
                    selection.addRange(selectionRange);
                }
            }

            return;
        }

        WebInspector.TextPrompt.prototype.onKeyDown.apply(this, arguments);
    },

    __proto__: WebInspector.TextPrompt.prototype
}

