/*
 * Copyright (C) 2008 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. ``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
 * 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.
 */

/**
 * @constructor
 * @implements {WebInspector.Searchable}
 * @extends {WebInspector.ProfileView}
 * @param {!WebInspector.CPUProfileHeader} profileHeader
 */
WebInspector.CPUProfileView = function(profileHeader)
{
    this._profileHeader = profileHeader;
    this.profile = new WebInspector.CPUProfileDataModel(profileHeader._profile || profileHeader.protocolProfile());
    this.adjustedTotal = this.profile.profileHead.total;
    this.adjustedTotal -= this.profile.idleNode ? this.profile.idleNode.total : 0;
    WebInspector.ProfileView.call(this, new WebInspector.CPUProfileView.NodeFormatter(this));
}

WebInspector.CPUProfileView.prototype = {
    /**
     * @override
     */
    wasShown: function()
    {
        WebInspector.ProfileView.prototype.wasShown.call(this);
        var lineLevelProfile = WebInspector.LineLevelProfile.instance();
        lineLevelProfile.reset();
        lineLevelProfile.appendCPUProfile(this.profile);
    },

    /**
     * @override
     * @param {string} columnId
     * @return {string}
     */
    columnHeader: function(columnId)
    {
        switch (columnId) {
        case "self": return WebInspector.UIString("Self Time");
        case "total": return WebInspector.UIString("Total Time");
        }
        return "";
    },

    /**
     * @override
     * @return {!WebInspector.FlameChartDataProvider}
     */
    createFlameChartDataProvider: function()
    {
        return new WebInspector.CPUFlameChartDataProvider(this.profile, this._profileHeader.target());
    },

    __proto__: WebInspector.ProfileView.prototype
}

/**
 * @constructor
 * @extends {WebInspector.ProfileType}
 */
WebInspector.CPUProfileType = function()
{
    WebInspector.ProfileType.call(this, WebInspector.CPUProfileType.TypeId, WebInspector.UIString("Record JavaScript CPU Profile"));
    this._recording = false;

    this._nextAnonymousConsoleProfileNumber = 1;
    this._anonymousConsoleProfileIdToTitle = {};

    WebInspector.CPUProfileType.instance = this;
    WebInspector.targetManager.addModelListener(WebInspector.CPUProfilerModel, WebInspector.CPUProfilerModel.EventTypes.ConsoleProfileStarted, this._consoleProfileStarted, this);
    WebInspector.targetManager.addModelListener(WebInspector.CPUProfilerModel, WebInspector.CPUProfilerModel.EventTypes.ConsoleProfileFinished, this._consoleProfileFinished, this);
}

WebInspector.CPUProfileType.TypeId = "CPU";

