text: // Copyright 2016 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  import {BottomUpProfileDataGridTree} from './BottomUpProfileDataGrid.js';
  import {CPUProfileFlameChart, ProfileFlameChartDataProvider} from './CPUProfileFlameChart.js';  // eslint-disable-line no-unused-vars
  import {Formatter, ProfileDataGridNode, ProfileDataGridTree} from './ProfileDataGrid.js';  // eslint-disable-line no-unused-vars
  import {DataDisplayDelegate, Events, ProfileHeader, ProfileType} from './ProfileHeader.js';  // eslint-disable-line no-unused-vars
  import {ProfileSidebarTreeElement} from './ProfileSidebarTreeElement.js';
  import {TopDownProfileDataGridTree} from './TopDownProfileDataGrid.js';
  
  /**
   * @implements {UI.Searchable}
   * @unrestricted
   */
  export class ProfileView extends UI.SimpleView {
    constructor() {
      super(Common.UIString('Profile'));
  
      this._profile = null;
  
      this._searchableView = new UI.SearchableView(this);
      this._searchableView.setPlaceholder(Common.UIString('Find by cost (>50ms), name or file'));
      this._searchableView.show(this.element);
  
      const columns = /** @type {!Array<!DataGrid.ColumnDescriptor>} */ ([]);
      columns.push({
        id: 'self',
        title: this.columnHeader('self'),
        width: '120px',
        fixedWidth: true,
        sortable: true,
        sort: DataGrid.DataGrid.Order.Descending
      });
      columns.push({id: 'total', title: this.columnHeader('total'), width: '120px', fixedWidth: true, sortable: true});
      columns.push({id: 'function', title: Common.UIString('Function'), disclosure: true, sortable: true});
  
      this.dataGrid = new DataGrid.DataGrid({displayName: ls`Profiler`, columns});
      this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortProfile, this);
      this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, this._nodeSelected.bind(this, true));
      this.dataGrid.addEventListener(DataGrid.DataGrid.Events.DeselectedNode, this._nodeSelected.bind(this, false));
      this.dataGrid.setRowContextMenuCallback(this._populateContextMenu.bind(this));
  
      this.viewSelectComboBox = new UI.ToolbarComboBox(this._changeView.bind(this), ls`Profile view mode`);
  
      this.focusButton = new UI.ToolbarButton(Common.UIString('Focus selected function'), 'largeicon-visibility');
      this.focusButton.setEnabled(false);
      this.focusButton.addEventListener(UI.ToolbarButton.Events.Click, this._focusClicked, this);
  
      this.excludeButton = new UI.ToolbarButton(Common.UIString('Exclude selected function'), 'largeicon-delete');
      this.excludeButton.setEnabled(false);
      this.excludeButton.addEventListener(UI.ToolbarButton.Events.Click, this._excludeClicked, this);
  
      this.resetButton = new UI.ToolbarButton(Common.UIString('Restore all functions'), 'largeicon-refresh');
      this.resetButton.setEnabled(false);
      this.resetButton.addEventListener(UI.ToolbarButton.Events.Click, this._resetClicked, this);
  
      this._linkifier = new Components.Linkifier(maxLinkLength);
    }
  
    /**
     * @param {!Array<!{title: string, value: string}>} entryInfo
     * @return {!Element}
     */
    static buildPopoverTable(entryInfo) {
      const table = createElement('table');
      for (const entry of entryInfo) {
        const row = table.createChild('tr');
        row.createChild('td').textContent = entry.title;
        row.createChild('td').textContent = entry.value;
      }
      return table;
    }
  
    /**
     * @param {!SDK.ProfileTreeModel} profile
     */
    setProfile(profile) {
      this._profile = profile;
      this._bottomUpProfileDataGridTree = null;
      this._topDownProfileDataGridTree = null;
      this._changeView();
      this.refresh();
    }
  
    /**
     * @return {?SDK.ProfileTreeModel}
     */
    profile() {
      return this._profile;
    }
  
    /**
     * @param {!Formatter} nodeFormatter
     * @param {!Array<string>=} viewTypes
     * @protected
     */
    initialize(nodeFormatter, viewTypes) {
      this._nodeFormatter = nodeFormatter;
  
      this._viewType = self.Common.settings.createSetting('profileView', ViewTypes.Heavy);
      viewTypes = viewTypes || [ViewTypes.Flame, ViewTypes.Heavy, ViewTypes.Tree];
  
      const optionNames = new Map([
        [ViewTypes.Flame, ls`Chart`],
        [ViewTypes.Heavy, ls`Heavy (Bottom Up)`],
        [ViewTypes.Tree, ls`Tree (Top Down)`],
        [ViewTypes.Text, ls`Text (Top Down)`],
      ]);
  
      const options =
          new Map(viewTypes.map(type => [type, this.viewSelectComboBox.createOption(optionNames.get(type), type)]));
      const optionName = this._viewType.get() || viewTypes[0];
      const option = options.get(optionName) || options.get(viewTypes[0]);
      this.viewSelectComboBox.select(option);
  
      this._changeView();
      if (this._flameChart) {
        this._flameChart.update();
      }
    }
  
    /**
     * @override
     */
    focus() {
      if (this._flameChart) {
        this._flameChart.focus();
      } else {
        super.focus();
      }
    }
  
    /**
     * @param {string} columnId
     * @return {string}
     */
    columnHeader(columnId) {
      throw 'Not implemented';
    }
  
    /**
     * @param {number} timeLeft
     * @param {number} timeRight
     */
    selectRange(timeLeft, timeRight) {
      if (!this._flameChart) {
        return;
      }
      this._flameChart.selectRange(timeLeft, timeRight);
    }
  
    /**
     * @override
     * @return {!Promise<!Array<!UI.ToolbarItem>>}
     */
    async toolbarItems() {
      return [this.viewSelectComboBox, this.focusButton, this.excludeButton, this.resetButton];
    }
  
    /**
     * @return {!ProfileDataGridTree}
     */
    _getBottomUpProfileDataGridTree() {
      if (!this._bottomUpProfileDataGridTree) {
        this._bottomUpProfileDataGridTree = new BottomUpProfileDataGridTree(
            this._nodeFormatter, this._searchableView, this._profile.root, this.adjustedTotal);
      }
      return this._bottomUpProfileDataGridTree;
    }
  
    /**
     * @return {!ProfileDataGridTree}
     */
    _getTopDownProfileDataGridTree() {
      if (!this._topDownProfileDataGridTree) {
        this._topDownProfileDataGridTree = new TopDownProfileDataGridTree(
            this._nodeFormatter, this._searchableView, this._profile.root, this.adjustedTotal);
      }
      return this._topDownProfileDataGridTree;
    }
  
    /**
     * @param {!UI.ContextMenu} contextMenu
     * @param {!DataGrid.DataGridNode} gridNode
     */
    _populateContextMenu(contextMenu, gridNode) {
      const node = /** @type {!ProfileDataGridNode} */ (gridNode);
      if (node.linkElement && !contextMenu.containsTarget(node.linkElement)) {
        contextMenu.appendApplicableItems(node.linkElement);
      }
    }
  
    /**
     * @override
     */
    willHide() {
      this._currentSearchResultIndex = -1;
    }
  
    refresh() {
      if (!this.profileDataGridTree) {
        return;
      }
      const selectedProfileNode = this.dataGrid.selectedNode ? this.dataGrid.selectedNode.profileNode : null;
  
      this.dataGrid.rootNode().removeChildren();
  
      const children = this.profileDataGridTree.children;
      const count = children.length;
  
      for (let index = 0; index < count; ++index) {
        this.dataGrid.rootNode().appendChild(children[index]);
      }
  
      if (selectedProfileNode) {
        selectedProfileNode.selected = true;
      }
    }
  
    refreshVisibleData() {
      let child = this.dataGrid.rootNode().children[0];
      while (child) {
        child.refresh();
        child = child.traverseNextNode(false, null, true);
      }
    }
  
    /**
     * @return {!UI.SearchableView}
     */
    searchableView() {
      return this._searchableView;
    }
  
    /**
     * @override
     * @return {boolean}
     */
    supportsCaseSensitiveSearch() {
      return true;
    }
  
    /**
     * @override
     * @return {boolean}
     */
    supportsRegexSearch() {
      return false;
    }
  
    /**
     * @override
     */
    searchCanceled() {
      this._searchableElement.searchCanceled();
    }
  
    /**
     * @override
     * @param {!UI.SearchableView.SearchConfig} searchConfig
     * @param {boolean} shouldJump
     * @param {boolean=} jumpBackwards
     */
    performSearch(searchConfig, shouldJump, jumpBackwards) {
      this._searchableElement.performSearch(searchConfig, shouldJump, jumpBackwards);
    }
  
    /**
     * @override
     */
    jumpToNextSearchResult() {
      this._searchableElement.jumpToNextSearchResult();
    }
  
    /**
     * @override
     */
    jumpToPreviousSearchResult() {
      this._searchableElement.jumpToPreviousSearchResult();
    }
  
    /**
     * @return {!Components.Linkifier}
     */
    linkifier() {
      return this._linkifier;
    }
  
    _ensureTextViewCreated() {
      if (this._textView) {
        return;
      }
      this._textView = new UI.SimpleView(ls`Call tree`);
      this._textView.registerRequiredCSS('profiler/profilesPanel.css');
      this.populateTextView(this._textView);
    }
  
    /**
     * @param {!UI.SimpleView} view
     */
    populateTextView(view) {
    }
  
    /**
     * @return {!ProfileFlameChartDataProvider}
     */
    createFlameChartDataProvider() {
      throw 'Not implemented';
    }
  
    _ensureFlameChartCreated() {
      if (this._flameChart) {
        return;
      }
      this._dataProvider = this.createFlameChartDataProvider();
      this._flameChart = new CPUProfileFlameChart(this._searchableView, this._dataProvider);
      this._flameChart.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this._onEntryInvoked.bind(this));
    }
  
    /**
     * @param {!Common.Event} event
     */
    _onEntryInvoked(event) {
      const entryIndex = event.data;
      const node = this._dataProvider._entryNodes[entryIndex];
      const debuggerModel = this._profileHeader._debuggerModel;
      if (!node || !node.scriptId || !debuggerModel) {
        return;
      }
      const script = debuggerModel.scriptForId(node.scriptId);
      if (!script) {
        return;
      }
      const location = /** @type {!SDK.DebuggerModel.Location} */ (
          debuggerModel.createRawLocation(script, node.lineNumber, node.columnNumber));
      Common.Revealer.reveal(self.Bindings.debuggerWorkspaceBinding.rawLocationToUILocation(location));
    }
  
    _changeView() {
      if (!this._profile) {
        return;
      }
  
      this._searchableView.closeSearch();
  
      if (this._visibleView) {
        this._visibleView.detach();
      }
  
      this._viewType.set(this.viewSelectComboBox.selectedOption().value);
      switch (this._viewType.get()) {
        case ViewTypes.Flame:
          this._ensureFlameChartCreated();
          this._visibleView = this._flameChart;
          this._searchableElement = this._flameChart;
          break;
        case ViewTypes.Tree:
          this.profileDataGridTree = this._getTopDownProfileDataGridTree();
          this._sortProfile();
          this._visibleView = this.dataGrid.asWidget();
          this._searchableElement = this.profileDataGridTree;
          break;
        case ViewTypes.Heavy:
          this.profileDataGridTree = this._getBottomUpProfileDataGridTree();
          this._sortProfile();
          this._visibleView = this.dataGrid.asWidget();
          this._searchableElement = this.profileDataGridTree;
          break;
        case ViewTypes.Text:
          this._ensureTextViewCreated();
          this._visibleView = this._textView;
          this._searchableElement = this._textView;
          break;
      }
  
      const isFlame = this._viewType.get() === ViewTypes.Flame;
      this.focusButton.setVisible(!isFlame);
      this.excludeButton.setVisible(!isFlame);
      this.resetButton.setVisible(!isFlame);
  
      this._visibleView.show(this._searchableView.element);
    }
  
    /**
     * @param {boolean} selected
     */
    _nodeSelected(selected) {
      this.focusButton.setEnabled(selected);
      this.excludeButton.setEnabled(selected);
    }
  
    /**
     * @param {!Common.Event} event
     */
    _focusClicked(event) {
      if (!this.dataGrid.selectedNode) {
        return;
      }
  
      this.resetButton.setEnabled(true);
      this.profileDataGridTree.focus(this.dataGrid.selectedNode);
      this.refresh();
      this.refreshVisibleData();
      Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuProfileNodeFocused);
    }
  
    /**
     * @param {!Common.Event} event
     */
    _excludeClicked(event) {
      const selectedNode = this.dataGrid.selectedNode;
  
      if (!selectedNode) {
        return;
      }
  
      selectedNode.deselect();
  
      this.resetButton.setEnabled(true);
      this.profileDataGridTree.exclude(selectedNode);
      this.refresh();
      this.refreshVisibleData();
      Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuProfileNodeExcluded);
    }
  
    /**
     * @param {!Common.Event} event
     */
    _resetClicked(event) {
      this.resetButton.setEnabled(false);
      this.profileDataGridTree.restore();
      this._linkifier.reset();
      this.refresh();
      this.refreshVisibleData();
    }
  
    _sortProfile() {
      const sortAscending = this.dataGrid.isSortOrderAscending();
      const sortColumnId = this.dataGrid.sortColumnId();
      const sortProperty = sortColumnId === 'function' ? 'functionName' : sortColumnId || '';
      this.profileDataGridTree.sort(ProfileDataGridTree.propertyComparator(sortProperty, sortAscending));
  
      this.refresh();
    }
  }
  
  export const maxLinkLength = 30;
  
  /** @enum {string} */
  export const ViewTypes = {
    Flame: 'Flame',
    Tree: 'Tree',
    Heavy: 'Heavy',
    Text: 'Text'
  };
  
  /**
   * @implements {Common.OutputStream}
   * @unrestricted
   */
  export class WritableProfileHeader extends ProfileHeader {
    /**
     * @param {?SDK.DebuggerModel} debuggerModel
     * @param {!ProfileType} type
     * @param {string=} title
     */
    constructor(debuggerModel, type, title) {
      super(type, title || Common.UIString('Profile %d', type.nextProfileUid()));
      this._debuggerModel = debuggerModel;
      this._tempFile = null;
    }
  
    /**
     * @param {!Bindings.ChunkedReader} reader
     */
    _onChunkTransferred(reader) {
      this.updateStatus(Common.UIString('Loading\u2026 %d%%', Number.bytesToString(this._jsonifiedProfile.length)));
    }
  
    /**
     * @param {!Bindings.ChunkedReader} reader
     */
    _onError(reader) {
      this.updateStatus(Common.UIString(`File '%s' read error: %s`, reader.fileName(), reader.error().message));
    }
  
    /**
     * @override
     * @param {string} text
     * @return {!Promise}
     */
    async write(text) {
      this._jsonifiedProfile += text;
    }
  
    /**
     * @override
     */
    close() {
    }
  
    /**
     * @override
     */
    dispose() {
      this.removeTempFile();
    }
  
    /**
     * @override
     * @param {!DataDisplayDelegate} panel
     * @return {!ProfileSidebarTreeElement}
     */
    createSidebarTreeElement(panel) {
      return new ProfileSidebarTreeElement(panel, this, 'profile-sidebar-tree-item');
    }
  
    /**
     * @override
     * @return {boolean}
     */
    canSaveToFile() {
      return !this.fromFile() && this._protocolProfile;
    }
  
    /**
     * @override
     */
    async saveToFile() {
      const fileOutputStream = new Bindings.FileOutputStream();
      this._fileName = this._fileName ||
          `${this.profileType().typeName()}-${new Date().toISO8601Compact()}${this.profileType().fileExtension()}`;
      const accepted = await fileOutputStream.open(this._fileName);
      if (!accepted || !this._tempFile) {
        return;
      }
      const data = await this._tempFile.read();
      if (data) {
        await fileOutputStream.write(data);
      }
      fileOutputStream.close();
    }
  
    /**
     * @override
     * @param {!File} file
     * @return {!Promise<?Error>}
     */
    async loadFromFile(file) {
      this.updateStatus(Common.UIString('Loading\u2026'), true);
      const fileReader = new Bindings.ChunkedFileReader(file, 10000000, this._onChunkTransferred.bind(this));
      this._jsonifiedProfile = '';
  
      const success = await fileReader.read(this);
      if (!success) {
        this._onError(fileReader);
        return new Error(Common.UIString('Failed to read file'));
      }
  
      this.updateStatus(Common.UIString('Parsing\u2026'), true);
      let error = null;
      try {
        this._profile = /** @type {!Protocol.Profiler.Profile} */ (JSON.parse(this._jsonifiedProfile));
        this.setProfile(this._profile);
        this.updateStatus(Common.UIString('Loaded'), false);
      } catch (e) {
        error = e;
        this.profileType().removeProfile(this);
      }
      this._jsonifiedProfile = null;
  
      if (this.profileType().profileBeingRecorded() === this) {
        this.profileType().setProfileBeingRecorded(null);
      }
      return error;
    }
  
    /**
     * @param {!Protocol.Profiler.Profile} profile
     */
    setProtocolProfile(profile) {
      this.setProfile(profile);
      this._protocolProfile = profile;
      this._tempFile = new Bindings.TempFile();
      this._tempFile.write([JSON.stringify(profile)]);
      if (this.canSaveToFile()) {
        this.dispatchEventToListeners(Events.ProfileReceived);
      }
    }
  }
  