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

const UserInitiatedProfileName = "org.webkit.profiles.user-initiated";

/**
 * @constructor
 * @extends {WebInspector.Object}
 * @param {string} id
 * @param {string} name
 */
WebInspector.ProfileType = function(id, name)
{
    this._id = id;
    this._name = name;
    /** @type {!Array.<!WebInspector.ProfileHeader>} */
    this._profiles = [];
    this._profilesIdMap = {};
    /** @type {WebInspector.SidebarSectionTreeElement} */
    this.treeElement = null;
}

WebInspector.ProfileType.Events = {
    AddProfileHeader: "add-profile-header",
    RemoveProfileHeader: "remove-profile-header",
    ProgressUpdated: "progress-updated",
    ViewUpdated: "view-updated"
}

WebInspector.ProfileType.prototype = {
    /**
     * @return {boolean}
     */
    hasTemporaryView: function()
    {
        return false;
    },

    /**
     * @return {string|null}
     */
    fileExtension: function()
    {
        return null;
    },

    get statusBarItems()
    {
        return [];
    },

    get buttonTooltip()
    {
        return "";
    },

    get id()
    {
        return this._id;
    },

    get treeItemTitle()
    {
        return this._name;
    },

    get name()
    {
        return this._name;
    },

    /**
     * @return {boolean}
     */
    buttonClicked: function()
    {
        return false;
    },

    get description()
    {
        return "";
    },

    /**
     * @return {boolean}
     */
    isInstantProfile: function()
    {
        return false;
    },

    /**
     * @return {boolean}
     */
    isEnabled: function()
    {
        return true;
    },

    /**
     * @return {!Array.<!WebInspector.ProfileHeader>}
     */
    getProfiles: function()
    {
        return this._profiles.filter(function(profile) { return !profile.isTemporary; });
    },

    /**
     * @return {Element}
     */
    decorationElement: function()
    {
        return null;
    },

    /**
     * @nosideeffects
     * @param {number} uid
     * @return {WebInspector.ProfileHeader}
     */
    getProfile: function(uid)
    {
        return this._profilesIdMap[this._makeKey(uid)];
    },

    // Must be implemented by subclasses.
    /**
     * @param {string=} title
     * @return {!WebInspector.ProfileHeader}
     */
    createTemporaryProfile: function(title)
    {
        throw new Error("Needs implemented.");
    },

    /**
     * @param {ProfilerAgent.ProfileHeader} profile
     * @return {!WebInspector.ProfileHeader}
     */
    createProfile: function(profile)
    {
        throw new Error("Not supported for " + this._name + " profiles.");
    },

    /**
     * @nosideeffects
     * @param {number} id
     * @return {string}
     */
    _makeKey: function(id)
    {
        return id + '/' + escape(this.id);
    },

    /**
     * @param {!WebInspector.ProfileHeader} profile
     */
    addProfile: function(profile)
    {
        this._profiles.push(profile);
        // FIXME: uid only based key should be enough.
        this._profilesIdMap[this._makeKey(profile.uid)] = profile;
        this.dispatchEventToListeners(WebInspector.ProfileType.Events.AddProfileHeader, profile);
    },

    /**
     * @param {!WebInspector.ProfileHeader} profile
     */
    removeProfile: function(profile)
    {
        for (var i = 0; i < this._profiles.length; ++i) {
            if (this._profiles[i].uid === profile.uid) {
                this._profiles.splice(i, 1);
                break;
            }
        }
        delete this._profilesIdMap[this._makeKey(profile.uid)];
    },

    /**
     * @nosideeffects
     * @return {WebInspector.ProfileHeader}
     */
    findTemporaryProfile: function()
    {
        for (var i = 0; i < this._profiles.length; ++i) {
            if (this._profiles[i].isTemporary)
                return this._profiles[i];
        }
        return null;
    },

    _reset: function()
    {
        var profiles = this._profiles.slice(0);
        for (var i = 0; i < profiles.length; ++i) {
            var profile = profiles[i];
            var view = profile.existingView();
            if (view) {
                view.detach();
                if ("dispose" in view)
                    view.dispose();
            }
            this.dispatchEventToListeners(WebInspector.ProfileType.Events.RemoveProfileHeader, profile);
        }
        this.treeElement.removeChildren();
        this._profiles = [];
        this._profilesIdMap = {};
    },

    /**
     * @param {function(this:WebInspector.ProfileType, ?string, !Array.<!ProfilerAgent.ProfileHeader>)} populateCallback
     */
    _requestProfilesFromBackend: function(populateCallback)
    {
    },

    _populateProfiles: function()
    {
        /**
         * @param {?string} error
         * @param {!Array.<!ProfilerAgent.ProfileHeader>} profileHeaders
         */
        function populateCallback(error, profileHeaders) {
            if (error)
                return;
            profileHeaders.sort(function(a, b) { return a.uid - b.uid; });
            var count = profileHeaders.length;
            for (var i = 0; i < count; ++i)
                this.addProfile(this.createProfile(profileHeaders[i]));
        }
        this._requestProfilesFromBackend(populateCallback.bind(this));
    },

    __proto__: WebInspector.Object.prototype
}

