/*
 * 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.
 */

WebInspector.QuickConsole = function(element)
{
    WebInspector.Object.call(this);

    this._toggleOrFocusKeyboardShortcut = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Escape, this._toggleOrFocus.bind(this));

    var mainFrameExecutionContext = new WebInspector.ExecutionContext(WebInspector.QuickConsole.MainFrameContextExecutionIdentifier, WebInspector.UIString("Main Frame"), true, null);
    this._mainFrameExecutionContextPathComponent = this._createExecutionContextPathComponent(mainFrameExecutionContext.name, mainFrameExecutionContext.identifier);
    this._selectedExecutionContextPathComponent = this._mainFrameExecutionContextPathComponent;

    this._otherExecutionContextPathComponents = [];
    this._frameIdentifierToExecutionContextPathComponentMap = {};

    this._element = element || document.createElement("div");
    this._element.classList.add(WebInspector.QuickConsole.StyleClassName);

    this.prompt = new WebInspector.ConsolePrompt(null, "text/javascript");
    this.prompt.element.classList.add(WebInspector.QuickConsole.TextPromptStyleClassName);
    this._element.appendChild(this.prompt.element);

    // FIXME: CodeMirror 4 has a default "Esc" key handler that always prevents default.
    // Our keyboard shortcut above will respect the default prevented and ignore the event
    // and not toggle the console. Install our own Escape key handler that will trigger
    // when the ConsolePrompt is empty, to restore toggling behavior. A better solution
    // would be for CodeMirror's event handler to pass if it doesn't do anything.
    this.prompt.escapeKeyHandlerWhenEmpty = function() { WebInspector.toggleSplitConsole(); };

    this.prompt.shown();

    this._navigationBar = new WebInspector.QuickConsoleNavigationBar;
    this._element.appendChild(this._navigationBar.element);

    this._executionContextSelectorItem = new WebInspector.HierarchicalPathNavigationItem;
    this._executionContextSelectorItem.showSelectorArrows = true;
    this._navigationBar.addNavigationItem(this._executionContextSelectorItem);

    this._executionContextSelectorDivider = new WebInspector.DividerNavigationItem;
    this._navigationBar.addNavigationItem(this._executionContextSelectorDivider);

    this._rebuildExecutionContextPathComponents();

    // COMPATIBILITY (iOS 6): Execution contexts did not exist, evaluation worked with frame ids.
    if (WebInspector.ExecutionContext.supported()) {
        WebInspector.Frame.addEventListener(WebInspector.Frame.Event.PageExecutionContextChanged, this._framePageExecutionContextsChanged, this);
        WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ExecutionContextsCleared, this._frameExecutionContextsCleared, this);
    } else {
        WebInspector.frameResourceManager.addEventListener(WebInspector.FrameResourceManager.Event.FrameWasAdded, this._frameAdded, this);
        WebInspector.frameResourceManager.addEventListener(WebInspector.FrameResourceManager.Event.FrameWasRemoved, this._frameRemoved, this);
        WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._frameMainResourceChanged, this);
    }

    WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._debuggerActiveCallFrameDidChange, this);
};

WebInspector.QuickConsole.StyleClassName = "quick-console";
WebInspector.QuickConsole.ShowingLogClassName = "showing-log";
WebInspector.QuickConsole.NavigationBarContainerStyleClassName = "navigation-bar-container";
WebInspector.QuickConsole.NavigationBarSpacerStyleClassName = "navigation-bar-spacer";
WebInspector.QuickConsole.TextPromptStyleClassName = "text-prompt";

WebInspector.QuickConsole.ToolbarSingleLineHeight = 21;
WebInspector.QuickConsole.ToolbarPromptPadding = 4;
WebInspector.QuickConsole.ToolbarTopBorder = 1;

WebInspector.QuickConsole.MainFrameContextExecutionIdentifier = undefined;

WebInspector.QuickConsole.Event = {
    DidResize: "quick-console-did-resize"
};

