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

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 * as Protocol from '../../../../generated/protocol.js';
import {Directives, html, type LitTemplate, nothing, render} from '../../../lit/lit.js';

const {ifDefined, repeat} = Directives;
const UIStrings = {
  /**
   * @description Text shown in the console object preview. Shown when the user is inspecting a
   * JavaScript object and there are multiple empty properties on the object (x =
   * 'times'/'multiply').
   * @example {3} PH1
   */
  emptyD: 'empty × {PH1}',
  /**
   * @description Shown when the user is inspecting a JavaScript object in the console and there is
   * an empty property on the object..
   */
  empty: 'empty',
  /**
   * @description Text shown when the user is inspecting a JavaScript object, but of the properties
   * is not immediately available because it is a JavaScript 'getter' function, which means we have
   * to run some code first in order to compute this property.
   */
  thePropertyIsComputedWithAGetter: 'The property is computed with a getter',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/object_ui/RemoteObjectPreviewFormatter.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

interface PropertyPreviewValue {
  name?: string;
  entry?: Protocol.Runtime.EntryPreview;
  value?: Protocol.Runtime.PropertyPreview;
  placeholder?: string;
}

export class RemoteObjectPreviewFormatter {
  private static objectPropertyComparator(a: Protocol.Runtime.PropertyPreview, b: Protocol.Runtime.PropertyPreview):
      number {
    return sortValue(a) - sortValue(b);

    function sortValue(property: Protocol.Runtime.PropertyPreview): number {
      // TODO(einbinder) expose whether preview properties are actually internal.
      if (property.name === InternalName.PROMISE_STATE) {
        return 1;
      }
      if (property.name === InternalName.PROMISE_RESULT) {
        return 2;
      }
      if (property.name === InternalName.GENERATOR_STATE || property.name === InternalName.PRIMITIVE_VALUE ||
          property.name === InternalName.WEAK_REF_TARGET) {
        return 3;
      }
      if (property.type !== Protocol.Runtime.PropertyPreviewType.Function && !property.name.startsWith('#')) {
        return 4;
      }
      return 5;
    }
  }

  renderObjectPreview(preview: Protocol.Runtime.ObjectPreview, includeNullOrUndefined = true): LitTemplate {
    const description = preview.description;
    const subTypesWithoutValuePreview = new Set<Protocol.Runtime.ObjectPreviewSubtype|'internal#entry'|'trustedtype'>([
      Protocol.Runtime.ObjectPreviewSubtype.Arraybuffer,
      Protocol.Runtime.ObjectPreviewSubtype.Dataview,
      Protocol.Runtime.ObjectPreviewSubtype.Error,
      Protocol.Runtime.ObjectPreviewSubtype.Null,
      Protocol.Runtime.ObjectPreviewSubtype.Regexp,
      Protocol.Runtime.ObjectPreviewSubtype.Webassemblymemory,
      'internal#entry',
      'trustedtype',
    ]);
    if (preview.type !== Protocol.Runtime.ObjectPreviewType.Object ||
        (preview.subtype && subTypesWithoutValuePreview.has(preview.subtype))) {
      return this.renderPropertyPreview(preview.type, preview.subtype, undefined, description);
    }
    const isArrayOrTypedArray = preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Array ||
        preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Typedarray;
    let objectDescription = '';
    if (description) {
      if (isArrayOrTypedArray) {
        const arrayLength = SDK.RemoteObject.RemoteObject.arrayLength(preview);
        const arrayLengthText = arrayLength > 1 ? ('(' + arrayLength + ')') : '';
        const arrayName = SDK.RemoteObject.RemoteObject.arrayNameFromDescription(description);
        objectDescription = arrayName === 'Array' ? arrayLengthText : (arrayName + arrayLengthText);
      } else {
        const hideDescription = description === 'Object';
        objectDescription = hideDescription ? '' : description;
      }
    }

    const items = Array.from(
        preview.entries         ? this.renderEntries(preview) :
            isArrayOrTypedArray ? this.renderArrayProperties(preview) :
                                  this.renderObjectProperties(preview, includeNullOrUndefined));

    // clang-format off
    const renderName = (name: string): LitTemplate  => html`<span class=name>${
      /^\s|\s$|^$|\n/.test(name)? '"' + name.replace(/\n/g, '\u21B5') + '"' : name}</span>`;

    const renderPlaceholder = (placeholder: string): LitTemplate =>
          html`<span class=object-value-undefined>${placeholder}</span>`;

    const renderValue = (value: Protocol.Runtime.PropertyPreview): LitTemplate=>
          this.renderPropertyPreview(value.type, value.subtype, value.name, value.value);

    const renderEntry = (entry: Protocol.Runtime.EntryPreview): LitTemplate=> html`${entry.key &&
          html`${this.renderPropertyPreview(entry.key.type, entry.key.subtype, undefined, entry.key.description)} => `}
          ${this.renderPropertyPreview(entry.value.type, entry.value.subtype, undefined, entry.value.description)}`;

    const renderItem = ({name, entry, value, placeholder}: PropertyPreviewValue, index: number): LitTemplate => html`${
        index > 0 ? ', ' : ''}${
        placeholder !== undefined ? renderPlaceholder(placeholder) : nothing}${
        name !== undefined ? renderName(name) : nothing}${
        name !== undefined && value ? ': ' : ''}${
        value ? renderValue(value) : nothing}${
        entry ? renderEntry(entry) : nothing}`;
    // clang-format on

    return html`${
        objectDescription.length > 0 ?
            html`<span class=object-description>${objectDescription + '\xA0'}</span>` :
            nothing}<span class=object-properties-preview>${isArrayOrTypedArray ? '[' : '{'}${
        repeat(items, renderItem)}${preview.overflow ? html`<span>${items.length > 0 ? ',\xA0…' : '…'}</span>` : ''}
    ${isArrayOrTypedArray ? ']' : '}'}</span>`;
  }

  private *
      renderObjectProperties(preview: Protocol.Runtime.ObjectPreview, includeNullOrUndefined: boolean):
          Generator<PropertyPreviewValue> {
    const properties = preview.properties.filter(p => p.type !== 'accessor')
                           .sort(RemoteObjectPreviewFormatter.objectPropertyComparator);
    for (let i = 0; i < properties.length; ++i) {
      const property = properties[i];
      const name = property.name;
      if (!includeNullOrUndefined &&
          (property.type === 'undefined' || (property.type === 'object' && property.subtype === 'null'))) {
        continue;
      }

      // Internal properties are given special formatting, e.g. Promises `<rejected>: 123`.
      if (preview.subtype === Protocol.Runtime.ObjectPreviewSubtype.Promise && name === InternalName.PROMISE_STATE) {
        const promiseResult =
            properties.at(i + 1)?.name === InternalName.PROMISE_RESULT ? properties.at(i + 1) : undefined;
        if (promiseResult) {
          i++;
        }
        yield {name: '<' + property.value + '>', value: property.value !== 'pending' ? promiseResult : undefined};
      } else if (preview.subtype === 'generator' && name === InternalName.GENERATOR_STATE) {
        yield {name: '<' + property.value + '>'};
      } else if (name === InternalName.PRIMITIVE_VALUE) {
        yield {value: property};
      } else if (name === InternalName.WEAK_REF_TARGET) {
        if (property.type === Protocol.Runtime.PropertyPreviewType.Undefined) {
          yield {name: '<cleared>'};
        } else {
          yield {value: property};
        }
      } else {
        yield {name, value: property};
      }
    }
  }

  private * renderArrayProperties(preview: Protocol.Runtime.ObjectPreview): Generator<PropertyPreviewValue> {
    const arrayLength = SDK.RemoteObject.RemoteObject.arrayLength(preview);
    const indexProperties = preview.properties.filter(p => toArrayIndex(p.name) !== -1).sort(arrayEntryComparator);
    const otherProperties = preview.properties.filter(p => toArrayIndex(p.name) === -1)
                                .sort(RemoteObjectPreviewFormatter.objectPropertyComparator);

    function arrayEntryComparator(a: Protocol.Runtime.PropertyPreview, b: Protocol.Runtime.PropertyPreview): number {
      return toArrayIndex(a.name) - toArrayIndex(b.name);
    }

    function toArrayIndex(name: string): number {
      // We need to differentiate between property accesses and array index accesses
      // Therefore, we need to make sure we are always dealing with an i32, in the event
      // that a particular property also exists, but as the literal string. For example
      // for {["1.5"]: true}, we don't want to return `true` if we provide `1.5` as the
      // value, but only want to do that if we provide `"1.5"`.
      const index = Number(name) >>> 0;
      if (String(index) === name && index < arrayLength) {
        return index;
      }
      return -1;
    }

    // Gaps can be shown when all properties are guaranteed to be in the preview.
    const canShowGaps = !preview.overflow;

    const indexedProperties:
        Array<{property: Protocol.Runtime.PropertyPreview, index: number, gap: number, hasGaps: boolean}> = [];
    for (const property of indexProperties) {
      const index = toArrayIndex(property.name);
      const gap = index - (indexedProperties.at(-1)?.index ?? -1) - 1;
      const hasGaps = index !== indexedProperties.length;
      indexedProperties.push({property, index, gap, hasGaps});
    }
    const trailingGap = arrayLength - (indexedProperties.at(-1)?.index ?? -1) - 1;

    // TODO(l10n): Plurals. Tricky because of a bug in the presubmit check for plurals.
    const renderGap = (count: number): {placeholder: string} =>
        ({placeholder: count !== 1 ? i18nString(UIStrings.emptyD, {PH1: count}) : i18nString(UIStrings.empty)});
    for (const {property, gap, hasGaps} of indexedProperties) {
      if (canShowGaps && gap > 0) {
        yield renderGap(gap);
      }
      yield {name: !canShowGaps && hasGaps ? property.name : undefined, value: property};
    }
    if (canShowGaps && trailingGap > 0) {
      yield renderGap(trailingGap);
    }

    for (const property of otherProperties) {
      yield {name: property.name, value: property};
    }
  }

  private * renderEntries(preview: Protocol.Runtime.ObjectPreview): Generator<PropertyPreviewValue> {
    for (const entry of preview.entries ?? []) {
      yield {entry};
    }
  }

  renderPropertyPreview(type: string, subtype?: string, className?: string|null, description?: string): LitTemplate {
    const title = type === 'accessor'   ? i18nString(UIStrings.thePropertyIsComputedWithAGetter) :
        (type === 'object' && !subtype) ? description :
                                          undefined;

    const abbreviateFullQualifiedClassName = (description: string): string => {
      const abbreviatedDescription = description.split('.');
      for (let i = 0; i < abbreviatedDescription.length - 1; ++i) {
        abbreviatedDescription[i] = Platform.StringUtilities.trimMiddle(abbreviatedDescription[i], 3);
      }
      return abbreviatedDescription.length === 1 && abbreviatedDescription[0] === 'Object' ?
          '{…}' :
          abbreviatedDescription.join('.');
    };

    const preview = (): string|LitTemplate|undefined|null => type === 'accessor' ? '(...)' :
        type === 'function'                                                      ? '\u0192' :
        type === 'object' && subtype === 'trustedtype' && className ? renderTrustedType(description ?? '', className) :
        type === 'object' && subtype === 'node' && description      ? renderNodeTitle(description) :
        type === 'string'             ? Platform.StringUtilities.formatAsJSLiteral(description ?? '') :
        type === 'object' && !subtype ? abbreviateFullQualifiedClassName(description ?? '') :
                                        description;

    return html`<span class='object-value-${(subtype || type)}' title=${ifDefined(title)}>${preview()}</span>`;
  }

  renderEvaluationResultPreview(result: SDK.RuntimeModel.EvaluationResult, allowErrors?: boolean): LitTemplate {
    if ('error' in result) {
      return nothing;
    }

    if (result.exceptionDetails?.exception?.description) {
      const exception = result.exceptionDetails.exception.description;
      if (exception.startsWith('TypeError: ') || allowErrors) {
        return html`<span>${result.exceptionDetails.text} ${exception}</span>`;
      }
      return nothing;
    }

    const {preview, type, subtype, className, description} = result.object;
    if (preview && type === 'object' && subtype !== 'node' && subtype !== 'trustedtype') {
      return this.renderObjectPreview(preview);
    }
    return this.renderPropertyPreview(
        type, subtype, className, Platform.StringUtilities.trimEndWithMaxLength(description || '', 400));
  }

  /** @deprecated (crbug.com/457388389) Use lit version instead */
  renderEvaluationResultPreviewFragment(result: SDK.RuntimeModel.EvaluationResult, allowErrors?: boolean):
      DocumentFragment {
    const fragment = document.createDocumentFragment();
    /* eslint-disable-next-line  @devtools/no-lit-render-outside-of-view */
    render(this.renderEvaluationResultPreview(result, allowErrors), fragment);
    return fragment;
  }
}

const enum InternalName {
  GENERATOR_STATE = '[[GeneratorState]]',
  PRIMITIVE_VALUE = '[[PrimitiveValue]]',
  PROMISE_STATE = '[[PromiseState]]',
  PROMISE_RESULT = '[[PromiseResult]]',
  WEAK_REF_TARGET = '[[WeakRefTarget]]',
}

export function renderNodeTitle(nodeTitle: string): LitTemplate|null {
  const match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/);
  if (!match) {
    return null;
  }
  return html`<span class=webkit-html-tag-name>${match[1]}</span>${
      match[2] && html`<span class=webkit-html-attribute-value>${match[2]}</span>`}${
      match[3] && html`<span class=webkit-html-attribute-name>${match[3]}</span>`}`;
}

export function renderTrustedType(description: string, className: string): LitTemplate {
  return html`${className} <span class=object-value-string>"${description.replace(/\n/g, '\u21B5')}"</span>`;
}
