// Copyright 2023 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-lit-render-outside-of-view, @devtools/enforce-custom-element-definitions-location */

import * as Platform from '../../../core/platform/platform.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
import * as Dialogs from '../dialogs/dialogs.js';

import {
  MenuGroup,
  type MenuItemSelectedEvent,
  type MenuItemValue,
} from './Menu.js';
import selectMenuStyles from './selectMenu.css.js';
import selectMenuButtonStyles from './selectMenuButton.css.js';

const {html} = Lit;

export interface SelectMenuData {
  /**
   * Determines where the dialog with the menu will show relative to
   * the show button.
   * Defaults to Bottom.
   */
  position: Dialogs.Dialog.DialogVerticalPosition;
  /**
   * Determines where the dialog with the menu will show horizontally
   * relative to the show button.
   * Defaults to Auto
   */
  horizontalAlignment: Dialogs.Dialog.DialogHorizontalAlignment;
  /**
   * The title of the menu button. Can be either a string or a function
   * that returns a Lit template.
   * If not set, the title of the button will default to the selected
   * item's text.
   */
  buttonTitle: string|TitleCallback;
  /**
   * Determines if an arrow, pointing to the opposite side of
   * the dialog, is shown at the end of the button.
   * Defaults to false.
   */
  showArrow: boolean;
  /**
   * Determines if the component is formed by two buttons:
   * one to open the meny and another that triggers a
   * selectmenusidebuttonclickEvent. The RecordMenu instance of
   * the component is an example of this use case.
   * Defaults to false.
   */
  sideButton: boolean;
  /**
   * Whether the menu button is disabled.
   * Defaults to false.
   */
  disabled: boolean;
  /**
   * Determines if dividing lines between the menu's options
   * are shown.
   */
  showDivider: boolean;
  /**
   * Determines if the selected item is marked using a checkmark.
   * Defaults to true.
   */
  showSelectedItem: boolean;
  /**
   * Specifies a context for the visual element.
   */
  jslogContext: string;
}
type TitleCallback = () => Lit.TemplateResult;

const deployMenuArrow = new URL('../../../Images/triangle-down.svg', import.meta.url).toString();

/**
 * @deprecated use `<select>` instead.
 */