WebInspector.QuickConsole.prototype = {
    constructor: WebInspector.QuickConsole,

    // Public

    get element()
    {
        return this._element;
    },

    get navigationBar()
    {
        return this._navigationBar;
    },

    get executionContextIdentifier()
    {
        return this._selectedExecutionContextPathComponent._executionContextIdentifier;
    },

    updateLayout: function()
    {
        // A hard maximum size of 33% of the window.
        const maximumAllowedHeight = Math.round(window.innerHeight * 0.33);
        this.prompt.element.style.maxHeight = maximumAllowedHeight + "px";
    },

    consoleLogVisibilityChanged: function(visible)
    {
        if (visible)
            this.element.classList.add(WebInspector.QuickConsole.ShowingLogClassName);
        else
            this.element.classList.remove(WebInspector.QuickConsole.ShowingLogClassName);

        this.dispatchEventToListeners(WebInspector.QuickConsole.Event.DidResize);
    },

    // Private

    _executionContextPathComponentsToDisplay: function()
    {
        // If we are in the debugger the console will use the active call frame, don't show the selector.
        if (WebInspector.debuggerManager.activeCallFrame)
            return [];

        // If there is only the Main Frame, don't show the selector.
        if (!this._otherExecutionContextPathComponents.length)
            return [];

        return [this._selectedExecutionContextPathComponent];
    },

    _rebuildExecutionContextPathComponents: function()
    {
        var components = this._executionContextPathComponentsToDisplay();
        var isEmpty = !components.length;

        this._executionContextSelectorItem.components = components;

        this._executionContextSelectorItem.hidden = isEmpty;
        this._executionContextSelectorDivider.hidden = isEmpty;
    },

    _framePageExecutionContextsChanged: function(event)
    {
        var frame = event.target;

        var shouldAutomaticallySelect = this._restoreSelectedExecutionContextForFrame === frame;

        var newExecutionContextPathComponent = this._insertExecutionContextPathComponentForFrame(frame, shouldAutomaticallySelect);

        if (shouldAutomaticallySelect) {
            delete this._restoreSelectedExecutionContextForFrame;
            this._selectedExecutionContextPathComponent = newExecutionContextPathComponent;
            this._rebuildExecutionContextPathComponents();
        }
    },

    _frameExecutionContextsCleared: function(event)
    {
        var frame = event.target;

        // If this frame is navigating and it is selected in the UI we want to reselect its new item after navigation.
        if (event.data.committingProvisionalLoad && !this._restoreSelectedExecutionContextForFrame) {
            var executionContextPathComponent = this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
            if (this._selectedExecutionContextPathComponent === executionContextPathComponent) {
                this._restoreSelectedExecutionContextForFrame = frame;
                // As a fail safe, if the frame never gets an execution context, clear the restore value.
                setTimeout(function() { delete this._restoreSelectedExecutionContextForFrame; }.bind(this), 10);
            }
        }

        this._removeExecutionContextPathComponentForFrame(frame);
    },

    _frameAdded: function(event)
    {
        var frame = event.data.frame;
        this._insertExecutionContextPathComponentForFrame(frame);
    },

    _frameRemoved: function(event)
    {
        var frame = event.data.frame;
        this._removeExecutionContextPathComponentForFrame(frame);
    },

    _frameMainResourceChanged: function(event)
    {
        var frame = event.target;
        this._updateExecutionContextPathComponentForFrame(frame);
    },

    _createExecutionContextPathComponent: function(name, identifier)
    {
        var pathComponent = new WebInspector.HierarchicalPathComponent(name, "execution-context", identifier, true, true);
        pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
        pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
        pathComponent.truncatedDisplayNameLength = 50;
        pathComponent._executionContextIdentifier = identifier;
        return pathComponent;
    },

    _createExecutionContextPathComponentFromFrame: function(frame)
    {
        var name = frame.name ? frame.name + " \u2014 " + frame.mainResource.displayName : frame.mainResource.displayName;
        var identifier = WebInspector.ExecutionContext.supported() ? frame.pageExecutionContext.id : frame.id;

        var pathComponent = this._createExecutionContextPathComponent(name, identifier);
        pathComponent._frame = frame;

        return pathComponent;
    },

    _compareExecutionContextPathComponents: function(a, b)
    {
        // "Main Frame" always on top.
        if (!a._frame)
            return -1;
        if (!b._frame)
            return 1;

        // Frames with a name above frames without a name.
        if (a._frame.name && !b._frame.name)
            return -1;
        if (!a._frame.name && b._frame.name)
            return 1;

        return a.displayName.localeCompare(b.displayName);
    },

    _insertExecutionContextPathComponentForFrame: function(frame, skipRebuild)
    {
        if (frame.isMainFrame())
            return this._mainFrameExecutionContextPathComponent;

        console.assert(!this._frameIdentifierToExecutionContextPathComponentMap[frame.id]);
        if (this._frameIdentifierToExecutionContextPathComponentMap[frame.id])
            return null;

        var executionContextPathComponent = this._createExecutionContextPathComponentFromFrame(frame);

        var index = insertionIndexForObjectInListSortedByFunction(executionContextPathComponent, this._otherExecutionContextPathComponents, this._compareExecutionContextPathComponents);

        var prev = index > 0 ? this._otherExecutionContextPathComponents[index - 1] : this._mainFrameExecutionContextPathComponent;
        var next = this._otherExecutionContextPathComponents[index] || null;
        if (prev) {
            prev.nextSibling = executionContextPathComponent;
            executionContextPathComponent.previousSibling = prev;
        }
        if (next) {
            next.previousSibling = executionContextPathComponent;
            executionContextPathComponent.nextSibling = next;
        }

        this._otherExecutionContextPathComponents.splice(index, 0, executionContextPathComponent);
        this._frameIdentifierToExecutionContextPathComponentMap[frame.id] = executionContextPathComponent;

        if (!skipRebuild)
            this._rebuildExecutionContextPathComponents();

        return executionContextPathComponent;
    },

    _removeExecutionContextPathComponentForFrame: function(frame, skipRebuild)
    {
        if (frame.isMainFrame())
            return;

        var executionContextPathComponent = this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
        console.assert(executionContextPathComponent);
        if (!executionContextPathComponent)
            return;

        executionContextPathComponent.removeEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
        executionContextPathComponent.removeEventListener(WebInspector.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);

        var prev = executionContextPathComponent.previousSibling;
        var next = executionContextPathComponent.nextSibling;
        if (prev)
            prev.nextSibling = next;
        if (next)
            next.previousSibling = prev;

        if (this._selectedExecutionContextPathComponent === executionContextPathComponent)
            this._selectedExecutionContextPathComponent = this._mainFrameExecutionContextPathComponent;

        this._otherExecutionContextPathComponents.remove(executionContextPathComponent, true);
        delete this._frameIdentifierToExecutionContextPathComponentMap[frame.id];

        if (!skipRebuild)
            this._rebuildExecutionContextPathComponents();
    },

    _updateExecutionContextPathComponentForFrame: function(frame)
    {
        if (frame.isMainFrame())
            return;

        var executionContextPathComponent = this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
        if (!executionContextPathComponent)
            return;

        var wasSelected = this._selectedExecutionContextPathComponent === executionContextPathComponent;

        this._removeExecutionContextPathComponentForFrame(frame, true);
        var newExecutionContextPathComponent = this._insertExecutionContextPathComponentForFrame(frame, true);

        if (wasSelected)
            this._selectedExecutionContextPathComponent = newExecutionContextPathComponent;

        this._rebuildExecutionContextPathComponents();
    },

    _pathComponentSelected: function(event)
    {
        if (event.data.pathComponent === this._selectedExecutionContextPathComponent)
            return;

        this._selectedExecutionContextPathComponent = event.data.pathComponent;

        this._rebuildExecutionContextPathComponents();
    },

    _pathComponentClicked: function(event)
    {
        this.prompt.focus();
    },

    _debuggerActiveCallFrameDidChange: function(event)
    {
        this._rebuildExecutionContextPathComponents();
    },

    _toggleOrFocus: function(event)
    {
        if (this.prompt.focused)
            WebInspector.toggleSplitConsole();
        else if (!WebInspector.isEditingAnyField() && !WebInspector.isEventTargetAnEditableField(event))
            this.prompt.focus();
    }
};

WebInspector.QuickConsole.prototype.__proto__ = WebInspector.Object.prototype;
