/*
 * Copyright (C) 2008 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. ``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
 * 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.SidebarPane}
 */
WebInspector.CallStackSidebarPane = function()
{
    WebInspector.SidebarPane.call(this, WebInspector.UIString("Call Stack"));
    this.element.addEventListener("keydown", this._keyDown.bind(this), true);
    this.element.tabIndex = 0;
    this.callFrameList = new WebInspector.UIList();
    this.callFrameList.show(this.element);
    this._linkifier = new WebInspector.Linkifier();
    WebInspector.moduleSetting("enableAsyncStackTraces").addChangeListener(this._asyncStackTracesStateChanged, this);
    WebInspector.moduleSetting("skipStackFramesPattern").addChangeListener(this._blackboxingStateChanged, this);
    /** @type {!Array<!WebInspector.CallStackSidebarPane.CallFrame>} */
    this.callFrames = [];
    this._locationPool = new WebInspector.LiveLocationPool();
}

/** @enum {string} */
WebInspector.CallStackSidebarPane.Events = {
    CallFrameSelected: "CallFrameSelected",
}

WebInspector.CallStackSidebarPane.prototype = {
    /**
     * @param {?WebInspector.DebuggerPausedDetails} details
     */
    update: function(details)
    {
        this.callFrameList.detach();
        this.callFrameList.clear();
        this._linkifier.reset();
        this.element.removeChildren();
        this._locationPool.disposeAll();

        if (!details) {
            var infoElement = this.element.createChild("div", "callstack-info");
            infoElement.textContent = WebInspector.UIString("Not Paused");
            return;
        }

        this.callFrameList.show(this.element);
        this._debuggerModel = details.debuggerModel;
        var asyncStackTrace = details.asyncStackTrace;

        delete this._statusMessageElement;
        delete this._hiddenCallFramesMessageElement;
        this.callFrames = [];
        this._hiddenCallFrames = 0;

        this._appendSidebarCallFrames(this._callFramesFromDebugger(details.callFrames));
        var topStackHidden = (this._hiddenCallFrames === this.callFrames.length);

        while (asyncStackTrace) {
            var title = WebInspector.asyncStackTraceLabel(asyncStackTrace.description);
            var asyncCallFrame = new WebInspector.UIList.Item(title, "", true);
            asyncCallFrame.setHoverable(false);
            asyncCallFrame.element.addEventListener("contextmenu", this._asyncCallFrameContextMenu.bind(this, this.callFrames.length), true);
            this._appendSidebarCallFrames(this._callFramesFromRuntime(asyncStackTrace.callFrames, asyncCallFrame), asyncCallFrame);
            asyncStackTrace = asyncStackTrace.parent;
        }

        if (topStackHidden)
            this._revealHiddenCallFrames();
        if (this._hiddenCallFrames) {
            var element = createElementWithClass("div", "hidden-callframes-message");
            if (this._hiddenCallFrames === 1)
                element.textContent = WebInspector.UIString("1 stack frame is hidden (black-boxed).");
            else
                element.textContent = WebInspector.UIString("%d stack frames are hidden (black-boxed).", this._hiddenCallFrames);
            element.createTextChild(" ");
            var showAllLink = element.createChild("span", "link");
            showAllLink.textContent = WebInspector.UIString("Show");
            showAllLink.addEventListener("click", this._revealHiddenCallFrames.bind(this), false);
            this.element.insertBefore(element, this.element.firstChild);
            this._hiddenCallFramesMessageElement = element;
        }
    },

    /**
     * @param {!Array.<!WebInspector.DebuggerModel.CallFrame>} callFrames
     * @return {!Array<!WebInspector.CallStackSidebarPane.CallFrame>}
     */
    _callFramesFromDebugger: function(callFrames)
    {
        var callFrameItems = [];
        for (var i = 0, n = callFrames.length; i < n; ++i) {
            var callFrame = callFrames[i];
            var callFrameItem = new WebInspector.CallStackSidebarPane.CallFrame(callFrame.functionName, callFrame.location(), this._linkifier, callFrame, this._locationPool);
            callFrameItem.element.addEventListener("click", this._callFrameSelected.bind(this, callFrameItem), false);
            callFrameItems.push(callFrameItem);
        }
        return callFrameItems;
    },

    /**
     * @param {!Array.<!RuntimeAgent.CallFrame>} callFrames
     * @param {!WebInspector.UIList.Item} asyncCallFrameItem
     * @return {!Array<!WebInspector.CallStackSidebarPane.CallFrame>}
     */
    _callFramesFromRuntime: function(callFrames, asyncCallFrameItem)
    {
        var callFrameItems = [];
        for (var i = 0, n = callFrames.length; i < n; ++i) {
            var callFrame = callFrames[i];
            // TODO(591496): conform location in debugger and runtime
            var lineNumber = callFrame.lineNumber ? callFrame.lineNumber - 1 : 0;
            var columnNumber = callFrame.columnNumber ? callFrame.columnNumber - 1 : 0;
            var location = new WebInspector.DebuggerModel.Location(this._debuggerModel, callFrame.scriptId, lineNumber, columnNumber);
            var callFrameItem = new WebInspector.CallStackSidebarPane.CallFrame(callFrame.functionName, location, this._linkifier, null, this._locationPool, asyncCallFrameItem);
            callFrameItem.element.addEventListener("click", this._asyncCallFrameClicked.bind(this, callFrameItem), false);
            callFrameItems.push(callFrameItem);
        }
        return callFrameItems;
    },

    /**
     * @param {!Array.<!WebInspector.CallStackSidebarPane.CallFrame>} callFrames
     * @param {!WebInspector.UIList.Item=} asyncCallFrameItem
     */
    _appendSidebarCallFrames: function(callFrames, asyncCallFrameItem)
    {
        if (asyncCallFrameItem)
            this.callFrameList.addItem(asyncCallFrameItem);

        var allCallFramesHidden = true;
        for (var i = 0, n = callFrames.length; i < n; ++i) {
            var callFrameItem = callFrames[i];
            callFrameItem.element.addEventListener("contextmenu", this._callFrameContextMenu.bind(this, callFrameItem), true);
            this.callFrames.push(callFrameItem);

            if (WebInspector.blackboxManager.isBlackboxedRawLocation(callFrameItem._location)) {
                callFrameItem.setHidden(true);
                callFrameItem.setDimmed(true);
                ++this._hiddenCallFrames;
            } else {
                this.callFrameList.addItem(callFrameItem);
                allCallFramesHidden = false;
            }
        }
        if (allCallFramesHidden && asyncCallFrameItem) {
            asyncCallFrameItem.setHidden(true);
            asyncCallFrameItem.element.remove();
        }
    },

    _revealHiddenCallFrames: function()
    {
        if (!this._hiddenCallFrames)
            return;
        this._hiddenCallFrames = 0;
        this.callFrameList.clear();
        for (var i = 0; i < this.callFrames.length; ++i) {
            var callFrame = this.callFrames[i];
            if (callFrame._asyncCallFrame) {
                callFrame._asyncCallFrame.setHidden(false);
                if (i && callFrame._asyncCallFrame !== this.callFrames[i - 1]._asyncCallFrame)
                    this.callFrameList.addItem(callFrame._asyncCallFrame);
            }
            callFrame.setHidden(false);
            this.callFrameList.addItem(callFrame);
        }
        if (this._hiddenCallFramesMessageElement) {
            this._hiddenCallFramesMessageElement.remove();
            delete this._hiddenCallFramesMessageElement;
        }
    },

    /**
     * @param {!WebInspector.CallStackSidebarPane.CallFrame} callFrame
     * @param {!Event} event
     */
    _callFrameContextMenu: function(callFrame, event)
    {
        var contextMenu = new WebInspector.ContextMenu(event);
        var debuggerCallFrame = callFrame._debuggerCallFrame;
        if (debuggerCallFrame)
            contextMenu.appendItem(WebInspector.UIString.capitalize("Restart ^frame"), debuggerCallFrame.restart.bind(debuggerCallFrame));

        contextMenu.appendItem(WebInspector.UIString.capitalize("Copy ^stack ^trace"), this._copyStackTrace.bind(this));

        var uiLocation = WebInspector.debuggerWorkspaceBinding.rawLocationToUILocation(callFrame._location);
        this.appendBlackboxURLContextMenuItems(contextMenu, uiLocation.uiSourceCode);

        contextMenu.show();
    },

    /**
     * @param {number} index
     * @param {!Event} event
     */
    _asyncCallFrameContextMenu: function(index, event)
    {
        for (; index < this.callFrames.length; ++index) {
            var callFrame = this.callFrames[index];
            if (!callFrame.isHidden()) {
                this._callFrameContextMenu(callFrame, event);
                break;
            }
        }
    },

    /**
     * @param {!WebInspector.ContextMenu} contextMenu
     * @param {!WebInspector.UISourceCode} uiSourceCode
     */
    appendBlackboxURLContextMenuItems: function(contextMenu, uiSourceCode)
    {
        var canBlackbox = WebInspector.blackboxManager.canBlackboxUISourceCode(uiSourceCode);
        var isBlackboxed = WebInspector.blackboxManager.isBlackboxedUISourceCode(uiSourceCode);
        var isContentScript = uiSourceCode.project().type() === WebInspector.projectTypes.ContentScripts;

        var manager = WebInspector.blackboxManager;
        if (canBlackbox) {
            if (isBlackboxed)
                contextMenu.appendItem(WebInspector.UIString.capitalize("Stop ^blackboxing"), manager.unblackboxUISourceCode.bind(manager, uiSourceCode));
            else
                contextMenu.appendItem(WebInspector.UIString.capitalize("Blackbox ^script"), manager.blackboxUISourceCode.bind(manager, uiSourceCode));
        }
        if (isContentScript) {
            if (isBlackboxed)
                contextMenu.appendItem(WebInspector.UIString.capitalize("Stop blackboxing ^all ^content ^scripts"), manager.blackboxContentScripts.bind(manager));
            else
                contextMenu.appendItem(WebInspector.UIString.capitalize("Blackbox ^all ^content ^scripts"), manager.unblackboxContentScripts.bind(manager));
        }
    },

    _blackboxingStateChanged: function()
    {
        if (!this._debuggerModel)
            return;
        var details = this._debuggerModel.debuggerPausedDetails();
        if (!details)
            return;
        this.update(details);
        var selectedCallFrame = this._debuggerModel.selectedCallFrame();
        if (selectedCallFrame)
            this.setSelectedCallFrame(selectedCallFrame);
    },

    _asyncStackTracesStateChanged: function()
    {
        var enabled = WebInspector.moduleSetting("enableAsyncStackTraces").get();
        if (!enabled && this.callFrames)
            this._removeAsyncCallFrames();
    },

    _removeAsyncCallFrames: function()
    {
        var shouldSelectTopFrame = false;
        var lastSyncCallFrameIndex = -1;
        for (var i = 0; i < this.callFrames.length; ++i) {
            var callFrame = this.callFrames[i];
            if (callFrame._asyncCallFrame) {
                if (callFrame.isSelected())
                    shouldSelectTopFrame = true;
                callFrame._asyncCallFrame.element.remove();
                callFrame.element.remove();
            } else {
                lastSyncCallFrameIndex = i;
            }
        }
        this.callFrames.length = lastSyncCallFrameIndex + 1;
        if (shouldSelectTopFrame)
            this._selectNextVisibleCallFrame(0);
    },

    /**
     * @param {!WebInspector.DebuggerModel.CallFrame} x
     */
    setSelectedCallFrame: function(x)
    {
        for (var i = 0; i < this.callFrames.length; ++i) {
            var callFrame = this.callFrames[i];
            callFrame.setSelected(callFrame._debuggerCallFrame === x);
            if (callFrame.isSelected() && callFrame.isHidden())
                this._revealHiddenCallFrames();
        }
    },

    /**
     * @return {boolean}
     */
    _selectNextCallFrameOnStack: function()
    {
        var index = this._selectedCallFrameIndex();
        if (index === -1)
            return false;
        return this._selectNextVisibleCallFrame(index + 1);
    },

    /**
     * @return {boolean}
     */
    _selectPreviousCallFrameOnStack: function()
    {
        var index = this._selectedCallFrameIndex();
        if (index === -1)
            return false;
        return this._selectNextVisibleCallFrame(index - 1, true);
    },

    /**
     * @param {number} index
     * @param {boolean=} backward
     * @return {boolean}
     */
    _selectNextVisibleCallFrame: function(index, backward)
    {
        while (0 <= index && index < this.callFrames.length) {
            var callFrame = this.callFrames[index];
            if (!callFrame.isHidden() && !callFrame.isLabel() && !callFrame._asyncCallFrame) {
                this._callFrameSelected(callFrame);
                return true;
            }
            index += backward ? -1 : 1;
        }
        return false;
    },

    /**
     * @return {number}
     */
    _selectedCallFrameIndex: function()
    {
        if (!this._debuggerModel)
            return -1;
        var selectedCallFrame = this._debuggerModel.selectedCallFrame();
        if (!selectedCallFrame)
            return -1;
        for (var i = 0; i < this.callFrames.length; ++i) {
            if (this.callFrames[i]._debuggerCallFrame === selectedCallFrame)
                return i;
        }
        return -1;
    },

    /**
     * @param {!WebInspector.CallStackSidebarPane.CallFrame} callFrameItem
     */
    _asyncCallFrameClicked: function(callFrameItem)
    {
        var uiLocation = WebInspector.debuggerWorkspaceBinding.rawLocationToUILocation(callFrameItem._location);
        WebInspector.Revealer.reveal(uiLocation);
    },

    /**
     * @param {!WebInspector.CallStackSidebarPane.CallFrame} callFrameItem
     */
    _callFrameSelected: function(callFrameItem)
    {
        callFrameItem.element.scrollIntoViewIfNeeded();
        var callFrame = callFrameItem._debuggerCallFrame;
        if (callFrame)
            this.dispatchEventToListeners(WebInspector.CallStackSidebarPane.Events.CallFrameSelected, callFrame);
    },

    _copyStackTrace: function()
    {
        var text = "";
        var lastCallFrame = null;
        for (var i = 0; i < this.callFrames.length; ++i) {
            var callFrame = this.callFrames[i];
            if (callFrame.isHidden())
                continue;
            if (lastCallFrame && callFrame._asyncCallFrame !== lastCallFrame._asyncCallFrame)
                text += callFrame._asyncCallFrame.title() + "\n";
            text += callFrame.title() + " (" + callFrame.subtitle() + ")\n";
            lastCallFrame = callFrame;
        }
        InspectorFrontendHost.copyText(text);
    },

    /**
     * @param {function(!Array.<!WebInspector.KeyboardShortcut.Descriptor>, function(!Event=):boolean)} registerShortcutDelegate
     */
    registerShortcuts: function(registerShortcutDelegate)
    {
        registerShortcutDelegate(WebInspector.ShortcutsScreen.SourcesPanelShortcuts.NextCallFrame, this._selectNextCallFrameOnStack.bind(this));
        registerShortcutDelegate(WebInspector.ShortcutsScreen.SourcesPanelShortcuts.PrevCallFrame, this._selectPreviousCallFrameOnStack.bind(this));
    },

    /**
     * @param {!Element|string} status
     */
    setStatus: function(status)
    {
        if (!this._statusMessageElement)
            this._statusMessageElement = this.element.createChild("div", "callstack-info status");
        if (typeof status === "string") {
            this._statusMessageElement.textContent = status;
        } else {
            this._statusMessageElement.removeChildren();
            this._statusMessageElement.appendChild(status);
        }
    },

    _keyDown: function(event)
    {
        if (event.altKey || event.shiftKey || event.metaKey || event.ctrlKey)
            return;
        if (event.key === "ArrowUp" && this._selectPreviousCallFrameOnStack() || event.key === "ArrowDown" && this._selectNextCallFrameOnStack())
            event.consume(true);
    },

    __proto__: WebInspector.SidebarPane.prototype
}

