/*
 * 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.TimelineManager = function()
{
    WebInspector.Object.call(this);

    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ProvisionalLoadStarted, this._startAutoRecording, this);
    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);

    this._recording = new WebInspector.TimelineRecording;
    this._recordingEnabled = false;
};

WebInspector.TimelineManager.Event = {
    RecordingStarted: "timeline-manager-recording-started",
    RecordingStopped: "timeline-manager-recording-stopped"
};

WebInspector.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds
WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10000; // 10 seconds
WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2000; // 2 seconds

WebInspector.TimelineManager.prototype = {
    constructor: WebInspector.TimelineManager,

    // Public

    get recording()
    {
        return this._recording;
    },

    get recordingEnabled()
    {
        return this._recordingEnabled;
    },

    startRecording: function()
    {
        if (this._recordingEnabled)
            return;

        this._recordingEnabled = true;

        TimelineAgent.start();

        this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingStarted);
    },

    stopRecording: function()
    {
        if (!this._recordingEnabled)
            return;

        if (this._stopRecordingTimeout) {
            clearTimeout(this._stopRecordingTimeout);
            delete this._stopRecordingTimeout;
        }

        if (this._deadTimeTimeout) {
            clearTimeout(this._deadTimeTimeout);
            delete this._deadTimeTimeout;
        }

        TimelineAgent.stop();

        this._recordingEnabled = false;
        this._autoRecordingMainResource = null;

        this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingStopped);
    },

    eventRecorded: function(originalRecordPayload)
    {
        // Called from WebInspector.TimelineObserver.

        if (!this._recordingEnabled)
            return;

        function processRecord(recordPayload, parentRecordPayload)
        {
            // Convert the timestamps to seconds to match the resource timestamps.
            var startTime = recordPayload.startTime / 1000;
            var endTime = recordPayload.endTime / 1000;

            var callFrames = this._callFramesFromPayload(recordPayload.stackTrace);

            var significantCallFrame = null;
            if (callFrames) {
                for (var i = 0; i < callFrames.length; ++i) {
                    if (callFrames[i].nativeCode)
                        continue;
                    significantCallFrame = callFrames[i];
                    break;
                }
            }

            var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation;

            switch (recordPayload.type) {
            case TimelineAgent.EventType.MarkLoad:
                console.assert(isNaN(endTime));

                var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId);
                console.assert(frame);
                if (!frame)
                    break;

                frame.markLoadEvent(startTime);

                if (!frame.isMainFrame())
                    break;

                var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.LoadEvent);
                this._recording.addEventMarker(eventMarker);

                this._stopAutoRecordingSoon();
                break;

            case TimelineAgent.EventType.MarkDOMContent:
                console.assert(isNaN(endTime));

                var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId);
                console.assert(frame);
                if (!frame)
                    break;

                frame.markDOMContentReadyEvent(startTime);

                if (!frame.isMainFrame())
                    break;

                var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.DOMContentEvent);
                this._recording.addEventMarker(eventMarker);
                break;

            case TimelineAgent.EventType.ScheduleStyleRecalculation:
                console.assert(isNaN(endTime));

                // Pass the startTime as the endTime since this record type has no duration.
                this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, callFrames, sourceCodeLocation));
                break;

            case TimelineAgent.EventType.RecalculateStyles:
                this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, callFrames, sourceCodeLocation));
                break;

            case TimelineAgent.EventType.InvalidateLayout:
                console.assert(isNaN(endTime));

                // Pass the startTime as the endTime since this record type has no duration.
                this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, callFrames, sourceCodeLocation));
                break;

            case TimelineAgent.EventType.Layout:
                // COMPATIBILITY (iOS 6): Layout records did not contain area properties. This is not exposed via a quad "root".
                var quad = recordPayload.data.root ? new WebInspector.Quad(recordPayload.data.root) : null;
                if (quad)
                    this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Layout, startTime, endTime, callFrames, sourceCodeLocation, quad.points[0].x, quad.points[0].y, quad.width, quad.height, quad));
                else
                    this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Layout, startTime, endTime, callFrames, sourceCodeLocation));
                break;

            case TimelineAgent.EventType.Paint:
                // COMPATIBILITY (iOS 6): Paint records data contained x, y, width, height properties. This became a quad "clip".
                var quad = recordPayload.data.clip ? new WebInspector.Quad(recordPayload.data.clip) : null;
                if (quad)
                    this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, null, null, quad.width, quad.height, quad));
                else
                    this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.x, recordPayload.data.y, recordPayload.data.width, recordPayload.data.height));
                break;

            case TimelineAgent.EventType.EvaluateScript:
                if (!sourceCodeLocation) {
                    var mainFrame = WebInspector.frameResourceManager.mainFrame;
                    var scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.url, true);
                    if (scriptResource) {
                        // The lineNumber is 1-based, but we expect 0-based.
                        var lineNumber = recordPayload.data.lineNumber - 1;

                        // FIXME: No column number is provided.
                        sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
                    }
                }

                switch (parentRecordPayload && parentRecordPayload.type) {
                case TimelineAgent.EventType.TimerFire:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId));
                    break;
                default:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, null));
                    break;
                }

                break;

            case TimelineAgent.EventType.TimeStamp:
                var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.TimeStamp);
                this._recording.addEventMarker(eventMarker);
                break;

            case TimelineAgent.EventType.FunctionCall:
                // FunctionCall always happens as a child of another record, and since the FunctionCall record
                // has useful info we just make the timeline record here (combining the data from both records).
                if (!parentRecordPayload)
                    break;

                if (!sourceCodeLocation) {
                    var mainFrame = WebInspector.frameResourceManager.mainFrame;
                    var scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.scriptName, true);
                    if (scriptResource) {
                        // The lineNumber is 1-based, but we expect 0-based.
                        var lineNumber = recordPayload.data.scriptLine - 1;

                        // FIXME: No column number is provided.
                        sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
                    }
                }

                switch (parentRecordPayload.type) {
                case TimelineAgent.EventType.TimerFire:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId));
                    break;
                case TimelineAgent.EventType.EventDispatch:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.type));
                    break;
                case TimelineAgent.EventType.XHRLoad:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "load"));
                    break;
                case TimelineAgent.EventType.XHRReadyStateChange:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "readystatechange"));
                    break;
                case TimelineAgent.EventType.FireAnimationFrame:
                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id));
                    break;
                }

                break;

            case TimelineAgent.EventType.TimerInstall:
                console.assert(isNaN(endTime));

                // Pass the startTime as the endTime since this record type has no duration.
                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
                break;

            case TimelineAgent.EventType.TimerRemove:
                console.assert(isNaN(endTime));

                // Pass the startTime as the endTime since this record type has no duration.
                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
                break;

            case TimelineAgent.EventType.RequestAnimationFrame:
                console.assert(isNaN(endTime));

                // Pass the startTime as the endTime since this record type has no duration.
                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameRequested, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
                break;

            case TimelineAgent.EventType.CancelAnimationFrame:
                console.assert(isNaN(endTime));

                // Pass the startTime as the endTime since this record type has no duration.
                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameCanceled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
                break;
            }
        }

        // Iterate over the records tree using a stack. Doing this recursively has
        // been known to cause a call stack overflow. https://webkit.org/b/79106
        var stack = [{array: [originalRecordPayload], parent: null, index: 0}];
        while (stack.length) {
            var entry = stack.lastValue;
            var recordPayloads = entry.array;
            var parentRecordPayload = entry.parent;

            if (entry.index < recordPayloads.length) {
                var recordPayload = recordPayloads[entry.index];

                processRecord.call(this, recordPayload, parentRecordPayload);

                if (recordPayload.children)
                    stack.push({array: recordPayload.children, parent: recordPayload, index: 0});
                ++entry.index;
            } else
                stack.pop();
        }
    },

    pageDidLoad: function(timestamp)
    {
        if (isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
            WebInspector.frameResourceManager.mainFrame.markLoadEvent(timestamp);
    },

    // Private

    _callFramesFromPayload: function(payload)
    {
        if (!payload)
            return null;

        function createCallFrame(payload)
        {
            var url = payload.url;
            var nativeCode = false;

            if (url === "[native code]") {
                nativeCode = true;
                url = null;
            }

            var sourceCode = WebInspector.frameResourceManager.resourceForURL(url);
            if (!sourceCode)
                sourceCode = WebInspector.debuggerManager.scriptsForURL(url)[0];

            // The lineNumber is 1-based, but we expect 0-based.
            var lineNumber = payload.lineNumber - 1;

            var sourceCodeLocation = sourceCode ? sourceCode.createSourceCodeLocation(lineNumber, payload.columnNumber) : null;
            var functionName = payload.functionName !== "global code" ? payload.functionName : null;

            return new WebInspector.CallFrame(null, sourceCodeLocation, functionName, null, null, nativeCode);
        }

        return payload.map(createCallFrame);
    },

    _addRecord: function(record)
    {
        this._recording.addRecord(record);

        // Only worry about dead time after the load event.
        if (!isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
            this._resetAutoRecordingDeadTimeTimeout();
    },

    _startAutoRecording: function(event)
    {
        if (!event.target.isMainFrame() || (this._recordingEnabled && !this._autoRecordingMainResource))
            return false;

        var mainResource = event.target.provisionalMainResource || event.target.mainResource;
        if (mainResource === this._autoRecordingMainResource)
            return false;

        this.stopRecording();

        this._autoRecordingMainResource = mainResource;

        this._recording.reset();

        this.startRecording();

        this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));

        if (this._stopRecordingTimeout)
            clearTimeout(this._stopRecordingTimeout);
        this._stopRecordingTimeout = setTimeout(this.stopRecording.bind(this), WebInspector.TimelineManager.MaximumAutoRecordDuration);

        return true;
    },

    _stopAutoRecordingSoon: function()
    {
        // Only auto stop when auto recording.
        if (!this._recordingEnabled || !this._autoRecordingMainResource)
            return;

        if (this._stopRecordingTimeout)
            clearTimeout(this._stopRecordingTimeout);
        this._stopRecordingTimeout = setTimeout(this.stopRecording.bind(this), WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent);
    },

    _resetAutoRecordingDeadTimeTimeout: function()
    {
        // Only monitor dead time when auto recording.
        if (!this._recordingEnabled || !this._autoRecordingMainResource)
            return;

        if (this._deadTimeTimeout)
            clearTimeout(this._deadTimeTimeout);
        this._deadTimeTimeout = setTimeout(this.stopRecording.bind(this), WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly);
    },

    _mainResourceDidChange: function(event)
    {
        // Ignore resource events when there isn't a main frame yet. Those events are triggered by
        // loading the cached resources when the inspector opens, and they do not have timing information.
        if (!WebInspector.frameResourceManager.mainFrame)
            return;

        if (this._startAutoRecording(event))
            return;

        if (!this._recordingEnabled)
            return;

        var mainResource = event.target.mainResource;
        if (mainResource === this._autoRecordingMainResource)
            return;

        this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
    },

    _resourceWasAdded: function(event)
    {
        // Ignore resource events when there isn't a main frame yet. Those events are triggered by
        // loading the cached resources when the inspector opens, and they do not have timing information.
        if (!WebInspector.frameResourceManager.mainFrame)
            return;

        if (!this._recordingEnabled)
            return;

        this._addRecord(new WebInspector.ResourceTimelineRecord(event.data.resource));
    }
};

WebInspector.TimelineManager.prototype.__proto__ = WebInspector.Object.prototype;
