/*
 * Copyright (C) 2013 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.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Object
{
    constructor(element, scrollElement, textPrompt, delegate, historySettingIdentifier)
    {
        super();

        console.assert(textPrompt instanceof WI.ConsolePrompt);
        console.assert(historySettingIdentifier);

        this._element = element;
        this._scrollElement = scrollElement;

        this._promptHistorySetting = new WI.Setting(historySettingIdentifier, null);

        this._prompt = textPrompt;
        this._prompt.delegate = this;
        this._prompt.history = this._promptHistorySetting.value;

        this.delegate = delegate;

        this._cleared = true;
        this._previousMessageView = null;
        this._lastConsoleMessageViewForTarget = new WeakMap;
        this._lastCommitted = {text: "", special: false};
        this._repeatCountWasInterrupted = false;

        this._sessions = [];
        this._currentSessionOrGroup = null;

        this._failedSourceMapsGroup = null;

        this.messagesAlternateClearKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, "L", this.requestClearMessages.bind(this), this._element);

        this._messagesFindNextKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "G", this._handleFindNextShortcut.bind(this), this._element);
        this._messagesFindPreviousKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._element);

        this._promptAlternateClearKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, "L", this.requestClearMessages.bind(this), this._prompt.element);
        this._promptFindNextKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "G", this._handleFindNextShortcut.bind(this), this._prompt.element);
        this._promptFindPreviousKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._prompt.element);

        WI.settings.showConsoleMessageTimestamps.addEventListener(WI.Setting.Event.Changed, this._handleShowConsoleMessageTimestampsSettingChanged, this);

        this._pendingMessagesForSessionOrGroup = new Map;
        this._scheduledRenderIdentifier = 0;

        this.startNewSession();
    }

    // Public

    clear()
    {
        this._cleared = true;

        const clearPreviousSessions = true;
        this.startNewSession(clearPreviousSessions, {newSessionReason: WI.ConsoleSession.NewSessionReason.ConsoleCleared});
    }

    startNewSession(clearPreviousSessions = false, data = {})
    {
        if (clearPreviousSessions) {
            this._pendingMessagesForSessionOrGroup.clear();

            if (this._sessions.length) {
                for (let session of this._sessions)
                    session.element.remove();

                this._sessions = [];
                this._currentSessionOrGroup = null;
            }
        }

        // First session shows the time when the console was opened.
        if (!this._sessions.length)
            data.timestamp = Date.now();

        let lastSession = this._sessions.lastValue;

        // Remove empty session.
        if (lastSession && !lastSession.hasMessages() && !this._pendingMessagesForSessionOrGroup.has(lastSession)) {
            this._sessions.pop();
            lastSession.element.remove();
        }

        let consoleSession = new WI.ConsoleSession(data);

        this._previousMessageView = null;
        this._lastConsoleMessageViewForTarget = new WeakMap;
        this._lastCommitted = {text: "", special: false};
        this._repeatCountWasInterrupted = false;

        this._sessions.push(consoleSession);
        this._currentSessionOrGroup = consoleSession;

        this._failedSourceMapsGroup = null;

        this._element.appendChild(consoleSession.element);

        // Make sure the new session is visible.
        consoleSession.element.scrollIntoView();
    }

    appendImmediateExecutionWithResult(text, result, {addSpecialUserLogClass, shouldRevealConsole, handleClick} = {})
    {
        console.assert(result instanceof WI.RemoteObject);

        if (this._lastCommitted.text !== text || this._lastCommitted.special !== addSpecialUserLogClass) {
            let classNames = [];
            if (addSpecialUserLogClass)
                classNames.push("special-user-log");

            let commandMessageView = new WI.ConsoleCommandView(text, {classNames, handleClick});
            this._appendConsoleCommandView(commandMessageView);
            this._lastCommitted = {text, special: addSpecialUserLogClass};
        }

        function saveResultCallback(savedResultIndex)
        {
            let commandResultMessage = new WI.ConsoleCommandResultMessage(result.target, result, false, savedResultIndex, { shouldRevealConsole });
            let commandResultMessageView = new WI.ConsoleMessageView(commandResultMessage);
            this._appendConsoleMessageView(commandResultMessageView, true);
        }

        WI.runtimeManager.saveResult(result, saveResultCallback.bind(this));
    }

    appendConsoleMessage(consoleMessage)
    {
        let consoleMessageView = new WI.ConsoleMessageView(consoleMessage);
        this._appendConsoleMessageView(consoleMessageView);
        return consoleMessageView;
    }

    updatePreviousMessageRepeatCount(target, count, timestamp)
    {
        let previousMessageView = this._previousMessageView;
        console.assert(previousMessageView);
        if (!previousMessageView)
            return false;

        if (previousMessageView.message.target !== target) {
            previousMessageView = this._lastConsoleMessageViewForTarget.get(target);
            console.assert(previousMessageView);
            if (!previousMessageView)
                return false;
            let messageView = this.appendConsoleMessage(previousMessageView.message);
            messageView.timestamp = timestamp;
            messageView[WI.JavaScriptLogViewController.IgnoredRepeatCount] = count - 1;
            messageView.repeatCount = 1;
            this._repeatCountWasInterrupted = false;
            return true;
        }

        let previousIgnoredCount = previousMessageView[WI.JavaScriptLogViewController.IgnoredRepeatCount] || 0;
        let previousVisibleCount = previousMessageView.repeatCount;

        if (!this._repeatCountWasInterrupted) {
            previousMessageView.repeatCount = count - previousIgnoredCount;
            previousMessageView.timestamp = timestamp;
            return true;
        }

        let consoleMessage = previousMessageView.message;
        let duplicatedConsoleMessageView = new WI.ConsoleMessageView(consoleMessage);
        duplicatedConsoleMessageView[WI.JavaScriptLogViewController.IgnoredRepeatCount] = previousIgnoredCount + previousVisibleCount;
        duplicatedConsoleMessageView.repeatCount = 1;
        duplicatedConsoleMessageView.timestamp = timestamp;
        this._appendConsoleMessageView(duplicatedConsoleMessageView);

        return true;
    }

    isScrolledToBottom()
    {
        // Lie about being scrolled to the bottom if we have a pending request to scroll to the bottom soon.
        return this._scrollToBottomTimeout || this._scrollElement.isScrolledToBottom();
    }

    scrollToBottom()
    {
        if (this._scrollToBottomTimeout)
            return;

        function delayedWork()
        {
            this._scrollToBottomTimeout = null;
            this._scrollElement.scrollTop = this._scrollElement.scrollHeight;
        }

        // Don't scroll immediately so we are not causing excessive layouts when there
        // are many messages being added at once.
        this._scrollToBottomTimeout = setTimeout(delayedWork.bind(this), 0);
    }

    requestClearMessages()
    {
        WI.consoleManager.requestClearMessages();
    }

    // Protected

    consolePromptHistoryDidChange(prompt)
    {
        this._promptHistorySetting.value = this._prompt.history;
    }

    consolePromptShouldCommitText(prompt, text, cursorIsAtLastPosition, handler)
    {
        // Always commit the text if we are not at the last position.
        if (!cursorIsAtLastPosition) {
            handler(true);
            return;
        }

        function parseFinished(error, result, message, range)
        {
            handler(result !== InspectorBackend.Enum.Runtime.SyntaxErrorType.Recoverable);
        }

        WI.runtimeManager.activeExecutionContext.target.RuntimeAgent.parse(text, parseFinished.bind(this));
    }

    consolePromptTextCommitted(prompt, text)
    {
        console.assert(text);

        if (this._lastCommitted.text !== text || this._lastCommitted.special) {
            let commandMessageView = new WI.ConsoleCommandView(text);
            this._appendConsoleCommandView(commandMessageView);
            this._lastCommitted = {text, special: false};
        }

        function printResult(result, wasThrown, savedResultIndex)
        {
            if (!result || this._cleared)
                return;

            let commandResultMessage = new WI.ConsoleCommandResultMessage(result.target, result, wasThrown, savedResultIndex, { shouldRevealConsole: true });
            let commandResultMessageView = new WI.ConsoleMessageView(commandResultMessage);
            this._appendConsoleMessageView(commandResultMessageView, true);
        }

        let options = {
            objectGroup: WI.RuntimeManager.ConsoleObjectGroup,
            includeCommandLineAPI: true,
            doNotPauseOnExceptionsAndMuteConsole: false,
            returnByValue: false,
            generatePreview: true,
            saveResult: true,
            emulateUserGesture: WI.settings.emulateInUserGesture.value,
            sourceURLAppender: appendWebInspectorConsoleEvaluationSourceURL,
        };

        WI.javaScriptRuntimeCompletionProvider.clearCachedPropertyNames();

        WI.runtimeManager.evaluateInInspectedWindow(text, options, printResult.bind(this));
    }

    // Private

    _handleFindNextShortcut()
    {
        this.delegate.highlightNextSearchMatch();
    }

    _handleFindPreviousShortcut()
    {
        this.delegate.highlightPreviousSearchMatch();
    }

    _appendConsoleMessageView(messageView, repeatCountWasInterrupted)
    {
        console.assert(messageView instanceof WI.ConsoleMessageView);

        this._addToPendingMessages(messageView);
        this._cleared = false;
        this._repeatCountWasInterrupted = repeatCountWasInterrupted || false;

        let message = messageView.message;

        if (!repeatCountWasInterrupted) {
            this._previousMessageView = messageView;
            this._lastConsoleMessageViewForTarget.set(message.target, messageView);
        }

        if (message.source !== WI.ConsoleMessage.MessageSource.JS)
            this._lastCommitted = {text: "", special: false};

        if (WI.consoleContentView.isAttached)
            this.renderPendingMessagesSoon();

        if (!WI.isShowingConsoleTab() && message.shouldRevealConsole)
            WI.showSplitConsole();
    }

    _appendConsoleCommandView(commandView)
    {
        console.assert(commandView instanceof WI.ConsoleCommandView);

        this._addToPendingMessages(commandView);
        this._cleared = false;
        this._repeatCountWasInterrupted = true;

        if (WI.consoleContentView.isAttached)
            this.renderPendingMessagesSoon();
    }

    _addToPendingMessages(messageView)
    {
        let pendingMessagesForSession = this._pendingMessagesForSessionOrGroup.getOrInsert(this._currentSessionOrGroup, []);
        pendingMessagesForSession.push(messageView);
        this._pendingMessagesForSessionOrGroup.set(this._currentSessionOrGroup, pendingMessagesForSession);
    }

    renderPendingMessages()
    {
        if (this._scheduledRenderIdentifier) {
            cancelAnimationFrame(this._scheduledRenderIdentifier);
            this._scheduledRenderIdentifier = 0;
        }

        if (!this._pendingMessagesForSessionOrGroup.size)
            return;

        let wasScrolledToBottom = this.isScrolledToBottom();
        let savedCurrentConsoleGroup = this._currentSessionOrGroup;
        let lastMessageView = null;

        const maxMessagesPerFrame = 100;
        let renderedMessages = 0;
        for (let [session, messages] of this._pendingMessagesForSessionOrGroup) {
            this._currentSessionOrGroup = session;

            let messagesToRender = messages.splice(0, maxMessagesPerFrame - renderedMessages);
            for (let message of messagesToRender) {
                message.render();
                this._didRenderConsoleMessageView(message);
            }

            lastMessageView = messagesToRender.lastValue;

            if (!messages.length)
                this._pendingMessagesForSessionOrGroup.delete(session);

            renderedMessages += messagesToRender.length;
            if (renderedMessages >= maxMessagesPerFrame)
                break;
        }

        this._currentSessionOrGroup = savedCurrentConsoleGroup;

        this._handleShowConsoleMessageTimestampsSettingChanged();

        if (wasScrolledToBottom || lastMessageView instanceof WI.ConsoleCommandView || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Result || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Image)
            this.scrollToBottom();

        WI.quickConsole.needsLayout();

        if (this._pendingMessagesForSessionOrGroup.size)
            this.renderPendingMessagesSoon();
    }

    renderPendingMessagesSoon()
    {
        if (this._scheduledRenderIdentifier)
            return;

        this._scheduledRenderIdentifier = requestAnimationFrame(() => this.renderPendingMessages());
    }

    _didRenderConsoleMessageView(messageView)
    {
        let type = messageView instanceof WI.ConsoleCommandView ? null : messageView.message.type;
        if (type === WI.ConsoleMessage.MessageType.EndGroup) {
            let parentGroup = this._currentSessionOrGroup.parentGroup;
            if (parentGroup)
                this._currentSessionOrGroup = parentGroup;
        } else {
            if (type === WI.ConsoleMessage.MessageType.StartGroup || type === WI.ConsoleMessage.MessageType.StartGroupCollapsed) {
                let group = new WI.ConsoleGroup(this._currentSessionOrGroup);
                let groupElement = group.render(messageView);
                this._currentSessionOrGroup.append(groupElement);
                this._currentSessionOrGroup = group;
            } else
                this._currentSessionOrGroup.addMessageView(messageView);
            
            // FIXME: <https://webkit.org/b/264490> (Improve failed SourceMap error grouping to avoid unnecessary work)
            if (WI.settings.experimentalGroupSourceMapErrors.value && WI.consoleManager.failedSourceMapConsoleMessages.has(messageView.message))
                this._addConsoleMessageToConsoleSourceMapErrorGroup(messageView);
        }

        if (this.delegate && typeof this.delegate.didAppendConsoleMessageView === "function")
            this.delegate.didAppendConsoleMessageView(messageView);
    }
    
    _addConsoleMessageToConsoleSourceMapErrorGroup(messageView)
    {
        if (!this._firstSourceMapErrorMessageView) {
            this._firstSourceMapErrorMessageView = messageView;
            return;
        }
        
        if (!this._failedSourceMapsGroup) {
            let currentTopLevelSession = this._sessions.lastValue;
            this._failedSourceMapsGroup = new WI.ConsoleSourceMapMessageGroup;
            currentTopLevelSession.append(this._failedSourceMapsGroup.element);
            this._failedSourceMapsGroup.addMessageView(this._firstSourceMapErrorMessageView);
        }

        this._failedSourceMapsGroup.addMessageView(messageView);
    }

    _handleShowConsoleMessageTimestampsSettingChanged()
    {
        this._currentSessionOrGroup.element.classList.toggle("timestamps-visible", WI.settings.showConsoleMessageTimestamps.value);
    }
};

WI.JavaScriptLogViewController.CachedPropertiesDuration = 30_000;
WI.JavaScriptLogViewController.IgnoredRepeatCount = Symbol("ignored-repeat-count");