WebInspector.CPUProfileType.prototype = {
    /**
     * @override
     * @return {string}
     */
    typeName: function()
    {
        return "CPU";
    },

    /**
     * @override
     * @return {string}
     */
    fileExtension: function()
    {
        return ".cpuprofile";
    },

    get buttonTooltip()
    {
        return this._recording ? WebInspector.UIString("Stop CPU profiling") : WebInspector.UIString("Start CPU profiling");
    },

    /**
     * @override
     * @return {boolean}
     */
    buttonClicked: function()
    {
        if (this._recording) {
            this.stopRecordingProfile();
            return false;
        } else {
            this.startRecordingProfile();
            return true;
        }
    },

    get treeItemTitle()
    {
        return WebInspector.UIString("CPU PROFILES");
    },

    get description()
    {
        return WebInspector.UIString("CPU profiles show where the execution time is spent in your page's JavaScript functions.");
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _consoleProfileStarted: function(event)
    {
        var data = /** @type {!WebInspector.CPUProfilerModel.EventData} */ (event.data);
        var resolvedTitle = data.title;
        if (!resolvedTitle) {
            resolvedTitle = WebInspector.UIString("Profile %s", this._nextAnonymousConsoleProfileNumber++);
            this._anonymousConsoleProfileIdToTitle[data.id] = resolvedTitle;
        }
        this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.Profile, data.scriptLocation, WebInspector.UIString("Profile '%s' started.", resolvedTitle));
    },

    /**
     * @param {!WebInspector.Event} event
     */
    _consoleProfileFinished: function(event)
    {
        var data = /** @type {!WebInspector.CPUProfilerModel.EventData} */ (event.data);
        var cpuProfile = /** @type {!ProfilerAgent.CPUProfile} */ (data.cpuProfile);
        var resolvedTitle = data.title;
        if (typeof resolvedTitle === "undefined") {
            resolvedTitle = this._anonymousConsoleProfileIdToTitle[data.id];
            delete this._anonymousConsoleProfileIdToTitle[data.id];
        }
        var profile = new WebInspector.CPUProfileHeader(data.scriptLocation.target(), this, resolvedTitle);
        profile.setProtocolProfile(cpuProfile);
        this.addProfile(profile);
        this._addMessageToConsole(WebInspector.ConsoleMessage.MessageType.ProfileEnd, data.scriptLocation, WebInspector.UIString("Profile '%s' finished.", resolvedTitle));
    },

    /**
     * @param {string} type
     * @param {!WebInspector.DebuggerModel.Location} scriptLocation
     * @param {string} messageText
     */
    _addMessageToConsole: function(type, scriptLocation, messageText)
    {
        var script = scriptLocation.script();
        var target = scriptLocation.target();
        var message = new WebInspector.ConsoleMessage(
            target,
            WebInspector.ConsoleMessage.MessageSource.ConsoleAPI,
            WebInspector.ConsoleMessage.MessageLevel.Debug,
            messageText,
            type,
            undefined,
            undefined,
            undefined,
            undefined,
            [{
                functionName: "",
                scriptId: scriptLocation.scriptId,
                url: script ? script.contentURL() : "",
                lineNumber: scriptLocation.lineNumber,
                columnNumber: scriptLocation.columnNumber || 0
            }]);

        target.consoleModel.addMessage(message);
    },

    startRecordingProfile: function()
    {
        var target = WebInspector.context.flavor(WebInspector.Target);
        if (this._profileBeingRecorded || !target)
            return;
        var profile = new WebInspector.CPUProfileHeader(target, this);
        this.setProfileBeingRecorded(profile);
        WebInspector.targetManager.suspendAllTargets();
        this.addProfile(profile);
        profile.updateStatus(WebInspector.UIString("Recording\u2026"));
        this._recording = true;
        target.cpuProfilerModel.startRecording();
    },

    stopRecordingProfile: function()
    {
        this._recording = false;
        if (!this._profileBeingRecorded || !this._profileBeingRecorded.target())
            return;

        var recordedProfile;

        /**
         * @param {?ProfilerAgent.CPUProfile} profile
         * @this {WebInspector.CPUProfileType}
         */
        function didStopProfiling(profile)
        {
            if (!this._profileBeingRecorded)
                return;
            console.assert(profile);
            this._profileBeingRecorded.setProtocolProfile(profile);
            this._profileBeingRecorded.updateStatus("");
            recordedProfile = this._profileBeingRecorded;
            this.setProfileBeingRecorded(null);
        }

        /**
         * @this {WebInspector.CPUProfileType}
         */
        function fireEvent()
        {
            this.dispatchEventToListeners(WebInspector.ProfileType.Events.ProfileComplete, recordedProfile);
        }

        this._profileBeingRecorded.target().cpuProfilerModel.stopRecording()
            .then(didStopProfiling.bind(this))
            .then(WebInspector.targetManager.resumeAllTargets.bind(WebInspector.targetManager))
            .then(fireEvent.bind(this));
    },

    /**
     * @override
     * @param {string} title
     * @return {!WebInspector.ProfileHeader}
     */
    createProfileLoadedFromFile: function(title)
    {
        return new WebInspector.CPUProfileHeader(null, this, title);
    },

    /**
     * @override
     */
    profileBeingRecordedRemoved: function()
    {
        this.stopRecordingProfile();
    },

    __proto__: WebInspector.ProfileType.prototype
}

/**
 * @constructor
 * @extends {WebInspector.WritableProfileHeader}
 * @param {?WebInspector.Target} target
 * @param {!WebInspector.CPUProfileType} type
 * @param {string=} title
 */
WebInspector.CPUProfileHeader = function(target, type, title)
{
    WebInspector.WritableProfileHeader.call(this, target, type, title);
}

WebInspector.CPUProfileHeader.prototype = {
    /**
     * @override
     * @return {!WebInspector.ProfileView}
     */
    createView: function()
    {
        return new WebInspector.CPUProfileView(this);
    },

    /**
     * @return {!ProfilerAgent.CPUProfile}
     */
    protocolProfile: function()
    {
        return this._protocolProfile;
    },

    __proto__: WebInspector.WritableProfileHeader.prototype
}

/**
 * @implements {WebInspector.ProfileDataGridNode.Formatter}
 * @constructor
 */
WebInspector.CPUProfileView.NodeFormatter = function(profileView)
{
    this._profileView = profileView;
}

WebInspector.CPUProfileView.NodeFormatter.prototype = {
    /**
     * @override
     * @param {number} value
     * @return {string}
     */
    formatValue: function(value)
    {
        return WebInspector.UIString("%.1f\u2009ms", value);
    },

    /**
     * @override
     * @param {number} value
     * @param {!WebInspector.ProfileDataGridNode} node
     * @return {string}
     */
    formatPercent: function(value, node)
    {
        return node.profileNode === this._profileView.profile.idleNode ? "" : WebInspector.UIString("%.2f\u2009%%", value);
    },

    /**
     * @override
     * @param  {!WebInspector.ProfileDataGridNode} node
     * @return {!Element}
     */
    linkifyNode: function(node)
    {
        var callFrame = node.profileNode.frame;
        return this._profileView.linkifier().linkifyConsoleCallFrame(this._profileView.target(), callFrame, "profile-node-file");
    }
}