/**
 * @constructor
 * @param {!WebInspector.ProfileType} profileType
 * @param {string} title
 * @param {number=} uid
 */
WebInspector.ProfileHeader = function(profileType, title, uid)
{
    this._profileType = profileType;
    this.title = title;
    this.isTemporary = uid === undefined;
    this.uid = this.isTemporary ? -1 : uid;
    this._fromFile = false;
}

WebInspector.ProfileHeader.prototype = {
    /**
     * @return {!WebInspector.ProfileType}
     */
    profileType: function()
    {
        return this._profileType;
    },

    /**
     * Must be implemented by subclasses.
     * @return {WebInspector.ProfileSidebarTreeElement}
     */
    createSidebarTreeElement: function()
    {
        throw new Error("Needs implemented.");
    },

    /**
     * @return {?WebInspector.View}
     */
    existingView: function()
    {
        return this._view;
    },

    /**
     * @param {!WebInspector.ProfilesPanel} panel
     * @return {!WebInspector.View}
     */
    view: function(panel)
    {
        if (!this._view)
            this._view = this.createView(panel);
        return this._view;
    },

    /**
     * @param {!WebInspector.ProfilesPanel} panel
     * @return {!WebInspector.View}
     */
    createView: function(panel)
    {
        throw new Error("Not implemented.");
    },

    dispose: function()
    {
    },

    /**
     * @param {Function} callback
     */
    load: function(callback)
    {
    },

    /**
     * @return {boolean}
     */
    canSaveToFile: function()
    {
        return false;
    },

    saveToFile: function()
    {
        throw new Error("Needs implemented");
    },

    /**
     * @param {File} file
     */
    loadFromFile: function(file)
    {
        throw new Error("Needs implemented");
    },

    /**
     * @return {boolean}
     */
    fromFile: function()
    {
        return this._fromFile;
    },

    setFromFile: function()
    {
        this._fromFile = true;
        this.uid = -2;
    }
}

/**
 * @constructor
 * @extends {WebInspector.Panel}
 * @implements {WebInspector.ContextMenu.Provider}
 * @param {string=} name
 * @param {WebInspector.ProfileType=} type
 */
WebInspector.ProfilesPanel = function(name, type)
{
    // If the name is not specified the ProfilesPanel works in multi-profile mode.
    var singleProfileMode = typeof name !== "undefined";
    name = name || "profiles";
    WebInspector.Panel.call(this, name);
    this.registerRequiredCSS("panelEnablerView.css");
    this.registerRequiredCSS("heapProfiler.css");
    this.registerRequiredCSS("profilesPanel.css");

    this.createSidebarViewWithTree();

    this.profilesItemTreeElement = new WebInspector.ProfilesSidebarTreeElement(this);
    this.sidebarTree.appendChild(this.profilesItemTreeElement);

    this._singleProfileMode = singleProfileMode;
    this._profileTypesByIdMap = {};

    this.profileViews = document.createElement("div");
    this.profileViews.id = "profile-views";
    this.splitView.mainElement.appendChild(this.profileViews);

    this._statusBarButtons = [];

    this.recordButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item");
    this.recordButton.addEventListener("click", this.toggleRecordButton, this);
    this._statusBarButtons.push(this.recordButton);

    this.clearResultsButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear all profiles."), "clear-status-bar-item");
    this.clearResultsButton.addEventListener("click", this._clearProfiles, this);
    this._statusBarButtons.push(this.clearResultsButton);

    this._profileTypeStatusBarItemsContainer = document.createElement("div");
    this._profileTypeStatusBarItemsContainer.className = "status-bar-items";

    this._profileViewStatusBarItemsContainer = document.createElement("div");
    this._profileViewStatusBarItemsContainer.className = "status-bar-items";

    if (singleProfileMode) {
        this._launcherView = this._createLauncherView();
        this._registerProfileType(/** @type {!WebInspector.ProfileType} */ (type));
        this._selectedProfileType = type;
        this._updateProfileTypeSpecificUI();
    } else {
        this._launcherView = new WebInspector.MultiProfileLauncherView(this);
        this._launcherView.addEventListener(WebInspector.MultiProfileLauncherView.EventTypes.ProfileTypeSelected, this._onProfileTypeSelected, this);

        this._registerProfileType(new WebInspector.CPUProfileType());
        this._registerProfileType(new WebInspector.HeapSnapshotProfileType());
        this._registerProfileType(new WebInspector.TrackingHeapSnapshotProfileType(this));
        if (!WebInspector.WorkerManager.isWorkerFrontend() && WebInspector.experimentsSettings.canvasInspection.isEnabled())
            this._registerProfileType(new WebInspector.CanvasProfileType());
    }

    this._profilesWereRequested = false;
    this._reset();

    this._createFileSelectorElement();
    this.element.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), true);
    this._registerShortcuts();

    WebInspector.ContextMenu.registerProvider(this);
}

