/*
 * 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.ConsolePrompt = class ConsolePrompt extends WI.View
{
    constructor(delegate, mimeType)
    {
        super();

        mimeType = parseMIMEType(mimeType).type;

        this.element.classList.add("console-prompt", WI.SyntaxHighlightedStyleClassName);

        this.element.appendChild(WI.ImageUtilities.useSVGSymbol("Images/UserInputPrompt.svg", "glyph"));

        this._delegate = delegate || null;

        this._codeMirror = WI.CodeMirrorEditor.create(this.element, {
            lineWrapping: true,
            mode: {name: mimeType, globalVars: true},
            matchBrackets: true
        });

        var keyMap = {
            "Up": this._handlePreviousKey.bind(this),
            "Down": this._handleNextKey.bind(this),
            "Ctrl-P": this._handlePreviousKey.bind(this),
            "Ctrl-N": this._handleNextKey.bind(this),
            "Enter": this._handleEnterKey.bind(this),
            "Cmd-Enter": this._handleCommandEnterKey.bind(this),
            "Tab": this._handleTabKey.bind(this),
            "Esc": this._handleEscapeKey.bind(this)
        };

        this._codeMirror.addKeyMap(keyMap);

        this._completionController = new WI.CodeMirrorCompletionController(this._codeMirror, this);
        this._completionController.addExtendedCompletionProvider("javascript", WI.javaScriptRuntimeCompletionProvider);

        let textarea = this._codeMirror.getInputField();
        if (textarea)
            textarea.ariaLabel = WI.UIString("Console prompt");

        this._history = [{}];
        this._historyIndex = 0;
    }

    // Public

    get delegate()
    {
        return this._delegate;
    }

    set delegate(delegate)
    {
        this._delegate = delegate || null;
    }

    set escapeKeyHandlerWhenEmpty(handler)
    {
        this._escapeKeyHandlerWhenEmpty = handler;
    }

    get text()
    {
        return this._codeMirror.getValue();
    }

    set text(text)
    {
        this._codeMirror.setValue(text || "");
        this._codeMirror.clearHistory();
        this._codeMirror.markClean();
    }

    get history()
    {
        this._history[this._historyIndex] = this._historyEntryForCurrentText();
        return this._history;
    }

    set history(history)
    {
        this._history = history instanceof Array ? history.slice(0, WI.ConsolePrompt.MaximumHistorySize) : [{}];
        this._historyIndex = 0;
        this._restoreHistoryEntry(0);
    }

    get focused()
    {
        return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused");
    }

    focus()
    {
        this._codeMirror.focus();
    }

    updateCompletions(completions, implicitSuffix)
    {
        this._completionController.updateCompletions(completions, implicitSuffix);
    }

    pushHistoryItem(text)
    {
        this._commitHistoryEntry({text});
    }

    // Protected

    completionControllerCompletionsNeeded(completionController, prefix, defaultCompletions, base, suffix, forced)
    {
        if (this.delegate && typeof this.delegate.consolePromptCompletionsNeeded === "function")
            this.delegate.consolePromptCompletionsNeeded(this, defaultCompletions, base, prefix, suffix, forced);
        else
            this._completionController.updateCompletions(defaultCompletions);
    }

    completionControllerShouldAllowEscapeCompletion(completionController)
    {
        // Only allow escape to complete if there is text in the prompt. Otherwise allow it to pass through
        // so escape to toggle the quick console still works.
        return !!this.text;
    }

    sizeDidChange()
    {
        super.sizeDidChange();

        if (this.text)
            this._codeMirror.refresh();
    }

    // Private

    _handleTabKey(codeMirror)
    {
        var cursor = codeMirror.getCursor();
        var line = codeMirror.getLine(cursor.line);

        if (!line.trim().length)
            return CodeMirror.Pass;

        var firstNonSpace = line.search(/[^\s]/);

        if (cursor.ch <= firstNonSpace)
            return CodeMirror.Pass;

        this._completionController.completeAtCurrentPositionIfNeeded().then(function(result) {
            if (result === WI.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound)
                InspectorFrontendHost.beep();
        });
    }

    _handleEscapeKey(codeMirror)
    {
        if (this.text)
            return CodeMirror.Pass;

        if (!this._escapeKeyHandlerWhenEmpty)
            return CodeMirror.Pass;

        this._escapeKeyHandlerWhenEmpty();
    }

    _handlePreviousKey(codeMirror)
    {
        if (this._codeMirror.somethingSelected())
            return CodeMirror.Pass;

        // Pass unless we are on the first line.
        if (this._codeMirror.getCursor().line)
            return CodeMirror.Pass;

        var historyEntry = this._history[this._historyIndex + 1];
        if (!historyEntry)
            return CodeMirror.Pass;

        this._rememberCurrentTextInHistory();

        ++this._historyIndex;

        this._restoreHistoryEntry(this._historyIndex);
    }

    _handleNextKey(codeMirror)
    {
        if (this._codeMirror.somethingSelected())
            return CodeMirror.Pass;

        // Pass unless we are on the last line.
        if (this._codeMirror.getCursor().line !== this._codeMirror.lastLine())
            return CodeMirror.Pass;

        var historyEntry = this._history[this._historyIndex - 1];
        if (!historyEntry)
            return CodeMirror.Pass;

        this._rememberCurrentTextInHistory();

        --this._historyIndex;

        this._restoreHistoryEntry(this._historyIndex);
    }

    _handleEnterKey(codeMirror, forceCommit, keepCurrentText)
    {
        var currentText = this.text;

        // Always do nothing when there is just whitespace.
        if (!currentText.trim())
            return;

        var cursor = this._codeMirror.getCursor();
        var lastLine = this._codeMirror.lastLine();
        var lastLineLength = this._codeMirror.getLine(lastLine).length;
        var cursorIsAtLastPosition = positionsEqual(cursor, {line: lastLine, ch: lastLineLength});

        function positionsEqual(a, b)
        {
            console.assert(a);
            console.assert(b);
            return a.line === b.line && a.ch === b.ch;
        }

        function commitTextOrInsertNewLine(commit)
        {
            if (!commit) {
                // Only insert a new line if the previous cursor and the current cursor are in the same position.
                if (positionsEqual(cursor, this._codeMirror.getCursor()))
                    CodeMirror.commands.newlineAndIndent(this._codeMirror);
                return;
            }

            this._commitHistoryEntry(this._historyEntryForCurrentText());

            if (!keepCurrentText) {
                this._codeMirror.setValue("");
                this._codeMirror.clearHistory();
            }

            if (this.delegate && typeof this.delegate.consolePromptHistoryDidChange === "function")
                this.delegate.consolePromptHistoryDidChange(this);

            if (this.delegate && typeof this.delegate.consolePromptTextCommitted === "function")
                this.delegate.consolePromptTextCommitted(this, currentText);
        }

        if (!forceCommit && this.delegate && typeof this.delegate.consolePromptShouldCommitText === "function") {
            this.delegate.consolePromptShouldCommitText(this, currentText, cursorIsAtLastPosition, commitTextOrInsertNewLine.bind(this));
            return;
        }

        commitTextOrInsertNewLine.call(this, true);
    }

    _commitHistoryEntry(historyEntry)
    {
        // Replace the previous entry if it does not have text or if the text is the same.
        if (this._history[1] && (!this._history[1].text || this._history[1].text === historyEntry.text)) {
            this._history[1] = historyEntry;
            this._history[0] = {};
        } else {
            // Replace the first history entry and push a new empty one.
            this._history[0] = historyEntry;
            this._history.unshift({});

            // Trim the history length if needed.
            if (this._history.length > WI.ConsolePrompt.MaximumHistorySize)
                this._history = this._history.slice(0, WI.ConsolePrompt.MaximumHistorySize);
        }

        this._historyIndex = 0;
    }

    _handleCommandEnterKey(codeMirror)
    {
        this._handleEnterKey(codeMirror, true, true);
    }

    _restoreHistoryEntry(index)
    {
        var historyEntry = this._history[index];

        this._codeMirror.setValue(historyEntry.text || "");

        if (historyEntry.undoHistory)
            this._codeMirror.setHistory(historyEntry.undoHistory);
        else
            this._codeMirror.clearHistory();

        this._codeMirror.setCursor(historyEntry.cursor || {line: 0});
    }

    _historyEntryForCurrentText()
    {
        return {text: this.text, undoHistory: this._codeMirror.getHistory(), cursor: this._codeMirror.getCursor()};
    }

    _rememberCurrentTextInHistory()
    {
        this._history[this._historyIndex] = this._historyEntryForCurrentText();

        if (this.delegate && typeof this.delegate.consolePromptHistoryDidChange === "function")
            this.delegate.consolePromptHistoryDidChange(this);
    }
};

WI.ConsolePrompt.MaximumHistorySize = 30;
