/*
 * Copyright (C) 2013, 2015-2016 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.TimelineOverview = class TimelineOverview extends WI.View
{
    constructor(timelineRecording)
    {
        super();

        console.assert(timelineRecording instanceof WI.TimelineRecording);

        this._timelinesViewModeSettings = this._createViewModeSettings(WI.TimelineOverview.ViewMode.Timelines, WI.TimelineOverview.MinimumDurationPerPixel, WI.TimelineOverview.MaximumDurationPerPixel, 0.01, 0, 15);
        this._instrumentTypes = WI.TimelineManager.availableTimelineTypes();

        let minimumDurationPerPixel = 1 / WI.TimelineRecordFrame.MaximumWidthPixels;
        let maximumDurationPerPixel = 1 / WI.TimelineRecordFrame.MinimumWidthPixels;
        this._renderingFramesViewModeSettings = this._createViewModeSettings(WI.TimelineOverview.ViewMode.RenderingFrames, minimumDurationPerPixel, maximumDurationPerPixel, minimumDurationPerPixel, 0, 100);

        this._recording = timelineRecording;
        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.MarkerAdded, this._markerAdded, this);
        this._recording.addEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this);

        this.element.classList.add("timeline-overview");
        this._updateWheelAndGestureHandlers();

        this._graphsContainerView = new WI.View;
        this._graphsContainerView.element.classList.add("graphs-container");
        this._graphsContainerView.element.addEventListener("click", this._handleGraphsContainerClick.bind(this));
        this.addSubview(this._graphsContainerView);

        this._selectedTimelineRecord = null;
        this._overviewGraphsByTypeMap = new Map;

        this._editInstrumentsButton = new WI.ActivateButtonNavigationItem("toggle-edit-instruments", WI.UIString("Edit configuration"), WI.UIString("Save configuration"));
        this._editInstrumentsButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleEditingInstruments, this);
        this._editingInstruments = false;
        this._updateEditInstrumentsButton();

        let instrumentsNavigationBar = new WI.NavigationBar;
        instrumentsNavigationBar.element.classList.add("timelines");
        instrumentsNavigationBar.addNavigationItem(new WI.FlexibleSpaceNavigationItem);
        instrumentsNavigationBar.addNavigationItem(this._editInstrumentsButton);
        this.addSubview(instrumentsNavigationBar);

        this._timelinesTreeOutline = new WI.TreeOutline;
        this._timelinesTreeOutline.element.classList.add("timelines");
        this._timelinesTreeOutline.disclosureButtons = false;
        this._timelinesTreeOutline.large = true;
        this._timelinesTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._timelinesTreeSelectionDidChange, this);
        this.element.appendChild(this._timelinesTreeOutline.element);

        this._treeElementsByTypeMap = new Map;

        this._timelineRuler = new WI.TimelineRuler;
        this._timelineRuler.allowsClippedLabels = true;
        this._timelineRuler.allowsTimeRangeSelection = true;
        this._timelineRuler.element.addEventListener("mousedown", this._timelineRulerMouseDown.bind(this));
        this._timelineRuler.element.addEventListener("click", this._timelineRulerMouseClicked.bind(this));
        this._timelineRuler.addEventListener(WI.TimelineRuler.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
        this.addSubview(this._timelineRuler);

        this._currentTimeMarker = new WI.TimelineMarker(0, WI.TimelineMarker.Type.CurrentTime);
        this._timelineRuler.addMarker(this._currentTimeMarker);

        this._scrollContainerElement = document.createElement("div");
        this._scrollContainerElement.classList.add("scroll-container");
        this._scrollContainerElement.addEventListener("scroll", this._handleScrollEvent.bind(this));
        this.element.appendChild(this._scrollContainerElement);

        this._scrollWidthSizer = document.createElement("div");
        this._scrollWidthSizer.classList.add("scroll-width-sizer");
        this._scrollContainerElement.appendChild(this._scrollWidthSizer);

        this._startTime = 0;
        this._currentTime = 0;
        this._revealCurrentTime = false;
        this._endTime = 0;
        this._pixelAlignDuration = false;
        this._mouseWheelDelta = 0;
        this._cachedScrollContainerWidth = NaN;
        this._timelineRulerSelectionChanged = false;
        this._viewMode = WI.TimelineOverview.ViewMode.Timelines;
        this._selectedTimeline = null;

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

        if (!WI.timelineManager.isCapturingPageReload())
            this._resetSelection();

        this._viewModeDidChange();

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

    // Import / Export

    exportData()
    {
        let json = {
            secondsPerPixel: this.secondsPerPixel,
            scrollStartTime: this.scrollStartTime,
            selectionStartTime: this.selectionStartTime,
            selectionDuration: this.selectionDuration,
        };

        if (this._selectedTimeline)
            json.selectedTimelineType = this._selectedTimeline.type;

        return json;
    }

    // Public

    get selectedTimeline()
    {
        return this._selectedTimeline;
    }

    set selectedTimeline(x)
    {
        if (this._editingInstruments)
            return;

        if (this._selectedTimeline === x)
            return;

        this._selectedTimeline = x;
        if (this._selectedTimeline) {
            let treeElement = this._treeElementsByTypeMap.get(this._selectedTimeline.type);
            console.assert(treeElement, "Missing tree element for timeline", this._selectedTimeline);

            let omitFocus = true;
            let wasSelectedByUser = false;
            treeElement.select(omitFocus, wasSelectedByUser);
        } else if (this._timelinesTreeOutline.selectedTreeElement)
            this._timelinesTreeOutline.selectedTreeElement.deselect();
    }

    get editingInstruments()
    {
        return this._editingInstruments;
    }

    get viewMode()
    {
        return this._viewMode;
    }

    set viewMode(x)
    {
        if (this._editingInstruments)
            return;

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

        this._viewMode = x;
        this._viewModeDidChange();
    }

    get startTime()
    {
        return this._startTime;
    }

    set startTime(x)
    {
        x = x || 0;

        if (this._startTime === x)
            return;

        if (this._viewMode !== WI.TimelineOverview.ViewMode.RenderingFrames) {
            let selectionOffset = this.selectionStartTime - this._startTime;
            this.selectionStartTime = selectionOffset + x;
        }

        this._startTime = x;

        this.needsLayout();
    }

    get currentTime()
    {
        return this._currentTime;
    }

    set currentTime(x)
    {
        x = x || 0;

        if (this._currentTime === x)
            return;

        this._currentTime = x;
        this._revealCurrentTime = true;

        this.needsLayout();
    }

    get secondsPerPixel()
    {
        return this._currentSettings.durationPerPixelSetting.value;
    }

    set secondsPerPixel(x)
    {
        x = Math.min(this._currentSettings.maximumDurationPerPixel, Math.max(this._currentSettings.minimumDurationPerPixel, x));

        if (this.secondsPerPixel === x)
            return;

        if (this._pixelAlignDuration) {
            x = 1 / Math.round(1 / x);
            if (this.secondsPerPixel === x)
                return;
        }

        this._currentSettings.durationPerPixelSetting.value = x;

        this.needsLayout();
    }

    get pixelAlignDuration()
    {
        return this._pixelAlignDuration;
    }

    set pixelAlignDuration(x)
    {
        if (this._pixelAlignDuration === x)
            return;

        this._mouseWheelDelta = 0;
        this._pixelAlignDuration = x;
        if (this._pixelAlignDuration)
            this.secondsPerPixel = 1 / Math.round(1 / this.secondsPerPixel);
    }

    get endTime()
    {
        return this._endTime;
    }

    set endTime(x)
    {
        x = x || 0;

        if (this._endTime === x)
            return;

        this._endTime = x;

        this.needsLayout();
    }

    get scrollStartTime()
    {
        return this._currentSettings.scrollStartTime;
    }

    set scrollStartTime(x)
    {
        x = x || 0;

        if (this.scrollStartTime === x)
            return;

        this._currentSettings.scrollStartTime = x;

        this.needsLayout();
    }

    get scrollContainerWidth()
    {
        return this._cachedScrollContainerWidth;
    }

    get visibleDuration()
    {
        if (isNaN(this._cachedScrollContainerWidth)) {
            this._cachedScrollContainerWidth = this._scrollContainerElement.offsetWidth;
            if (!this._cachedScrollContainerWidth)
                this._cachedScrollContainerWidth = NaN;
        }

        return this._cachedScrollContainerWidth * this.secondsPerPixel;
    }

    get selectionStartTime()
    {
        return this._timelineRuler.selectionStartTime;
    }

    set selectionStartTime(x)
    {
        x = x || 0;

        if (this._timelineRuler.selectionStartTime === x)
            return;

        let selectionDuration = this.selectionDuration;
        this._timelineRuler.selectionStartTime = x;
        this._timelineRuler.selectionEndTime = x + selectionDuration;
    }

    get selectionDuration()
    {
        return this._timelineRuler.selectionEndTime - this._timelineRuler.selectionStartTime;
    }

    set selectionDuration(x)
    {
        x = Math.max(this._timelineRuler.minimumSelectionDuration, x);

        this._timelineRuler.selectionEndTime = this._timelineRuler.selectionStartTime + x;
    }

    get height()
    {
        let height = 0;
        for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
            if (!overviewGraph.hidden)
                height += overviewGraph.height;
        }
        return height;
    }

    attached()
    {
        super.attached();

        for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
            if (this._canShowTimelineType(type))
                overviewGraph.hidden = false;
        }

        this.needsLayout(WI.View.LayoutReason.Resize);
    }

    detached()
    {
        for (let overviewGraph of this._overviewGraphsByTypeMap.values())
            overviewGraph.hidden = true;

        this.hideScanner();

        super.detached();
    }

    closed()
    {
        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
        WI.timelineManager.removeEventListener(WI.TimelineManager.Event.RecordingImported, this._recordingImported, this);

        super.closed();
    }

    reset()
    {
        this._selectedTimelineRecord = null;
        for (let overviewGraph of this._overviewGraphsByTypeMap.values())
            overviewGraph.reset();

        this._mouseWheelDelta = 0;

        this._resetSelection();
    }

    revealMarker(marker)
    {
        this.scrollStartTime = marker.time - (this.visibleDuration / 2);
    }

    recordWasFiltered(timeline, record, filtered)
    {
        let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
        console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type);
        if (!overviewGraph)
            return;

        console.assert(!overviewGraph.hidden, "Record filtered in hidden overview graph", record);

        overviewGraph.recordWasFiltered(record, filtered);
    }

    selectRecord(timeline, record)
    {
        let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
        console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type);
        if (!overviewGraph)
            return;

        console.assert(!overviewGraph.hidden, "Record selected in hidden overview graph", record);

        overviewGraph.selectedRecord = record;
    }

    showScanner(time)
    {
        this._timelineRuler.showScanner(time);
    }

    hideScanner()
    {
        this._timelineRuler.hideScanner();
    }

    updateLayoutIfNeeded(layoutReason)
    {
        if (this.layoutPending) {
            super.updateLayoutIfNeeded(layoutReason);
            return;
        }

        this._timelineRuler.updateLayoutIfNeeded(layoutReason);

        for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
            if (!overviewGraph.hidden)
                overviewGraph.updateLayoutIfNeeded(layoutReason);
        }
    }

    discontinuitiesInTimeRange(startTime, endTime)
    {
        return this._recording.discontinuitiesInTimeRange(startTime, endTime);
    }

    // Protected

    get timelineRuler()
    {
        return this._timelineRuler;
    }

    layout()
    {
        let startTime = this._startTime;
        let endTime = this._endTime;
        let currentTime = this._currentTime;
        if (this._viewMode === WI.TimelineOverview.ViewMode.RenderingFrames) {
            let renderingFramesTimeline = this._recording.timelines.get(WI.TimelineRecord.Type.RenderingFrame);
            console.assert(renderingFramesTimeline, "Recoring missing rendering frames timeline");

            startTime = 0;
            endTime = renderingFramesTimeline.records.length;
            currentTime = endTime;
        }

        // Calculate the required width based on the duration and seconds per pixel.
        let duration = endTime - startTime;
        let newWidth = Math.ceil(duration / this.secondsPerPixel);

        // Update all relevant elements to the new required width.
        this._updateElementWidth(this._scrollWidthSizer, newWidth);

        this._currentTimeMarker.time = currentTime;

        if (this._revealCurrentTime) {
            this.revealMarker(this._currentTimeMarker);
            this._revealCurrentTime = false;
        }

        const visibleDuration = this.visibleDuration;

        // Clamp the scroll start time to match what the scroll bar would allow.
        let scrollStartTime = Math.min(this.scrollStartTime, endTime - visibleDuration);
        scrollStartTime = Math.max(startTime, scrollStartTime);

        this._timelineRuler.zeroTime = startTime;
        this._timelineRuler.startTime = scrollStartTime;
        this._timelineRuler.secondsPerPixel = this.secondsPerPixel;

        if (!this._dontUpdateScrollLeft) {
            this._ignoreNextScrollEvent = true;
            let scrollLeft = Math.ceil((scrollStartTime - startTime) / this.secondsPerPixel);
            if (scrollLeft)
                this._scrollContainerElement.scrollLeft = scrollLeft;
        }

        for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
            if (overviewGraph.hidden)
                continue;

            overviewGraph.zeroTime = startTime;
            overviewGraph.startTime = scrollStartTime;
            overviewGraph.currentTime = currentTime;
            overviewGraph.endTime = scrollStartTime + visibleDuration;
        }
    }

    sizeDidChange()
    {
        this._cachedScrollContainerWidth = NaN;
    }

    // Private

    _updateElementWidth(element, newWidth)
    {
        var currentWidth = parseInt(element.style.width);
        if (currentWidth !== newWidth)
            element.style.width = newWidth + "px";
    }

    _handleScrollEvent(event)
    {
        if (this._ignoreNextScrollEvent) {
            this._ignoreNextScrollEvent = false;
            return;
        }

        this._dontUpdateScrollLeft = true;

        let scrollOffset = this._scrollContainerElement.scrollLeft;
        if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
            this.scrollStartTime = this._startTime - (scrollOffset * this.secondsPerPixel);
        else
            this.scrollStartTime = this._startTime + (scrollOffset * this.secondsPerPixel);

        // Force layout so we can update with the scroll position synchronously.
        this.updateLayoutIfNeeded();

        this._dontUpdateScrollLeft = false;

        this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1);
    }

    _handleWheelEvent(event)
    {
        // Ignore cloned events that come our way, we already handled the original.
        if (event.__cloned)
            return;

        // Ignore wheel events while handing gestures.
        if (this._handlingGesture)
            return;

        // Require twice the vertical delta to overcome horizontal scrolling. This prevents most
        // cases of inadvertent zooming for slightly diagonal scrolls.
        if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) {
            // Clone the event to dispatch it on the scroll container. Mark it as cloned so we don't get into a loop.
            let newWheelEvent = new event.constructor(event.type, event);
            newWheelEvent.__cloned = true;

            this._scrollContainerElement.dispatchEvent(newWheelEvent);
            return;
        }

        // Remember the mouse position in time.
        let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft;
        let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel);
        let deviceDirection = event.webkitDirectionInvertedFromDevice ? 1 : -1;
        let delta = event.deltaY * (this.secondsPerPixel / WI.TimelineOverview.ScrollDeltaDenominator) * deviceDirection;

        // Reset accumulated wheel delta when direction changes.
        if (this._pixelAlignDuration && (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0))
            this._mouseWheelDelta = 0;

        let previousDurationPerPixel = this.secondsPerPixel;
        this._mouseWheelDelta += delta;
        this.secondsPerPixel += this._mouseWheelDelta;

        if (this.secondsPerPixel === this._currentSettings.minimumDurationPerPixel && delta < 0 || this.secondsPerPixel === this._currentSettings.maximumDurationPerPixel && delta >= 0)
            this._mouseWheelDelta = 0;
        else
            this._mouseWheelDelta = previousDurationPerPixel + this._mouseWheelDelta - this.secondsPerPixel;

        // Center the zoom around the mouse based on the remembered mouse position time.
        this.scrollStartTime = mousePositionTime - (mouseOffset * this.secondsPerPixel);

        this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1);

        event.preventDefault();
        event.stopPropagation();
    }

    _handleGestureStart(event)
    {
        if (this._handlingGesture) {
            // FIXME: <https://webkit.org/b/151068> [Mac] Unexpected gesturestart events when already handling gesture
            return;
        }

        let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft;
        let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel);

        this._handlingGesture = true;
        this._gestureStartStartTime = mousePositionTime;
        this._gestureStartDurationPerPixel = this.secondsPerPixel;

        this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1);

        event.preventDefault();
        event.stopPropagation();
    }

    _handleGestureChange(event)
    {
        // Cap zooming out at 5x.
        let scale = Math.max(1 / 5, event.scale);

        let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft;
        let newSecondsPerPixel = this._gestureStartDurationPerPixel / scale;

        this.secondsPerPixel = newSecondsPerPixel;
        this.scrollStartTime = this._gestureStartStartTime - (mouseOffset * this.secondsPerPixel);

        event.preventDefault();
        event.stopPropagation();
    }

    _handleGestureEnd(event)
    {
        this._handlingGesture = false;
        this._gestureStartStartTime = NaN;
        this._gestureStartDurationPerPixel = NaN;
    }

    _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._overviewGraphsByTypeMap.has(timeline.type), timeline);
        console.assert(!this._treeElementsByTypeMap.has(timeline.type), timeline);

        let treeElement = new WI.TimelineTreeElement(timeline);
        let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this));
        this._timelinesTreeOutline.insertChild(treeElement, insertionIndex);
        this._treeElementsByTypeMap.set(timeline.type, treeElement);

        let overviewGraph = WI.TimelineOverviewGraph.createForTimeline(timeline, this);
        overviewGraph.addEventListener(WI.TimelineOverviewGraph.Event.RecordSelected, this._handleOverviewGraphRecordSelected, this);
        this._overviewGraphsByTypeMap.set(timeline.type, overviewGraph);
        this._graphsContainerView.insertSubviewBefore(overviewGraph, this._graphsContainerView.subviews[insertionIndex]);

        treeElement.element.style.height = overviewGraph.height + "px";

        if (!this._canShowTimelineType(timeline.type)) {
            overviewGraph.hidden = true;
            treeElement.hidden = true;
        }

        this.needsLayout();
    }

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

        let timeline = this._recording.timelineForInstrument(instrument);
        let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
        console.assert(overviewGraph, "Missing overview graph for timeline type", timeline.type);

        let treeElement = this._treeElementsByTypeMap.get(timeline.type);
        let shouldSuppressOnDeselect = false;
        let shouldSuppressSelectSibling = true;
        this._timelinesTreeOutline.removeChild(treeElement, shouldSuppressOnDeselect, shouldSuppressSelectSibling);

        overviewGraph.removeEventListener(WI.TimelineOverviewGraph.Event.RecordSelected, this._handleOverviewGraphRecordSelected, this);
        this._graphsContainerView.removeSubview(overviewGraph);

        this._overviewGraphsByTypeMap.delete(timeline.type);
        this._treeElementsByTypeMap.delete(timeline.type);
    }

    _markerAdded(event)
    {
        this._timelineRuler.addMarker(event.data.marker);
    }

    _handleGraphsContainerClick(event)
    {
        // Set when a WI.TimelineRecordBar receives the "click" first and is about to be selected.
        if (event.__timelineRecordClickEventHandled)
            return;

        this._recordSelected(null, null);
    }

    _timelineRulerMouseDown(event)
    {
        this._timelineRulerSelectionChanged = false;
    }

    _timelineRulerMouseClicked(event)
    {
        if (this._timelineRulerSelectionChanged)
            return;

        for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
            if (overviewGraph.hidden)
                continue;

            let graphRect = overviewGraph.element.getBoundingClientRect();
            if (!(event.pageX >= graphRect.left && event.pageX <= graphRect.right && event.pageY >= graphRect.top && event.pageY <= graphRect.bottom))
                continue;

            // Clone the event to dispatch it on the overview graph element.
            let newClickEvent = new event.constructor(event.type, event);
            overviewGraph.element.dispatchEvent(newClickEvent);
            return;
        }
    }

    _timeRangeSelectionChanged(event)
    {
        this._timelineRulerSelectionChanged = true;

        let startTime = this._viewMode === WI.TimelineOverview.ViewMode.Timelines ? this._startTime : 0;
        this._currentSettings.selectionStartValueSetting.value = this.selectionStartTime - startTime;
        this._currentSettings.selectionDurationSetting.value = this.selectionDuration;

        this.dispatchEventToListeners(WI.TimelineOverview.Event.TimeRangeSelectionChanged);
    }

    _handleOverviewGraphRecordSelected(event)
    {
        let {record, recordBar} = event.data;

        // Ignore deselection events, as they are handled by the newly selected record's timeline.
        if (!record)
            return;

        this._recordSelected(record, recordBar);
    }

    _recordSelected(record, recordBar)
    {
        if (record === this._selectedTimelineRecord)
            return;

        if (this._selectedTimelineRecord && (!record || this._selectedTimelineRecord.type !== record.type)) {
            let timelineOverviewGraph = this._overviewGraphsByTypeMap.get(this._selectedTimelineRecord.type);
            console.assert(timelineOverviewGraph);
            if (timelineOverviewGraph)
                timelineOverviewGraph.selectedRecord = null;
        }

        this._selectedTimelineRecord = record;

        if (this._selectedTimelineRecord) {
            let firstRecord = this._selectedTimelineRecord;
            let lastRecord = this._selectedTimelineRecord;
            if (recordBar) {
                firstRecord = recordBar.records[0];
                lastRecord = recordBar.records.lastValue;
            }

            let startTime = firstRecord instanceof WI.RenderingFrameTimelineRecord ? firstRecord.frameIndex : firstRecord.startTime;
            let endTime = lastRecord instanceof WI.RenderingFrameTimelineRecord ? lastRecord.frameIndex : lastRecord.endTime;
            if (startTime < this.selectionStartTime || (endTime > this.selectionStartTime + this.selectionDuration) || firstRecord instanceof WI.CPUTimelineRecord) {
                let selectionPadding = this.secondsPerPixel * 10;
                this.selectionStartTime = startTime - selectionPadding;
                this.selectionDuration = endTime - startTime + (selectionPadding * 2);
            }
        }

        this.dispatchEventToListeners(WI.TimelineOverview.Event.RecordSelected, {record: this._selectedTimelineRecord});
    }

    _resetSelection()
    {
        function reset(settings)
        {
            settings.durationPerPixelSetting.reset();
            settings.selectionStartValueSetting.reset();
            settings.selectionDurationSetting.reset();
        }

        reset(this._timelinesViewModeSettings);
        if (this._renderingFramesViewModeSettings)
            reset(this._renderingFramesViewModeSettings);

        this.secondsPerPixel = this._currentSettings.durationPerPixelSetting.value;
        this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value;
        this.selectionDuration = this._currentSettings.selectionDurationSetting.value;
    }

    _recordingReset(event)
    {
        this._timelineRuler.clearMarkers();

        this._timelineRuler.addMarker(this._currentTimeMarker);
    }

    _canShowTimelineType(type)
    {
        let timelineViewMode = WI.TimelineOverview.ViewMode.Timelines;
        if (type === WI.TimelineRecord.Type.RenderingFrame)
            timelineViewMode = WI.TimelineOverview.ViewMode.RenderingFrames;

        return timelineViewMode === this._viewMode;
    }

    _viewModeDidChange()
    {
        let startTime = 0;
        let isRenderingFramesMode = this._viewMode === WI.TimelineOverview.ViewMode.RenderingFrames;
        if (isRenderingFramesMode) {
            this._timelineRuler.minimumSelectionDuration = 1;
            this._timelineRuler.snapInterval = 1;
            this._timelineRuler.formatLabelCallback = (value) => value.maxDecimals(0).toLocaleString();
        } else {
            this._timelineRuler.minimumSelectionDuration = 0.01;
            this._timelineRuler.snapInterval = NaN;
            this._timelineRuler.formatLabelCallback = null;

            startTime = this._startTime;
        }

        this.pixelAlignDuration = isRenderingFramesMode;
        this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value + startTime;
        this.selectionDuration = this._currentSettings.selectionDurationSetting.value;

        for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
            let treeElement = this._treeElementsByTypeMap.get(type);
            console.assert(treeElement, "Missing tree element for timeline type", type);

            let hidden = !this._canShowTimelineType(type);
            treeElement.hidden = hidden;
            overviewGraph.hidden = hidden;
        }

        this.element.classList.toggle("frames", isRenderingFramesMode);

        this.updateLayout(WI.View.LayoutReason.Resize);
    }

    _createViewModeSettings(viewMode, minimumDurationPerPixel, maximumDurationPerPixel, durationPerPixel, selectionStartValue, selectionDuration)
    {
        durationPerPixel = Math.min(maximumDurationPerPixel, Math.max(minimumDurationPerPixel, durationPerPixel));

        let durationPerPixelSetting = new WI.Setting(viewMode + "-duration-per-pixel", durationPerPixel);
        let selectionStartValueSetting = new WI.Setting(viewMode + "-selection-start-value", selectionStartValue);
        let selectionDurationSetting = new WI.Setting(viewMode + "-selection-duration", selectionDuration);

        return {
            scrollStartTime: 0,
            minimumDurationPerPixel,
            maximumDurationPerPixel,
            durationPerPixelSetting,
            selectionStartValueSetting,
            selectionDurationSetting
        };
    }

    get _currentSettings()
    {
        return this._viewMode === WI.TimelineOverview.ViewMode.Timelines ? this._timelinesViewModeSettings : this._renderingFramesViewModeSettings;
    }

    _timelinesTreeSelectionDidChange(event)
    {
        let timeline = null;
        let selectedTreeElement = this._timelinesTreeOutline.selectedTreeElement;
        if (selectedTreeElement) {
            timeline = selectedTreeElement.representedObject;
            console.assert(timeline instanceof WI.Timeline, timeline);
            console.assert(this._recording.timelines.get(timeline.type) === timeline, timeline);

            for (let [type, overviewGraph] of this._overviewGraphsByTypeMap)
                overviewGraph.selected = type === timeline.type;
        }

        this._selectedTimeline = timeline;
        this.dispatchEventToListeners(WI.TimelineOverview.Event.TimelineSelected);
    }

    _toggleEditingInstruments(event)
    {
        if (this._editingInstruments)
            this._stopEditingInstruments();
        else
            this._startEditingInstruments();
    }

    _editingInstrumentsDidChange()
    {
        this.element.classList.toggle(WI.TimelineOverview.EditInstrumentsStyleClassName, this._editingInstruments);
        this._timelineRuler.enabled = !this._editingInstruments;

        this._updateWheelAndGestureHandlers();
        this._updateEditInstrumentsButton();

        this.dispatchEventToListeners(WI.TimelineOverview.Event.EditingInstrumentsDidChange);
    }

    _updateEditInstrumentsButton()
    {
        let newLabel = this._editingInstruments ? WI.UIString("Done") : WI.UIString("Edit");
        this._editInstrumentsButton.label = newLabel;
        this._editInstrumentsButton.activated = this._editingInstruments;
        this._editInstrumentsButton.enabled = !WI.timelineManager.isCapturing();
    }

    _updateWheelAndGestureHandlers()
    {
        if (this._editingInstruments) {
            this.element.removeEventListener("wheel", this._handleWheelEventListener);
            this.element.removeEventListener("gesturestart", this._handleGestureStartEventListener);
            this.element.removeEventListener("gesturechange", this._handleGestureChangeEventListener);
            this.element.removeEventListener("gestureend", this._handleGestureEndEventListener);
            this._handleWheelEventListener = null;
            this._handleGestureStartEventListener = null;
            this._handleGestureChangeEventListener = null;
            this._handleGestureEndEventListener = null;
        } else {
            this._handleWheelEventListener = this._handleWheelEvent.bind(this);
            this._handleGestureStartEventListener = this._handleGestureStart.bind(this);
            this._handleGestureChangeEventListener = this._handleGestureChange.bind(this);
            this._handleGestureEndEventListener = this._handleGestureEnd.bind(this);
            this.element.addEventListener("wheel", this._handleWheelEventListener);
            this.element.addEventListener("gesturestart", this._handleGestureStartEventListener);
            this.element.addEventListener("gesturechange", this._handleGestureChangeEventListener);
            this.element.addEventListener("gestureend", this._handleGestureEndEventListener);
        }
    }

    _startEditingInstruments()
    {
        console.assert(this._viewMode === WI.TimelineOverview.ViewMode.Timelines);

        if (this._editingInstruments)
            return;

        this._editingInstruments = true;

        for (let type of this._instrumentTypes) {
            let treeElement = this._treeElementsByTypeMap.get(type);
            if (!treeElement) {
                let timeline = this._recording.timelines.get(type);
                console.assert(timeline, "Missing timeline for type " + type);

                const placeholder = true;
                treeElement = new WI.TimelineTreeElement(timeline, placeholder);

                let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this));
                this._timelinesTreeOutline.insertChild(treeElement, insertionIndex);

                let placeholderGraph = new WI.View;
                placeholderGraph.element.classList.add("timeline-overview-graph");
                treeElement[WI.TimelineOverview.PlaceholderOverviewGraph] = placeholderGraph;
                this._graphsContainerView.insertSubviewBefore(placeholderGraph, this._graphsContainerView.subviews[insertionIndex]);
            }

            treeElement.editing = true;
            treeElement.addEventListener(WI.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this);
        }

        this._editingInstrumentsDidChange();
    }

    _stopEditingInstruments()
    {
        if (!this._editingInstruments)
            return;

        this._editingInstruments = false;

        let instruments = this._recording.instruments;
        for (let treeElement of this._treeElementsByTypeMap.values()) {
            if (treeElement.status.checked) {
                treeElement.editing = false;
                treeElement.removeEventListener(WI.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this);
                continue;
            }

            let timelineInstrument = instruments.find((instrument) => instrument.timelineRecordType === treeElement.representedObject.type);
            this._recording.removeInstrument(timelineInstrument);
        }

        let placeholderTreeElements = this._timelinesTreeOutline.children.filter((treeElement) => treeElement.placeholder);
        for (let treeElement of placeholderTreeElements) {
            this._timelinesTreeOutline.removeChild(treeElement);

            let placeholderGraph = treeElement[WI.TimelineOverview.PlaceholderOverviewGraph];
            console.assert(placeholderGraph);
            this._graphsContainerView.removeSubview(placeholderGraph);

            if (treeElement.status.checked) {
                let instrument = WI.Instrument.createForTimelineType(treeElement.representedObject.type);
                this._recording.addInstrument(instrument);
            }
        }

        let instrumentTypes = instruments.map((instrument) => instrument.timelineRecordType);
        WI.timelineManager.enabledTimelineTypes = instrumentTypes;

        this._editingInstrumentsDidChange();
    }

    _handleTimelineCapturingStateChanged(event)
    {
        switch (WI.timelineManager.capturingState) {
        case WI.TimelineManager.CapturingState.Active:
            this._editInstrumentsButton.enabled = false;
            this._stopEditingInstruments();
            break;

        case WI.TimelineManager.CapturingState.Inactive:
            this._editInstrumentsButton.enabled = true;
            break;
        }
    }

    _recordingImported(event)
    {
        let {overviewData} = event.data;

        if (overviewData.secondsPerPixel !== undefined)
            this.secondsPerPixel = overviewData.secondsPerPixel;
        if (overviewData.scrollStartTime !== undefined)
            this.scrollStartTime = overviewData.scrollStartTime;
        if (overviewData.selectionStartTime !== undefined)
            this.selectionStartTime = overviewData.selectionStartTime;
        if (overviewData.selectionDuration !== undefined) {
            if (overviewData.selectionDuration === Number.MAX_VALUE)
                this._timelineRuler.selectEntireRange();
            else
                this.selectionDuration = overviewData.selectionDuration;
        }
        if (overviewData.selectedTimelineType !== undefined) {
            let timeline = this._recording.timelineForRecordType(overviewData.selectedTimelineType);
            if (timeline)
                this.selectedTimeline = timeline;
        }
    }

    _compareTimelineTreeElements(a, b)
    {
        let aTimelineType = a.representedObject.type;
        let bTimelineType = b.representedObject.type;

        // Always sort the Rendering Frames timeline last.
        if (aTimelineType === WI.TimelineRecord.Type.RenderingFrame)
            return 1;
        if (bTimelineType === WI.TimelineRecord.Type.RenderingFrame)
            return -1;

        if (a.placeholder !== b.placeholder)
            return a.placeholder ? 1 : -1;

        let aTimelineIndex = this._instrumentTypes.indexOf(aTimelineType);
        let bTimelineIndex = this._instrumentTypes.indexOf(bTimelineType);
        return aTimelineIndex - bTimelineIndex;
    }

    _timelineTreeElementEnabledDidChange(event)
    {
        let enabled = this._timelinesTreeOutline.children.some((treeElement) => {
            let timelineType = treeElement.representedObject.type;
            return this._canShowTimelineType(timelineType) && treeElement.status.checked;
        });

        this._editInstrumentsButton.enabled = enabled;
    }
};

WI.TimelineOverview.PlaceholderOverviewGraph = Symbol("placeholder-overview-graph");

WI.TimelineOverview.ScrollDeltaDenominator = 500;
WI.TimelineOverview.EditInstrumentsStyleClassName = "edit-instruments";
WI.TimelineOverview.MinimumDurationPerPixel = 0.0001;
WI.TimelineOverview.MaximumDurationPerPixel = 60;

WI.TimelineOverview.ViewMode = {
    Timelines: "timeline-overview-view-mode-timelines",
    RenderingFrames: "timeline-overview-view-mode-rendering-frames"
};

WI.TimelineOverview.Event = {
    EditingInstrumentsDidChange: "editing-instruments-did-change",
    RecordSelected: "timeline-overview-record-selected",
    TimelineSelected: "timeline-overview-timeline-selected",
    TimeRangeSelectionChanged: "timeline-overview-time-range-selection-changed"
};