WebInspector.ProfilesPanel.prototype = {
    _createFileSelectorElement: function()
    {
        if (this._fileSelectorElement)
            this.element.removeChild(this._fileSelectorElement);
        this._fileSelectorElement = WebInspector.createFileSelectorElement(this._loadFromFile.bind(this));
        this.element.appendChild(this._fileSelectorElement);
    },

    /**
     * @return {!WebInspector.ProfileLauncherView}
     */
    _createLauncherView: function()
    {
        return new WebInspector.ProfileLauncherView(this);
    },

    _findProfileTypeByExtension: function(fileName)
    {
        for (var id in this._profileTypesByIdMap) {
            var type = this._profileTypesByIdMap[id];
            var extension = type.fileExtension();
            if (!extension)
                continue;
            if (fileName.endsWith(type.fileExtension()))
                return type;
        }
        return null;
    },

    _registerShortcuts: function()
    {
        this.registerShortcuts(WebInspector.ProfilesPanelDescriptor.ShortcutKeys.StartStopRecording, this.toggleRecordButton.bind(this));
    },

    /**
     * @param {!File} file
     */
    _loadFromFile: function(file)
    {
        this._createFileSelectorElement();

        var profileType = this._findProfileTypeByExtension(file.name);
        if (!profileType) {
            var extensions = [];
            for (var id in this._profileTypesByIdMap) {
                var extension = this._profileTypesByIdMap[id].fileExtension();
                if (!extension)
                    continue;
                extensions.push(extension);
            }
            WebInspector.log(WebInspector.UIString("Can't load file. Only files with extensions '%s' can be loaded.", extensions.join("', '")));
            return;
        }

        if (!!profileType.findTemporaryProfile()) {
            WebInspector.log(WebInspector.UIString("Can't load profile when other profile is recording."));
            return;
        }

        var temporaryProfile = profileType.createTemporaryProfile(WebInspector.ProfilesPanelDescriptor.UserInitiatedProfileName + "." + file.name);
        temporaryProfile.setFromFile();
        profileType.addProfile(temporaryProfile);
        temporaryProfile.loadFromFile(file);
    },

    get statusBarItems()
    {
        return this._statusBarButtons.select("element").concat(this._profileTypeStatusBarItemsContainer, this._profileViewStatusBarItemsContainer);
    },

    /**
     * @param {WebInspector.Event|Event=} event
     * @return {boolean}
     */
    toggleRecordButton: function(event)
    {
        var isProfiling = this._selectedProfileType.buttonClicked();
        this.setRecordingProfile(this._selectedProfileType.id, isProfiling);
        return true;
    },

    _populateAllProfiles: function()
    {
        if (this._profilesWereRequested)
            return;
        this._profilesWereRequested = true;
        for (var typeId in this._profileTypesByIdMap)
            this._profileTypesByIdMap[typeId]._populateProfiles();
    },

    wasShown: function()
    {
        WebInspector.Panel.prototype.wasShown.call(this);
        this._populateAllProfiles();
    },

    /**
     * @param {WebInspector.Event} event
     */
    _onProfileTypeSelected: function(event)
    {
        this._selectedProfileType = /** @type {!WebInspector.ProfileType} */ (event.data);
        this._updateProfileTypeSpecificUI();
    },

    _updateProfileTypeSpecificUI: function()
    {
        this.recordButton.title = this._selectedProfileType.buttonTooltip;

        this._launcherView.updateProfileType(this._selectedProfileType);
        this._profileTypeStatusBarItemsContainer.removeChildren();
        var statusBarItems = this._selectedProfileType.statusBarItems;
        if (statusBarItems) {
            for (var i = 0; i < statusBarItems.length; ++i)
                this._profileTypeStatusBarItemsContainer.appendChild(statusBarItems[i]);
        }
        this._resize(this.splitView.sidebarWidth());
    },

    _reset: function()
    {
        WebInspector.Panel.prototype.reset.call(this);

        for (var typeId in this._profileTypesByIdMap)
            this._profileTypesByIdMap[typeId]._reset();

        delete this.visibleView;
        delete this.currentQuery;
        this.searchCanceled();

        this._profileGroups = {};
        this.recordButton.toggled = false;
        if (this._selectedProfileType)
            this.recordButton.title = this._selectedProfileType.buttonTooltip;
        this._launcherView.profileFinished();

        this.sidebarTreeElement.removeStyleClass("some-expandable");

        this.profileViews.removeChildren();
        this._profileViewStatusBarItemsContainer.removeChildren();

        this.removeAllListeners();

        this.recordButton.visible = true;
        this._profileViewStatusBarItemsContainer.removeStyleClass("hidden");
        this.clearResultsButton.element.removeStyleClass("hidden");
        this.profilesItemTreeElement.select();
        this._showLauncherView();
    },

    _showLauncherView: function()
    {
        this.closeVisibleView();
        this._profileViewStatusBarItemsContainer.removeChildren();
        this._launcherView.show(this.splitView.mainElement);
        this.visibleView = this._launcherView;
    },

    _clearProfiles: function()
    {
        ProfilerAgent.clearProfiles();
        HeapProfilerAgent.clearProfiles();
        this._reset();
    },

    _garbageCollectButtonClicked: function()
    {
        HeapProfilerAgent.collectGarbage();
    },

    /**
     * @param {!WebInspector.ProfileType} profileType
     */
    _registerProfileType: function(profileType)
    {
        this._profileTypesByIdMap[profileType.id] = profileType;
        this._launcherView.addProfileType(profileType);
        profileType.treeElement = new WebInspector.SidebarSectionTreeElement(profileType.treeItemTitle, null, true);
        profileType.treeElement.hidden = !this._singleProfileMode;
        this.sidebarTree.appendChild(profileType.treeElement);
        profileType.treeElement.childrenListElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), true);
        function onAddProfileHeader(event)
        {
            this._addProfileHeader(event.data);
        }
        function onRemoveProfileHeader(event)
        {
            this._removeProfileHeader(event.data);
        }
        function onProgressUpdated(event)
        {
            this._reportProfileProgress(event.data.profile, event.data.done, event.data.total);
        }
        profileType.addEventListener(WebInspector.ProfileType.Events.ViewUpdated, this._updateProfileTypeSpecificUI, this);
        profileType.addEventListener(WebInspector.ProfileType.Events.AddProfileHeader, onAddProfileHeader, this);
        profileType.addEventListener(WebInspector.ProfileType.Events.RemoveProfileHeader, onRemoveProfileHeader, this);
        profileType.addEventListener(WebInspector.ProfileType.Events.ProgressUpdated, onProgressUpdated, this);
    },

    /**
     * @param {Event} event
     */
    _handleContextMenuEvent: function(event)
    {
        var element = event.srcElement;
        while (element && !element.treeElement && element !== this.element)
            element = element.parentElement;
        if (!element)
            return;
        if (element.treeElement && element.treeElement.handleContextMenuEvent) {
            element.treeElement.handleContextMenuEvent(event, this);
            return;
        }

        var contextMenu = new WebInspector.ContextMenu(event);
        if (this.visibleView instanceof WebInspector.HeapSnapshotView) {
            this.visibleView.populateContextMenu(contextMenu, event);
        }
        if (element !== this.element || event.srcElement === this.sidebarElement) {
            contextMenu.appendItem(WebInspector.UIString("Load\u2026"), this._fileSelectorElement.click.bind(this._fileSelectorElement));
        }
        contextMenu.show();
    },

    /**
     * @nosideeffects
     * @param {string} text
     * @param {string} profileTypeId
     * @return {string}
     */
    _makeTitleKey: function(text, profileTypeId)
    {
        return escape(text) + '/' + escape(profileTypeId);
    },

    /**
     * @param {!WebInspector.ProfileHeader} profile
     */
    _addProfileHeader: function(profile)
    {
        var profileType = profile.profileType();
        var typeId = profileType.id;
        var sidebarParent = profileType.treeElement;
        sidebarParent.hidden = false;
        var small = false;
        var alternateTitle;

        if (!WebInspector.ProfilesPanelDescriptor.isUserInitiatedProfile(profile.title) && !profile.isTemporary) {
            var profileTitleKey = this._makeTitleKey(profile.title, typeId);
            if (!(profileTitleKey in this._profileGroups))
                this._profileGroups[profileTitleKey] = [];

            var group = this._profileGroups[profileTitleKey];
            group.push(profile);
            if (group.length === 2) {
                // Make a group TreeElement now that there are 2 profiles.
                group._profilesTreeElement = new WebInspector.ProfileGroupSidebarTreeElement(this, profile.title);

                // Insert at the same index for the first profile of the group.
                var index = sidebarParent.children.indexOf(group[0]._profilesTreeElement);
                sidebarParent.insertChild(group._profilesTreeElement, index);

                // Move the first profile to the group.
                var selected = group[0]._profilesTreeElement.selected;
                sidebarParent.removeChild(group[0]._profilesTreeElement);
                group._profilesTreeElement.appendChild(group[0]._profilesTreeElement);
                if (selected)
                    group[0]._profilesTreeElement.revealAndSelect();

                group[0]._profilesTreeElement.small = true;
                group[0]._profilesTreeElement.mainTitle = WebInspector.UIString("Run %d", 1);

                this.sidebarTreeElement.addStyleClass("some-expandable");
            }

            if (group.length >= 2) {
                sidebarParent = group._profilesTreeElement;
                alternateTitle = WebInspector.UIString("Run %d", group.length);
                small = true;
            }
        }

        var profileTreeElement = profile.createSidebarTreeElement();
        profile.sidebarElement = profileTreeElement;
        profileTreeElement.small = small;
        if (alternateTitle)
            profileTreeElement.mainTitle = alternateTitle;
        profile._profilesTreeElement = profileTreeElement;

        var temporaryProfile = profileType.findTemporaryProfile();
        if (profile.isTemporary || !temporaryProfile)
            sidebarParent.appendChild(profileTreeElement);
        else {
            if (temporaryProfile) {
                sidebarParent.insertBeforeChild(profileTreeElement, temporaryProfile._profilesTreeElement);
                this._removeTemporaryProfile(profile.profileType().id);
            }

            if (!this.visibleView || this.visibleView === this._launcherView)
                this._showProfile(profile);

            this.dispatchEventToListeners("profile added", {
                type: typeId
            });
        }
    },

    /**
     * @param {!WebInspector.ProfileHeader} profile
     */
    _removeProfileHeader: function(profile)
    {
        profile.dispose();
        profile.profileType().removeProfile(profile);

        var sidebarParent = profile.profileType().treeElement;
        var profileTitleKey = this._makeTitleKey(profile.title, profile.profileType().id);
        var group = this._profileGroups[profileTitleKey];
        if (group) {
            group.splice(group.indexOf(profile), 1);
            if (group.length === 1) {
                // Move the last profile out of its group and remove the group.
                var index = sidebarParent.children.indexOf(group._profilesTreeElement);
                sidebarParent.insertChild(group[0]._profilesTreeElement, index);
                group[0]._profilesTreeElement.small = false;
                group[0]._profilesTreeElement.mainTitle = group[0].title;
                sidebarParent.removeChild(group._profilesTreeElement);
            }
            if (group.length !== 0)
                sidebarParent = group._profilesTreeElement;
            else
                delete this._profileGroups[profileTitleKey];
        }
        sidebarParent.removeChild(profile._profilesTreeElement);

        // No other item will be selected if there aren't any other profiles, so
        // make sure that view gets cleared when the last profile is removed.
        if (!sidebarParent.children.length) {
            this.profilesItemTreeElement.select();
            this._showLauncherView();
            sidebarParent.hidden = !this._singleProfileMode;
        }
    },

    /**
     * @param {!WebInspector.ProfileHeader} profile
     * @return {WebInspector.View}
     */
    _showProfile: function(profile)
    {
        if (!profile || (profile.isTemporary && !profile.profileType().hasTemporaryView()))
            return null;

        var view = profile.view(this);
        if (view === this.visibleView)
            return view;

        this.closeVisibleView();

        view.show(this.profileViews);

        profile._profilesTreeElement._suppressOnSelect = true;
        profile._profilesTreeElement.revealAndSelect();
        delete profile._profilesTreeElement._suppressOnSelect;

        this.visibleView = view;

        this._profileViewStatusBarItemsContainer.removeChildren();

        var statusBarItems = view.statusBarItems;
        if (statusBarItems)
            for (var i = 0; i < statusBarItems.length; ++i)
                this._profileViewStatusBarItemsContainer.appendChild(statusBarItems[i]);

        return view;
    },

    /**
     * @param {HeapProfilerAgent.HeapSnapshotObjectId} snapshotObjectId
     * @param {string} viewName
     */
    showObject: function(snapshotObjectId, viewName)
    {
        var heapProfiles = this.getProfileType(WebInspector.HeapSnapshotProfileType.TypeId).getProfiles();
        for (var i = 0; i < heapProfiles.length; i++) {
            var profile = heapProfiles[i];
            // FIXME: allow to choose snapshot if there are several options.
            if (profile.maxJSObjectId >= snapshotObjectId) {
                this._showProfile(profile);
                var view = profile.view(this);
                view.changeView(viewName, function() {
                    view.dataGrid.highlightObjectByHeapSnapshotId(snapshotObjectId);
                });
                break;
            }
        }
    },

    /**
     * @param {string} typeId
     */
    _createTemporaryProfile: function(typeId)
    {
        var type = this.getProfileType(typeId);
        if (!type.findTemporaryProfile())
            type.addProfile(type.createTemporaryProfile());
    },

    /**
     * @param {string} typeId
     */
    _removeTemporaryProfile: function(typeId)
    {
        var temporaryProfile = this.getProfileType(typeId).findTemporaryProfile();
        if (!!temporaryProfile)
            this._removeProfileHeader(temporaryProfile);
    },

    /**
     * @param {string} typeId
     * @param {number} uid
     */
    getProfile: function(typeId, uid)
    {
        return this.getProfileType(typeId).getProfile(uid);
    },

    /**
     * @param {WebInspector.View} view
     */
    showView: function(view)
    {
        this._showProfile(view.profile);
    },

    /**
     * @param {string} typeId
     */
    getProfileType: function(typeId)
    {
        return this._profileTypesByIdMap[typeId];
    },

    /**
     * @param {string} typeId
     * @param {string} uid
     * @return {WebInspector.View}
     */
    showProfile: function(typeId, uid)
    {
        return this._showProfile(this.getProfile(typeId, Number(uid)));
    },

    closeVisibleView: function()
    {
        if (this.visibleView)
            this.visibleView.detach();
        delete this.visibleView;
    },

    /**
     * @param {string} query
     * @param {boolean} shouldJump
     */
    performSearch: function(query, shouldJump)
    {
        this.searchCanceled();

        var searchableViews = this._searchableViews();
        if (!searchableViews || !searchableViews.length)
            return;

        var visibleView = this.visibleView;

        var matchesCountUpdateTimeout = null;

        function updateMatchesCount()
        {
            WebInspector.searchController.updateSearchMatchesCount(this._totalSearchMatches, this);
            WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this);
            matchesCountUpdateTimeout = null;
        }

        function updateMatchesCountSoon()
        {
            if (matchesCountUpdateTimeout)
                return;
            // Update the matches count every half-second so it doesn't feel twitchy.
            matchesCountUpdateTimeout = setTimeout(updateMatchesCount.bind(this), 500);
        }

        function finishedCallback(view, searchMatches)
        {
            if (!searchMatches)
                return;

            this._totalSearchMatches += searchMatches;
            this._searchResults.push(view);

            this.searchMatchFound(view, searchMatches);

            updateMatchesCountSoon.call(this);

            if (shouldJump && view === visibleView)
                view.jumpToFirstSearchResult();
        }

        var i = 0;
        var panel = this;
        var boundFinishedCallback = finishedCallback.bind(this);
        var chunkIntervalIdentifier = null;

        // Split up the work into chunks so we don't block the
        // UI thread while processing.

        function processChunk()
        {
            var view = searchableViews[i];

            if (++i >= searchableViews.length) {
                if (panel._currentSearchChunkIntervalIdentifier === chunkIntervalIdentifier)
                    delete panel._currentSearchChunkIntervalIdentifier;
                clearInterval(chunkIntervalIdentifier);
            }

            if (!view)
                return;

            view.currentQuery = query;
            view.performSearch(query, boundFinishedCallback);
        }

        processChunk();

        chunkIntervalIdentifier = setInterval(processChunk, 25);
        this._currentSearchChunkIntervalIdentifier = chunkIntervalIdentifier;
    },

    jumpToNextSearchResult: function()
    {
        if (!this.showView || !this._searchResults || !this._searchResults.length)
            return;

        var showFirstResult = false;

        this._currentSearchResultIndex = this._searchResults.indexOf(this.visibleView);
        if (this._currentSearchResultIndex === -1) {
            this._currentSearchResultIndex = 0;
            showFirstResult = true;
        }

        var currentView = this._searchResults[this._currentSearchResultIndex];

        if (currentView.showingLastSearchResult()) {
            if (++this._currentSearchResultIndex >= this._searchResults.length)
                this._currentSearchResultIndex = 0;
            currentView = this._searchResults[this._currentSearchResultIndex];
            showFirstResult = true;
        }

        WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this);

        if (currentView !== this.visibleView) {
            this.showView(currentView);
            WebInspector.searchController.showSearchField();
        }

        if (showFirstResult)
            currentView.jumpToFirstSearchResult();
        else
            currentView.jumpToNextSearchResult();
    },

    jumpToPreviousSearchResult: function()
    {
        if (!this.showView || !this._searchResults || !this._searchResults.length)
            return;

        var showLastResult = false;

        this._currentSearchResultIndex = this._searchResults.indexOf(this.visibleView);
        if (this._currentSearchResultIndex === -1) {
            this._currentSearchResultIndex = 0;
            showLastResult = true;
        }

        var currentView = this._searchResults[this._currentSearchResultIndex];

        if (currentView.showingFirstSearchResult()) {
            if (--this._currentSearchResultIndex < 0)
                this._currentSearchResultIndex = (this._searchResults.length - 1);
            currentView = this._searchResults[this._currentSearchResultIndex];
            showLastResult = true;
        }

        WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this);

        if (currentView !== this.visibleView) {
            this.showView(currentView);
            WebInspector.searchController.showSearchField();
        }

        if (showLastResult)
            currentView.jumpToLastSearchResult();
        else
            currentView.jumpToPreviousSearchResult();
    },

    /**
     * @return {!Array.<!WebInspector.ProfileHeader>}
     */
    _getAllProfiles: function()
    {
        var profiles = [];
        for (var typeId in this._profileTypesByIdMap)
            profiles = profiles.concat(this._profileTypesByIdMap[typeId].getProfiles());
        return profiles;
    },

    /**
     * @return {!Array.<!WebInspector.View>}
     */
    _searchableViews: function()
    {
        var profiles = this._getAllProfiles();
        var searchableViews = [];
        for (var i = 0; i < profiles.length; ++i) {
            var view = profiles[i].view(this);
            if (view.performSearch)
                searchableViews.push(view)
        }
        var index = searchableViews.indexOf(this.visibleView);
        if (index > 0) {
            // Move visibleView to the first position.
            searchableViews[index] = searchableViews[0];
            searchableViews[0] = this.visibleView;
        }
        return searchableViews;
    },

    searchMatchFound: function(view, matches)
    {
        view.profile._profilesTreeElement.searchMatches = matches;
    },

    searchCanceled: function()
    {
        if (this._searchResults) {
            for (var i = 0; i < this._searchResults.length; ++i) {
                var view = this._searchResults[i];
                if (view.searchCanceled)
                    view.searchCanceled();
                delete view.currentQuery;
            }
        }

        WebInspector.Panel.prototype.searchCanceled.call(this);

        if (this._currentSearchChunkIntervalIdentifier) {
            clearInterval(this._currentSearchChunkIntervalIdentifier);
            delete this._currentSearchChunkIntervalIdentifier;
        }

        this._totalSearchMatches = 0;
        this._currentSearchResultIndex = 0;
        this._searchResults = [];

        var profiles = this._getAllProfiles();
        for (var i = 0; i < profiles.length; ++i)
            profiles[i]._profilesTreeElement.searchMatches = 0;
    },

    /**
     * @param {!WebInspector.Event} event
     */
    sidebarResized: function(event)
    {
        var sidebarWidth = /** @type {number} */ (event.data);
        this._resize(sidebarWidth);
    },

    onResize: function()
    {
        this._resize(this.splitView.sidebarWidth());
    },

    /**
     * @param {number} sidebarWidth
     */
    _resize: function(sidebarWidth)
    {
        var lastItemElement = this._statusBarButtons[this._statusBarButtons.length - 1].element;
        var left = lastItemElement.totalOffsetLeft() + lastItemElement.offsetWidth;
        this._profileTypeStatusBarItemsContainer.style.left = left + "px";
        left += this._profileTypeStatusBarItemsContainer.offsetWidth - 1;
        this._profileViewStatusBarItemsContainer.style.left = Math.max(left, sidebarWidth) + "px";
    },

    /**
     * @param {string} profileType
     * @param {boolean} isProfiling
     */
    setRecordingProfile: function(profileType, isProfiling)
    {
        var profileTypeObject = this.getProfileType(profileType);
        this.recordButton.toggled = isProfiling;
        this.recordButton.title = profileTypeObject.buttonTooltip;
        if (isProfiling) {
            this._launcherView.profileStarted();
            this._createTemporaryProfile(profileType);
            if (profileTypeObject.hasTemporaryView())
                this._showProfile(profileTypeObject.findTemporaryProfile());
        } else
            this._launcherView.profileFinished();
    },

    /**
     * @param {!WebInspector.ProfileHeader} profile
     * @param {number} done
     * @param {number} total
     */
    _reportProfileProgress: function(profile, done, total)
    {
        profile.sidebarElement.subtitle = WebInspector.UIString("%.0f%", (done / total) * 100);
        profile.sidebarElement.wait = true;
    },

    /** 
     * @param {WebInspector.ContextMenu} contextMenu
     * @param {Object} target
     */
    appendApplicableItems: function(event, contextMenu, target)
    {
        if (WebInspector.inspectorView.currentPanel() !== this)
            return;

        var object = /** @type {WebInspector.RemoteObject} */ (target);
        var objectId = object.objectId;
        if (!objectId)
            return;

        var heapProfiles = this.getProfileType(WebInspector.HeapSnapshotProfileType.TypeId).getProfiles();
        if (!heapProfiles.length)
            return;

        function revealInView(viewName)
        {
            HeapProfilerAgent.getHeapObjectId(objectId, didReceiveHeapObjectId.bind(this, viewName));
        }

        function didReceiveHeapObjectId(viewName, error, result)
        {
            if (WebInspector.inspectorView.currentPanel() !== this)
                return;
            if (!error)
                this.showObject(result, viewName);
        }

        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in Dominators view" : "Reveal in Dominators View"), revealInView.bind(this, "Dominators"));
        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in Summary view" : "Reveal in Summary View"), revealInView.bind(this, "Summary"));
    },

    __proto__: WebInspector.Panel.prototype
}

