// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */

import '../../ui/components/icon_button/icon_button.js';

import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as CPUProfile from '../../models/cpu_profile/cpu_profile.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import type * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as SettingsUI from '../../ui/legacy/components/settings_ui/settings_ui.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import {BottomUpProfileDataGridTree} from './BottomUpProfileDataGrid.js';
import {Events, HeapTimelineOverview, type IdsRangeChangedEvent, type Samples} from './HeapTimelineOverview.js';
import {type Formatter, type ProfileDataGridNode, ProfileDataGridTree} from './ProfileDataGrid.js';
import {ProfileFlameChart, ProfileFlameChartDataProvider} from './ProfileFlameChartDataProvider.js';
import {ProfileEvents, type ProfileHeader, ProfileType} from './ProfileHeader.js';
import {TopDownProfileDataGridTree} from './TopDownProfileDataGrid.js';
import {WritableProfileHeader} from './WritableProfileHeader.js';

const {repeat, ref} = Directives;

const UIStrings = {
  /**
   * @description The reported total size used in the selected time frame of the allocation sampling profile
   * @example {3 MB} PH1
   */
  selectedSizeS: 'Selected size: {PH1}',
  /**
   * @description Name of column header that reports the size (in terms of bytes) used for a particular part of the heap, excluding the size of the children nodes of this part of the heap
   */
  selfSizeBytes: 'Self size',
  /**
   * @description Name of column header that reports the total size (in terms of bytes) used for a particular part of the heap
   */
  totalSizeBytes: 'Total size',
  /**
   * @description Button text to stop profiling the heap
   */
  stopHeapProfiling: 'Stop heap profiling',
  /**
   * @description Button text to start profiling the heap
   */
  startHeapProfiling: 'Start heap profiling',
  /**
   * @description Progress update that the profiler is recording the contents of the heap
   */
  recording: 'Recording…',
  /**
   * @description Icon title in Heap Profile View of a profiler tool
   */
  heapProfilerIsRecording: 'Heap profiler is recording',
  /**
   * @description Progress update that the profiler is in the process of stopping its recording of the heap
   */
  stopping: 'Stopping…',
  /**
   * @description Sampling category to only profile allocations happening on the heap
   */
  allocationSampling: 'Allocation sampling',
  /**
   * @description The title for the collection of profiles that are gathered from various snapshots of the heap, using a sampling (e.g. every 1/100) technique.
   */
  samplingProfiles: 'Sampling profiles',
  /**
   * @description Description in Heap Profile View of a profiler tool
   */
  recordMemoryAllocations:
      'Approximate memory allocations by sampling long operations with minimal overhead and get a breakdown by JavaScript execution stack',
  /**
   * @description Name of a profile
   * @example {2} PH1
   */
  profileD: 'Profile {PH1}',
  /**
   * @description Accessible text for the value in bytes in memory allocation or coverage view.
   * @example {12345} PH1
   */
  sBytes: '{PH1} bytes',
  /**
   * @description Text in CPUProfile View of a profiler tool
   * @example {21.33} PH1
   */
  formatPercent: '{PH1} %',
  /**
   * @description The formatted size in kilobytes, abbreviated to kB
   * @example {1,021} PH1
   */
  skb: '{PH1} kB',
  /**
   * @description Text for the name of something
   */
  name: 'Name',
  /**
   * @description Tooltip of a cell that reports the size used for a particular part of the heap, excluding the size of the children nodes of this part of the heap
   */
  selfSize: 'Self size',
  /**
   * @description Tooltip of a cell that reports the total size used for a particular part of the heap
   */
  totalSize: 'Total size',
  /**
   * @description Text for web URLs
   */
  url: 'URL',
  /**
   * @description Label for a checkbox in the memory panel to enable sampling heap profiler timeline.
   */
  samplingHeapProfilerTimeline: 'Sampling heap profiler timeline',
  /**
   * @description Text in Profile View of a profiler tool
   */
  profile: 'Profile',
  /**
   * @description Placeholder text in the search box of the JavaScript profiler tool. Users can search
   *the results by the cost in milliseconds, the name of the function, or the file name.
   */
  findByCostMsNameOrFile: 'Find by cost (>50ms), name or file',
  /**
   * @description Text for a programming function
   */
  function: 'Function',
  /**
   * @description Title of the Profiler tool
   */
  profiler: 'Profiler',
  /**
   * @description Aria-label for profiles view combobox in memory tool
   */
  profileViewMode: 'Profile view mode',
  /**
   * @description Tooltip text that appears when hovering over the largeicon visibility button in the Profile View of a profiler tool
   */
  focusSelectedFunction: 'Focus selected function',
  /**
   * @description Tooltip text that appears when hovering over the largeicon delete button in the Profile View of a profiler tool
   */
  excludeSelectedFunction: 'Exclude selected function',
  /**
   * @description Tooltip text that appears when hovering over the largeicon refresh button in the Profile View of a profiler tool
   */
  restoreAllFunctions: 'Restore all functions',
  /**
   * @description Text in Profile View of a profiler tool
   */
  chart: 'Chart',
  /**
   * @description Text in Profile View of a profiler tool
   */
  heavyBottomUp: 'Heavy (Bottom Up)',
  /**
   * @description Text for selecting different profile views in the JS profiler tool. This option is a tree view.
   */
  treeTopDown: 'Tree (Top Down)',

  /**
   * @description Tooltip to alert developers that some parts of code in execution were not optimized.
   * @example {Optimized too many times} PH1
   */
  notOptimizedS: 'Not optimized: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/profiler/HeapProfileView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
function convertToSamplingHeapProfile(profileHeader: SamplingHeapProfileHeader):
    Protocol.HeapProfiler.SamplingHeapProfile {
  return (profileHeader.profile || profileHeader.protocolProfile()) as Protocol.HeapProfiler.SamplingHeapProfile;
}

export const maxLinkLength = 30;

export const enum ViewTypes {
  FLAME = 'Flame',
  TREE = 'Tree',
  HEAVY = 'Heavy',
}

export class HeapProfileView extends UI.View.SimpleView implements UI.SearchableView.Searchable {
  profileHeader: SamplingHeapProfileHeader;
  readonly profileType: SamplingHeapProfileTypeBase;
  adjustedTotal: number;
  selectedSizeText: HTMLElement|undefined;
  timestamps: number[] = [];
  sizes: number[] = [];
  max: number[] = [];
  ordinals: number[] = [];
  totalTime = 0;
  lastOrdinal = 0;
  readonly timelineOverview: HeapTimelineOverview = new HeapTimelineOverview();
  profileInternal: CPUProfile.ProfileTreeModel.ProfileTreeModel|null = null;
  searchableViewInternal!: UI.SearchableView.SearchableView;
  dataGrid: UI.Widget.Widget;
  viewSelectComboBox: HTMLSelectElement|undefined;
  focusButton: Buttons.Button.Button|undefined;
  excludeButton: Buttons.Button.Button|undefined;
  resetButton: Buttons.Button.Button|undefined;
  #sortColumnId = 'self';
  #sortAscending = false;
  #selectedNode: ProfileDataGridNode|null = null;

  readonly linkifierInternal: Components.Linkifier.Linkifier = new Components.Linkifier.Linkifier(maxLinkLength);
  nodeFormatter!: Formatter;
  viewType!: Common.Settings.Setting<ViewTypes>;
  bottomUpProfileDataGridTree?: BottomUpProfileDataGridTree|null;
  topDownProfileDataGridTree?: TopDownProfileDataGridTree|null;
  currentSearchResultIndex?: number;
  dataProvider?: ProfileFlameChartDataProvider;
  flameChart?: ProfileFlameChart;
  visibleView?: UI.Widget.Widget;
  searchableElement?: ProfileDataGridTree|ProfileFlameChart;
  profileDataGridTree?: ProfileDataGridTree;

  #isNodeSelected = false;
  #isResetEnabled = false;
  #selectedSize: number|null = null;
  #minId: number|null = null;
  #maxId: number|null = null;
  #lastAppliedRange: {minId: number, maxId: number}|null = null;
  #lastAppliedViewType: ViewTypes|null = null;

  constructor(profileHeader: SamplingHeapProfileHeader) {
    super({
      title: i18nString(UIStrings.profile),
      viewId: 'profile',
    });

    this.#setupSearchableView();

    this.dataGrid = new UI.Widget.VBox();

    this.profileHeader = profileHeader;
    this.profileType = profileHeader.profileType();
    this.initialize(new NodeFormatter(this));
    const profile = new SamplingHeapProfileModel(convertToSamplingHeapProfile(profileHeader));
    this.adjustedTotal = profile.total;
    this.setProfile(profile);

    this.#setupTimelineOverview();
  }

  #setupTimelineOverview(): void {
    if (this.profileType.hasTemporaryView()) {
      this.timelineOverview.addEventListener(Events.IDS_RANGE_CHANGED, this.onIdsRangeChanged.bind(this));
      this.timelineOverview.show(this.element, this.element.firstChild);
      this.timelineOverview.start();

      this.profileType.addEventListener(SamplingHeapProfileType.Events.STATS_UPDATE, this.onStatsUpdate, this);
      void this.profileType.once(ProfileEvents.PROFILE_COMPLETE).then(() => {
        this.profileType.removeEventListener(SamplingHeapProfileType.Events.STATS_UPDATE, this.onStatsUpdate, this);
        this.timelineOverview.stop();
        this.timelineOverview.updateGrid();
      });
    }
  }

  #setupSearchableView(): void {
    this.searchableViewInternal = new UI.SearchableView.SearchableView(this, null);
    this.searchableViewInternal.setPlaceholder(i18nString(UIStrings.findByCostMsNameOrFile));
    this.searchableViewInternal.show(this.element);
  }

  #renderDataGrid(): void {
    if (!this.profileDataGridTree) {
      return;
    }
    const onSort = (e: CustomEvent<{columnId: string, ascending: boolean}>): void => {
      this.#sortColumnId = e.detail.columnId;
      this.#sortAscending = e.detail.ascending;
      this.sortProfile();
    };

    const onDeselect = (): void => {
      this.#selectedNode = null;
      this.nodeSelected(false);
    };

    // clang-format off
    // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
    render(html`
      <devtools-data-grid class="flex-auto" name=${i18nString(UIStrings.profiler)} striped autofocus resize="last"
                          @sort=${onSort} @deselect=${onDeselect} .template=${html`
        <table>
          <tr>
            <th id="self" width="120px" fixed weight="1" sortable
                sort=${this.#sortColumnId === 'self' ? (this.#sortAscending ? 'ascending' : 'descending') : 'none'}>
              ${this.columnHeader('self')}
            </th>
            <th id="total" width="120px" fixed weight="1" sortable
                sort=${this.#sortColumnId === 'total' ? (this.#sortAscending ? 'ascending' : 'descending') : 'none'}>
              ${this.columnHeader('total')}
            </th>
            <th id="function" weight="3" sortable disclosure
                sort=${this.#sortColumnId === 'function' ? (this.#sortAscending ? 'ascending' : 'descending') : 'none'}>
              ${i18nString(UIStrings.function)}
            </th>
          </tr>
          ${repeat(this.profileDataGridTree.children,
                   (node: ProfileDataGridNode) => node.callUID,
                   (node: ProfileDataGridNode) => this.#renderNode(node))}
        </table>`}>
      </devtools-data-grid>
    `, this.dataGrid.element);
    // clang-format on
  }

  #renderNode(node: ProfileDataGridNode): TemplateResult {
    const onSelect = (): void => {
      this.#selectedNode = node;
      this.nodeSelected(true);
    };
    const onContextMenu = (event: CustomEvent<UI.ContextMenu.ContextMenu>): void => {
      this.populateContextMenu(event.detail, node);
    };
    const onExpand = (): void => {
      node.expanded = true;
      node.populate();
      this.refresh();
    };
    const onCollapse = (): void => {
      node.expanded = false;
      this.refresh();
    };

    if (node.profileNode.scriptId !== '0' && !node.linkElement) {
      node.linkElement = this.nodeFormatter.linkifyNode(node);
      if (node.linkElement) {
        (node.linkElement as HTMLElement).style.maxWidth = '75%';
      }
    }

    // clang-format off
    return html`
      <tr data-uid=${node.callUID} ?selected=${this.#selectedNode === node} ?expanded=${node.expanded} @select=${onSelect}
          @contextmenu=${onContextMenu} @expand=${onExpand} @collapse=${onCollapse}>
        <td data-value=${node.self} class="numeric-column ${node.searchMatchedSelfColumn ? 'highlight' : ''}"
            aria-label=${`${this.nodeFormatter.formatValueAccessibleText(node.self, node)}, ${this.nodeFormatter.formatPercent(node.selfPercent, node)}`}>
          <div class="profile-multiple-values">
            <span>${this.nodeFormatter.formatValue(node.self, node)}</span>
            <span class="percent-column">${this.nodeFormatter.formatPercent(node.selfPercent, node)}</span>
          </div>
        </td>
        <td data-value=${node.total} class="numeric-column ${node.searchMatchedTotalColumn ? 'highlight' : ''}"
            aria-label=${`${this.nodeFormatter.formatValueAccessibleText(node.total, node)}, ${this.nodeFormatter.formatPercent(node.totalPercent, node)}`}>
          <div class="profile-multiple-values">
            <span>${this.nodeFormatter.formatValue(node.total, node)}</span>
            <span class="percent-column">${this.nodeFormatter.formatPercent(node.totalPercent, node)}</span>
          </div>
        </td>
        <td data-value=${node.functionName} class="${node.searchMatchedFunctionColumn ? 'highlight' : ''} ${node.deoptReason ? 'not-optimized' : ''}">
          ${node.deoptReason ? html`
            <devtools-icon name="warning-filled" class="profile-warn-marker small"
                           title=${i18nString(UIStrings.notOptimizedS, {PH1: node.deoptReason})}>
            </devtools-icon>` : nothing}
          ${node.functionName}
          ${node.linkElement ? node.linkElement : nothing}
        </td>
        ${node.hasChildren() ? html`
          <td><table>
            ${node.expanded ? html`${repeat(
                node.children as ProfileDataGridNode[],
                child => child.callUID,
                child => this.#renderNode(child))}` : nothing}
          </table></td>` : nothing}
      </tr>`;
    // clang-format on
  }

  override async toolbarItems(): Promise<TemplateResult> {
    const currentViewType = this.viewType.get();
    const isFlame = currentViewType === ViewTypes.FLAME;

    // clang-format off
    return html`
      <select title=${i18nString(UIStrings.profileViewMode)} aria-label=${i18nString(UIStrings.profileViewMode)}
              @change=${this.changeView.bind(this)}
              jslog=${VisualLogging.dropDown('profile-view.selected-view').track({change: true})}
              ${ref(e => { this.viewSelectComboBox = e as HTMLSelectElement; })}>
        <option value=${ViewTypes.FLAME} ?selected=${currentViewType === ViewTypes.FLAME}>
          ${i18nString(UIStrings.chart)}
        </option>
        <option value=${ViewTypes.HEAVY} ?selected=${currentViewType === ViewTypes.HEAVY}>
          ${i18nString(UIStrings.heavyBottomUp)}
        </option>
        <option value=${ViewTypes.TREE} ?selected=${currentViewType === ViewTypes.TREE}>
          ${i18nString(UIStrings.treeTopDown)}
        </option>
      </select>
      <devtools-button .data=${{
                         iconName: 'eye',
                         variant: Buttons.Button.Variant.TOOLBAR,
                         title: i18nString(UIStrings.focusSelectedFunction),
                         jslogContext: 'profile-view.focus-selected-function',
                         disabled: !this.#isNodeSelected,
                       } as Buttons.Button.ButtonData}
                       @click=${this.focusClicked.bind(this)}
                       ?hidden=${isFlame}
                       ${ref(e => { this.focusButton = e as Buttons.Button.Button; })}>
      </devtools-button>
      <devtools-button .data=${{
                         iconName: 'cross',
                         variant: Buttons.Button.Variant.TOOLBAR,
                         title: i18nString(UIStrings.excludeSelectedFunction),
                         jslogContext: 'profile-view.exclude-selected-function',
                         disabled: !this.#isNodeSelected,
                       } as Buttons.Button.ButtonData}
                       @click=${this.excludeClicked.bind(this)}
                       ?hidden=${isFlame}
                       ${ref(e => { this.excludeButton = e as Buttons.Button.Button; })}>
      </devtools-button>
      <devtools-button .data=${{
                         iconName: 'refresh',
                         variant: Buttons.Button.Variant.TOOLBAR,
                         title: i18nString(UIStrings.restoreAllFunctions),
                         jslogContext: 'profile-view.restore-all-functions',
                         disabled: !this.#isResetEnabled,
                       } as Buttons.Button.ButtonData}
                       @click=${this.resetClicked.bind(this)}
                       ?hidden=${isFlame}
                       ${ref(e => {this.resetButton = e as Buttons.Button.Button; })}>
        </devtools-button>
      <span ${ref(e => { this.selectedSizeText = e as HTMLElement; })}>
        ${this.#selectedSize !== null ?
          i18nString(UIStrings.selectedSizeS, {PH1: i18n.ByteUtilities.bytesToString(this.#selectedSize)})
          : nothing}
      </span>`;
    // clang-format on
  }

  onIdsRangeChanged(event: Common.EventTarget.EventTargetEvent<IdsRangeChangedEvent>): void {
    const {minId, maxId} = event.data;
    this.#selectedSize = event.data.size;
    this.#minId = minId;
    this.#maxId = maxId;
    this.performUpdate();
  }

  setSelectionRange(minId: number, maxId: number): void {
    const profileData = convertToSamplingHeapProfile((this.profileHeader));
    const profile = new SamplingHeapProfileModel(profileData, minId, maxId);
    this.adjustedTotal = profile.total;
    this.setProfile(profile);
  }

  onStatsUpdate(event: Common.EventTarget.EventTargetEvent<Protocol.HeapProfiler.SamplingHeapProfile|null>): void {
    const profile = event.data;

    if (!this.totalTime) {
      this.timestamps = [];
      this.sizes = [];
      this.max = [];
      this.ordinals = [];
      this.totalTime = 30000;
      this.lastOrdinal = 0;
    }

    this.sizes.fill(0);
    this.sizes.push(0);
    this.timestamps.push(Date.now());
    this.ordinals.push(this.lastOrdinal + 1);
    for (const sample of profile?.samples ?? []) {
      this.lastOrdinal = Math.max(this.lastOrdinal, sample.ordinal);
      const bucket = Platform.ArrayUtilities.upperBound(this.ordinals, sample.ordinal,
                                                        Platform.ArrayUtilities.DEFAULT_COMPARATOR) -
          1;
      this.sizes[bucket] += sample.size;
    }
    this.max.push(this.sizes[this.sizes.length - 1]);

    const lastTimestamp = this.timestamps[this.timestamps.length - 1];
    if (lastTimestamp - this.timestamps[0] > this.totalTime) {
      this.totalTime *= 2;
    }

    this.performUpdate();
  }

  columnHeader(columnId: string): Common.UIString.LocalizedString {
    switch (columnId) {
      case 'self':
        return i18nString(UIStrings.selfSizeBytes);
      case 'total':
        return i18nString(UIStrings.totalSizeBytes);
    }
    return Common.UIString.LocalizedEmptyString;
  }

  createFlameChartDataProvider(): ProfileFlameChartDataProvider {
    return new HeapFlameChartDataProvider((this.profile() as SamplingHeapProfileModel),
                                          this.profileHeader.heapProfilerModel());
  }

  static buildPopoverTable(popoverInfo: Array<{
    title: string,
    value: string,
  }>): Element {
    const table = document.createElement('table');
    for (const entry of popoverInfo) {
      const row = table.createChild('tr');
      row.createChild('td').textContent = entry.title;
      row.createChild('td').textContent = entry.value;
    }
    return table;
  }

  setProfile(profile: CPUProfile.ProfileTreeModel.ProfileTreeModel): void {
    this.profileInternal = profile;
    this.bottomUpProfileDataGridTree = null;
    this.topDownProfileDataGridTree = null;
    this.changeView();
    this.refresh();
  }

  profile(): CPUProfile.ProfileTreeModel.ProfileTreeModel|null {
    return this.profileInternal;
  }

  initialize(nodeFormatter: Formatter): void {
    this.nodeFormatter = nodeFormatter;

    this.viewType = Common.Settings.Settings.instance().createSetting('profile-view', ViewTypes.HEAVY);

    this.changeView();
    if (this.flameChart) {
      this.flameChart.update();
    }
  }

  override focus(): void {
    if (this.flameChart) {
      this.flameChart.focus();
    } else {
      super.focus();
    }
  }

  selectRange(timeLeft: number, timeRight: number): void {
    if (!this.flameChart) {
      return;
    }
    this.flameChart.selectRange(timeLeft, timeRight);
  }

  getBottomUpProfileDataGridTree(): ProfileDataGridTree {
    if (!this.bottomUpProfileDataGridTree) {
      this.bottomUpProfileDataGridTree = new BottomUpProfileDataGridTree(
          this.nodeFormatter, this.searchableViewInternal,
          (this.profileInternal as CPUProfile.ProfileTreeModel.ProfileTreeModel).root, this.adjustedTotal);
    }
    return this.bottomUpProfileDataGridTree;
  }

  getTopDownProfileDataGridTree(): ProfileDataGridTree {
    if (!this.topDownProfileDataGridTree) {
      this.topDownProfileDataGridTree = new TopDownProfileDataGridTree(
          this.nodeFormatter, this.searchableViewInternal,
          (this.profileInternal as CPUProfile.ProfileTreeModel.ProfileTreeModel).root, this.adjustedTotal);
    }
    return this.topDownProfileDataGridTree;
  }

  populateContextMenu(contextMenu: UI.ContextMenu.ContextMenu,
                      gridNode: DataGrid.DataGrid.DataGridNode<unknown>): void {
    const node = (gridNode as ProfileDataGridNode);
    if (node.linkElement) {
      contextMenu.appendApplicableItems(node.linkElement);
    }
  }

  override willHide(): void {
    super.willHide();
    this.currentSearchResultIndex = -1;
  }

  refresh(): void {
    if (!this.profileDataGridTree) {
      return;
    }
    this.#renderDataGrid();
  }

  refreshVisibleData(): void {
    this.#renderDataGrid();
  }

  searchableView(): UI.SearchableView.SearchableView {
    return this.searchableViewInternal;
  }

  supportsCaseSensitiveSearch(): boolean {
    return true;
  }

  supportsWholeWordSearch(): boolean {
    return false;
  }

  supportsRegexSearch(): boolean {
    return false;
  }

  onSearchCanceled(): void {
    if (this.searchableElement) {
      this.searchableElement.onSearchCanceled();
      this.refresh();
    }
  }

  performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void {
    if (this.searchableElement) {
      this.searchableElement.performSearch(searchConfig, shouldJump, jumpBackwards);
      this.refresh();
    }
  }

  jumpToNextSearchResult(): void {
    if (this.searchableElement) {
      this.searchableElement.jumpToNextSearchResult();
      this.#syncSearchSelection();
    }
  }

  jumpToPreviousSearchResult(): void {
    if (this.searchableElement) {
      this.searchableElement.jumpToPreviousSearchResult();
      this.#syncSearchSelection();
    }
  }

  #syncSearchSelection(): void {
    if (this.searchableElement === this.profileDataGridTree && this.profileDataGridTree) {
      const searchResult = this.profileDataGridTree.searchResults[this.profileDataGridTree.searchResultIndex];
      this.#selectedNode = searchResult?.profileNode || null;

      let node = this.#selectedNode;
      while (node?.parent) {
        node.parent.expanded = true;
        node = node.parent as ProfileDataGridNode;
      }

      this.nodeSelected(!!this.#selectedNode);
      this.refresh();
    }
  }

  linkifier(): Components.Linkifier.Linkifier {
    return this.linkifierInternal;
  }

  ensureFlameChartCreated(): void {
    if (this.flameChart) {
      return;
    }
    this.dataProvider = this.createFlameChartDataProvider();
    this.flameChart = new ProfileFlameChart(this.searchableViewInternal, this.dataProvider);
    this.flameChart.addEventListener(PerfUI.FlameChart.Events.ENTRY_INVOKED, event => {
      void this.onEntryInvoked(event);
    });
  }

  async onEntryInvoked(event: Common.EventTarget.EventTargetEvent<number>): Promise<void> {
    if (!this.dataProvider) {
      return;
    }
    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 = (debuggerModel.createRawLocation(script, node.lineNumber, node.columnNumber));
    const uiLocation =
        await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(location);
    void Common.Revealer.reveal(uiLocation);
  }

  changeView(e?: Event): void {
    if (!this.profileInternal) {
      return;
    }

    if (e) {
      const select = e.target as HTMLSelectElement;
      this.viewType.set(select.value as ViewTypes);
    }
    this.#selectedNode = null;
    this.#isNodeSelected = false;
    this.performUpdate();
  }

  nodeSelected(selected: boolean): void {
    this.#isNodeSelected = selected;
    this.performUpdate();
  }

  focusClicked(): void {
    if (!this.#selectedNode) {
      return;
    }

    this.#isResetEnabled = true;
    this.performUpdate();
    this.resetButton?.focus();
    if (this.profileDataGridTree) {
      this.profileDataGridTree.focus(this.#selectedNode);
    }
    this.refresh();
    this.refreshVisibleData();
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuProfileNodeFocused);
  }

  excludeClicked(): void {
    const selectedNode = this.#selectedNode;

    if (!selectedNode) {
      return;
    }

    this.#isResetEnabled = true;
    this.performUpdate();
    this.resetButton?.focus();

    this.#selectedNode = null;
    this.nodeSelected(false);

    if (this.profileDataGridTree) {
      this.profileDataGridTree.exclude(selectedNode);
    }
    this.refresh();
    this.refreshVisibleData();
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuProfileNodeExcluded);
  }

  resetClicked(): void {
    this.viewSelectComboBox?.focus();
    this.#isResetEnabled = false;
    this.#selectedNode = null;
    this.#isNodeSelected = false;
    this.performUpdate();
    if (this.profileDataGridTree) {
      this.profileDataGridTree.restore();
    }
    this.linkifierInternal.reset();
    this.refresh();
    this.refreshVisibleData();
  }

  sortProfile(): void {
    if (!this.profileDataGridTree) {
      return;
    }
    const sortAscending = this.#sortAscending;
    const sortColumnId = this.#sortColumnId;
    const sortProperty = sortColumnId === 'function' ? 'functionName' : sortColumnId || '';
    this.profileDataGridTree.sort(ProfileDataGridTree.propertyComparator(sortProperty, sortAscending), false);

    this.refresh();
  }

  override performUpdate(): void {
    const currentViewType = this.viewType ? this.viewType.get() : null;
    if (currentViewType && currentViewType !== this.#lastAppliedViewType) {
      this.searchableViewInternal.closeSearch();

      if (this.visibleView) {
        this.visibleView.detach();
      }

      switch (currentViewType) {
        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;
          this.searchableElement = this.profileDataGridTree;
          break;
        case ViewTypes.HEAVY:
          this.profileDataGridTree = this.getBottomUpProfileDataGridTree();
          this.sortProfile();
          this.visibleView = this.dataGrid;
          this.searchableElement = this.profileDataGridTree;
          break;
      }

      if (this.visibleView) {
        this.visibleView.show(this.searchableViewInternal.element);
      }

      this.#lastAppliedViewType = currentViewType;
    }

    const isFlame = currentViewType === ViewTypes.FLAME;

    if (this.focusButton) {
      this.focusButton.hidden = isFlame;
      this.focusButton.disabled = !this.#isNodeSelected;
    }
    if (this.excludeButton) {
      this.excludeButton.hidden = isFlame;
      this.excludeButton.disabled = !this.#isNodeSelected;
    }
    if (this.resetButton) {
      this.resetButton.hidden = isFlame;
      this.resetButton.disabled = !this.#isResetEnabled;
    }

    if (this.#selectedSize !== null && this.selectedSizeText) {
      this.selectedSizeText.textContent =
          i18nString(UIStrings.selectedSizeS, {PH1: i18n.ByteUtilities.bytesToString(this.#selectedSize)});
    }

    if (this.#minId !== null && this.#maxId !== null) {
      const rangeChanged = !this.#lastAppliedRange || this.#lastAppliedRange.minId !== this.#minId ||
          this.#lastAppliedRange.maxId !== this.#maxId;

      if (rangeChanged) {
        this.setSelectionRange(this.#minId, this.#maxId);
        this.#lastAppliedRange = {minId: this.#minId, maxId: this.#maxId};
      }
    }

    if (this.sizes.length > 0) {
      const samples = ({
        sizes: this.sizes,
        max: this.max,
        ids: this.ordinals,
        timestamps: this.timestamps,
        totalTime: this.totalTime,
      } as Samples);

      this.timelineOverview.setSamples(samples);
    }
  }
}

export class SamplingHeapProfileTypeBase extends
    Common.ObjectWrapper.eventMixin<SamplingHeapProfileType.EventTypes, typeof ProfileType>(ProfileType) {
  recording: boolean;
  clearedDuringRecording: boolean;

  constructor(typeId: string, description: string) {
    super(typeId, description);
    this.recording = false;
    this.clearedDuringRecording = false;
  }

  override profileBeingRecorded(): SamplingHeapProfileHeader|null {
    return super.profileBeingRecorded() as SamplingHeapProfileHeader | null;
  }

  override typeName(): string {
    return 'Heap';
  }

  override fileExtension(): string {
    return '.heapprofile';
  }

  override get buttonTooltip(): Common.UIString.LocalizedString {
    return this.recording ? i18nString(UIStrings.stopHeapProfiling) : i18nString(UIStrings.startHeapProfiling);
  }

  override buttonClicked(): boolean {
    if (this.recording) {
      void this.stopRecordingProfile();
    } else {
      void this.startRecordingProfile();
    }
    return this.recording;
  }

  async startRecordingProfile(): Promise<void> {
    const heapProfilerModel = UI.Context.Context.instance().flavor(SDK.HeapProfilerModel.HeapProfilerModel);
    if (this.profileBeingRecorded() || !heapProfilerModel) {
      return;
    }
    const profileHeader = new SamplingHeapProfileHeader(heapProfilerModel, this);
    this.setProfileBeingRecorded(profileHeader);
    this.addProfile(profileHeader);
    profileHeader.updateStatus(i18nString(UIStrings.recording));

    const warnings = [i18nString(UIStrings.heapProfilerIsRecording)];
    UI.InspectorView.InspectorView.instance().setPanelWarnings('heap-profiler', warnings);

    this.recording = true;
    this.startSampling();
  }

  async stopRecordingProfile(): Promise<void> {
    this.recording = false;
    const recordedProfile = this.profileBeingRecorded();
    if (!recordedProfile?.heapProfilerModel()) {
      return;
    }

    recordedProfile.updateStatus(i18nString(UIStrings.stopping));
    const profile = await this.stopSampling();
    if (recordedProfile) {
      console.assert(profile !== undefined);
      recordedProfile.setProtocolProfile(profile as unknown as Protocol.Profiler.Profile);
      recordedProfile.updateStatus('');
      this.setProfileBeingRecorded(null);
    }
    UI.InspectorView.InspectorView.instance().setPanelWarnings('heap-profiler', []);

    // If the data was cleared during the middle of the recording we no
    // longer treat the profile as being completed. This means we avoid
    // a change of view to the profile list.
    const wasClearedDuringRecording = this.clearedDuringRecording;
    this.clearedDuringRecording = false;
    if (wasClearedDuringRecording) {
      return;
    }
    this.dispatchEventToListeners(ProfileEvents.PROFILE_COMPLETE, recordedProfile);
  }

  override createProfileLoadedFromFile(title: string): ProfileHeader {
    return new SamplingHeapProfileHeader(null, this, title);
  }

  override profileBeingRecordedRemoved(): void {
    this.clearedDuringRecording = true;
    void this.stopRecordingProfile();
  }

  startSampling(): void {
    throw new Error('Not implemented');
  }

  stopSampling(): Promise<Protocol.HeapProfiler.SamplingHeapProfile> {
    throw new Error('Not implemented');
  }
}

let samplingHeapProfileTypeInstance: SamplingHeapProfileType;

export class SamplingHeapProfileType extends SamplingHeapProfileTypeBase {
  updateTimer: number;
  updateIntervalMs: number;
  readonly #recordTimelineSetting: Common.Settings.Setting<boolean>;
  customContentInternal: UI.UIUtils.CheckboxLabel|null = null;

  constructor() {
    super(SamplingHeapProfileType.TypeId, i18nString(UIStrings.allocationSampling));
    if (!samplingHeapProfileTypeInstance) {
      samplingHeapProfileTypeInstance = this;
    }

    this.updateTimer = 0;
    this.updateIntervalMs = 200;
    this.#recordTimelineSetting =
        Common.Settings.Settings.instance().createSetting('record-sampling-heap-profiler-timeline', false);
  }

  static get instance(): SamplingHeapProfileType {
    return samplingHeapProfileTypeInstance;
  }

  override get treeItemTitle(): Common.UIString.LocalizedString {
    return i18nString(UIStrings.samplingProfiles);
  }

  override get description(): string {
    // TODO(l10n): Do not concatenate localized strings.
    const formattedDescription = [i18nString(UIStrings.recordMemoryAllocations)];
    return formattedDescription.join('\n');
  }

  override hasTemporaryView(): boolean {
    return this.#recordTimelineSetting.get();
  }

  override customContent(): Element|null {
    const checkboxSetting = SettingsUI.SettingsUI.createSettingCheckbox(
        i18nString(UIStrings.samplingHeapProfilerTimeline), this.#recordTimelineSetting);
    this.customContentInternal = checkboxSetting;
    checkboxSetting.setAttribute(
        'jslog', `${VisualLogging.toggle('record-sampling-heap-profiler-timeline').track({click: true})}`);
    return checkboxSetting;
  }

  override setCustomContentEnabled(enable: boolean): void {
    if (this.customContentInternal) {
      this.customContentInternal.disabled = !enable;
    }
  }

  override startSampling(): void {
    const heapProfilerModel = this.obtainRecordingProfile();
    if (!heapProfilerModel) {
      return;
    }

    void heapProfilerModel.startSampling();
    if (this.#recordTimelineSetting.get()) {
      this.updateTimer = window.setTimeout(() => {
        void this.updateStats();
      }, this.updateIntervalMs);
    }
  }

  obtainRecordingProfile(): SDK.HeapProfilerModel.HeapProfilerModel|null {
    const recordingProfile = this.profileBeingRecorded();
    if (recordingProfile) {
      const heapProfilerModel = recordingProfile.heapProfilerModel();
      return heapProfilerModel;
    }
    return null;
  }

  override async stopSampling(): Promise<Protocol.HeapProfiler.SamplingHeapProfile> {
    window.clearTimeout(this.updateTimer);
    this.updateTimer = 0;
    this.dispatchEventToListeners(SamplingHeapProfileType.Events.RECORDING_STOPPED);
    const heapProfilerModel = this.obtainRecordingProfile();
    if (!heapProfilerModel) {
      throw new Error('No heap profiler model');
    }

    const samplingProfile = await heapProfilerModel.stopSampling();
    if (!samplingProfile) {
      throw new Error('No sampling profile found');
    }
    return samplingProfile;
  }

  async updateStats(): Promise<void> {
    const heapProfilerModel = this.obtainRecordingProfile();
    if (!heapProfilerModel) {
      return;
    }

    const profile = await heapProfilerModel.getSamplingProfile();
    if (!this.updateTimer) {
      return;
    }
    this.dispatchEventToListeners(SamplingHeapProfileType.Events.STATS_UPDATE, profile);
    this.updateTimer = window.setTimeout(() => {
      void this.updateStats();
    }, this.updateIntervalMs);
  }

  // eslint-disable-next-line @typescript-eslint/naming-convention
  static readonly TypeId = 'SamplingHeap';
}

export namespace SamplingHeapProfileType {
  export const enum Events {
    RECORDING_STOPPED = 'RecordingStopped',
    STATS_UPDATE = 'StatsUpdate',
  }

  export interface EventTypes {
    [Events.RECORDING_STOPPED]: void;
    [Events.STATS_UPDATE]: Protocol.HeapProfiler.SamplingHeapProfile|null;
  }
}

export class SamplingHeapProfileHeader extends WritableProfileHeader {
  readonly heapProfilerModelInternal: SDK.HeapProfilerModel.HeapProfilerModel|null;
  override protocolProfileInternal: {
    head: {
      callFrame: {
        functionName: string,
        scriptId: Protocol.Runtime.ScriptId,
        url: string,
        lineNumber: number,
        columnNumber: number,
      },
      children: never[],
      selfSize: number,
      id: number,
    },
    samples: never[],
    startTime: number,
    endTime: number,
    nodes: never[],
  };
  constructor(
      heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null,
      type: SamplingHeapProfileTypeBase,
      title?: string,
  ) {
    super(
        heapProfilerModel?.debuggerModel() ?? null,
        type,
        title || i18nString(UIStrings.profileD, {PH1: type.nextProfileUid()}),
    );
    this.heapProfilerModelInternal = heapProfilerModel;
    this.protocolProfileInternal = {
      head: {
        callFrame: {
          functionName: '',
          scriptId: '' as Protocol.Runtime.ScriptId,
          url: '',
          lineNumber: 0,
          columnNumber: 0,
        },
        children: [],
        selfSize: 0,
        id: 0,
      },
      samples: [],
      startTime: 0,
      endTime: 0,
      nodes: [],
    };
  }

  protocolProfile(): Protocol.HeapProfiler.SamplingHeapProfile {
    return this.protocolProfileInternal;
  }

  heapProfilerModel(): SDK.HeapProfilerModel.HeapProfilerModel|null {
    return this.heapProfilerModelInternal;
  }

  override profileType(): SamplingHeapProfileTypeBase {
    return super.profileType() as SamplingHeapProfileTypeBase;
  }
}

export class SamplingHeapProfileNode extends CPUProfile.ProfileTreeModel.ProfileNode {
  override self: number;
  constructor(node: Protocol.HeapProfiler.SamplingHeapProfileNode) {
    const callFrame = node.callFrame || ({
                        // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
                        // @ts-expect-error
                        functionName: node['functionName'],
                        // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
                        // @ts-expect-error
                        scriptId: node['scriptId'],
                        // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
                        // @ts-expect-error
                        url: node['url'],
                        // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
                        // @ts-expect-error
                        lineNumber: node['lineNumber'] - 1,
                        // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
                        // @ts-expect-error
                        columnNumber: node['columnNumber'] - 1,
                      } as Protocol.Runtime.CallFrame);
    super(callFrame);
    this.self = node.selfSize;
  }
}

export class SamplingHeapProfileModel extends CPUProfile.ProfileTreeModel.ProfileTreeModel {
  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  modules: any;

  constructor(profile: Protocol.HeapProfiler.SamplingHeapProfile, minOrdinal?: number, maxOrdinal?: number) {
    super();
    // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    this.modules = (profile as any).modules || [];

    let nodeIdToSizeMap: Map<number, number>|null = null;
    if (minOrdinal || maxOrdinal) {
      nodeIdToSizeMap = new Map<number, number>();
      minOrdinal = minOrdinal || 0;
      maxOrdinal = maxOrdinal || Infinity;
      for (const sample of profile.samples) {
        if (sample.ordinal < minOrdinal || sample.ordinal > maxOrdinal) {
          continue;
        }
        const size = nodeIdToSizeMap.get(sample.nodeId) || 0;
        nodeIdToSizeMap.set(sample.nodeId, size + sample.size);
      }
    }

    this.initialize(translateProfileTree(profile.head));

    function translateProfileTree(root: Protocol.HeapProfiler.SamplingHeapProfileNode): SamplingHeapProfileNode {
      const resultRoot = new SamplingHeapProfileNode(root);
      const sourceNodeStack = [root];
      const targetNodeStack = [resultRoot];
      while (sourceNodeStack.length) {
        const sourceNode = (sourceNodeStack.pop() as Protocol.HeapProfiler.SamplingHeapProfileNode);
        const targetNode = (targetNodeStack.pop() as SamplingHeapProfileNode);
        targetNode.children = sourceNode.children.map(child => {
          const targetChild = new SamplingHeapProfileNode(child);
          if (nodeIdToSizeMap) {
            targetChild.self = nodeIdToSizeMap.get(child.id) || 0;
          }
          return targetChild;
        });
        sourceNodeStack.push(...sourceNode.children);
        targetNodeStack.push(...targetNode.children);
      }
      pruneEmptyBranches(resultRoot);
      return resultRoot;
    }

    function pruneEmptyBranches(node: CPUProfile.ProfileTreeModel.ProfileNode): boolean {
      node.children = node.children.filter(pruneEmptyBranches);
      return Boolean(node.children.length || node.self);
    }
  }
}

export class NodeFormatter implements Formatter {
  readonly profileView: HeapProfileView;
  readonly #formattedValueCache = new Map<number, string>();
  readonly #formattedValueAccessibleTextCache = new Map<number, string>();
  readonly #formattedPercentCache = new Map<number, string>();

  constructor(profileView: HeapProfileView) {
    this.profileView = profileView;
  }

  formatValue(value: number): string {
    let result = this.#formattedValueCache.get(value);
    if (!result) {
      result = i18n.ByteUtilities.bytesToString(value);
      this.#formattedValueCache.set(value, result);
    }
    return result;
  }

  formatValueAccessibleText(value: number): string {
    let result = this.#formattedValueAccessibleTextCache.get(value);
    if (!result) {
      result = i18nString(UIStrings.sBytes, {PH1: value});
      this.#formattedValueAccessibleTextCache.set(value, result);
    }
    return result;
  }

  formatPercent(value: number, _node: ProfileDataGridNode): string {
    let result = this.#formattedPercentCache.get(value);
    if (!result) {
      result = i18nString(UIStrings.formatPercent, {PH1: value.toFixed(2)});
      this.#formattedPercentCache.set(value, result);
    }
    return result;
  }

  linkifyNode(node: ProfileDataGridNode): Element|null {
    const heapProfilerModel = this.profileView.profileHeader.heapProfilerModel();
    const target = heapProfilerModel ? heapProfilerModel.target() : null;
    const options = {
      className: 'profile-node-file',
    };
    return this.profileView.linkifier().maybeLinkifyConsoleCallFrame(target, node.profileNode.callFrame, options);
  }
}

export class HeapFlameChartDataProvider extends ProfileFlameChartDataProvider {
  readonly profile: CPUProfile.ProfileTreeModel.ProfileTreeModel;
  readonly heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null;

  constructor(profile: CPUProfile.ProfileTreeModel.ProfileTreeModel,
              heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null) {
    super();
    this.profile = profile;
    this.heapProfilerModel = heapProfilerModel;
  }

  override minimumBoundary(): number {
    return 0;
  }

  override totalTime(): number {
    return this.profile.root.total;
  }

  override entryHasDeoptReason(_entryIndex: number): boolean {
    return false;
  }

  override formatValue(value: number, _precision?: number): string {
    return i18nString(UIStrings.skb, {PH1: Platform.NumberUtilities.withThousandsSeparator(value / 1e3)});
  }

  override calculateTimelineData(): PerfUI.FlameChart.FlameChartTimelineData {
    function nodesCount(node: CPUProfile.ProfileTreeModel.ProfileNode): number {
      return node.children.reduce((count, node) => count + nodesCount(node), 1);
    }
    const count = nodesCount(this.profile.root);
    const entryNodes: CPUProfile.ProfileTreeModel.ProfileNode[] = new Array(count);
    const entryLevels = new Uint16Array(count);
    const entryTotalTimes = new Float32Array(count);
    const entryStartTimes = new Float64Array(count);
    let depth = 0;
    let maxDepth = 0;
    let position = 0;
    let index = 0;

    function addNode(node: CPUProfile.ProfileTreeModel.ProfileNode): void {
      const start = position;
      entryNodes[index] = node;
      entryLevels[index] = depth;
      entryTotalTimes[index] = node.total;
      entryStartTimes[index] = position;
      ++index;
      ++depth;
      node.children.forEach(addNode);
      --depth;
      maxDepth = Math.max(maxDepth, depth);
      position = start + node.total;
    }
    addNode(this.profile.root);

    this.maxStackDepthInternal = maxDepth + 1;
    this.entryNodes = entryNodes;
    this.timelineDataInternal =
        PerfUI.FlameChart.FlameChartTimelineData.create({entryLevels, entryTotalTimes, entryStartTimes, groups: null});

    return this.timelineDataInternal;
  }

  override preparePopoverElement(entryIndex: number): Element|null {
    const node = this.entryNodes[entryIndex];
    if (!node) {
      return null;
    }
    const popoverInfo: Array<{
      title: string,
      value: string,
    }> = [];
    function pushRow(title: string, value: string): void {
      popoverInfo.push({title, value});
    }
    pushRow(i18nString(UIStrings.name), UI.UIUtils.beautifyFunctionName(node.functionName));
    pushRow(i18nString(UIStrings.selfSize), i18n.ByteUtilities.bytesToString(node.self));
    pushRow(i18nString(UIStrings.totalSize), i18n.ByteUtilities.bytesToString(node.total));
    const linkifier = new Components.Linkifier.Linkifier();
    const link = linkifier.maybeLinkifyConsoleCallFrame(this.heapProfilerModel ? this.heapProfilerModel.target() : null,
                                                        node.callFrame);
    if (link) {
      pushRow(i18nString(UIStrings.url), link.textContent);
    }
    linkifier.dispose();
    return HeapProfileView.buildPopoverTable(popoverInfo);
  }
}