/**
 * @constructor
 * @extends {WebInspector.ProfileFlameChartDataProvider}
 * @param {!WebInspector.CPUProfileDataModel} cpuProfile
 * @param {?WebInspector.Target} target
 */
WebInspector.CPUFlameChartDataProvider = function(cpuProfile, target)
{
    WebInspector.ProfileFlameChartDataProvider.call(this, target);
    this._cpuProfile = cpuProfile;
}

WebInspector.CPUFlameChartDataProvider.prototype = {
    /**
     * @override
     * @return {!WebInspector.FlameChart.TimelineData}
     */
    _calculateTimelineData: function()
    {
        /**
         * @constructor
         * @param {number} depth
         * @param {number} duration
         * @param {number} startTime
         * @param {number} selfTime
         * @param {!ProfilerAgent.CPUProfileNode} node
         */
        function ChartEntry(depth, duration, startTime, selfTime, node)
        {
            this.depth = depth;
            this.duration = duration;
            this.startTime = startTime;
            this.selfTime = selfTime;
            this.node = node;
        }

        /** @type {!Array.<?ChartEntry>} */
        var entries = [];
        /** @type {!Array.<number>} */
        var stack = [];
        var maxDepth = 5;

        function onOpenFrame()
        {
            stack.push(entries.length);
            // Reserve space for the entry, as they have to be ordered by startTime.
            // The entry itself will be put there in onCloseFrame.
            entries.push(null);
        }
        function onCloseFrame(depth, node, startTime, totalTime, selfTime)
        {
            var index = stack.pop();
            entries[index] = new ChartEntry(depth, totalTime, startTime, selfTime, node);
            maxDepth = Math.max(maxDepth, depth);
        }
        this._cpuProfile.forEachFrame(onOpenFrame, onCloseFrame);

        /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */
        var entryNodes = new Array(entries.length);
        var entryLevels = new Uint8Array(entries.length);
        var entryTotalTimes = new Float32Array(entries.length);
        var entrySelfTimes = new Float32Array(entries.length);
        var entryStartTimes = new Float64Array(entries.length);
        var minimumBoundary = this.minimumBoundary();

        for (var i = 0; i < entries.length; ++i) {
            var entry = entries[i];
            entryNodes[i] = entry.node;
            entryLevels[i] = entry.depth;
            entryTotalTimes[i] = entry.duration;
            entryStartTimes[i] = entry.startTime;
            entrySelfTimes[i] = entry.selfTime;
        }

        this._maxStackDepth = maxDepth;

        this._timelineData = new WebInspector.FlameChart.TimelineData(entryLevels, entryTotalTimes, entryStartTimes, null);

        /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */
        this._entryNodes = entryNodes;
        this._entrySelfTimes = entrySelfTimes;

        return this._timelineData;
    },

    /**
     * @override
     * @param {number} entryIndex
     * @return {?Array<!{title: string, value: (string|!Element)}>}
     */
    prepareHighlightedEntryInfo: function(entryIndex)
    {
        var timelineData = this._timelineData;
        var node = this._entryNodes[entryIndex];
        if (!node)
            return null;

        var entryInfo = [];
        /**
         * @param {string} title
         * @param {string} value
         */
        function pushEntryInfoRow(title, value)
        {
            entryInfo.push({ title: title, value: value });
        }
        /**
         * @param {number} ms
         * @return {string}
         */
        function millisecondsToString(ms)
        {
            if (ms === 0)
                return "0";
            if (ms < 1000)
                return WebInspector.UIString("%.1f\u2009ms", ms);
            return Number.secondsToString(ms / 1000, true);
        }
        var name = WebInspector.beautifyFunctionName(node.functionName);
        pushEntryInfoRow(WebInspector.UIString("Name"), name);
        var selfTime = millisecondsToString(this._entrySelfTimes[entryIndex]);
        var totalTime = millisecondsToString(timelineData.entryTotalTimes[entryIndex]);
        pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
        pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
        var callFrame = /** @type {!RuntimeAgent.CallFrame} */ (node);
        var linkifier = new WebInspector.Linkifier();
        var text = linkifier.linkifyConsoleCallFrame(this._target, callFrame).textContent;
        linkifier.dispose();
        pushEntryInfoRow(WebInspector.UIString("URL"), text);
        pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
        pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
        if (node.deoptReason && node.deoptReason !== "no reason")
            pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);

        return entryInfo;
    },

    __proto__: WebInspector.ProfileFlameChartDataProvider.prototype
}