/**
 * @constructor
 * @extends {WebInspector.SidebarTreeElement}
 * @param {!WebInspector.ProfileHeader} profile
 * @param {string} titleFormat
 * @param {string} className
 */
WebInspector.ProfileSidebarTreeElement = function(profile, titleFormat, className)
{
    this.profile = profile;
    this._titleFormat = titleFormat;

    if (WebInspector.ProfilesPanelDescriptor.isUserInitiatedProfile(this.profile.title))
        this._profileNumber = WebInspector.ProfilesPanelDescriptor.userInitiatedProfileIndex(this.profile.title);

    WebInspector.SidebarTreeElement.call(this, className, "", "", profile, false);

    this.refreshTitles();
}

WebInspector.ProfileSidebarTreeElement.prototype = {
    onselect: function()
    {
        if (!this._suppressOnSelect)
            this.treeOutline.panel._showProfile(this.profile);
    },

    ondelete: function()
    {
        this.treeOutline.panel._removeProfileHeader(this.profile);
        return true;
    },

    get mainTitle()
    {
        if (this._mainTitle)
            return this._mainTitle;
        if (WebInspector.ProfilesPanelDescriptor.isUserInitiatedProfile(this.profile.title))
            return WebInspector.UIString(this._titleFormat, this._profileNumber);
        return this.profile.title;
    },

    set mainTitle(x)
    {
        this._mainTitle = x;
        this.refreshTitles();
    },

    set searchMatches(matches)
    {
        if (!matches) {
            if (!this.bubbleElement)
                return;
            this.bubbleElement.removeStyleClass("search-matches");
            this.bubbleText = "";
            return;
        }

        this.bubbleText = matches;
        this.bubbleElement.addStyleClass("search-matches");
    },

    /**
     * @param {!Event} event
     * @param {!WebInspector.ProfilesPanel} panel
     */
    handleContextMenuEvent: function(event, panel)
    {
        var profile = this.profile;
        var contextMenu = new WebInspector.ContextMenu(event);
        // FIXME: use context menu provider
        contextMenu.appendItem(WebInspector.UIString("Load\u2026"), panel._fileSelectorElement.click.bind(panel._fileSelectorElement));
        if (profile.canSaveToFile())
            contextMenu.appendItem(WebInspector.UIString("Save\u2026"), profile.saveToFile.bind(profile));
        contextMenu.appendItem(WebInspector.UIString("Delete"), this.ondelete.bind(this));
        contextMenu.show();
    },

    __proto__: WebInspector.SidebarTreeElement.prototype
}

