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

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

        this._identifier = identifier;
        this._timelines = new Map;
        this._displayName = displayName;
        this._capturing = false;
        this._readonly = false;
        this._imported = false;
        this._instruments = instruments || [];

        this._startTime = NaN;
        this._endTime = NaN;

        this._discontinuityStartTime = NaN;
        this._discontinuities = null;
        this._firstRecordOfTypeAfterDiscontinuity = new Set;

        this._exportDataMarkers = null;
        this._exportDataRecords = null;
        this._exportDataMemoryPressureEvents = null;
        this._exportDataSamplesForTarget = new Map;

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

        this.reset(true);
    }

    // Static

    static sourceCodeTimelinesSupported()
    {
        // FIXME: Support Network Timeline in ServiceWorker.
        return WI.sharedApp.isWebDebuggable();
    }

    // Import / Export

    static async import(identifier, json, displayName)
    {
        let {startTime, endTime, discontinuities, instrumentTypes, records, markers, memoryPressureEvents, samples} = json;
        let importedDisplayName = WI.UIString("Imported - %s").format(displayName);
        let instruments = instrumentTypes.map((type) => WI.Instrument.createForTimelineType(type));
        let recording = new WI.TimelineRecording(identifier, importedDisplayName, instruments);

        recording._readonly = true;
        recording._imported = true;
        recording._startTime = startTime;
        recording._endTime = endTime;
        recording._discontinuities = discontinuities;

        if (samples) {
            for (let {target, stackTraces, durations} of samples)
                recording.updateCallingContextTrees(WI.ImportedTarget.import(target), stackTraces, durations);
        } else if (json.sampleStackTraces && json.sampleDurations) {
            // COMPATIBILITY: `samples` replaced `sampleStackTraces` and `sampleDurations`.
            recording.updateCallingContextTrees(WI.assumingMainTarget(), json.sampleStackTraces, json.sampleDurations);
        }

        for (let recordJSON of records) {
            let record = await WI.TimelineRecord.fromJSON(recordJSON);
            if (record) {
                recording.addRecord(record);

                if (record instanceof WI.ScriptTimelineRecord) {
                    let scriptTimeline = recording.timelineForRecordType(WI.TimelineRecord.Type.Script);
                    let topDownCallingContextTree = scriptTimeline?.callingContextTree(record.target || WI.assumingMainTarget(), WI.CallingContextTree.Type.TopDown);
                    console.assert(topDownCallingContextTree, scriptTimeline);
                    if (topDownCallingContextTree)
                        record.profilePayload = topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
                }
            }
        }

        for (let memoryPressureJSON of memoryPressureEvents) {
            let memoryPressureEvent = WI.MemoryPressureEvent.fromJSON(memoryPressureJSON);
            if (memoryPressureEvent)
                recording.addMemoryPressureEvent(memoryPressureEvent);
        }

        // Add markers once we've transitioned the active recording.
        setTimeout(() => {
            recording.__importing = true;

            for (let markerJSON of markers) {
                let marker = WI.TimelineMarker.fromJSON(markerJSON);
                if (marker)
                    recording.addEventMarker(marker);
            }

            recording.__importing = false;
        });

        return recording;
    }

    exportData()
    {
        console.assert(this.canExport(), "Attempted to export a recording which should not be exportable.");

        // FIXME: Overview data (sourceCodeTimelinesMap).
        // FIXME: Record hierarchy (parent / child relationship) is lost.

        return {
            displayName: this._displayName,
            startTime: this._startTime,
            endTime: this._endTime,
            discontinuities: this._discontinuities,
            instrumentTypes: this._instruments.map((instrument) => instrument.timelineRecordType),
            records: this._exportDataRecords,
            markers: this._exportDataMarkers,
            memoryPressureEvents: this._exportDataMemoryPressureEvents,
            samples: Array.from(this._exportDataSamplesForTarget.values()),
        };
    }

    // Public

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

    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);

        if (!isNaN(this._discontinuityStartTime)) {
            for (let instrument of this._instruments)
                this._firstRecordOfTypeAfterDiscontinuity.add(instrument.timelineRecordType);
        }
    }

    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);
    }

    capturingStarted(startTime)
    {
        // A discontinuity occurs when the recording is stopped and resumed at
        // a future time. Capturing started signals the end of the current
        // discontinuity, if one exists.
        if (!isNaN(this._discontinuityStartTime)) {
            this._discontinuities.push({
                startTime: this._discontinuityStartTime,
                endTime: startTime,
            });
            this._discontinuityStartTime = NaN;
        }
    }

    capturingStopped(endTime)
    {
        this._discontinuityStartTime = endTime;
    }

    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(importing)
    {
        console.assert(importing || !this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");

        this._readonly = true;

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

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

        this._sourceCodeTimelinesMap = new Map;

        this._startTime = NaN;
        this._endTime = NaN;

        this._discontinuityStartTime = NaN;
        this._discontinuities = [];
        this._firstRecordOfTypeAfterDiscontinuity.clear();

        this._exportDataMarkers = [];
        this._exportDataRecords = [];
        this._exportDataMemoryPressureEvents = [];
        this._exportDataSamplesForTarget.clear();

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

        WI.RenderingFrameTimelineRecord.resetFrameIndex();

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

    get sourceCodeTimelines()
    {
        let timelines = [];
        for (let timelinesForSourceCode of this._sourceCodeTimelinesMap.values())
            timelines.pushAll(timelinesForSourceCode.values());
        return timelines;
    }

    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 WI.Instrument, instrument);
        console.assert(!this._instruments.includes(instrument), this._instruments, instrument);

        this._instruments.push(instrument);

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

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

        this._instruments.remove(instrument);

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

    addEventMarker(marker)
    {
        this._exportDataMarkers.push(marker);

        if (!this._capturing && !this.__importing)
            return;

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

    addRecord(record)
    {
        this._exportDataRecords.push(record);

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

        let discontinuity = null;
        if (this._firstRecordOfTypeAfterDiscontinuity.take(record.type))
            discontinuity = this._discontinuities.lastValue;

        // Add the record to the global timeline by type.
        timeline.addRecord(record, {discontinuity});

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

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

        // Add the record to the source code timelines.
        let sourceCode = null;
        if (record.sourceCodeLocation)
            sourceCode = record.sourceCodeLocation.sourceCode;
        else if (record.type === WI.TimelineRecord.Type.Media) {
            if (record.domNode && record.domNode.frame)
                sourceCode = record.domNode.frame.mainResource;
        }
        if (!sourceCode)
            sourceCode = WI.networkManager.mainFrame.provisionalMainResource || WI.networkManager.mainFrame.mainResource;

        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 WI.SourceCodeTimeline(sourceCode, record.sourceCodeLocation, record.type, record.eventType);
            sourceCodeTimelines.set(key, sourceCodeTimeline);
            newTimeline = true;
        }

        sourceCodeTimeline.addRecord(record);

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

    addMemoryPressureEvent(memoryPressureEvent)
    {
        this._exportDataMemoryPressureEvents.push(memoryPressureEvent);

        let memoryTimeline = this._timelines.get(WI.TimelineRecord.Type.Memory);
        console.assert(memoryTimeline, this._timelines);
        if (!memoryTimeline)
            return;

        memoryTimeline.addMemoryPressureEvent(memoryPressureEvent);
    }

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

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

        this.addInstrument(new WI.ScriptInstrument);

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

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

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

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

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

    updateCallingContextTrees(target, stackTraces, sampleDurations)
    {
        let exportDataSamples = this._exportDataSamplesForTarget.getOrInitialize(target, () => {
            return {
                target: target.exportData(),
                stackTraces: [],
                durations: [],
            };
        });
        exportDataSamples.stackTraces.pushAll(stackTraces);
        exportDataSamples.durations.pushAll(sampleDurations);

        let scriptTimeline = this._timelines.get(WI.TimelineRecord.Type.Script);
        console.assert(scriptTimeline, this._timelines);
        if (!scriptTimeline)
            return;

        scriptTimeline.updateCallingContextTrees(target, stackTraces, sampleDurations);
    }

    get exportMode()
    {
        return WI.FileUtilities.SaveMode.SingleFile;
    }

    canExport()
    {
        if (!WI.FileUtilities.canSave(this.exportMode))
            return false;

        if (this._capturing)
            return false;

        if (isNaN(this._startTime))
            return false;

        return true;
    }

    // Testing

    get markersForTesting() { return this._exportDataMarkers; }

    // Private

    _keyForRecord(record)
    {
        var key = record.type;
        if (record instanceof WI.ScriptTimelineRecord || record instanceof WI.LayoutTimelineRecord)
            key += ":" + record.eventType;
        if (record instanceof WI.ScriptTimelineRecord && record.eventType === WI.ScriptTimelineRecord.EventType.EventDispatched)
            key += ":" + record.details;
        if (record instanceof WI.MediaTimelineRecord) {
            key += ":" + record.eventType;
            if (record.eventType === WI.MediaTimelineRecord.EventType.DOMEvent) {
                if (record.domEvent && record.domEvent.eventName)
                    key += ":" + record.domEvent.eventName;
            } else if (record.eventType === WI.MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged)
                key += ":" + (record.isPowerEfficient ? "enabled" : "disabled");
        }
        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(WI.TimelineRecording.Event.TimesUpdated);
    }
};

WI.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",
    TargetAdded: "timeline-recording-target-added",
    TimesUpdated: "timeline-recording-times-updated",
    MarkerAdded: "timeline-recording-marker-added",
};

WI.TimelineRecording.SerializationVersion = 1;
