import {AbstractMultiSelectElement} from "./multi-select.mjs";
import HTMLStringTagsElement from "./string-tags.mjs";
import Autocomplete from "../ux/autocomplete.mjs";

/**
 * @import {FormInputConfig} from "../../../common/data/_types.mjs";
 * @import {AutocompleteEntry} from "../ux/_types.mjs";
 */

/**
 * Provide a multi-select workflow as a text input which offers autocompletion over the available options. Chosen
 * options are displayed as a list of tags which may be individually removed.
 *
 * @extends {AbstractMultiSelectElement}
 *
 * @example Autocomplete Tags HTML Markup
 * ```html
 * <autocomplete-tags name="select-many-things">
 *   <option value="foo">Foo</option>
 *   <option value="bar">Bar</option>
 *   <option value="baz">Baz</option>
 * </autocomplete-tags>
 * ```
 */
export default class HTMLAutocompleteTagsElement extends AbstractMultiSelectElement {

  /** @override */
  static tagName = "autocomplete-tags";

  /**
   * The autocomplete widget which offers matching options.
   * @type {Autocomplete}
   */
  #autocomplete;

  /**
   * A text input used to search the available options.
   * @type {HTMLInputElement}
   */
  #input;

  /**
   * The available options in their declared order, with labels normalized for prefix matching.
   * @type {(AutocompleteEntry & {search: string})[]}
   */
  #options = [];

  /**
   * A display element which lists the chosen options.
   * @type {HTMLDivElement}
   */
  #tags;

  /* -------------------------------------------- */

  /** @override */
  _activateListeners() {
    this.#autocomplete = new Autocomplete.implementation({onSelect: this.#onSelectOption.bind(this)});
    const refresh = this.#refreshMenu.bind(this);
    this.#input.addEventListener("click", refresh);
    this.#input.addEventListener("focus", refresh);
    this.#input.addEventListener("input", refresh);
    this.#tags.addEventListener("click", this.#onClickTag.bind(this));
    this.ownerDocument.body.addEventListener("click", this.#onClickAway.bind(this), {signal: this.abortSignal});
  }

  /* -------------------------------------------- */

  /** @override */
  _buildElements() {
    this.#tags = document.createElement("div");
    this.#tags.className = "tags input-element-tags";
    const input = this.#input = this._primaryInput = document.createElement("input");
    input.type = "text";
    input.placeholder = this.getAttribute("placeholder") ?? "";
    return [this.#tags, input];
  }

  /* -------------------------------------------- */

  /** @override */
  _disconnect() {
    this.#dismissMenu();
  }

  /* -------------------------------------------- */

  /** @inheritDoc */
  _initialize() {
    super._initialize();
    for ( const [value, label] of Object.entries(this._choices) ) {
      this.#options.push({label, identifier: value, search: HTMLAutocompleteTagsElement.#normalize(label)});
    }
  }

  /* -------------------------------------------- */

  /** @override */
  _refresh() {
    const tags = Array.from(this._value).map(id => {
      return HTMLStringTagsElement.renderTag(id, this._choices[id], this.editable && !this._disabledOptions.has(id));
    });
    this.#tags.replaceChildren(...tags);
  }

  /* -------------------------------------------- */

  /** @override */
  _toggleDisabled(disabled) {
    this.#input.disabled = disabled;
    if ( disabled ) this.#dismissMenu();
  }

  /* -------------------------------------------- */

  /**
   * Dismiss the autocomplete menu if this element is the one currently using it.
   */
  #dismissMenu() {
    if ( ui.autocomplete === this.#autocomplete ) this.#autocomplete.dismiss();
  }

  /* -------------------------------------------- */

  /**
   * Offer the options whose labels begin with the current search text, omitting those which are already chosen or are
   * disabled. The full list is offered when no search text has been entered.
   */
  #refreshMenu() {
    if ( !this.editable ) return this.#dismissMenu();
    const prefix = HTMLAutocompleteTagsElement.#normalize(this.#input.value);
    const entries = this.#options.filter(o => {
      return o.search.startsWith(prefix) && !this._value.has(o.identifier) && !this._disabledOptions.has(o.identifier);
    });
    if ( entries.length ) this.#autocomplete.activate(this.#input, entries);
    else this.#dismissMenu();
  }

  /* -------------------------------------------- */
  /*  Event Listeners & Handlers                  */
  /* -------------------------------------------- */

  /**
   * Dismiss the menu when a click occurs outside this element's input or the menu itself.
   * @param {PointerEvent} event  The originating click event
   */
  #onClickAway(event) {
    if ( (event.target === this.#input) || this.#autocomplete.element?.contains(event.target) ) return;
    this.#dismissMenu();
  }

  /* -------------------------------------------- */

  /**
   * Handle click events on a chosen tag, removing it from the chosen set.
   * @param {PointerEvent} event  The originating click event on a chosen tag
   */
  #onClickTag(event) {
    event.preventDefault();
    if ( !event.target.classList.contains("remove") || !this.editable ) return;
    const tag = event.target.closest(".tag");
    this.unselect(tag.dataset.key);
  }

  /* -------------------------------------------- */

  /**
   * Add an option to the chosen set when it is committed in the autocomplete menu.
   * @param {string} identifier  The identifier of the committed entry
   */
  #onSelectOption(identifier) {
    this.select(identifier);
    this.#input.value = "";
  }

  /* -------------------------------------------- */

  /**
   * Create a HTMLAutocompleteTagsElement using provided configuration data.
   * @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
   * @returns {HTMLAutocompleteTagsElement}
   */
  static create(config) {
    return foundry.applications.fields.createMultiSelectInput({...config, type: "autocomplete"});
  }

  /* -------------------------------------------- */
  /*  Helpers                                     */
  /* -------------------------------------------- */

  /**
   * Standardize a label or a search query for prefix matching.
   * @param {string} text  The text to normalize
   * @returns {string}
   */
  static #normalize(text) {
    return text.trim().stripDiacritics().toLocaleLowerCase(game.i18n.lang);
  }
}
