// Copyright 2020 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 rulesdir/no-lit-render-outside-of-view */

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

import * as i18n from '../../../core/i18n/i18n.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';

import valueInterpreterDisplayStyles from './valueInterpreterDisplay.css.js';
import {
  Endianness,
  format,
  getDefaultValueTypeMapping,
  getPointerAddress,
  isNumber,
  isPointer,
  isValidMode,
  VALUE_TYPE_MODE_LIST,
  ValueType,
  ValueTypeMode,
} from './ValueInterpreterDisplayUtils.js';

const UIStrings = {
  /**
   * @description Tooltip text that appears when hovering over an unsigned interpretation of the memory under the Value Interpreter
   */
  unsignedValue: '`Unsigned` value',
  /**
   * @description Tooltip text that appears when hovering over the element to change value type modes of under the Value Interpreter. Value type modes
   *             are different ways of viewing a certain value, e.g.: 10 (decimal) can be 0xa in hexadecimal mode, or 12 in octal mode.
   */
  changeValueTypeMode: 'Change mode',
  /**
   * @description Tooltip text that appears when hovering over a signed interpretation of the memory under the Value Interpreter
   */
  signedValue: '`Signed` value',
  /**
   * @description Tooltip text that appears when hovering over a 'jump-to-address' button that is next to a pointer (32-bit or 64-bit) under the Value Interpreter
   */
  jumpToPointer: 'Jump to address',
  /**
   * @description Tooltip text that appears when hovering over a 'jump-to-address' button that is next to a pointer (32-bit or 64-bit) with an invalid address under the Value Interpreter.
   */
  addressOutOfRange: 'Address out of memory range',

} as const;
const str_ =
    i18n.i18n.registerUIStrings('panels/linear_memory_inspector/components/ValueInterpreterDisplay.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {render, html} = Lit;

const SORTED_VALUE_TYPES = Array.from(getDefaultValueTypeMapping().keys());

export interface ValueDisplayData {
  buffer: ArrayBuffer;
  valueTypes: Set<ValueType>;
  endianness: Endianness;
  memoryLength: number;
  valueTypeModes?: Map<ValueType, ValueTypeMode>;
}

export class ValueTypeModeChangedEvent extends Event {
  static readonly eventName = 'valuetypemodechanged';
  data: {type: ValueType, mode: ValueTypeMode};

  constructor(type: ValueType, mode: ValueTypeMode) {
    super(ValueTypeModeChangedEvent.eventName, {
      composed: true,
    });
    this.data = {type, mode};
  }
}

export class JumpToPointerAddressEvent extends Event {
  static readonly eventName = 'jumptopointeraddress';
  data: number;

  constructor(address: number) {
    super(JumpToPointerAddressEvent.eventName, {
      composed: true,
    });
    this.data = address;
  }
}

export class ValueInterpreterDisplay extends HTMLElement {
  readonly #shadow = this.attachShadow({mode: 'open'});
  #endianness = Endianness.LITTLE;
  #buffer = new ArrayBuffer(0);
  #valueTypes = new Set<ValueType>();
  #valueTypeModeConfig: Map<ValueType, ValueTypeMode> = getDefaultValueTypeMapping();
  #memoryLength = 0;

  set data(data: ValueDisplayData) {
    this.#buffer = data.buffer;
    this.#endianness = data.endianness;
    this.#valueTypes = data.valueTypes;
    this.#memoryLength = data.memoryLength;

    if (data.valueTypeModes) {
      data.valueTypeModes.forEach((mode, valueType) => {
        if (isValidMode(valueType, mode)) {
          this.#valueTypeModeConfig.set(valueType, mode);
        }
      });
    }

    this.#render();
  }

  #render(): void {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    render(html`
      <style>${UI.inspectorCommonStyles}</style>
      <style>${valueInterpreterDisplayStyles}</style>
      <div class="value-types">
        ${SORTED_VALUE_TYPES.map(type => this.#valueTypes.has(type) ? this.#showValue(type) : '')}
      </div>
    `, this.#shadow, {host: this},
    );
    // clang-format on
  }

  #showValue(type: ValueType): Lit.TemplateResult {
    if (isNumber(type)) {
      return this.#renderNumberValues(type);
    }
    if (isPointer(type)) {
      return this.#renderPointerValue(type);
    }
    throw new Error(`No known way to format ${type}`);
  }

  #renderPointerValue(type: ValueType): Lit.TemplateResult {
    const unsignedValue = this.#parse({type, signed: false});
    const address = getPointerAddress(type, this.#buffer, this.#endianness);
    const jumpDisabled = Number.isNaN(address) || BigInt(address) >= BigInt(this.#memoryLength);
    const buttonTitle = jumpDisabled ? i18nString(UIStrings.addressOutOfRange) : i18nString(UIStrings.jumpToPointer);
    const iconColor = jumpDisabled ? 'var(--icon-default)' : 'var(--icon-link)';
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    return html`
      <span class="value-type-cell-no-mode value-type-cell selectable-text">${i18n.i18n.lockedString(type)}</span>
      <div class="value-type-cell">
        <div class="value-type-value-with-link" data-value="true">
        <span class="selectable-text">${unsignedValue}</span>
          ${
            html`
              <button class="jump-to-button" data-jump="true" title=${buttonTitle} ?disabled=${jumpDisabled}
                jslog=${VisualLogging.action('linear-memory-inspector.jump-to-address').track({click: true})}
                @click=${this.#onJumpToAddressClicked.bind(this, Number(address))}>
                <devtools-icon name="open-externally" class="medium" style="color: ${iconColor}">
                </devtools-icon>
              </button>`}
        </div>
      </div>
    `;
    // clang-format on
  }

  #onJumpToAddressClicked(address: number): void {
    this.dispatchEvent(new JumpToPointerAddressEvent(address));
  }

  #renderNumberValues(type: ValueType): Lit.TemplateResult {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    return html`
      <span class="value-type-cell selectable-text">${i18n.i18n.lockedString(type)}</span>
      <div>
        <select title=${i18nString(UIStrings.changeValueTypeMode)}
          data-mode-settings="true"
          jslog=${VisualLogging.dropDown('linear-memory-inspector.value-type-mode').track({change: true})}
          @change=${this.#onValueTypeModeChange.bind(this, type)}>
            ${VALUE_TYPE_MODE_LIST.filter(x => isValidMode(type, x)).map(mode => {
              return html`
                <option value=${mode} .selected=${this.#valueTypeModeConfig.get(type) === mode}
                        jslog=${VisualLogging.item(mode).track({click: true})}>${
                  i18n.i18n.lockedString(mode)}
                </option>`;
            })}
        </select>
      </div>
      ${this.#renderSignedAndUnsigned(type)}
    `;
    // clang-format on
  }

  #renderSignedAndUnsigned(type: ValueType): Lit.TemplateResult {
    const unsignedValue = this.#parse({type, signed: false});
    const signedValue = this.#parse({type, signed: true});
    const mode = this.#valueTypeModeConfig.get(type);
    const showSignedAndUnsigned =
        signedValue !== unsignedValue && mode !== ValueTypeMode.HEXADECIMAL && mode !== ValueTypeMode.OCTAL;

    const unsignedRendered = html`<span class="value-type-cell selectable-text"  title=${
        i18nString(UIStrings.unsignedValue)} data-value="true">${unsignedValue}</span>`;
    if (!showSignedAndUnsigned) {
      return unsignedRendered;
    }

    // Some values are too long to show in one line, we're putting them into the next line.
    const showInMultipleLines = type === ValueType.INT32 || type === ValueType.INT64;
    const signedRendered = html`<span class="selectable-text" data-value="true" title=${
        i18nString(UIStrings.signedValue)}>${signedValue}</span>`;

    if (showInMultipleLines) {
      return html`
        <div class="value-type-cell">
          ${unsignedRendered}
          ${signedRendered}
        </div>
        `;
    }

    return html`
      <div class="value-type-cell" style="flex-direction: row;">
        ${unsignedRendered}
        <span class="signed-divider"></span>
        ${signedRendered}
      </div>
    `;
  }

  #onValueTypeModeChange(type: ValueType, event: Event): void {
    event.preventDefault();
    const select = event.target as HTMLInputElement;
    const mode = select.value as ValueTypeMode;
    this.dispatchEvent(new ValueTypeModeChangedEvent(type, mode));
  }

  #parse(data: {type: ValueType, signed?: boolean}): string {
    const mode = this.#valueTypeModeConfig.get(data.type);
    return format(
        {buffer: this.#buffer, type: data.type, endianness: this.#endianness, signed: data.signed || false, mode});
  }
}

customElements.define('devtools-linear-memory-inspector-interpreter-display', ValueInterpreterDisplay);

declare global {
  interface HTMLElementTagNameMap {
    'devtools-linear-memory-inspector-interpreter-display': ValueInterpreterDisplay;
  }
}
