// 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.

import '../../../ui/kit/kit.js';

import * as i18n from '../../../core/i18n/i18n.js';
import * as Buttons from '../../../ui/components/buttons/buttons.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, nothing, 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 interface ViewInput {
  buffer: ArrayBuffer;
  valueTypes: ValueType[];
  endianness: Endianness;
  memoryLength: number;
  valueTypeModes: Map<ValueType, ValueTypeMode>;
  onValueTypeModeChange: (type: ValueType, mode: ValueTypeMode) => void;
  onJumpToAddressClicked: (address: number) => void;
}
type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;

export const DEFAULT_VIEW: View = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
  function parse(signed: boolean, type: ValueType): string {
    return format(
        {buffer: input.buffer, endianness: input.endianness, type, signed, mode: input.valueTypeModes.get(type)});
  }
  const parseSigned = parse.bind(this, true);
  const parseUnsigned = parse.bind(this, false);

  // Disabled until https://crbug.com/1079231 is fixed.
  // clang-format off
    render(html`
      <style>${UI.inspectorCommonStyles}</style>
      <style>${valueInterpreterDisplayStyles}</style>
      <div class="value-types">
        ${input.valueTypes.map(type => {
          const address = isPointer(type)? getPointerAddress(type, input.buffer, input.endianness): 0;
          const jumpDisabled = Number.isNaN(address) || BigInt(address) >= BigInt(input.memoryLength);
          const signed = parseSigned(type);
          const unsigned = parseUnsigned(type);
          return isNumber(type) ? 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=${(e: Event) =>
                    input.onValueTypeModeChange(type, (e.target as HTMLSelectElement).value as ValueTypeMode)}>
                    ${VALUE_TYPE_MODE_LIST.filter(x => isValidMode(type, x)).map(mode => {
                      return html`
                        <option value=${mode} .selected=${input.valueTypeModes.get(type) === mode}
                                jslog=${VisualLogging.item(mode).track({click: true, resize: true})}>${
                          i18n.i18n.lockedString(mode)}
                        </option>`;
                    })}
                </select>
              </div>
            ${renderSignedAndUnsigned(signed, unsigned, type, input.valueTypeModes.get(type))}`:
          isPointer(type) ? 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">${unsigned}</span>
                <devtools-button
                  data-jump="true"
                  title=${jumpDisabled ? i18nString(UIStrings.addressOutOfRange) : i18nString(UIStrings.jumpToPointer)}
                  .disabled=${jumpDisabled}
                  jslog=${VisualLogging.action('linear-memory-inspector.jump-to-address').track({click: true})}
                  @click=${() => input.onJumpToAddressClicked(Number(address))}
                  .variant=${Buttons.Button.Variant.ICON_TOGGLE}
                  .iconName=${'open-externally'}
                  .size=${Buttons.Button.Size.SMALL}>
                </devtools-button>
              </div>
            </div>`: nothing;
        })}
      </div>
    `, target);
  // clang-format on
};

function renderSignedAndUnsigned(
    signedValue: string, unsignedValue: string, type: ValueType, mode: ValueTypeMode|undefined): Lit.TemplateResult {
  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>
    `;
}

export class ValueInterpreterDisplay extends UI.Widget.Widget {
  readonly #view: View;
  #endianness = Endianness.LITTLE;
  #buffer = new ArrayBuffer(0);
  #valueTypes = new Set<ValueType>();
  #valueTypeModeConfig: Map<ValueType, ValueTypeMode> = getDefaultValueTypeMapping();
  #memoryLength = 0;
  #onValueTypeModeChange: (type: ValueType, mode: ValueTypeMode) => void = () => {};
  #onJumpToAddressClicked: (address: number) => void = () => {};

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

  set onValueTypeModeChange(callback: (type: ValueType, mode: ValueTypeMode) => void) {
    this.#onValueTypeModeChange = callback;
    this.performUpdate();
  }

  get onValueTypeModeChange(): (type: ValueType, mode: ValueTypeMode) => void {
    return this.#onValueTypeModeChange;
  }

  set onJumpToAddressClicked(callback: (address: number) => void) {
    this.#onJumpToAddressClicked = callback;
    this.performUpdate();
  }

  get onJumpToAddressClicked(): (address: number) => void {
    return this.#onJumpToAddressClicked;
  }

  get valueTypeModes(): Map<ValueType, ValueTypeMode> {
    return this.#valueTypeModeConfig;
  }

  set valueTypeModes(modes: Map<ValueType, ValueTypeMode>) {
    const newMap = getDefaultValueTypeMapping();
    modes.forEach((mode, type) => {
      if (isValidMode(type, mode)) {
        newMap.set(type, mode);
      }
    });
    this.#valueTypeModeConfig = newMap;
    this.requestUpdate();
  }

  get valueTypes(): Set<ValueType> {
    return this.#valueTypes;
  }

  set valueTypes(valueTypes: Set<ValueType>) {
    this.#valueTypes = valueTypes;
    this.requestUpdate();
  }

  get buffer(): ArrayBuffer {
    return this.#buffer;
  }

  set buffer(buffer: ArrayBuffer) {
    this.#buffer = buffer;
    this.requestUpdate();
  }

  get endianness(): Endianness {
    return this.#endianness;
  }

  set endianness(endianness: Endianness) {
    this.#endianness = endianness;
    this.requestUpdate();
  }

  get memoryLength(): number {
    return this.#memoryLength;
  }

  set memoryLength(memoryLength: number) {
    this.#memoryLength = memoryLength;
    this.requestUpdate();
  }

  override performUpdate(): void {
    const valueTypes = SORTED_VALUE_TYPES.filter(type => this.#valueTypes.has(type));
    this.#view(
        {
          buffer: this.#buffer,
          valueTypes,
          endianness: this.#endianness,
          memoryLength: this.#memoryLength,
          valueTypeModes: this.#valueTypeModeConfig,
          onValueTypeModeChange: this.#onValueTypeModeChange,
          onJumpToAddressClicked: this.#onJumpToAddressClicked,
        },
        undefined, this.contentElement);
  }
}
