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

WebInspector.TimelineSidebarPanel = class TimelineSidebarPanel extends WebInspector.NavigationSidebarPanel
{
    constructor(contentBrowser)
    {
        super("timeline", WebInspector.UIString("Timelines"));

        this.contentBrowser = contentBrowser;

        this._timelineEventsTitleBarContainer = document.createElement("div");
        this._timelineEventsTitleBarContainer.classList.add(WebInspector.TimelineSidebarPanel.TitleBarStyleClass);
        this._timelineEventsTitleBarContainer.classList.add(WebInspector.TimelineSidebarPanel.TimelineEventsTitleBarStyleClass);

        this._timelineEventsTitleBarElement = document.createElement("div");
        this._timelineEventsTitleBarElement.classList.add(WebInspector.TimelineSidebarPanel.TitleBarTextStyleClass);
        this._timelineEventsTitleBarContainer.appendChild(this._timelineEventsTitleBarElement);

        this._timelineEventsTitleBarScopeContainer = document.createElement("div");
        this._timelineEventsTitleBarScopeContainer.classList.add(WebInspector.TimelineSidebarPanel.TitleBarScopeBarStyleClass);
        this._timelineEventsTitleBarContainer.appendChild(this._timelineEventsTitleBarScopeContainer);

        this.element.insertBefore(this._timelineEventsTitleBarContainer, this.element.firstChild);

        this.contentTreeOutlineLabel = "";

        this._timelinesContentContainerElement = document.createElement("div");
        this._timelinesContentContainerElement.classList.add(WebInspector.TimelineSidebarPanel.TimelinesContentContainerStyleClass);
        this.element.insertBefore(this._timelinesContentContainerElement, this.element.firstChild);

        this._displayedRecording = null;
        this._displayedContentView = null;
        this._viewMode = null;
        this._previousSelectedTimelineType = null;

        // Maintain an invisible tree outline containing tree elements for all recordings.
        // The visible recording's tree element is selected when the content view changes.
        this._recordingTreeElementMap = new Map;
        this._recordingsTreeOutline = this.createContentTreeOutline(true, true);
        this._recordingsTreeOutline.disclosureButtons = false;
        this._recordingsTreeOutline.hidden = true;
        this._recordingsTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._recordingsTreeSelectionDidChange, this);
        this._timelinesContentContainerElement.appendChild(this._recordingsTreeOutline.element);

        // Maintain a tree outline with tree elements for each timeline of the selected recording.
        this._timelinesTreeOutline = this.createContentTreeOutline(true, true);
        this._timelinesTreeOutline.disclosureButtons = false;
        this._timelinesTreeOutline.large = true;
        this._timelinesTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._timelinesTreeSelectionDidChange, this);
        this._timelinesContentContainerElement.appendChild(this._timelinesTreeOutline.element);

        this._timelineTreeElementMap = new Map;

        this._basicTitleBar = document.createElement("div");
        this._basicTitleBar.textContent = WebInspector.UIString("Timelines");
        this._basicTitleBar.classList.add(WebInspector.TimelineSidebarPanel.TitleBarStyleClass);
        this._basicTitleBar.classList.add(WebInspector.TimelineSidebarPanel.TimelinesTitleBarStyleClass);
        this.element.insertBefore(this._basicTitleBar, this.element.firstChild);

        if (WebInspector.FPSInstrument.supported()) {
            let timelinesNavigationItem = new WebInspector.RadioButtonNavigationItem(WebInspector.TimelineOverview.ViewMode.Timelines, WebInspector.UIString("Timelines"))
            let renderingFramesNavigationItem = new WebInspector.RadioButtonNavigationItem(WebInspector.TimelineOverview.ViewMode.RenderingFrames, WebInspector.UIString("Rendering Frames"))
            this._viewModeNavigationBar = new WebInspector.NavigationBar(null, [timelinesNavigationItem, renderingFramesNavigationItem], "tablist");
            this._viewModeNavigationBar.addEventListener(WebInspector.NavigationBar.Event.NavigationItemSelected, this._viewModeSelected, this);

            this._renderingFramesTitleBar = document.createElement("div");
            this._renderingFramesTitleBar.className = "navigation-bar-container";
            this._renderingFramesTitleBar.appendChild(this._viewModeNavigationBar.element);
            this._renderingFramesTitleBar.hidden = true;
            this.element.insertBefore(this._renderingFramesTitleBar, this.element.firstChild);

            this._chartColors = new Map;
            this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Script, "rgb(153, 113, 185)");
            this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Layout, "rgb(212, 108, 108)");
            this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Paint, "rgb(152, 188, 77)");
            this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Other, "rgb(221, 221, 221)");

            this._frameSelectionChartRow = new WebInspector.ChartDetailsSectionRow(this, 74, 0.5);
            this._frameSelectionChartRow.addEventListener(WebInspector.ChartDetailsSectionRow.Event.LegendItemChecked, this._frameSelectionLegendItemChecked, this);

            for (let key in WebInspector.RenderingFrameTimelineRecord.TaskType) {
                let taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
                let label = WebInspector.RenderingFrameTimelineRecord.displayNameForTaskType(taskType);
                let color = this._chartColors.get(taskType);
                let checkbox = taskType !== WebInspector.RenderingFrameTimelineRecord.TaskType.Other;
                this._frameSelectionChartRow.addItem(taskType, label, 0, color, checkbox, true);
            }

            this._renderingFrameTaskFilter = new Set;

            var chartGroup = new WebInspector.DetailsSectionGroup([this._frameSelectionChartRow]);
            this._frameSelectionChartSection = new WebInspector.DetailsSection("frames-selection-chart", WebInspector.UIString("Selected Frames"), [chartGroup], null, true);
            this._timelinesContentContainerElement.appendChild(this._frameSelectionChartSection.element);
        }

        this._toggleRecordingShortcut = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Space, this._toggleRecordingOnSpacebar.bind(this));
        this._toggleRecordingShortcut.implicitlyPreventsDefault = false;
        this._toggleRecordingShortcut.disabled = true;

        this._toggleNewRecordingShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Shift, WebInspector.KeyboardShortcut.Key.Space, this._toggleNewRecordingOnSpacebar.bind(this));
        this._toggleNewRecordingShortcut.implicitlyPreventsDefault = false;
        this._toggleNewRecordingShortcut.disabled = true;

        this._recordingNavigationBar = new WebInspector.NavigationBar;
        this.element.insertBefore(this._recordingNavigationBar.element, this.element.firstChild);

        let toolTip = WebInspector.UIString("Start recording (%s)\nCreate new recording (%s)").format(this._toggleRecordingShortcut.displayName, this._toggleNewRecordingShortcut.displayName);
        let altToolTip = WebInspector.UIString("Stop recording (%s)").format(this._toggleRecordingShortcut.displayName);

        this._recordButton = new WebInspector.ToggleButtonNavigationItem("record-start-stop", toolTip, altToolTip, "Images/Record.svg", "Images/Stop.svg", 13, 13);
        this._recordButton.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._recordButtonClicked, this);
        this._recordButton.element.addEventListener("mouseover", this._recordButtonMousedOver.bind(this));
        this._recordButton.element.addEventListener("mouseout", this._recordButtonMousedOut.bind(this));
        this._recordingNavigationBar.addNavigationItem(this._recordButton);

        this._recordingStatusItem = new WebInspector.FlexibleSpaceNavigationItem;
        this._recordingNavigationBar.addNavigationItem(this._recordingStatusItem);

        WebInspector.showReplayInterfaceSetting.addEventListener(WebInspector.Setting.Event.Changed, this._updateReplayInterfaceVisibility, this);

        // We always create a replay navigation bar; its visibility is controlled by WebInspector.showReplayInterfaceSetting.
        this._replayNavigationBar = new WebInspector.NavigationBar;
        this.element.appendChild(this._replayNavigationBar.element);

        toolTip = WebInspector.UIString("Begin Capturing");
        altToolTip = WebInspector.UIString("End Capturing");
        this._replayCaptureButtonItem = new WebInspector.ActivateButtonNavigationItem("replay-capture", toolTip, altToolTip, "Images/Circle.svg", 16, 16);
        this._replayCaptureButtonItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._replayCaptureButtonClicked, this);
        this._replayCaptureButtonItem.enabled = true;
        this._replayNavigationBar.addNavigationItem(this._replayCaptureButtonItem);

        toolTip = WebInspector.UIString("Start Playback");
        altToolTip = WebInspector.UIString("Pause Playback");
        this._replayPauseResumeButtonItem = new WebInspector.ToggleButtonNavigationItem("replay-pause-resume", toolTip, altToolTip, "Images/Resume.svg", "Images/Pause.svg", 15, 15);
        this._replayPauseResumeButtonItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._replayPauseResumeButtonClicked, this);
        this._replayPauseResumeButtonItem.enabled = false;
        this._replayNavigationBar.addNavigationItem(this._replayPauseResumeButtonItem);

        WebInspector.replayManager.addEventListener(WebInspector.ReplayManager.Event.CaptureStarted, this._captureStarted, this);
        WebInspector.replayManager.addEventListener(WebInspector.ReplayManager.Event.CaptureStopped, this._captureStopped, this);

        this._recordingNavigationBar.element.oncontextmenu = this._contextMenuNavigationBarOrStatusBar.bind(this);
        this._replayNavigationBar.element.oncontextmenu = this._contextMenuNavigationBarOrStatusBar.bind(this);
        this._updateReplayInterfaceVisibility();

        WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.RecordingCreated, this._recordingCreated, this);
        WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.RecordingLoaded, this._recordingLoaded, this);

        this.contentBrowser.addEventListener(WebInspector.ContentBrowser.Event.CurrentContentViewDidChange, this._contentBrowserCurrentContentViewDidChange, this);
        WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStartedOrStopped, this);
        WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStartedOrStopped, this);

        for (var recording of WebInspector.timelineManager.recordings)
            this._addRecording(recording);

        this._recordingCountChanged();

        if (WebInspector.timelineManager.activeRecording)
            this._recordingLoaded();
    }

    // Public

    get minimumWidth()
    {
        if (WebInspector.FPSInstrument.supported())
            return Math.max(this._replayNavigationBar.minimumWidth, this._viewModeNavigationBar.minimumWidth);
        return this._replayNavigationBar.minimumWidth;
    }

    shown()
    {
        super.shown();

        if (this._displayedContentView)
            this.contentBrowser.showContentView(this._displayedContentView);

        this._toggleRecordingShortcut.disabled = false;
        this._toggleNewRecordingShortcut.disabled = false;

        this._updateTimelineOverviewHeight();
    }

    hidden()
    {
        super.hidden();

        this._toggleRecordingShortcut.disabled = true;
        this._toggleNewRecordingShortcut.disabled = true;
    }

    closed()
    {
        super.closed();

        WebInspector.showReplayInterfaceSetting.removeEventListener(null, null, this);
        WebInspector.replayManager.removeEventListener(null, null, this);
        WebInspector.timelineManager.removeEventListener(null, null, this);

        WebInspector.timelineManager.reset();
    }

    showDefaultContentView()
    {
        if (this._displayedContentView)
            this.showTimelineOverview();
    }

    treeElementForRepresentedObject(representedObject)
    {
        if (representedObject instanceof WebInspector.TimelineRecording)
            return this._recordingTreeElementMap.get(representedObject);

        // This fails if the timeline does not belong to the selected recording.
        if (representedObject instanceof WebInspector.Timeline) {
            var foundTreeElement = this._timelineTreeElementMap.get(representedObject);
            if (foundTreeElement)
                return foundTreeElement;
        }

        // The main resource is used as the representedObject instead of Frame in our tree.
        if (representedObject instanceof WebInspector.Frame)
            representedObject = representedObject.mainResource;

        var foundTreeElement = this.contentTreeOutline.getCachedTreeElement(representedObject);
        if (foundTreeElement)
            return foundTreeElement;

        // Look for TreeElements loosely based on represented objects that can contain the represented
        // object we are really looking for. This allows a SourceCodeTimelineTreeElement or a
        // TimelineRecordTreeElement to stay selected when the Resource it represents is showing.

        function looselyCompareRepresentedObjects(candidateTreeElement)
        {
            if (!candidateTreeElement)
                return false;

            var candidateRepresentedObject = candidateTreeElement.representedObject;
            if (candidateRepresentedObject instanceof WebInspector.SourceCodeTimeline) {
                if (candidateRepresentedObject.sourceCode === representedObject)
                    return true;
                return false;
            } else if (candidateRepresentedObject instanceof WebInspector.Timeline && representedObject instanceof WebInspector.Timeline) {
                // Reopen to the same timeline, even if a different parent recording is currently shown.
                if (candidateRepresentedObject.type === representedObject.type)
                    return true;
                return false;
            }

            if (candidateRepresentedObject instanceof WebInspector.TimelineRecord) {
                if (!candidateRepresentedObject.sourceCodeLocation)
                    return false;
                if (candidateRepresentedObject.sourceCodeLocation.sourceCode === representedObject)
                    return true;
                return false;
            }

            if (candidateRepresentedObject instanceof WebInspector.ProfileNode)
                return false;

            console.error("Unknown TreeElement", candidateTreeElement);
            return false;
        }

        // Check the selected tree element first so we don't need to do a longer search and it is
        // likely to be the best candidate for the current view.
        if (looselyCompareRepresentedObjects(this.contentTreeOutline.selectedTreeElement))
            return this.contentTreeOutline.selectedTreeElement;

        var currentTreeElement = this._contentTreeOutline.children[0];
        while (currentTreeElement && !currentTreeElement.root) {
            if (looselyCompareRepresentedObjects(currentTreeElement))
                return currentTreeElement;
            currentTreeElement = currentTreeElement.traverseNextTreeElement(false, null, false);
        }

        return null;
    }

    get contentTreeOutlineLabel()
    {
        return this._timelineEventsTitleBarElement.textContent;
    }

    set contentTreeOutlineLabel(label)
    {
        label = label || WebInspector.UIString("Timeline Events");

        this._timelineEventsTitleBarElement.textContent = label;
        this.filterBar.placeholder = WebInspector.UIString("Filter %s").format(label);
    }

    get contentTreeOutlineScopeBar()
    {
        return this._timelineEventsTitleBarScopeContainer.children;
    }

    set contentTreeOutlineScopeBar(scopeBar)
    {
        this._timelineEventsTitleBarScopeContainer.removeChildren();

        if (!scopeBar || !scopeBar.element)
            return;

        this._timelineEventsTitleBarScopeContainer.appendChild(scopeBar.element);
    }

    showTimelineOverview()
    {
        if (this._timelinesTreeOutline.selectedTreeElement)
            this._timelinesTreeOutline.selectedTreeElement.deselect();

        this._displayedContentView.showOverviewTimelineView();
        this.contentBrowser.showContentView(this._displayedContentView);
    }

    showTimelineViewForTimeline(timeline)
    {
        console.assert(timeline instanceof WebInspector.Timeline, timeline);
        console.assert(this._displayedRecording.timelines.has(timeline.type), "Cannot show timeline because it does not belong to the shown recording.", timeline.type);

        if (this._timelineTreeElementMap.has(timeline)) {
            // Defer showing the relevant timeline to the onselect handler of the timelines tree element.
            var wasSelectedByUser = true;
            var shouldSuppressOnSelect = false;
            this._timelineTreeElementMap.get(timeline).select(true, wasSelectedByUser, shouldSuppressOnSelect, true);
        } else {
            this._displayedContentView.showTimelineViewForTimeline(timeline);
            this.contentBrowser.showContentView(this._displayedContentView);
        }
    }

    updateFrameSelection(startFrameIndex, endFrameIndex)
    {
        console.assert(startFrameIndex <= endFrameIndex, startFrameIndex, endFrameIndex);
        if (this._startFrameIndex === startFrameIndex && this._endFrameIndex === endFrameIndex)
            return;

        this._startFrameIndex = startFrameIndex;
        this._endFrameIndex = endFrameIndex;

        this._refreshFrameSelectionChart();
    }

    formatChartValue(value)
    {
        return this._frameSelectionChartRow.total === 0 ? "" : Number.secondsToString(value);
    }

    // Protected

    representedObjectWasFiltered(representedObject, filtered)
    {
        super.representedObjectWasFiltered(representedObject, filtered);

        if (representedObject instanceof WebInspector.TimelineRecord)
            this._displayedContentView.recordWasFiltered(representedObject, filtered);
    }

    updateFilter()
    {
        super.updateFilter();

        this._displayedContentView.filterDidChange();
    }

    shouldFilterPopulate()
    {
        return false;
    }

    hasCustomFilters()
    {
        return true;
    }

    matchTreeElementAgainstCustomFilters(treeElement)
    {
        if (!this._displayedContentView)
            return true;

        if (this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames && this._renderingFrameTaskFilter.size) {
            while (treeElement && !(treeElement.record instanceof WebInspector.TimelineRecord))
                treeElement = treeElement.parent;

            console.assert(treeElement, "Cannot apply task filter: no TimelineRecord found.");
            if (!treeElement)
                return false;

            let records;
            if (treeElement.record instanceof WebInspector.RenderingFrameTimelineRecord)
                records = treeElement.record.children;
            else
                records = [treeElement.record];

            const filtered = records.every(function(record) {
                var taskType = WebInspector.RenderingFrameTimelineRecord.taskTypeForTimelineRecord(record);
                return this._renderingFrameTaskFilter.has(taskType);
            }, this);

            if (filtered)
                return false;
        }

        return this._displayedContentView.matchTreeElementAgainstCustomFilters(treeElement);
    }

    treeElementAddedOrChanged(treeElement)
    {
        if (treeElement.status)
            return;

        if (!treeElement.treeOutline || typeof treeElement.treeOutline.__canShowContentViewForTreeElement !== "function")
            return;

        if (!treeElement.treeOutline.__canShowContentViewForTreeElement(treeElement))
            return;

        var fragment = document.createDocumentFragment();

        var closeButton = new WebInspector.TreeElementStatusButton(useSVGSymbol("Images/Close.svg", null, WebInspector.UIString("Close resource view")));
        closeButton.element.classList.add("close");
        closeButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._treeElementCloseButtonClicked, this);
        fragment.appendChild(closeButton.element);

        var goToButton = new WebInspector.TreeElementStatusButton(WebInspector.createGoToArrowButton());
        goToButton.__treeElement = treeElement;
        goToButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._treeElementGoToArrowWasClicked, this);
        fragment.appendChild(goToButton.element);

        treeElement.status = fragment;
    }

    canShowDifferentContentView()
    {
        if (this._clickedTreeElementGoToArrow)
            return true;

        if (this.contentBrowser.currentContentView instanceof WebInspector.TimelineRecordingContentView)
            return false;

        return !this.restoringState || !this._restoredShowingTimelineRecordingContentView;
    }

    saveStateToCookie(cookie)
    {
        console.assert(cookie);

        cookie[WebInspector.TimelineSidebarPanel.ShowingTimelineRecordingContentViewCookieKey] = this.contentBrowser.currentContentView instanceof WebInspector.TimelineRecordingContentView;

        if (this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames)
            cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey] = WebInspector.TimelineRecord.Type.RenderingFrame;
        else {
            var selectedTreeElement = this._timelinesTreeOutline.selectedTreeElement;
            if (selectedTreeElement)
                cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey] = selectedTreeElement.representedObject.type;
            else
                cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey] = WebInspector.TimelineSidebarPanel.OverviewTimelineIdentifierCookieValue;    
        }

        super.saveStateToCookie(cookie);
    }

    restoreStateFromCookie(cookie, relaxedMatchDelay)
    {
        console.assert(cookie);

        // The _displayedContentView is not ready on initial load, so delay the restore.
        // This matches the delayed work in the WebInspector.TimelineSidebarPanel constructor.
        if (!this._displayedContentView) {
            setTimeout(this.restoreStateFromCookie.bind(this, cookie, relaxedMatchDelay), 0);
            return;
        }

        this._restoredShowingTimelineRecordingContentView = cookie[WebInspector.TimelineSidebarPanel.ShowingTimelineRecordingContentViewCookieKey];

        var selectedTimelineViewIdentifier = cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey];
        if (selectedTimelineViewIdentifier === WebInspector.TimelineRecord.Type.RenderingFrame && !WebInspector.FPSInstrument.supported())
            selectedTimelineViewIdentifier = null;

        if (selectedTimelineViewIdentifier && this._displayedRecording.timelines.has(selectedTimelineViewIdentifier))
            this.showTimelineViewForTimeline(this._displayedRecording.timelines.get(selectedTimelineViewIdentifier));
        else
            this.showTimelineOverview();

        // Don't call NavigationSidebarPanel.restoreStateFromCookie, because it tries to match based
        // on type only as a last resort. This would cause the first recording to be reselected on reload.
    }

    // Private

    _toggleRecordingOnSpacebar(event)
    {
        if (WebInspector.isEventTargetAnEditableField(event))
            return;

        this._toggleRecording();

        event.preventDefault();
    }

    _toggleNewRecordingOnSpacebar(event)
    {
        if (WebInspector.isEventTargetAnEditableField(event))
            return;

        this._toggleRecording(true);

        event.preventDefault();
    }

    _toggleRecording(shouldCreateRecording)
    {
        if (WebInspector.timelineManager.isCapturing()) {
            WebInspector.timelineManager.stopCapturing();
        } else {
            WebInspector.timelineManager.startCapturing(shouldCreateRecording);
            // Show the timeline to which events will be appended.
            this._recordingLoaded();
        }
    }

    _treeElementGoToArrowWasClicked(event)
    {
        this._clickedTreeElementGoToArrow = true;

        var treeElement = event.target.__treeElement;
        console.assert(treeElement instanceof WebInspector.TreeElement);

        treeElement.select(true, true);

        this._clickedTreeElementGoToArrow = false;
    }

    _treeElementCloseButtonClicked(event)
    {
        var currentTimelineView = this._displayedContentView ? this._displayedContentView.currentTimelineView : null;
        if (currentTimelineView && currentTimelineView.representedObject instanceof WebInspector.Timeline)
            this.showTimelineViewForTimeline(currentTimelineView.representedObject);
        else
            this.showTimelineOverview();
    }

    _recordingsTreeSelectionDidChange(event)
    {
        let treeElement = event.data.selectedElement;
        if (!treeElement)
            return;

        console.assert(treeElement.representedObject instanceof WebInspector.TimelineRecording);

        this._recordingSelected(treeElement.representedObject);
    }

    _renderingFrameTimelineTimesUpdated(event)
    {
        if (this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames)
            this._refreshFrameSelectionChart();
    }

    _timelinesTreeSelectionDidChange(event)
    {
        let treeElement = event.data.selectedElement;
        if (!treeElement)
            return;

        console.assert(this._timelineTreeElementMap.get(treeElement.representedObject) === treeElement, treeElement);

        // If not selected by user, then this selection merely synced the tree element with the content view's contents.
        let selectedByUser = event.data.selectedByUser;
        if (!selectedByUser) {
            console.assert(this._displayedContentView.currentTimelineView.representedObject === treeElement.representedObject);
            return;
        }

        let timeline = treeElement.representedObject;
        console.assert(timeline instanceof WebInspector.Timeline, timeline);
        console.assert(this._displayedRecording.timelines.get(timeline.type) === timeline, timeline);

        this._previousSelectedTimelineType = timeline.type;

        this._displayedContentView.showTimelineViewForTimeline(timeline);
        this.contentBrowser.showContentView(this._displayedContentView);
    }

    _contentBrowserCurrentContentViewDidChange(event)
    {
        let didShowTimelineRecordingContentView = this.contentBrowser.currentContentView instanceof WebInspector.TimelineRecordingContentView;
        this.element.classList.toggle(WebInspector.TimelineSidebarPanel.TimelineRecordingContentViewShowingStyleClass, didShowTimelineRecordingContentView);

        if (!didShowTimelineRecordingContentView)
            return;

        this._updateViewModeIfNeeded();
        this._timelineCountChanged();

        this.updateFilter();

        let timelineView = this.contentBrowser.currentContentView.currentTimelineView;
        if (timelineView.representedObject.type === WebInspector.TimelineRecord.Type.RenderingFrame)
            this._refreshFrameSelectionChart();
    }

    _capturingStartedOrStopped(event)
    {
        let isCapturing = WebInspector.timelineManager.isCapturing();
        this._updateRecordButton(isCapturing);
    }

    _recordingCreated(event)
    {
        this._addRecording(event.data.recording)
        this._recordingCountChanged();
    }

    _addRecording(recording)
    {
        console.assert(recording instanceof WebInspector.TimelineRecording, recording);

        var recordingTreeElement = new WebInspector.GeneralTreeElement(WebInspector.TimelineSidebarPanel.StopwatchIconStyleClass, recording.displayName, null, recording);
        this._recordingTreeElementMap.set(recording, recordingTreeElement);
        this._recordingsTreeOutline.appendChild(recordingTreeElement);
    }

    _recordingCountChanged()
    {
        var previousTreeElement = null;
        for (var treeElement of this._recordingTreeElementMap.values()) {
            if (previousTreeElement) {
                previousTreeElement.nextSibling = treeElement;
                treeElement.previousSibling = previousTreeElement;
            }

            previousTreeElement = treeElement;
        }
    }

    _recordingSelected(recording)
    {
        console.assert(recording instanceof WebInspector.TimelineRecording, recording);

        var oldRecording = this._displayedRecording || null;

        if (oldRecording) {
            oldRecording.removeEventListener(WebInspector.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
            oldRecording.removeEventListener(WebInspector.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);

            // Destroy tree elements in one operation to avoid unnecessary fixups.
            this._timelinesTreeOutline.removeChildren();
            this._timelineTreeElementMap.clear();
        }

        this._clearInstruments();

        this._displayedRecording = recording;
        this._displayedRecording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
        this._displayedRecording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);

        for (let instrument of recording.instruments)
            this._instrumentAdded(instrument);

        // Save the current state incase we need to restore it to a new recording.
        var cookie = {};
        this.saveStateToCookie(cookie);

        if (this._displayedContentView)
            this._displayedContentView.removeEventListener(WebInspector.ContentView.Event.NavigationItemsDidChange, this._updateViewModeIfNeeded, this);

        // Try to get the recording content view if it exists already, if it does we don't want to restore the cookie.
        var onlyExisting = true;
        this._displayedContentView = this.contentBrowser.contentViewForRepresentedObject(this._displayedRecording, onlyExisting, {timelineSidebarPanel: this});
        if (this._displayedContentView) {
            this._displayedContentView.addEventListener(WebInspector.ContentView.Event.NavigationItemsDidChange, this._updateViewModeIfNeeded, this);

            // Show the timeline that was being shown to update the sidebar tree state.
            var currentTimelineView = this._displayedContentView.currentTimelineView;
            if (currentTimelineView && currentTimelineView.representedObject instanceof WebInspector.Timeline)
                this.showTimelineViewForTimeline(currentTimelineView.representedObject);
            else
                this.showTimelineOverview();

            this.updateFilter();
            return;
        }

        onlyExisting = false;
        this._displayedContentView = this.contentBrowser.contentViewForRepresentedObject(this._displayedRecording, onlyExisting, {timelineSidebarPanel: this});
        if (this._displayedContentView)
            this._displayedContentView.addEventListener(WebInspector.ContentView.Event.NavigationItemsDidChange, this._updateViewModeIfNeeded, this);

        // Restore the cookie to carry over the previous recording view state to the new recording.
        this.restoreStateFromCookie(cookie);

        this.updateFilter();
    }

    _recordingLoaded(event)
    {
        this._recordingSelected(WebInspector.timelineManager.activeRecording);
    }

    _clearInstruments()
    {
        this._timelineTreeElementMap.clear();

        if (WebInspector.FPSInstrument.supported())
            this._removedFPSInstrument();
    }

    _instrumentAdded(instrumentOrEvent)
    {
        let instrument = instrumentOrEvent instanceof WebInspector.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument;
        console.assert(instrument instanceof WebInspector.Instrument, instrument);

        let timeline = this._displayedRecording.timelineForInstrument(instrument);
        console.assert(!this._timelineTreeElementMap.has(timeline), timeline);

        if (timeline.type === WebInspector.TimelineRecord.Type.RenderingFrame) {
            this._addedFPSInstrument();
            timeline.addEventListener(WebInspector.Timeline.Event.TimesUpdated, this._renderingFrameTimelineTimesUpdated, this);
            return;
        }

        let displayName = WebInspector.TimelineTabContentView.displayNameForTimeline(timeline);
        let iconClassName = WebInspector.TimelineTabContentView.iconClassNameForTimeline(timeline);
        let genericClassName = WebInspector.TimelineTabContentView.genericClassNameForTimeline(timeline);
        let timelineTreeElement = new WebInspector.GeneralTreeElement([iconClassName, genericClassName, WebInspector.TimelineSidebarPanel.LargeIconStyleClass], displayName, null, timeline);
        let tooltip = WebInspector.UIString("Close %s timeline view").format(displayName);
        let button = new WebInspector.TreeElementStatusButton(useSVGSymbol("Images/CloseLarge.svg", "close-button", tooltip));
        button.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this.showTimelineOverview, this);
        timelineTreeElement.status = button.element;

        this._timelinesTreeOutline.appendChild(timelineTreeElement);
        this._timelineTreeElementMap.set(timeline, timelineTreeElement);

        this._timelineCountChanged();
    }

    _instrumentRemoved(event)
    {
        let instrument = event.data.instrument;
        console.assert(instrument instanceof WebInspector.Instrument, instrument);

        let timeline = this._displayedRecording.timelineForInstrument(instrument);

        if (timeline.type === WebInspector.TimelineRecord.Type.RenderingFrame) {
            this._removedFPSInstrument();
            timeline.removeEventListener(WebInspector.Timeline.Event.TimesUpdated, this._renderingFrameTimelineTimesUpdated, this);
            return;
        }

        console.assert(this._timelineTreeElementMap.has(timeline), timeline);

        let timelineTreeElement = this._timelineTreeElementMap.take(timeline);
        let shouldSuppressOnDeselect = false;
        let shouldSuppressSelectSibling = true;
        this._timelinesTreeOutline.removeChild(timelineTreeElement, shouldSuppressOnDeselect, shouldSuppressSelectSibling);
        this._timelineTreeElementMap.delete(timeline);

        this._timelineCountChanged();
    }

    _addedFPSInstrument()
    {
        this._basicTitleBar.hidden = true;
        this._renderingFramesTitleBar.hidden = false;
    }

    _removedFPSInstrument()
    {
        this._basicTitleBar.hidden = false;
        this._renderingFramesTitleBar.hidden = true;
    }

    _timelineCountChanged()
    {
        var previousTreeElement = null;
        for (var treeElement of this._timelineTreeElementMap.values()) {
            if (previousTreeElement) {
                previousTreeElement.nextSibling = treeElement;
                treeElement.previousSibling = previousTreeElement;
            }

            previousTreeElement = treeElement;
        }

        this._updateTimelineOverviewHeight();
    }

    _updateTimelineOverviewHeight()
    {
        const eventTitleBarOffset = 58;
        const contentElementOffset = 81;

        if (!this._displayedContentView)
            return;

        let overviewHeight = this._displayedContentView.timelineOverviewHeight;
        this._timelineEventsTitleBarContainer.style.top = (overviewHeight + eventTitleBarOffset) + "px";
        this.contentView.element.style.top = (overviewHeight + contentElementOffset) + "px";
    }

    _recordButtonClicked(event)
    {
        let isCapturing = !WebInspector.timelineManager.isCapturing();

        this._updateRecordButton(isCapturing);
        this._toggleRecording(event.shiftKey);
    }

    _recordButtonMousedOver(event)
    {
        if (WebInspector.timelineManager.isCapturing())
            this._recordingStatusItem.element.textContent = WebInspector.UIString("Stop Recording");
        else
            this._recordingStatusItem.element.textContent = WebInspector.UIString("Start Recording");
    }

    _recordButtonMousedOut(event)
    {
        if (WebInspector.timelineManager.isCapturing())
            this._recordingStatusItem.element.textContent = WebInspector.UIString("Recording");
        else
            this._recordingStatusItem.element.textContent = "";
    }

    _updateRecordButton(isCapturing)
    {
        this._recordingStatusItem.element.textContent = isCapturing ? WebInspector.UIString("Recording") : "";
        this._recordButton.toggled = isCapturing;
    }

    _viewModeSelected(event)
    {
        console.assert(event.target.selectedNavigationItem);
        if (!event.target.selectedNavigationItem)
            return;

        let selectedNavigationItem = event.target.selectedNavigationItem;
        let newViewMode = selectedNavigationItem.identifier;
        if (this._viewMode === newViewMode)
            return;

        let timelineType = this._previousSelectedTimelineType;
        if (newViewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames) {
            this._previousSelectedTimelineType = this._timelinesTreeOutline.selectedTreeElement ? this._timelinesTreeOutline.selectedTreeElement.representedObject.type : null;
            timelineType = WebInspector.TimelineRecord.Type.RenderingFrame;
        }

        if (timelineType) {
            console.assert(this._displayedRecording.timelines.has(timelineType), timelineType);
            this.showTimelineViewForTimeline(this._displayedRecording.timelines.get(timelineType));
            return;
        }

        this.showTimelineOverview();
    }

    _viewModeForTimeline(timeline)
    {
        if (timeline && timeline.type === WebInspector.TimelineRecord.Type.RenderingFrame)
            return WebInspector.TimelineOverview.ViewMode.RenderingFrames;

        return WebInspector.TimelineOverview.ViewMode.Timelines;
    }

    _frameSelectionLegendItemChecked(event)
    {
        if (event.data.checked)
            this._renderingFrameTaskFilter.delete(event.data.id);
        else
            this._renderingFrameTaskFilter.add(event.data.id);

        this.updateFilter();
    }

    _refreshFrameSelectionChart()
    {
        console.assert(WebInspector.FPSInstrument.supported());
        if (!WebInspector.FPSInstrument.supported())
            return;

        if (!this.visible)
            return;

        function getSelectedRecords()
        {
            console.assert(this._displayedRecording);
            console.assert(this._displayedRecording.timelines.has(WebInspector.TimelineRecord.Type.RenderingFrame), "Cannot find rendering frames timeline in displayed recording");

            var timeline = this._displayedRecording.timelines.get(WebInspector.TimelineRecord.Type.RenderingFrame);
            var selectedRecords = [];
            for (var record of timeline.records) {
                console.assert(record instanceof WebInspector.RenderingFrameTimelineRecord);
                // If this frame is completely before the bounds of the graph, skip this record.
                if (record.frameIndex < this._startFrameIndex)
                    continue;

                // If this record is completely after the end time, break out now.
                // Records are sorted, so all records after this will be beyond the end time too.
                if (record.frameIndex > this._endFrameIndex)
                    break;

                selectedRecords.push(record);
            }

            return selectedRecords;
        }

        let records = getSelectedRecords.call(this);
        for (let key in WebInspector.RenderingFrameTimelineRecord.TaskType) {
            let taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
            let value = records.reduce(function(previousValue, currentValue) { return previousValue + currentValue.durationForTask(taskType); }, 0);
            this._frameSelectionChartRow.setItemValue(taskType, value);
        }

        if (!records.length) {
            this._frameSelectionChartRow.title = WebInspector.UIString("Frames: None Selected");
            return;
        }

        var firstRecord = records[0];
        var lastRecord = records.lastValue;

        if (records.length > 1) {
            this._frameSelectionChartRow.title = WebInspector.UIString("Frames: %d \u2013 %d (%s \u2013 %s)").format(firstRecord.frameNumber, lastRecord.frameNumber,
                Number.secondsToString(firstRecord.startTime), Number.secondsToString(lastRecord.endTime));
        } else {
            this._frameSelectionChartRow.title = WebInspector.UIString("Frame: %d (%s \u2013 %s)").format(firstRecord.frameNumber,
                Number.secondsToString(firstRecord.startTime), Number.secondsToString(lastRecord.endTime));
        }
    }

    // These methods are only used when ReplayAgent is available.

    _updateReplayInterfaceVisibility()
    {
        var shouldShowReplayInterface = !!(window.ReplayAgent && WebInspector.showReplayInterfaceSetting.value);

        this._recordingNavigationBar.element.classList.toggle(WebInspector.TimelineSidebarPanel.HiddenStyleClassName, shouldShowReplayInterface);
        this._replayNavigationBar.element.classList.toggle(WebInspector.TimelineSidebarPanel.HiddenStyleClassName, !shouldShowReplayInterface);
    }

    _contextMenuNavigationBarOrStatusBar()
    {
        if (!window.ReplayAgent)
            return;

        let toggleReplayInterface = () => {
            WebInspector.showReplayInterfaceSetting.value = !WebInspector.showReplayInterfaceSetting.value;
        };

        let contextMenu = WebInspector.ContextMenu.createFromEvent(event);
        if (WebInspector.showReplayInterfaceSetting.value)
            contextMenu.appendItem(WebInspector.UIString("Hide Replay Controls"), toggleReplayInterface);
        else
            contextMenu.appendItem(WebInspector.UIString("Show Replay Controls"), toggleReplayInterface);
    }

    _replayCaptureButtonClicked()
    {
        if (!this._replayCaptureButtonItem.activated) {
            WebInspector.replayManager.startCapturing();
            WebInspector.timelineManager.startCapturing();

            // De-bounce further presses until the backend has begun capturing.
            this._replayCaptureButtonItem.activated = true;
            this._replayCaptureButtonItem.enabled = false;
        } else {
            WebInspector.replayManager.stopCapturing();
            WebInspector.timelineManager.stopCapturing();

            this._replayCaptureButtonItem.enabled = false;
        }
    }

    _replayPauseResumeButtonClicked()
    {
        if (this._replayPauseResumeButtonItem.toggled)
            WebInspector.replayManager.pausePlayback();
        else
            WebInspector.replayManager.replayToCompletion();
    }

    _captureStarted()
    {
        this._replayCaptureButtonItem.enabled = true;
    }

    _captureStopped()
    {
        this._replayCaptureButtonItem.activated = false;
        this._replayPauseResumeButtonItem.enabled = true;
    }

    _playbackStarted()
    {
        this._replayPauseResumeButtonItem.toggled = true;
    }

    _playbackPaused()
    {
        this._replayPauseResumeButtonItem.toggled = false;
    }

    _updateViewModeIfNeeded()
    {
        let timelineView = this.contentBrowser.currentContentView.currentTimelineView;
        let newViewMode = WebInspector.TimelineOverview.ViewMode.Timelines;
        if (timelineView.representedObject instanceof WebInspector.Timeline && timelineView.representedObject.type === WebInspector.TimelineRecord.Type.RenderingFrame)
            newViewMode = WebInspector.TimelineOverview.ViewMode.RenderingFrames;

        if (newViewMode === this._viewMode)
            return;

        this._viewMode = newViewMode;

        let isShowingTimelines = this._viewMode === WebInspector.TimelineOverview.ViewMode.Timelines;
        this._timelinesTreeOutline.hidden = !isShowingTimelines;

        if (WebInspector.FPSInstrument.supported()) {
            this._frameSelectionChartSection.collapsed = isShowingTimelines;
            this._viewModeNavigationBar.selectedNavigationItem = this._viewMode;
        }
    }
};

