/*
 * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
 * Copyright (C) 2015 University of Washington.
 *
 * 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.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.ContentView
{
    constructor(recording)
    {
        super(recording);

        this._recording = recording;

        this.element.classList.add("timeline-recording");

        this._timelineOverview = new WI.TimelineOverview(this._recording);
        this._timelineOverview.addEventListener(WI.TimelineOverview.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
        this._timelineOverview.addEventListener(WI.TimelineOverview.Event.RecordSelected, this._recordSelected, this);
        this._timelineOverview.addEventListener(WI.TimelineOverview.Event.TimelineSelected, this._timelineSelected, this);
        this._timelineOverview.addEventListener(WI.TimelineOverview.Event.EditingInstrumentsDidChange, this._editingInstrumentsDidChange, this);
        this.addSubview(this._timelineOverview);

        const disableBackForward = true;
        const disableFindBanner = true;
        this._timelineContentBrowser = new WI.ContentBrowser(null, this, disableBackForward, disableFindBanner);
        this._timelineContentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this);

        this._entireRecordingPathComponent = this._createTimelineRangePathComponent(WI.UIString("Entire Recording"));
        this._timelineSelectionPathComponent = this._createTimelineRangePathComponent();
        this._timelineSelectionPathComponent.previousSibling = this._entireRecordingPathComponent;
        this._selectedTimeRangePathComponent = this._entireRecordingPathComponent;

        this._filterBarNavigationItem = new WI.FilterBarNavigationItem;
        this._filterBarNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._filterDidChange, this);
        this._timelineContentBrowser.navigationBar.addNavigationItem(this._filterBarNavigationItem);
        this.addSubview(this._timelineContentBrowser);

        if (WI.sharedApp.isWebDebuggable()) {
            this._autoStopCheckboxNavigationItem = new WI.CheckboxNavigationItem("auto-stop-recording", WI.UIString("Stop recording once page loads"), WI.settings.timelinesAutoStop.value);
            this._autoStopCheckboxNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
            this._autoStopCheckboxNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleAutoStopCheckboxCheckedDidChange, this);

            WI.settings.timelinesAutoStop.addEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
        }

        this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
        this._exportButtonNavigationItem.toolTip = WI.UIString("Export (%s)").format(WI.saveKeyboardShortcut.displayName);
        this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
        this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
        this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._exportButtonNavigationItemClicked, this);
        this._exportButtonNavigationItem.enabled = false;

        this._importButtonNavigationItem = new WI.ButtonNavigationItem("import", WI.UIString("Import"), "Images/Import.svg", 15, 15);
        this._importButtonNavigationItem.toolTip = WI.UIString("Import");
        this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
        this._importButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
        this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._importButtonNavigationItemClicked, this);

        this._clearTimelineNavigationItem = new WI.ButtonNavigationItem("clear-timeline", WI.UIString("Clear Timeline (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
        this._clearTimelineNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
        this._clearTimelineNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._clearTimeline, this);

        this._overviewTimelineView = new WI.OverviewTimelineView(recording);
        this._overviewTimelineView.secondsPerPixel = this._timelineOverview.secondsPerPixel;

        this._progressView = new WI.TimelineRecordingProgressView;
        this._timelineContentBrowser.addSubview(this._progressView);

        this._timelineViewMap = new Map;
        this._pathComponentMap = new Map;

        this._updating = false;
        this._currentTime = NaN;
        this._lastUpdateTimestamp = NaN;
        this._startTimeNeedsReset = true;
        this._renderingFrameTimeline = null;

        this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
        this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
        this._recording.addEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this);
        this._recording.singleFireEventListener(WI.TimelineRecording.Event.Unloaded, this._recordingUnloaded, this);

        WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);

        WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Paused, this._debuggerPaused, this);
        WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Resumed, this._debuggerResumed, this);

        WI.ContentView.addEventListener(WI.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
        WI.ContentView.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);

        WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasFiltered, this._handleTimelineViewRecordFiltered, this);
        WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasSelected, this._handleTimelineViewRecordSelected, this);
        WI.TimelineView.addEventListener(WI.TimelineView.Event.ScannerShow, this._handleTimelineViewScannerShow, this);
        WI.TimelineView.addEventListener(WI.TimelineView.Event.ScannerHide, this._handleTimelineViewScannerHide, this);
        WI.TimelineView.addEventListener(WI.TimelineView.Event.NeedsEntireSelectedRange, this._handleTimelineViewNeedsEntireSelectedRange, this);
        WI.TimelineView.addEventListener(WI.TimelineView.Event.NeedsFiltersCleared, this._handleTimelineViewNeedsFiltersCleared, this);


        WI.notifications.addEventListener(WI.Notification.VisibilityStateDidChange, this._inspectorVisibilityStateChanged, this);

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

        this.showOverviewTimelineView();

        if (this._recording.imported) {
            let {startTime, endTime} = this._recording;
            this._updateTimes(startTime, endTime, endTime);
        }
    }

    // Public

    showOverviewTimelineView()
    {
        this._timelineContentBrowser.showContentView(this._overviewTimelineView);
    }

    showTimelineViewForTimeline(timeline)
    {
        console.assert(timeline instanceof WI.Timeline, timeline);
        console.assert(this._timelineViewMap.has(timeline), timeline);
        if (!this._timelineViewMap.has(timeline))
            return;

        let contentView = this._timelineContentBrowser.showContentView(this._timelineViewMap.get(timeline));

        // FIXME: `WI.HeapAllocationsTimelineView` relies on it's `_dataGrid` for determining what
        // object is currently selected. If that `_dataGrid` hasn't yet called `layout()` when first
        // shown, we will lose the selection.
        if (!contentView.didInitialLayout)
            contentView.updateLayout();
    }

    get supportsSplitContentBrowser()
    {
        // The layout of the overview and split content browser don't work well.
        return false;
    }

    get selectionPathComponents()
    {
        if (!this._timelineContentBrowser.currentContentView)
            return [];

        let pathComponents = [];
        let representedObject = this._timelineContentBrowser.currentContentView.representedObject;
        if (representedObject instanceof WI.Timeline)
            pathComponents.push(this._pathComponentMap.get(representedObject));

        pathComponents.push(this._selectedTimeRangePathComponent);
        return pathComponents;
    }

    get supplementalRepresentedObjects()
    {
        if (!this._timelineContentBrowser.currentContentView)
            return [];
        return this._timelineContentBrowser.currentContentView.supplementalRepresentedObjects;
    }

    get navigationItems()
    {
        let navigationItems = [];
        if (this._autoStopCheckboxNavigationItem)
            navigationItems.push(this._autoStopCheckboxNavigationItem);
        navigationItems.push(new WI.DividerNavigationItem);
        navigationItems.push(this._importButtonNavigationItem);
        navigationItems.push(this._exportButtonNavigationItem);
        navigationItems.push(new WI.DividerNavigationItem);
        navigationItems.push(this._clearTimelineNavigationItem);
        return navigationItems;
    }

    get handleCopyEvent()
    {
        let currentContentView = this._timelineContentBrowser.currentContentView;
        return currentContentView && typeof currentContentView.handleCopyEvent === "function" ? currentContentView.handleCopyEvent.bind(currentContentView) : null;
    }

    get supportsSave()
    {
        return this._recording.canExport();
    }

    get saveData()
    {
        return {customSaveHandler: () => { this._exportTimelineRecording(); }};
    }

    get currentTimelineView()
    {
        return this._timelineContentBrowser.currentContentView;
    }

    attached()
    {
        super.attached();

        this._clearTimelineNavigationItem.enabled = !this._recording.readonly && !isNaN(this._recording.startTime);
        this._exportButtonNavigationItem.enabled = this._recording.canExport();

        if (!this._updating && WI.timelineManager.activeRecording === this._recording && WI.timelineManager.isCapturing())
            this._startUpdatingCurrentTime(this._currentTime);
    }

    detached()
    {
        if (this._updating)
            this._stopUpdatingCurrentTime();

        super.detached();
    }

    initialLayout()
    {
        super.initialLayout();

        this._currentContentViewDidChange();
    }

    closed()
    {
        super.closed();

        this._timelineContentBrowser.contentViewContainer.closeAllContentViews();

        this._recording.removeEventListener(WI.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
        this._recording.removeEventListener(WI.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
        this._recording.removeEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this);
        if (!this._recording.readonly)
            this._recording.removeEventListener(WI.TimelineRecording.Event.Unloaded, this._recordingUnloaded, this);

        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);

        WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.Paused, this._debuggerPaused, this);
        WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.Resumed, this._debuggerResumed, this);

        WI.ContentView.removeEventListener(WI.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
        WI.ContentView.removeEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);

        WI.TimelineView.removeEventListener(WI.TimelineView.Event.RecordWasFiltered, this._handleTimelineViewRecordFiltered, this);
        WI.TimelineView.removeEventListener(WI.TimelineView.Event.RecordWasSelected, this._handleTimelineViewRecordSelected, this);
        WI.TimelineView.removeEventListener(WI.TimelineView.Event.ScannerShow, this._handleTimelineViewScannerShow, this);
        WI.TimelineView.removeEventListener(WI.TimelineView.Event.ScannerHide, this._handleTimelineViewScannerHide, this);
        WI.TimelineView.removeEventListener(WI.TimelineView.Event.NeedsEntireSelectedRange, this._handleTimelineViewNeedsEntireSelectedRange, this);
    }

    canGoBack()
    {
        return this._timelineContentBrowser.canGoBack();
    }

    canGoForward()
    {
        return this._timelineContentBrowser.canGoForward();
    }

    goBack()
    {
        this._timelineContentBrowser.goBack();
    }

    goForward()
    {
        this._timelineContentBrowser.goForward();
    }

    handleClearShortcut(event)
    {
        this._clearTimeline();
    }

    get canFocusFilterBar()
    {
        return !this._filterBarNavigationItem.hidden;
    }

    focusFilterBar()
    {
        this._filterBarNavigationItem.filterBar.focus();
    }

    // ContentBrowser delegate

    contentBrowserTreeElementForRepresentedObject(contentBrowser, representedObject)
    {
        if (!(representedObject instanceof WI.Timeline) && !(representedObject instanceof WI.TimelineRecording))
            return null;

        let iconClassName;
        let title;
        if (representedObject instanceof WI.Timeline) {
            iconClassName = WI.TimelineTabContentView.iconClassNameForTimelineType(representedObject.type);
            title = WI.UIString("Details");
        } else {
            iconClassName = WI.TimelineTabContentView.StopwatchIconStyleClass;
            title = WI.UIString("Overview");
        }

        const hasChildren = false;
        return new WI.GeneralTreeElement(iconClassName, title, representedObject, hasChildren);
    }

    // Private

    _currentContentViewDidChange(event)
    {
        let newViewMode;
        let timelineView = this.currentTimelineView;
        if (timelineView && timelineView.representedObject.type === WI.TimelineRecord.Type.RenderingFrame)
            newViewMode = WI.TimelineOverview.ViewMode.RenderingFrames;
        else
            newViewMode = WI.TimelineOverview.ViewMode.Timelines;

        this._timelineOverview.viewMode = newViewMode;
        this._updateTimelineOverviewHeight();
        this._updateProgressView();
        this._updateFilterBar();

        if (timelineView) {
            this._updateTimelineViewTimes(timelineView);
            this._filterDidChange();

            let timeline = null;
            if (timelineView.representedObject instanceof WI.Timeline)
                timeline = timelineView.representedObject;

            this._timelineOverview.selectedTimeline = timeline;
        }

        this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
        this.dispatchEventToListeners(WI.ContentView.Event.NavigationItemsDidChange);
    }

    _timelinePathComponentSelected(event)
    {
        let selectedTimeline = event.data.pathComponent.representedObject;
        this.showTimelineViewForTimeline(selectedTimeline);
    }

    _timeRangePathComponentSelected(event)
    {
        let selectedPathComponent = event.data.pathComponent;
        if (selectedPathComponent === this._selectedTimeRangePathComponent)
            return;

        let timelineRuler = this._timelineOverview.timelineRuler;
        if (selectedPathComponent === this._entireRecordingPathComponent)
            timelineRuler.selectEntireRange();
        else {
            let timelineRange = selectedPathComponent.representedObject;
            timelineRuler.selectionStartTime = timelineRuler.zeroTime + timelineRange.startValue;
            timelineRuler.selectionEndTime = timelineRuler.zeroTime + timelineRange.endValue;
        }
    }

    _contentViewSelectionPathComponentDidChange(event)
    {
        if (!this.isAttached)
            return;

        if (event.target !== this._timelineContentBrowser.currentContentView)
            return;

        this._updateFilterBar();

        this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);

        if (this.currentTimelineView === this._overviewTimelineView)
            return;

        let record = null;
        if (this.currentTimelineView.selectionPathComponents) {
            let recordPathComponent = this.currentTimelineView.selectionPathComponents.find((element) => element.representedObject instanceof WI.TimelineRecord);
            record = recordPathComponent ? recordPathComponent.representedObject : null;
        }

        this._timelineOverview.selectRecord(event.target.representedObject, record);
    }

    _contentViewSupplementalRepresentedObjectsDidChange(event)
    {
        if (event.target !== this._timelineContentBrowser.currentContentView)
            return;
        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
    }

    _inspectorVisibilityStateChanged()
    {
        if (WI.timelineManager.activeRecording !== this._recording)
            return;

        // Stop updating since the results won't be rendered anyway.
        if (!WI.visible && this._updating) {
            this._stopUpdatingCurrentTime();
            return;
        }

        // Nothing else to do if the current time was not being updated.
        if (!WI.visible)
            return;

        let {startTime, endTime} = this.representedObject;
        if (!WI.timelineManager.isCapturing()) {
            // Force the overview to render data from the entire recording.
            // This is necessary if the recording was started when the inspector was not
            // visible because the views were never updated with currentTime/endTime.
            this._updateTimes(startTime, endTime, endTime);
            return;
        }

        this._startUpdatingCurrentTime(endTime);
    }

    _update(timestamp)
    {
        // FIXME: <https://webkit.org/b/153634> Web Inspector: some background tabs think they are the foreground tab and do unnecessary work
        if (!(WI.tabBrowser.selectedTabContentView instanceof WI.TimelineTabContentView))
            return;

        if (this._waitingToResetCurrentTime) {
            requestAnimationFrame(this._updateCallback);
            return;
        }

        var startTime = this._recording.startTime;
        var currentTime = this._currentTime || startTime;
        var endTime = this._recording.endTime;
        var timespanSinceLastUpdate = (timestamp - this._lastUpdateTimestamp) / 1000 || 0;

        currentTime += timespanSinceLastUpdate;

        this._updateTimes(startTime, currentTime, endTime);

        // Only stop updating if the current time is greater than the end time, or the end time is NaN.
        // The recording end time will be NaN if no records were added.
        if (!this._updating && (currentTime >= endTime || isNaN(endTime))) {
            if (this.isAttached)
                this._lastUpdateTimestamp = NaN;
            return;
        }

        this._lastUpdateTimestamp = timestamp;

        requestAnimationFrame(this._updateCallback);
    }

    _updateTimes(startTime, currentTime, endTime)
    {
        if (this._startTimeNeedsReset && !isNaN(startTime)) {
            this._timelineOverview.startTime = startTime;
            this._overviewTimelineView.zeroTime = startTime;
            for (let timelineView of this._timelineViewMap.values())
                timelineView.zeroTime = startTime;

            this._startTimeNeedsReset = false;
        }

        if (WI.timelineManager.capturingState !== WI.TimelineManager.CapturingState.Stopping) {
            // Only update end time while not stopping, otherwise the interface continues scrolling.
            this._timelineOverview.endTime = Math.max(endTime, currentTime);

            if (WI.timelineManager.capturingState !== WI.TimelineManager.CapturingState.Inactive) {
                // Only update current time while active/starting or else the interface continues scrolling.
                this._currentTime = currentTime;
                this._timelineOverview.currentTime = currentTime;
            }
        }

        if (this.currentTimelineView)
            this._updateTimelineViewTimes(this.currentTimelineView);

        // Force a layout now since we are already in an animation frame and don't need to delay it until the next.
        this._timelineOverview.updateLayoutIfNeeded();
        if (this.currentTimelineView)
            this.currentTimelineView.updateLayoutIfNeeded();
    }

    _startUpdatingCurrentTime(startTime)
    {
        console.assert(!this._updating);
        if (this._updating)
            return;

        // Don't update the current time if the Inspector is not visible, as the requestAnimationFrames won't work.
        if (!WI.visible)
            return;

        if (typeof startTime === "number")
            this._currentTime = startTime;
        else if (!isNaN(this._currentTime)) {
            // This happens when you stop and later restart recording. We likely need to jump into
            // the future to a better current time which we can ascertain from a new incoming
            // timeline record, so we wait for a Timeline to update.
            console.assert(!this._waitingToResetCurrentTime);
            this._waitingToResetCurrentTime = true;
            this._recording.addEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
        }

        this._updating = true;

        if (!this._updateCallback)
            this._updateCallback = this._update.bind(this);

        requestAnimationFrame(this._updateCallback);
    }

    _stopUpdatingCurrentTime()
    {
        console.assert(this._updating);
        this._updating = false;

        if (this._waitingToResetCurrentTime) {
            // Did not get any event while waiting for the current time, but we should stop waiting.
            this._recording.removeEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
            this._waitingToResetCurrentTime = false;
        }
    }

    _handleTimelineCapturingStateChanged(event)
    {
        let {startTime, endTime} = event.data;

        this._updateProgressView();

        switch (WI.timelineManager.capturingState) {
        case WI.TimelineManager.CapturingState.Active:
            if (!this._updating)
                this._startUpdatingCurrentTime(startTime);

            this._clearTimelineNavigationItem.enabled = !this._recording.readonly;
            this._exportButtonNavigationItem.enabled = false;
            break;

        case WI.TimelineManager.CapturingState.Inactive:
            if (this._updating)
                this._stopUpdatingCurrentTime();

            if (this.currentTimelineView)
                this._updateTimelineViewTimes(this.currentTimelineView);

            this._exportButtonNavigationItem.enabled = this._recording.canExport();
            break;
        }
    }

    _debuggerPaused(event)
    {
        if (this._updating)
            this._stopUpdatingCurrentTime();
    }

    _debuggerResumed(event)
    {
        if (!this._updating)
            this._startUpdatingCurrentTime();
    }

    _recordingTimesUpdated(event)
    {
        if (!this._waitingToResetCurrentTime)
            return;

        // Make the current time be the start time of the last added record. This is the best way
        // currently to jump to the right period of time after recording starts.

        for (var timeline of this._recording.timelines.values()) {
            var lastRecord = timeline.records.lastValue;
            if (!lastRecord)
                continue;
            this._currentTime = Math.max(this._currentTime, lastRecord.startTime);
        }

        this._recording.removeEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
        this._waitingToResetCurrentTime = false;
    }

    _handleAutoStopCheckboxCheckedDidChange(event)
    {
        WI.settings.timelinesAutoStop.value = this._autoStopCheckboxNavigationItem.checked;
    }

    _handleTimelinesAutoStopSettingChanged(event)
    {
        this._autoStopCheckboxNavigationItem.checked = WI.settings.timelinesAutoStop.value;
    }

    _exportTimelineRecording()
    {
        let json = {
            version: WI.TimelineRecording.SerializationVersion,
            recording: this._recording.exportData(),
            overview: this._timelineOverview.exportData(),
        };
        if (!json.recording || !json.overview) {
            InspectorFrontendHost.beep();
            return;
        }

        let frameName = null;
        let mainFrame = WI.networkManager.mainFrame;
        if (mainFrame)
            frameName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName;

        let filename = frameName ? `${frameName}-recording` : this._recording.displayName;

        WI.FileUtilities.save({
            content: JSON.stringify(json),
            suggestedName: filename + ".json",
            forceSaveAs: true,
        });
    }

    _exportButtonNavigationItemClicked(event)
    {
        this._exportTimelineRecording();
    }

    _importButtonNavigationItemClicked(event)
    {
        WI.FileUtilities.importJSON((result) => WI.timelineManager.processJSON(result), {multiple: true});
    }

    _clearTimeline(event)
    {
        if (this._recording.readonly)
            return;

        if (WI.timelineManager.activeRecording === this._recording && WI.timelineManager.isCapturing())
            WI.timelineManager.stopCapturing();

        this._recording.reset();
    }

    _updateTimelineOverviewHeight()
    {
        if (this._timelineOverview.editingInstruments)
            this._timelineOverview.element.style.height = "";
        else {
            const rulerHeight = 23;

            let styleValue = (rulerHeight + this._timelineOverview.height) + "px";
            this._timelineOverview.element.style.height = styleValue;
            this._timelineContentBrowser.element.style.top = styleValue;
        }
    }

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

        let timeline = this._recording.timelineForInstrument(instrument);
        console.assert(!this._timelineViewMap.has(timeline), timeline);

        this._timelineViewMap.set(timeline, WI.ContentView.createFromRepresentedObject(timeline, {recording: this._recording}));
        if (timeline.type === WI.TimelineRecord.Type.RenderingFrame)
            this._renderingFrameTimeline = timeline;

        let displayName = WI.TimelineTabContentView.displayNameForTimelineType(timeline.type);
        let iconClassName = WI.TimelineTabContentView.iconClassNameForTimelineType(timeline.type);
        let pathComponent = new WI.HierarchicalPathComponent(displayName, iconClassName, timeline);
        pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._timelinePathComponentSelected, this);
        this._pathComponentMap.set(timeline, pathComponent);

        this._timelineCountChanged();
    }

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

        let timeline = this._recording.timelineForInstrument(instrument);
        console.assert(this._timelineViewMap.has(timeline), timeline);

        let timelineView = this._timelineViewMap.take(timeline);
        if (this.currentTimelineView === timelineView)
            this.showOverviewTimelineView();
        if (timeline.type === WI.TimelineRecord.Type.RenderingFrame)
            this._renderingFrameTimeline = null;

        this._pathComponentMap.delete(timeline);

        this._timelineCountChanged();
    }

    _timelineCountChanged()
    {
        var previousPathComponent = null;
        for (var pathComponent of this._pathComponentMap.values()) {
            if (previousPathComponent) {
                previousPathComponent.nextSibling = pathComponent;
                pathComponent.previousSibling = previousPathComponent;
            }

            previousPathComponent = pathComponent;
        }

        this._updateTimelineOverviewHeight();
    }

    _recordingReset(event)
    {
        for (let timelineView of this._timelineViewMap.values())
            timelineView.reset();

        this._currentTime = NaN;

        if (!this._updating) {
            // Force the time ruler and views to reset to 0.
            this._startTimeNeedsReset = true;
            this._updateTimes(0, 0, 0);
        }

        this._lastUpdateTimestamp = NaN;
        this._startTimeNeedsReset = true;

        this._recording.removeEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
        this._waitingToResetCurrentTime = false;

        this._timelineOverview.reset();
        this._overviewTimelineView.reset();
        this._clearTimelineNavigationItem.enabled = false;
        this._exportButtonNavigationItem.enabled = false;
    }

    _recordingUnloaded(event)
    {
        console.assert(!this._updating);

        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
    }

    _timeRangeSelectionChanged(event)
    {
        console.assert(this.currentTimelineView);
        if (!this.currentTimelineView)
            return;

        this._updateTimelineViewTimes(this.currentTimelineView);

        let selectedPathComponent;
        if (this._timelineOverview.timelineRuler.entireRangeSelected)
            selectedPathComponent = this._entireRecordingPathComponent;
        else {
            let timelineRange = this._timelineSelectionPathComponent.representedObject;
            timelineRange.startValue = this.currentTimelineView.startTime;
            timelineRange.endValue = this.currentTimelineView.endTime;

            if (!(this.currentTimelineView instanceof WI.RenderingFrameTimelineView)) {
                timelineRange.startValue -= this.currentTimelineView.zeroTime;
                timelineRange.endValue -= this.currentTimelineView.zeroTime;
            }

            this._updateTimeRangePathComponents();
            selectedPathComponent = this._timelineSelectionPathComponent;
        }

        if (this._selectedTimeRangePathComponent !== selectedPathComponent) {
            this._selectedTimeRangePathComponent = selectedPathComponent;
            this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
        }
    }

    _recordSelected(event)
    {
        let {record} = event.data;

        this._selectRecordInTimelineView(record);
    }

    _timelineSelected()
    {
        let timeline = this._timelineOverview.selectedTimeline;
        if (timeline)
            this.showTimelineViewForTimeline(timeline);
        else
            this.showOverviewTimelineView();
    }

    _updateTimeRangePathComponents()
    {
        let timelineRange = this._timelineSelectionPathComponent.representedObject;
        let startValue = timelineRange.startValue;
        let endValue = timelineRange.endValue;
        if (isNaN(startValue) || isNaN(endValue)) {
            this._entireRecordingPathComponent.nextSibling = null;
            return;
        }

        this._entireRecordingPathComponent.nextSibling = this._timelineSelectionPathComponent;

        let displayName;
        if (this._timelineOverview.viewMode === WI.TimelineOverview.ViewMode.Timelines) {
            const higherResolution = true;
            let selectionStart = Number.secondsToString(startValue, higherResolution);
            let selectionEnd = Number.secondsToString(endValue, higherResolution);
            const epsilon = 0.0001;
            if (startValue < epsilon)
                displayName = WI.UIString("%s \u2013 %s").format(selectionStart, selectionEnd);
            else {
                let duration = Number.secondsToString(endValue - startValue, higherResolution);
                displayName = WI.UIString("%s \u2013 %s (%s)").format(selectionStart, selectionEnd, duration);
            }
        } else {
            startValue += 1; // Convert index to frame number.
            if (startValue === endValue)
                displayName = WI.UIString("Frame %d").format(startValue);
            else
                displayName = WI.UIString("Frames %d \u2013 %d").format(startValue, endValue);
        }

        this._timelineSelectionPathComponent.displayName = displayName;
        this._timelineSelectionPathComponent.title = displayName;
    }

    _createTimelineRangePathComponent(title)
    {
        let range = new WI.TimelineRange(NaN, NaN);
        let pathComponent = new WI.HierarchicalPathComponent(title || enDash, "time-icon", range);
        pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._timeRangePathComponentSelected, this);

        return pathComponent;
    }

    _updateTimelineViewTimes(timelineView)
    {
        let timelineRuler = this._timelineOverview.timelineRuler;
        let entireRangeSelected = timelineRuler.entireRangeSelected;
        let endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;

        if (entireRangeSelected) {
            if (timelineView instanceof WI.RenderingFrameTimelineView)
                endTime = this._renderingFrameTimeline.records.length;
            else if (timelineView instanceof WI.HeapAllocationsTimelineView) {
                // Since heap snapshots can be added at any time, including when not actively recording,
                // make sure to set the end time to an effectively infinite number so any new records
                // that are added in the future aren't filtered out.
                endTime = Number.MAX_VALUE;
            } else {
                // Clamp selection to the end of the recording (with padding),
                // so graph views will show an auto-sized graph without a lot of
                // empty space at the end.
                endTime = isNaN(this._recording.endTime) ? this._recording.currentTime : this._recording.endTime;
                endTime += timelineRuler.minimumSelectionDuration;
            }
        }

        timelineView.startTime = this._timelineOverview.selectionStartTime;
        timelineView.currentTime = this._currentTime;
        timelineView.endTime = endTime;
    }

    _editingInstrumentsDidChange(event)
    {
        let editingInstruments = this._timelineOverview.editingInstruments;
        this.element.classList.toggle(WI.TimelineOverview.EditInstrumentsStyleClassName, editingInstruments);

        this._updateTimelineOverviewHeight();
    }

    _filterDidChange()
    {
        if (!this.currentTimelineView)
            return;

        this.currentTimelineView.updateFilter(this._filterBarNavigationItem.filterBar.filters);
    }

    _handleTimelineViewRecordFiltered(event)
    {
        if (event.target !== this.currentTimelineView)
            return;

        console.assert(this.currentTimelineView);

        let timeline = this.currentTimelineView.representedObject;
        if (!(timeline instanceof WI.Timeline))
            return;

        let record = event.data.record;
        let filtered = event.data.filtered;
        this._timelineOverview.recordWasFiltered(timeline, record, filtered);
    }

    _handleTimelineViewRecordSelected(event)
    {
        if (!this.isAttached)
            return;

        let {record} = event.data;

        this._selectRecordInTimelineOverview(record);
        this._selectRecordInTimelineView(record);
    }

    _selectRecordInTimelineOverview(record)
    {
        let timeline = this._recording.timelineForRecordType(record.type);
        if (!timeline)
            return;

        this._timelineOverview.selectRecord(timeline, record);
    }

    _selectRecordInTimelineView(record)
    {
        for (let timelineView of this._timelineViewMap.values()) {
            let recordMatchesTimeline = record && timelineView.representedObject.type === record.type;

            if (recordMatchesTimeline && timelineView !== this.currentTimelineView)
                this.showTimelineViewForTimeline(timelineView.representedObject);

            if (!record || recordMatchesTimeline)
                timelineView.selectRecord(record);
        }
    }

    _handleTimelineViewScannerShow(event)
    {
        if (!this.isAttached)
            return;

        let {time} = event.data;
        this._timelineOverview.showScanner(time);
    }

    _handleTimelineViewScannerHide(event)
    {
        if (!this.isAttached)
            return;

        this._timelineOverview.hideScanner();
    }

    _handleTimelineViewNeedsEntireSelectedRange(event)
    {
        if (!this.isAttached)
            return;

        this._timelineOverview.timelineRuler.selectEntireRange();
    }

    _handleTimelineViewNeedsFiltersCleared(event)
    {
        if (!this.isAttached)
            return;

        this._filterBarNavigationItem.filterBar.clear();
    }

    _updateProgressView()
    {
        let isCapturing = WI.timelineManager.isCapturing();
        this._progressView.visible = isCapturing && this.currentTimelineView && !this.currentTimelineView.showsLiveRecordingData;
    }

    _updateFilterBar()
    {
        this._filterBarNavigationItem.hidden = !this.currentTimelineView || !this.currentTimelineView.showsFilterBar;
    }
};