/**
 * @constructor
 * @extends {WebInspector.SidebarTreeElement}
 * @param {WebInspector.ProfilesPanel} panel
 * @param {string} title
 * @param {string=} subtitle
 */
WebInspector.ProfileGroupSidebarTreeElement = function(panel, title, subtitle)
{
    WebInspector.SidebarTreeElement.call(this, "profile-group-sidebar-tree-item", title, subtitle, null, true);
    this._panel = panel;
}

WebInspector.ProfileGroupSidebarTreeElement.prototype = {
    onselect: function()
    {
        if (this.children.length > 0)
            this._panel._showProfile(this.children[this.children.length - 1].profile);
    },

    __proto__: WebInspector.SidebarTreeElement.prototype
}

/**
 * @constructor
 * @extends {WebInspector.SidebarTreeElement}
 * @param {!WebInspector.ProfilesPanel} panel
 */
WebInspector.ProfilesSidebarTreeElement = function(panel)
{
    this._panel = panel;
    this.small = false;

    WebInspector.SidebarTreeElement.call(this, "profile-launcher-view-tree-item", WebInspector.UIString("Profiles"), "", null, false);
}

WebInspector.ProfilesSidebarTreeElement.prototype = {
    onselect: function()
    {
        this._panel._showLauncherView();
    },

    get selectable()
    {
        return true;
    },

    __proto__: WebInspector.SidebarTreeElement.prototype
}


