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

WebInspector.TimelineRecording = class TimelineRecording extends WebInspector.Object
{
    constructor(identifier, displayName, instruments)
    {
        super();

        this._identifier = identifier;
        this._timelines = new Map;
        this._displayName = displayName;
        this._capturing = false;
        this._readonly = false;
        this._instruments = instruments || [];
        this._topDownCallingContextTree = new WebInspector.CallingContextTree(WebInspector.CallingContextTree.Type.TopDown);
        this._bottomUpCallingContextTree = new WebInspector.CallingContextTree(WebInspector.CallingContextTree.Type.BottomUp);
        this._topFunctionsTopDownCallingContextTree = new WebInspector.CallingContextTree(WebInspector.CallingContextTree.Type.TopFunctionsTopDown);
        this._topFunctionsBottomUpCallingContextTree = new WebInspector.CallingContextTree(WebInspector.CallingContextTree.Type.TopFunctionsBottomUp);

        for (let type of WebInspector.TimelineManager.availableTimelineTypes()) {
            let timeline = WebInspector.Timeline.create(type);
            this._timelines.set(type, timeline);
            timeline.addEventListener(WebInspector.Timeline.Event.TimesUpdated, this._timelineTimesUpdated, this);
        }

        // For legacy backends, we compute the elapsed time of records relative to this timestamp.
        this._legacyFirstRecordedTimestamp = NaN;

        this.reset(true);
    }

    // Static

    static sourceCodeTimelinesSupported()
    {
        return WebInspector.debuggableType === WebInspector.DebuggableType.Web;
    }

    // Public

    get displayName() { return this._displayName; }
    get identifier() { return this._identifier; }
    get timelines() { return this._timelines; }
    get instruments() { return this._instruments; }
    get readonly() { return this._readonly; }
    get startTime() { return this._startTime; }
    get endTime() { return this._endTime; }

    get topDownCallingContextTree() { return this._topDownCallingContextTree; }
    get bottomUpCallingContextTree() { return this._bottomUpCallingContextTree; }
    get topFunctionsTopDownCallingContextTree() { return this._topFunctionsTopDownCallingContextTree; }
    get topFunctionsBottomUpCallingContextTree() { return this._topFunctionsBottomUpCallingContextTree; }

    start(initiatedByBackend)
    {
        console.assert(!this._capturing, "Attempted to start an already started session.");
        console.assert(!this._readonly, "Attempted to start a readonly session.");

        this._capturing = true;

        for (let instrument of this._instruments)
            instrument.startInstrumentation(initiatedByBackend);
    }

    stop(initiatedByBackend)
    {
        console.assert(this._capturing, "Attempted to stop an already stopped session.");
        console.assert(!this._readonly, "Attempted to stop a readonly session.");

        this._capturing = false;

        for (let instrument of this._instruments)
            instrument.stopInstrumentation(initiatedByBackend);
    }

    saveIdentityToCookie()
    {
        // Do nothing. Timeline recordings are not persisted when the inspector is
        // re-opened, so do not attempt to restore by identifier or display name.
    }

    isEmpty()
    {
        for (var timeline of this._timelines.values()) {
            if (timeline.records.length)
                return false;
        }

        return true;
    }

    unloaded()
    {
        console.assert(!this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");

        this._readonly = true;

        this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.Unloaded);
    }

    reset(suppressEvents)
    {
        console.assert(!this._readonly, "Can't reset a read-only recording.");

        this._sourceCodeTimelinesMap = new Map;
        this._eventMarkers = [];
        this._startTime = NaN;
        this._endTime = NaN;
        this._discontinuities = [];

        this._topDownCallingContextTree.reset();
        this._bottomUpCallingContextTree.reset();
        this._topFunctionsTopDownCallingContextTree.reset();
        this._topFunctionsBottomUpCallingContextTree.reset();

        for (var timeline of this._timelines.values())
            timeline.reset(suppressEvents);

        WebInspector.RenderingFrameTimelineRecord.resetFrameIndex();

        if (!suppressEvents) {
            this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.Reset);
            this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
        }
    }

    sourceCodeTimelinesForSourceCode(sourceCode)
    {
        var timelines = this._sourceCodeTimelinesMap.get(sourceCode);
        if (!timelines)
            return [];
        return [...timelines.values()];
    }

    timelineForInstrument(instrument)
    {
        return this._timelines.get(instrument.timelineRecordType);
    }

    instrumentForTimeline(timeline)
    {
        return this._instruments.find((instrument) => instrument.timelineRecordType === timeline.type);
    }

    timelineForRecordType(recordType)
    {
        return this._timelines.get(recordType);
    }

    addInstrument(instrument)
    {
        console.assert(instrument instanceof WebInspector.Instrument, instrument);
        console.assert(!this._instruments.includes(instrument), this._instruments, instrument);

        this._instruments.push(instrument);

        this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.InstrumentAdded, {instrument});
    }

    removeInstrument(instrument)
    {
        console.assert(instrument instanceof WebInspector.Instrument, instrument);
        console.assert(this._instruments.includes(instrument), this._instruments, instrument);

        this._instruments.remove(instrument);

        this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.InstrumentRemoved, {instrument});
    }

    addEventMarker(marker)
    {
        if (!this._capturing)
            return;

        this._eventMarkers.push(marker);

        this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.MarkerAdded, {marker});
    }

    addRecord(record)
    {
        var timeline = this._timelines.get(record.type);
        console.assert(timeline, record, this._timelines);
        if (!timeline)
            return;

        // Add the record to the global timeline by type.
        timeline.addRecord(record);

        // Some records don't have source code timelines.
        if (record.type === WebInspector.TimelineRecord.Type.Network
            || record.type === WebInspector.TimelineRecord.Type.RenderingFrame
            || record.type === WebInspector.TimelineRecord.Type.Memory
            || record.type === WebInspector.TimelineRecord.Type.HeapAllocations)
            return;

        if (!WebInspector.TimelineRecording.sourceCodeTimelinesSupported())
            return;

        // Add the record to the source code timelines.
        var activeMainResource = WebInspector.frameResourceManager.mainFrame.provisionalMainResource || WebInspector.frameResourceManager.mainFrame.mainResource;
        var sourceCode = record.sourceCodeLocation ? record.sourceCodeLocation.sourceCode : activeMainResource;

        var sourceCodeTimelines = this._sourceCodeTimelinesMap.get(sourceCode);
        if (!sourceCodeTimelines) {
            sourceCodeTimelines = new Map;
            this._sourceCodeTimelinesMap.set(sourceCode, sourceCodeTimelines);
        }

        var newTimeline = false;
        var key = this._keyForRecord(record);
        var sourceCodeTimeline = sourceCodeTimelines.get(key);
        if (!sourceCodeTimeline) {
            sourceCodeTimeline = new WebInspector.SourceCodeTimeline(sourceCode, record.sourceCodeLocation, record.type, record.eventType);
            sourceCodeTimelines.set(key, sourceCodeTimeline);
            newTimeline = true;
        }

        sourceCodeTimeline.addRecord(record);

        if (newTimeline)
            this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.SourceCodeTimelineAdded, {sourceCodeTimeline});
    }

    addMemoryPressureEvent(memoryPressureEvent)
    {
        let memoryTimeline = this._timelines.get(WebInspector.TimelineRecord.Type.Memory);
        console.assert(memoryTimeline, this._timelines);
        if (!memoryTimeline)
            return;

        memoryTimeline.addMemoryPressureEvent(memoryPressureEvent);
    }

    addDiscontinuity(startTime, endTime)
    {
        this._discontinuities.push({startTime, endTime});
    }

    discontinuitiesInTimeRange(startTime, endTime)
    {
        return this._discontinuities.filter((item) => item.startTime < endTime && item.endTime > startTime);
    }

    addScriptInstrumentForProgrammaticCapture()
    {
        for (let instrument of this._instruments) {
            if (instrument instanceof WebInspector.ScriptInstrument)
                return;
        }

        this.addInstrument(new WebInspector.ScriptInstrument);

        let instrumentTypes = this._instruments.map((instrument) => instrument.timelineRecordType);
        WebInspector.timelineManager.enabledTimelineTypes = instrumentTypes;
    }

    computeElapsedTime(timestamp)
    {
        if (!timestamp || isNaN(timestamp))
            return NaN;

        // COMPATIBILITY (iOS 8): old backends send timestamps (seconds or milliseconds since the epoch),
        // rather than seconds elapsed since timeline capturing started. We approximate the latter by
        // subtracting the start timestamp, as old versions did not use monotonic times.
        if (WebInspector.TimelineRecording.isLegacy === undefined)
            WebInspector.TimelineRecording.isLegacy = timestamp > WebInspector.TimelineRecording.TimestampThresholdForLegacyRecordConversion;

        if (!WebInspector.TimelineRecording.isLegacy)
            return timestamp;

        // If the record's start time is large, but not really large, then it is seconds since epoch
        // not millseconds since epoch, so convert it to milliseconds.
        if (timestamp < WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
            timestamp *= 1000;

        if (isNaN(this._legacyFirstRecordedTimestamp))
            this._legacyFirstRecordedTimestamp = timestamp;

        // Return seconds since the first recorded value.
        return (timestamp - this._legacyFirstRecordedTimestamp) / 1000.0;
    }

    setLegacyBaseTimestamp(timestamp)
    {
        console.assert(isNaN(this._legacyFirstRecordedTimestamp));

        if (timestamp < WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
            timestamp *= 1000;

        this._legacyFirstRecordedTimestamp = timestamp;
    }

    initializeTimeBoundsIfNecessary(timestamp)
    {
        if (isNaN(this._startTime)) {
            console.assert(isNaN(this._endTime));

            this._startTime = timestamp;
            this._endTime = timestamp;

            this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
        }
    }

    // Private

    _keyForRecord(record)
    {
        var key = record.type;
        if (record instanceof WebInspector.ScriptTimelineRecord || record instanceof WebInspector.LayoutTimelineRecord)
            key += ":" + record.eventType;
        if (record instanceof WebInspector.ScriptTimelineRecord && record.eventType === WebInspector.ScriptTimelineRecord.EventType.EventDispatched)
            key += ":" + record.details;
        if (record.sourceCodeLocation)
            key += ":" + record.sourceCodeLocation.lineNumber + ":" + record.sourceCodeLocation.columnNumber;
        return key;
    }

    _timelineTimesUpdated(event)
    {
        var timeline = event.target;
        var changed = false;

        if (isNaN(this._startTime) || timeline.startTime < this._startTime) {
            this._startTime = timeline.startTime;
            changed = true;
        }

        if (isNaN(this._endTime) || this._endTime < timeline.endTime) {
            this._endTime = timeline.endTime;
            changed = true;
        }

        if (changed)
            this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
    }
};

WebInspector.TimelineRecording.Event = {
    Reset: "timeline-recording-reset",
    Unloaded: "timeline-recording-unloaded",
    SourceCodeTimelineAdded: "timeline-recording-source-code-timeline-added",
    InstrumentAdded: "timeline-recording-instrument-added",
    InstrumentRemoved: "timeline-recording-instrument-removed",
    TimesUpdated: "timeline-recording-times-updated",
    MarkerAdded: "timeline-recording-marker-added",
};

WebInspector.TimelineRecording.isLegacy = undefined;
WebInspector.TimelineRecording.TimestampThresholdForLegacyRecordConversion = 10000000; // Some value not near zero.
WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds = 1420099200000; // Date.parse("Jan 1, 2015"). Milliseconds since epoch.