/**
 * @constructor
 * @extends {WebInspector.UIList.Item}
 * @param {string} functionName
 * @param {!WebInspector.DebuggerModel.Location} location
 * @param {!WebInspector.Linkifier} linkifier
 * @param {?WebInspector.DebuggerModel.CallFrame} debuggerCallFrame
 * @param {!WebInspector.LiveLocationPool} locationPool
 * @param {!WebInspector.UIList.Item=} asyncCallFrame
 */
WebInspector.CallStackSidebarPane.CallFrame = function(functionName, location, linkifier, debuggerCallFrame, locationPool, asyncCallFrame)
{
    WebInspector.UIList.Item.call(this, WebInspector.beautifyFunctionName(functionName), "");
    this._location = location;
    this._debuggerCallFrame = debuggerCallFrame;
    this._asyncCallFrame = asyncCallFrame;

    if (asyncCallFrame) {
        var locationElement = linkifier.linkifyRawLocation(location, location.script().sourceURL);
        this.subtitleElement.appendChild(locationElement);
    } else {
        this._liveLocationPool = new WebInspector.LiveLocationPool();
        WebInspector.debuggerWorkspaceBinding.createCallFrameLiveLocation(location, this._update.bind(this), locationPool);
    }
}

WebInspector.CallStackSidebarPane.CallFrame.prototype = {
    /**
     * @param {!WebInspector.LiveLocation} liveLocation
     */
    _update: function(liveLocation)
    {
        var uiLocation = liveLocation.uiLocation();
        if (!uiLocation)
            return;
        var text = uiLocation.linkText();
        this.setSubtitle(text.trimMiddle(30));
        this.subtitleElement.title = text;
    },

    __proto__: WebInspector.UIList.Item.prototype
}