/**
 * @constructor
 * @extends {WebInspector.ProfilesPanel}
 */
WebInspector.CPUProfilerPanel = function()
{
    WebInspector.ProfilesPanel.call(this, "cpu-profiler", new WebInspector.CPUProfileType());
}

WebInspector.CPUProfilerPanel.prototype = {
    __proto__: WebInspector.ProfilesPanel.prototype
}


/**
 * @constructor
 * @extends {WebInspector.ProfilesPanel}
 */
WebInspector.HeapProfilerPanel = function()
{
    var heapSnapshotProfileType = new WebInspector.HeapSnapshotProfileType();
    WebInspector.ProfilesPanel.call(this, "heap-profiler", heapSnapshotProfileType);
    this._singleProfileMode = false;
    this._registerProfileType(new WebInspector.TrackingHeapSnapshotProfileType(this));
    this._launcherView.addEventListener(WebInspector.MultiProfileLauncherView.EventTypes.ProfileTypeSelected, this._onProfileTypeSelected, this);
    this._launcherView._profileTypeChanged(heapSnapshotProfileType);
}

WebInspector.HeapProfilerPanel.prototype = {
    _createLauncherView: function()
    {
        return new WebInspector.MultiProfileLauncherView(this);
    },

    __proto__: WebInspector.ProfilesPanel.prototype
}


/**
 * @constructor
 * @extends {WebInspector.ProfilesPanel}
 */
WebInspector.CanvasProfilerPanel = function()
{
    WebInspector.ProfilesPanel.call(this, "canvas-profiler", new WebInspector.CanvasProfileType());
}

WebInspector.CanvasProfilerPanel.prototype = {
    __proto__: WebInspector.ProfilesPanel.prototype
}


importScript("ProfileDataGridTree.js");
importScript("BottomUpProfileDataGridTree.js");
importScript("CPUProfileView.js");
importScript("FlameChart.js");
importScript("HeapSnapshot.js");
importScript("HeapSnapshotDataGrids.js");
importScript("HeapSnapshotGridNodes.js");
importScript("HeapSnapshotLoader.js");
importScript("HeapSnapshotProxy.js");
importScript("HeapSnapshotView.js");
importScript("HeapSnapshotWorkerDispatcher.js");
importScript("JSHeapSnapshot.js");
importScript("ProfileLauncherView.js");
importScript("TopDownProfileDataGridTree.js");
importScript("CanvasProfileView.js");
importScript("CanvasReplayStateView.js");