export class SelectMenu extends HTMLElement {
  readonly #shadow = this.attachShadow({mode: 'open'});
  #button: SelectMenuButton|null = null;
  #open = false;
  #props: SelectMenuData = {
    buttonTitle: '',
    position: Dialogs.Dialog.DialogVerticalPosition.BOTTOM,
    horizontalAlignment: Dialogs.Dialog.DialogHorizontalAlignment.AUTO,
    showArrow: false,
    sideButton: false,
    showDivider: false,
    disabled: false,
    showSelectedItem: true,
    jslogContext: '',
  };

  get buttonTitle(): string|TitleCallback {
    return this.#props.buttonTitle;
  }

  set buttonTitle(buttonTitle: string|TitleCallback) {
    this.#props.buttonTitle = buttonTitle;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get position(): Dialogs.Dialog.DialogVerticalPosition {
    return this.#props.position;
  }

  set position(position: Dialogs.Dialog.DialogVerticalPosition) {
    this.#props.position = position;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get horizontalAlignment(): Dialogs.Dialog.DialogHorizontalAlignment {
    return this.#props.horizontalAlignment;
  }

  set horizontalAlignment(horizontalAlignment: Dialogs.Dialog.DialogHorizontalAlignment) {
    this.#props.horizontalAlignment = horizontalAlignment;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get showArrow(): boolean {
    return this.#props.showArrow;
  }

  set showArrow(showArrow: boolean) {
    this.#props.showArrow = showArrow;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get sideButton(): boolean {
    return this.#props.sideButton;
  }

  set sideButton(sideButton: boolean) {
    this.#props.sideButton = sideButton;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get disabled(): boolean {
    return this.#props.disabled;
  }

  set disabled(disabled: boolean) {
    this.#props.disabled = disabled;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get showDivider(): boolean {
    return this.#props.showDivider;
  }

  set showDivider(showDivider: boolean) {
    this.#props.showDivider = showDivider;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get showSelectedItem(): boolean {
    return this.#props.showSelectedItem;
  }

  set showSelectedItem(showSelectedItem: boolean) {
    this.#props.showSelectedItem = showSelectedItem;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get jslogContext(): string {
    return this.#props.jslogContext;
  }

  set jslogContext(jslogContext: string) {
    this.#props.jslogContext = jslogContext;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  #getButton(): SelectMenuButton {
    if (!this.#button) {
      this.#button = this.#shadow.querySelector('devtools-select-menu-button');
      if (!this.#button) {
        throw new Error('Arrow not found');
      }
    }
    return this.#button;
  }

  #showMenu(): void {
    this.#open = true;
    this.setAttribute('has-open-dialog', 'has-open-dialog');
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  override click(): void {
    this.#getButton().click();
  }

  #sideButtonClicked(): void {
    this.dispatchEvent(new SelectMenuSideButtonClickEvent());
  }

  #getButtonText(): Lit.TemplateResult|string {
    return this.buttonTitle instanceof Function ? this.buttonTitle() : this.buttonTitle;
  }

  #renderButton(): Lit.TemplateResult {
    const buttonLabel = this.#getButtonText();
    if (!this.sideButton) {
      // clang-format off
      /* eslint-disable @devtools/no-deprecated-component-usages */
      return html`
          <devtools-select-menu-button
            @selectmenubuttontrigger=${this.#showMenu}
            .open=${this.#open} .showArrow=${this.showArrow}
            .arrowDirection=${this.position}
            .disabled=${this.disabled}
            .jslogContext=${this.jslogContext}>
              ${buttonLabel}
            </devtools-select-menu-button>
        `;
      /* eslint-enable @devtools/no-deprecated-component-usages */
      // clang-format on
    }

    // clang-format off
    /* eslint-disable @devtools/no-deprecated-component-usages */
    return html`
      <button id="side-button" @click=${this.#sideButtonClicked} ?disabled=${this.disabled}>
        ${buttonLabel}
      </button>
      <devtools-select-menu-button
        @click=${this.#showMenu}
        @selectmenubuttontrigger=${this.#showMenu}
        .singleArrow=${true}
        .open=${this.#open}
        .showArrow=${true}
        .arrowDirection=${this.position}
        .disabled=${this.disabled}>
      </devtools-select-menu-button>
    `;
    /* eslint-enable @devtools/no-deprecated-component-usages */
    // clang-format on
  }

  #onMenuClose(evt?: Dialogs.Dialog.ClickOutsideDialogEvent): void {
    if (evt) {
      evt.stopImmediatePropagation();
    }
    void RenderCoordinator.write(() => {
      this.removeAttribute('has-open-dialog');
    });
    this.#open = false;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  #onItemSelected(evt: MenuItemSelectedEvent): void {
    this.dispatchEvent(new SelectMenuItemSelectedEvent(evt.itemValue));
  }

  async #render(): Promise<void> {
    if (!ComponentHelpers.ScheduledRender.isScheduledRender(this)) {
      throw new Error('SelectMenu render was not scheduled');
    }
    // clang-format off
    Lit.render(html`
        <style>${selectMenuStyles}</style>
        <devtools-menu
            @menucloserequest=${this.#onMenuClose}
            @menuitemselected=${this.#onItemSelected}
            .position=${this.position}
            .origin=${this}
            .showDivider=${this.showDivider}
            .showSelectedItem=${this.showSelectedItem}
            .open=${this.#open}
            .getConnectorCustomXPosition=${null}>
          <slot></slot>
        </devtools-menu>
        ${this.#renderButton()}`,
        this.#shadow, {host: this});
    // clang-format on
  }
}

export interface SelectMenuButtonData {
  showArrow: boolean;
  arrowDirection: Dialogs.Dialog.DialogVerticalPosition;
  disabled: boolean;
  singleArrow: boolean;
  jslogContext: string;
}
export class SelectMenuButton extends HTMLElement {
  readonly #shadow = this.attachShadow({mode: 'open'});
  #showButton: HTMLButtonElement|null = null;

