// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../../ui/components/report_view/report_view.js';
import '../../ui/legacy/legacy.js';

import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import * as ApplicationComponents from './components/components.js';
import type {
  Database, DatabaseId, Entry, Index, IndexedDBModel, ObjectStore, ObjectStoreMetadata} from './IndexedDBModel.js';
import indexedDBViewsStyles from './indexedDBViews.css.js';

type IDBKeyValue = number|string|Date|IDBKeyValue[];

const UIStrings = {
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  version: 'Version',
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  objectStores: 'Object stores',
  /**
   * @description Text of button in Indexed DBViews of the Application panel
   */
  deleteDatabase: 'Delete database',
  /**
   * @description Text of button in Indexed DBViews of the Application panel
   */
  refreshDatabase: 'Refresh database',
  /**
   * @description Text in Application panel IndexedDB delete confirmation dialog
   * @example {msb} PH1
   */
  confirmDeleteDatabase: 'Delete "{PH1}" database?',
  /**
   * @description Explanation text in Application panel IndexedDB delete confirmation dialog
   */
  databaseWillBeRemoved: 'The selected database and contained data will be removed.',
  /**
   * @description Title of the confirmation dialog in the IndexedDB tab of the Application panel
   *              that the user is about to clear an object store and this cannot be undone.
   * @example {table1} PH1
   */
  confirmClearObjectStore: 'Clear "{PH1}" object store?',
  /**
   * @description Description in the confirmation dialog in the IndexedDB tab of the Application
   *              panel that the user is about to clear an object store and this cannot be undone.
   */
  objectStoreWillBeCleared: 'The data contained in the selected object store will be removed.',
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  idb: 'IDB',
  /**
   * @description Text to refresh the page
   */
  refresh: 'Refresh',
  /**
   * @description Tooltip text that appears when hovering over the delete button in the Indexed DBViews of the Application panel
   */
  deleteSelected: 'Delete selected',
  /**
   * @description Tooltip text that appears when hovering over the clear button in the Indexed DBViews of the Application panel
   */
  clearObjectStore: 'Clear object store',
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  dataMayBeStale: 'Data may be stale',
  /**
   * @description Title of needs refresh in indexed dbviews of the application panel
   */
  someEntriesMayHaveBeenModified: 'Some entries may have been modified',
  /**
   * @description Text in DOMStorage Items View of the Application panel
   */
  keyString: 'Key',
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  primaryKey: 'Primary key',
  /**
   * @description Text for the value of something
   */
  valueString: 'Value',
  /**
   * @description Data grid name for Indexed DB data grids
   */
  indexedDb: 'Indexed DB',
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  keyPath: 'Key path: ',
  /**
   * @description Tooltip text that appears when hovering over the triangle left button in the Indexed DBViews of the Application panel
   */
  showPreviousPage: 'Show previous page',
  /**
   * @description Tooltip text that appears when hovering over the triangle right button in the Indexed DBViews of the Application panel
   */
  showNextPage: 'Show next page',
  /**
   * @description Text in Indexed DBViews of the Application panel
   */
  filterByKey: 'Filter by key (show keys greater or equal to)',
  /**
   * @description Text in Context menu for expanding objects in IndexedDB tables
   */
  expandRecursively: 'Expand Recursively',
  /**
   * @description Text in Context menu for collapsing objects in IndexedDB tables
   */
  collapse: 'Collapse',
  /**
   * @description Span text content in Indexed DBViews of the Application panel
   * @example {2} PH1
   */
  totalEntriesS: 'Total entries: {PH1}',
  /**
   * @description Text in Indexed DBViews of the Application panel
   * @example {2} PH1
   */
  keyGeneratorValueS: 'Key generator value: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/IndexedDBViews.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {repeat} = Directives;
const {widget} = UI.Widget;

export class IDBDatabaseView extends ApplicationComponents.StorageMetadataView.StorageMetadataView {
  private readonly model: IndexedDBModel;
  private database!: Database;
  constructor(model: IndexedDBModel, database: Database|null) {
    super();

    this.model = model;
    this.setShowOnlyBucket(true);
    if (database) {
      this.update(database);
    }
  }

  override getTitle(): string|undefined {
    return this.database?.databaseId.name;
  }

  override async renderReportContent(): Promise<LitTemplate> {
    if (!this.database) {
      return nothing;
    }
    return html`
      ${await super.renderReportContent()}
      ${this.key(i18nString(UIStrings.version))}
      ${this.value(this.database.version.toString())}
      ${this.key(i18nString(UIStrings.objectStores))}
      ${this.value(this.database.objectStores.size.toString())}
      <devtools-report-divider></devtools-report-divider>
      <devtools-report-section>
      <devtools-button
          aria-label=${i18nString(UIStrings.deleteDatabase)}
          .variant=${Buttons.Button.Variant.OUTLINED}
          @click=${this.deleteDatabase}
          jslog=${VisualLogging.action('delete-database').track({
      click: true,
    })}>
        ${i18nString(UIStrings.deleteDatabase)}
      </devtools-button>&nbsp;
      <devtools-button
          aria-label=${i18nString(UIStrings.refreshDatabase)}
          .variant=${Buttons.Button.Variant.OUTLINED}
          @click=${this.refreshDatabaseButtonClicked}
          jslog=${VisualLogging.action('refresh-database').track({
      click: true,
    })}>
        ${i18nString(UIStrings.refreshDatabase)}
      </devtools-button>
      </devtools-report-section>
      `;
  }

  private refreshDatabaseButtonClicked(): void {
    this.model.refreshDatabase(this.database.databaseId);
  }

  update(database: Database): void {
    this.database = database;
    const bucketInfo =
        this.model.target()
            .model(SDK.StorageBucketsModel.StorageBucketsModel)
            ?.getBucketByName(database.databaseId.storageBucket.storageKey, database.databaseId.storageBucket.name);
    if (bucketInfo) {
      this.setStorageBucket(bucketInfo);
    } else {
      this.setStorageKey(database.databaseId.storageBucket.storageKey);
    }

    void this.render().then(() => this.updatedForTests());
  }

  private updatedForTests(): void {
    // Sniffed in tests.
  }

  private async deleteDatabase(): Promise<void> {
    const ok = await UI.UIUtils.ConfirmDialog.show(
        i18nString(UIStrings.databaseWillBeRemoved),
        i18nString(UIStrings.confirmDeleteDatabase, {PH1: this.database.databaseId.name}), this,
        {jslogContext: 'delete-database-confirmation'});
    if (ok) {
      void this.model.deleteDatabase(this.database.databaseId);
    }
  }
  override wasShown(): void {
    super.wasShown();
  }
}

customElements.define('devtools-idb-database-view', IDBDatabaseView);

declare global {
  interface HTMLElementTagNameMap {
    'devtools-idb-database-view': IDBDatabaseView;
  }
}
export interface IndexedDBDataViewInput {
  isIndex: boolean;
  index: Index|null;
  objectStore: ObjectStore;
  entries: Entry[];
  skipCount: number;
  selectedRowNumber: number;
  clearButtonEnabled: boolean;
  hasMore: boolean;
  keyFilter: string;
  needsRefreshVisible: boolean;
  metadata: ObjectStoreMetadata|null;

  refreshButtonClicked: () => void;
  clearButtonClicked: () => Promise<void>;
  deleteButtonClicked: () => Promise<void>;
  pageBackButtonClicked: () => void;
  pageForwardButtonClicked: () => void;
  onKeyFilterChange: (value: string) => void;
  onRowSelected: (rowNumber: number) => void;
  deleteEntry: (entry: Entry) => Promise<void>;
  populateContextMenu: (e: CustomEvent<UI.ContextMenu.ContextMenu>, entry: Entry) => void;
}

const renderKeyPathString = (keyPathString: string): LitTemplate => {
  return html`"<span class="source-code indexed-db-key-path">${keyPathString}</span>"`;
};

const renderKeyColumnHeader = (prefix: string, keyPath: string|string[]|null|undefined): LitTemplate => {
  if (keyPath === undefined || keyPath === null || keyPath === '') {
    return html`${prefix}`;
  }
  return html`
    ${prefix} (${i18nString(UIStrings.keyPath)}${
      Array.isArray(keyPath) ?
          html`[${keyPath.map((path, i) => html`${i > 0 ? ', ' : ''}${renderKeyPathString(path)}`)}]` :
          renderKeyPathString(keyPath)})`;
};

const renderDataGrid = (input: IndexedDBDataViewInput): LitTemplate => {
  const keyPath = input.isIndex && input.index ? input.index.keyPath : input.objectStore.keyPath;
  // clang-format off
  return html`<devtools-data-grid striped style="flex: auto;" name=${i18nString(UIStrings.indexedDb)} .template=${html`
    <style>${indexedDBViewsStyles}</style>
    <table>
      <tr>
        <th id="number" fixed width="50px">#</th>
        <th id="key">${renderKeyColumnHeader(i18nString(UIStrings.keyString), keyPath)}</th>
        ${input.isIndex ? html`<th id="primary-key">${renderKeyColumnHeader(i18nString(UIStrings.primaryKey), input.objectStore.keyPath)}</th>` : nothing}
        <th id="value">${i18nString(UIStrings.valueString)}</th>
      </tr>
      ${repeat(input.entries, (_entry, index) => index, (entry, index) => html`
        <tr ?selected=${index + input.skipCount === input.selectedRowNumber}
            @select=${() => input.onRowSelected(index + input.skipCount)}
            @delete=${() => input.deleteEntry(entry)}
            @contextmenu=${(e: CustomEvent<UI.ContextMenu.ContextMenu>) => input.populateContextMenu(e, entry)}>
          <td>${index + input.skipCount}</td>
          <td>${widget(ObjectPropertiesSectionWidget, {value: entry.key})}</td>
          ${input.isIndex ? html`<td>${widget(ObjectPropertiesSectionWidget, {value: entry.primaryKey})}</td>` : nothing}
          <td class="value-column">${widget(ObjectPropertiesSectionWidget, {value: entry.value})}</td>
        </tr>`
      )}
    </table>`}>
  </devtools-data-grid>`;
  // clang-format on
};

const renderToolbar = (input: IndexedDBDataViewInput): LitTemplate => {
  // clang-format off
  return html`
    <devtools-toolbar class="data-view-toolbar" jslog=${VisualLogging.toolbar()}>
      <devtools-button
        class="toolbar-button"
        .iconName=${'refresh'}
        .title=${i18nString(UIStrings.refresh)}
        jslog=${VisualLogging.action('refresh').track({click: true})}
        @click=${input.refreshButtonClicked}
        .variant=${Buttons.Button.Variant.TOOLBAR}
      ></devtools-button>
      <devtools-button
        class="toolbar-button"
        .iconName=${'clear'}
        .title=${i18nString(UIStrings.clearObjectStore)}
        jslog=${VisualLogging.action('clear-all').track({click: true})}
        @click=${input.clearButtonClicked}
        .disabled=${input.isIndex || !input.clearButtonEnabled}
        .variant=${Buttons.Button.Variant.TOOLBAR}>
      </devtools-button>
      <devtools-button
        class="toolbar-button"
        .iconName=${'bin'}
        .title=${i18nString(UIStrings.deleteSelected)}
        jslog=${VisualLogging.action('delete-selected').track({click: true})}
        @click=${input.deleteButtonClicked}
        .disabled=${input.selectedRowNumber < 0 || input.entries.length === 0}
        .variant=${Buttons.Button.Variant.TOOLBAR}>
      </devtools-button>

      <div class="toolbar-divider"></div>

      <devtools-button
        class="toolbar-button"
        .iconName=${'triangle-left'}
        .title=${i18nString(UIStrings.showPreviousPage)}
        .disabled=${input.skipCount <= 0}
        @click=${input.pageBackButtonClicked}
        .variant=${Buttons.Button.Variant.TOOLBAR}>
      </devtools-button>
      <devtools-button
        class="toolbar-button"
        .iconName=${'triangle-right'}
        .title=${i18nString(UIStrings.showNextPage)}
        .disabled=${!input.hasMore}
        @click=${input.pageForwardButtonClicked}
        .variant=${Buttons.Button.Variant.TOOLBAR}>
      </devtools-button>

      <devtools-toolbar-input
        type="filter"
        placeholder=${i18nString(UIStrings.filterByKey)}
        class="key-filter-input"
        .value=${input.keyFilter}
        @change=${(e: CustomEvent<string>) => {
          input.onKeyFilterChange(e.detail);
        }}>
      </devtools-toolbar-input>

      ${input.needsRefreshVisible ?  html`
        <div class="toolbar-divider"></div>
        <div class="toolbar-item stale-data-warning" title=${
          i18nString(
              UIStrings
                  .someEntriesMayHaveBeenModified)}>
          <devtools-icon name="warning" class="warning-icon"></devtools-icon>
          <span>${i18nString(UIStrings.dataMayBeStale)}</span>
        </div>
      ` : nothing}
    </devtools-toolbar>`;
  // clang-format on
};

const renderSummaryBar = (input: IndexedDBDataViewInput): LitTemplate => {
  const metadata = input.metadata;
  if (!metadata) {
    return nothing;
  }
  // clang-format off
  return html`
    <div class="object-store-summary-bar">
      <span>${i18nString(UIStrings.totalEntriesS, { PH1: String(metadata.entriesCount)})}</span>
      ${input.objectStore.autoIncrement ? html`
        <span class="separator">\u2758</span>
        <span>${i18nString(UIStrings.keyGeneratorValueS, {PH1: String(metadata.keyGeneratorValue)})}</span>`
        : nothing}
    </div>`;
  // clang-format on
};

export type IDBDataViewView = (input: IndexedDBDataViewInput, output: undefined, target: HTMLElement) => void;

// clang-format off
export const IDB_DATA_VIEW_DEFAULT_VIEW: IDBDataViewView = (input, _output, target) => {
  render(html`
    ${renderToolbar(input)}
    <div class="data-grid-container">
      ${renderDataGrid(input)}
    </div>
    ${renderSummaryBar(input)}
  `, target, {container: {classes: ['indexed-db-data-view', 'storage-view']}});
};
// clang-format on

export class IDBDataView extends UI.View.SimpleView {
  private readonly model: IndexedDBModel;
  private readonly databaseId: DatabaseId;
  private isIndex: boolean;
  private readonly refreshObjectStoreCallback: () => void;
  private clearingObjectStore: boolean;
  private pageSize: number;
  private skipCount: number;
  // Used in Web Tests
  protected entries: Entry[];
  #hasMore = false;
  #selectedRowNumber = -1;

  #needsRefreshVisible = false;
  #clearButtonEnabled = true;
  #metadata: ObjectStoreMetadata|null = null;
  #keyFilter = '';

  private objectStore!: ObjectStore;
  private index!: Index|null;
  private lastPageSize!: number;
  private lastSkipCount!: number;
  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private lastKey?: any;
  readonly #view: IDBDataViewView;

  constructor(model: IndexedDBModel, databaseId: DatabaseId, objectStore: ObjectStore, index: Index|null,
              refreshObjectStoreCallback: () => void, view = IDB_DATA_VIEW_DEFAULT_VIEW) {
    super({
      title: i18nString(UIStrings.idb),
      viewId: 'idb',
      jslog: `${VisualLogging.pane('indexed-db-data-view')}`,
    });
    this.#view = view;
    this.registerRequiredCSS(indexedDBViewsStyles);
    this.registerRequiredCSS(DataGrid.dataGridStyles);

    this.model = model;
    this.databaseId = databaseId;
    this.isIndex = Boolean(index);
    this.refreshObjectStoreCallback = refreshObjectStoreCallback;

    this.clearingObjectStore = false;

    this.pageSize = 50;
    this.skipCount = 0;
    this.entries = [];

    this.update(objectStore, index);
  }

  private pageBackButtonClicked(): void {
    this.skipCount = Math.max(0, this.skipCount - this.pageSize);
    this.updateData(false);
  }

  private pageForwardButtonClicked(): void {
    this.skipCount = this.skipCount + this.pageSize;
    this.updateData(false);
  }

  private populateContextMenu(e: CustomEvent<UI.ContextMenu.ContextMenu>, {value}: Entry): void {
    const contextMenu = e.detail;
    if (value && value.hasChildren) {
      const tr = e.currentTarget as HTMLElement;
      const valueTd = tr.querySelector('.value-column');
      if (valueTd) {
        const widgetEl = valueTd.querySelector('devtools-widget');
        if (widgetEl) {
          const widget = UI.Widget.Widget.get(widgetEl);
          if (widget instanceof ObjectPropertiesSectionWidget) {
            const objectUi = widget.objectPropertiesSection;
            if (objectUi) {
              contextMenu.revealSection().appendItem(i18nString(UIStrings.expandRecursively), () => {
                void objectUi.objectTreeElement().expandRecursively();
              }, {jslogContext: 'expand-recursively'});
              contextMenu.revealSection().appendItem(i18nString(UIStrings.collapse), () => {
                objectUi.objectTreeElement().collapse();
              }, {jslogContext: 'collapse'});
            }
          }
        }
      }
    }
  }

  refreshData(): void {
    this.updateData(true);
  }

  update(objectStore: ObjectStore|null = null, index: Index|null = null): void {
    if (!objectStore) {
      return;
    }
    this.objectStore = objectStore;
    this.index = index;

    this.#selectedRowNumber = -1;

    this.skipCount = 0;
    this.updateData(true);
    this.performUpdate();
  }

  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private parseKey(keyString: string): any {
    let result;
    try {
      result = JSON.parse(keyString);
    } catch {
      result = keyString;
    }
    return result;
  }

  private updateData(force: boolean): void {
    const key = this.parseKey(this.#keyFilter);

    const pageSize = this.pageSize;
    let skipCount: 0|number = this.skipCount;
    const selected = this.#selectedRowNumber !== -1 ? this.#selectedRowNumber : 0;
    this.#selectedRowNumber = Math.max(selected, this.skipCount);  // Page forward should select top entry

    if (!force && this.lastKey === key && this.lastPageSize === pageSize && this.lastSkipCount === skipCount) {
      return;
    }

    if (this.lastKey !== key || this.lastPageSize !== pageSize) {
      skipCount = 0;
      this.skipCount = 0;
    }
    this.lastKey = key;
    this.lastPageSize = pageSize;
    this.lastSkipCount = skipCount;

    function callback(this: IDBDataView, entries: Entry[], hasMore: boolean): void {
      this.entries = entries;
      this.#hasMore = hasMore;
      this.#needsRefreshVisible = false;
      if (this.entries.length === 0) {
        this.#selectedRowNumber = -1;
      } else {
        this.#selectedRowNumber = Math.min(this.#selectedRowNumber, this.skipCount + this.entries.length - 1);
        if (this.#selectedRowNumber < this.skipCount) {
          this.#selectedRowNumber = -1;
        }
      }
      this.performUpdate();
      this.updatedDataForTests();
    }

    const idbKeyRange = key ? window.IDBKeyRange.lowerBound(key) : null;
    if (this.isIndex && this.index) {
      this.model.loadIndexData(this.databaseId, this.objectStore.name, this.index.name, idbKeyRange, skipCount,
                               pageSize, callback.bind(this));
    } else {
      this.model.loadObjectStoreData(this.databaseId, this.objectStore.name, idbKeyRange, skipCount, pageSize,
                                     callback.bind(this));
    }
    void this.model.getMetadata(this.databaseId, this.objectStore).then(metadata => {
      this.#metadata = metadata;
      this.performUpdate();
    });
  }

  private updatedDataForTests(): void {
    // Sniffed in tests.
  }

  private refreshButtonClicked(): void {
    this.updateData(true);
  }

  private async clearButtonClicked(): Promise<void> {
    const ok =
        await UI.UIUtils.ConfirmDialog.show(i18nString(UIStrings.objectStoreWillBeCleared),
                                            i18nString(UIStrings.confirmClearObjectStore, {PH1: this.objectStore.name}),
                                            // TODO(b/407750537): Fix the linter false positive
                                            // eslint-disable-next-line @devtools/no-imperative-dom-api
                                            this.element, {jslogContext: 'clear-object-store-confirmation'});
    if (ok) {
      this.#clearButtonEnabled = false;
      this.performUpdate();
      this.clearingObjectStore = true;
      await this.model.clearObjectStore(this.databaseId, this.objectStore.name);
      this.clearingObjectStore = false;
      this.#clearButtonEnabled = true;
      this.performUpdate();
      this.updateData(true);
    }
  }

  markNeedsRefresh(): void {
    // We expect that calling clearObjectStore() will cause the backend to send us an update.
    if (this.clearingObjectStore) {
      return;
    }
    this.#needsRefreshVisible = true;
    this.performUpdate();
  }

  private async resolveArrayKey(key: SDK.RemoteObject.RemoteObject): Promise<IDBKeyValue> {
    const {properties} = await key.getOwnProperties(false /* generatePreview */);
    if (!properties) {
      return [];
    }
    const result: IDBKeyValue = [];
    const propertyPromises = properties.filter(property => !isNaN(Number(property.name))).map(async property => {
      const value = property.value;
      if (!value) {
        return;
      }
      let propertyValue;
      if (value.subtype === 'array') {
        propertyValue = await this.resolveArrayKey(value);
      } else {
        propertyValue = value.value;
      }
      result[Number(property.name)] = propertyValue;
    });
    await Promise.all(propertyPromises);
    return result;
  }

  private async deleteButtonClicked(): Promise<void> {
    if (this.#selectedRowNumber < 0) {
      return;
    }
    const entry = this.entries[this.#selectedRowNumber - this.skipCount];
    if (entry) {
      await this.deleteEntry(entry);
    }
  }

  private async deleteEntry(entry: Entry): Promise<void> {
    const key = (this.isIndex ? entry.primaryKey : entry.key);
    const keyValue: IDBKeyValue = key.subtype === 'array' ? await this.resolveArrayKey(key) : key.value;
    await this.model.deleteEntries(this.databaseId, this.objectStore.name, window.IDBKeyRange.only(keyValue));
    this.refreshObjectStoreCallback();
  }

  clear(): void {
    this.entries = [];
    this.#selectedRowNumber = -1;
    this.performUpdate();
  }

  private onRowSelected(rowNumber: number): void {
    this.#selectedRowNumber = rowNumber;
    this.performUpdate();
  }

  override performUpdate(): void {
    this.#view({
      isIndex: this.isIndex,
      index: this.index,
      objectStore: this.objectStore,
      entries: this.entries,
      skipCount: this.skipCount,
      selectedRowNumber: this.#selectedRowNumber,
      clearButtonEnabled: this.#clearButtonEnabled,
      hasMore: this.#hasMore,
      keyFilter: this.#keyFilter,
      needsRefreshVisible: this.#needsRefreshVisible,
      metadata: this.#metadata,
      refreshButtonClicked: this.refreshButtonClicked.bind(this),
      clearButtonClicked: this.clearButtonClicked.bind(this),
      deleteButtonClicked: this.deleteButtonClicked.bind(this),
      pageBackButtonClicked: this.pageBackButtonClicked.bind(this),
      pageForwardButtonClicked: this.pageForwardButtonClicked.bind(this),
      onKeyFilterChange: (value: string) => {
        this.#keyFilter = value;
        this.updateData(false);
      },
      onRowSelected: this.onRowSelected.bind(this),
      deleteEntry: this.deleteEntry.bind(this),
      populateContextMenu: this.populateContextMenu.bind(this),
    },
               undefined, this.element);
  }
}

interface ObjectPropertiesSectionWidgetInput {
  value: SDK.RemoteObject.RemoteObject|null;
}

interface ObjectPropertiesSectionWidgetOutput {
  objectPropSection: ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection|null;
}

type ObjectPropertiesSectionWidgetView = (
    input: ObjectPropertiesSectionWidgetInput,
    output: ObjectPropertiesSectionWidgetOutput,
    target: HTMLElement,
    ) => void;

const OBJECT_PROPERTIES_SECTION_WIDGET_DEFAULT_VIEW: ObjectPropertiesSectionWidgetView = (input, output, target) => {
  if (!input.value) {
    output.objectPropSection = null;
    render(nothing, target);
    return;
  }
  const objectPropSection = ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.defaultObjectPropertiesSection(
      input.value, undefined /* linkifier */, true /* skipProto */, true /* readOnly */);
  output.objectPropSection = objectPropSection;

  const element = input.value.hasChildren ? objectPropSection.element : objectPropSection.titleElement;
  render(html`${element}`, target);
};

class ObjectPropertiesSectionWidget extends UI.Widget.Widget {
  #value: SDK.RemoteObject.RemoteObject|null = null;
  #objectPropSection: ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection|null = null;
  readonly #view: ObjectPropertiesSectionWidgetView;

  constructor(element?: HTMLElement, view = OBJECT_PROPERTIES_SECTION_WIDGET_DEFAULT_VIEW) {
    super(element);
    this.#view = view;
  }

  set value(value: SDK.RemoteObject.RemoteObject|null) {
    if (this.#value === value) {
      return;
    }
    this.#value = value;
    this.requestUpdate();
  }

  get objectPropertiesSection(): ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection|null {
    return this.#objectPropSection;
  }

  override performUpdate(): void {
    const output: ObjectPropertiesSectionWidgetOutput = {objectPropSection: null};
    this.#view({value: this.#value}, output, this.contentElement);
    this.#objectPropSection = output.objectPropSection;
  }
}