WebInspector.TimelineSidebarPanel.HiddenStyleClassName = "hidden";
WebInspector.TimelineSidebarPanel.TitleBarStyleClass = "title-bar";
WebInspector.TimelineSidebarPanel.TitleBarTextStyleClass = "title-bar-text";
WebInspector.TimelineSidebarPanel.TitleBarScopeBarStyleClass = "title-bar-scope-bar";
WebInspector.TimelineSidebarPanel.TimelinesTitleBarStyleClass = "timelines";
WebInspector.TimelineSidebarPanel.TimelineEventsTitleBarStyleClass = "timeline-events";
WebInspector.TimelineSidebarPanel.TimelinesContentContainerStyleClass = "timelines-content";
WebInspector.TimelineSidebarPanel.LargeIconStyleClass = "large";
WebInspector.TimelineSidebarPanel.StopwatchIconStyleClass = "stopwatch-icon";
WebInspector.TimelineSidebarPanel.TimelineRecordingContentViewShowingStyleClass = "timeline-recording-content-view-showing";

WebInspector.TimelineSidebarPanel.ShowingTimelineRecordingContentViewCookieKey = "timeline-sidebar-panel-showing-timeline-recording-content-view";
WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey = "timeline-sidebar-panel-selected-timeline-view-identifier";
WebInspector.TimelineSidebarPanel.OverviewTimelineIdentifierCookieValue = "overview";