  connectedCallback(): void {
    this.style.setProperty('--deploy-menu-arrow', `url(${deployMenuArrow})`);
    void RenderCoordinator.write(() => {
      switch (this.arrowDirection) {
        case Dialogs.Dialog.DialogVerticalPosition.AUTO:
        case Dialogs.Dialog.DialogVerticalPosition.TOP: {
          this.style.setProperty('--arrow-angle', '180deg');
          break;
        }
        case Dialogs.Dialog.DialogVerticalPosition.BOTTOM: {
          this.style.setProperty('--arrow-angle', '0deg');
          break;
        }
        default:
          Platform.assertNever(this.arrowDirection, `Unknown position type: ${this.arrowDirection}`);
      }
    });
  }
  #props: SelectMenuButtonData = {
    showArrow: false,
    arrowDirection: Dialogs.Dialog.DialogVerticalPosition.BOTTOM,
    disabled: false,
    singleArrow: false,
    jslogContext: '',
  };

  get showArrow(): boolean {
    return this.#props.showArrow;
  }

  set showArrow(showArrow: boolean) {
    this.#props.showArrow = showArrow;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get arrowDirection(): Dialogs.Dialog.DialogVerticalPosition {
    return this.#props.arrowDirection;
  }

  set arrowDirection(arrowDirection: Dialogs.Dialog.DialogVerticalPosition) {
    this.#props.arrowDirection = arrowDirection;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get disabled(): boolean {
    return this.#props.disabled;
  }

  set disabled(disabled: boolean) {
    this.#props.disabled = disabled;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  set open(open: boolean) {
    void RenderCoordinator.write(() => {
      this.#getShowButton()?.setAttribute('aria-expanded', String(open));
    });
  }

  set singleArrow(singleArrow: boolean) {
    this.#props.singleArrow = singleArrow;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  get jslogContext(): string {
    return this.#props.jslogContext;
  }

  set jslogContext(jslogContext: string) {
    this.#props.jslogContext = jslogContext;
    void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
  }

  override click(): void {
    this.#getShowButton()?.click();
  }

  #getShowButton(): HTMLButtonElement|null {
    if (!this.#showButton) {
      this.#showButton = this.#shadow.querySelector('button');
    }
    return this.#showButton;
  }

  #handleButtonKeyDown(evt: KeyboardEvent): void {
    const key = evt.key;
    const shouldShowDialogBelow = this.arrowDirection === Dialogs.Dialog.DialogVerticalPosition.BOTTOM &&
        key === Platform.KeyboardUtilities.ArrowKey.DOWN;
    const shouldShowDialogAbove = this.arrowDirection === Dialogs.Dialog.DialogVerticalPosition.TOP &&
        key === Platform.KeyboardUtilities.ArrowKey.UP;
    const isEnter = key === Platform.KeyboardUtilities.ENTER_KEY;
    const isSpace = evt.code === 'Space';
    if (shouldShowDialogBelow || shouldShowDialogAbove || isEnter || isSpace) {
      this.dispatchEvent(new SelectMenuButtonTriggerEvent());
      evt.preventDefault();
    }
  }

  #handleClick(): void {
    this.dispatchEvent(new SelectMenuButtonTriggerEvent());
  }

  async #render(): Promise<void> {
    if (!ComponentHelpers.ScheduledRender.isScheduledRender(this)) {
      throw new Error('SelectMenuItem render was not scheduled');
    }
    const arrow = this.#props.showArrow ? html`<span id="arrow"></span>` : Lit.nothing;
    const classMap = {'single-arrow': this.#props.singleArrow};
    // clang-format off
    const buttonTitle = html`
      <span id="button-label-wrapper">
        <span id="label" ?witharrow=${this.showArrow} class=${Lit.Directives.classMap(classMap)}>
          <slot></slot>
        </span>
        ${arrow}
      </span>`;

    // clang-format off
    Lit.render(html`
        <style>${selectMenuButtonStyles}</style>
        <button
            aria-haspopup="true" aria-expanded="false" class="show"
            @keydown=${this.#handleButtonKeyDown} @click=${this.#handleClick}
            ?disabled=${this.disabled}
            jslog=${VisualLogging.dropDown(this.jslogContext)}>
          ${buttonTitle}
        </button>`,
        this.#shadow, { host: this });
    // clang-format on
  }
}

customElements.define('devtools-select-menu', SelectMenu);
customElements.define('devtools-select-menu-button', SelectMenuButton);

declare global {
  interface HTMLElementTagNameMap {
    'devtools-select-menu': SelectMenu;
    'devtools-select-menu-button': SelectMenuButton;
  }

  interface HTMLElementEventMap {
    [SelectMenuItemSelectedEvent.eventName]: SelectMenuItemSelectedEvent;
  }
}

export class SelectMenuItemSelectedEvent extends Event {
  static readonly eventName = 'selectmenuselected';

  constructor(public itemValue: SelectMenuItemValue) {
    super(SelectMenuItemSelectedEvent.eventName, {bubbles: true, composed: true});
  }
}

export class SelectMenuSideButtonClickEvent extends Event {
  static readonly eventName = 'selectmenusidebuttonclick';
  constructor() {
    super(SelectMenuSideButtonClickEvent.eventName, {bubbles: true, composed: true});
  }
}

export class SelectMenuButtonTriggerEvent extends Event {
  static readonly eventName = 'selectmenubuttontrigger';
  constructor() {
    super(SelectMenuButtonTriggerEvent.eventName, {bubbles: true, composed: true});
  }
}

/**
 * Exported artifacts used in this component and that belong to the Menu are
 * renamed to only make reference to the SelectMenu. This way, the Menu API
 * doesn't have to be used in SelectMenu usages and the SelectMenu implementation
 * can remain transparent to its users.
 **/
export type SelectMenuItemValue = MenuItemValue;
export {MenuGroup as SelectMenuGroup};
