/**
 * @import {AutocompleteCallback, AutocompleteEntry} from "./_types.mjs";
 * @import {ApplicationPosition} from "../_types.mjs";
 */

/**
 * A widget that provides a drop-down list of autocompletion options.
 */
export default class Autocomplete {
/**
 * @param {object} [options]
 * @param {AutocompleteCallback} [options.onSelect]  Callback to fire when an entry is selected and committed.
 */
  constructor({onSelect}={}) {
    this.#onCommit = onSelect;
  }

  /* -------------------------------------------- */
  /*  Properties                                  */
  /* -------------------------------------------- */

  /**
   * The auto-complete menu.
   * @type {HTMLMenuElement}
   */
  get element() {
    return foundry.applications.detached.querySelector("#autocomplete");
  }

  /**
   * Callback to fire when an entry is committed.
   * @type {AutocompleteCallback}
   */
  #onCommit;

  /**
   * The matched prefix that triggered the autocomplete menu.
   * @type {string}
   */
  #prefix;

  /* -------------------------------------------- */
  /*  Public API                                  */
  /* -------------------------------------------- */

  /**
   * Commit a choice.
   */
  commit() {
    const item = this.element.querySelector("li.active");
    if ( item ) this.#onCommit?.(item.dataset.identifier, item.textContent, {prefix: this.#prefix});
    this.dismiss();
  }

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

  /**
   * Dismiss the auto-complete menu.
   */
  dismiss() {
    this.element?.remove();
  }

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

  /**
   * Move the current auto-complete selection.
   * @param {number} increment  The number of entries to advance the selection. A negative number moves up, and a
   *                            positive number moves down.
   */
  select(increment=0) {
    const selected = this.element.querySelector("li.active");
    const nonDisabled = Array.from(this.element.children).filter(c => !("disabled" in c.dataset));

    // Case 1 - Nothing currently selected.
    if ( !selected ) {
      nonDisabled[increment < 0 ? nonDisabled.length - 1 : 0]?.classList.add("active");
      return;
    }

    // Case 2 - No change in selection.
    if ( !increment ) return;

    // Case 3 - Move selection up or down from current selection.
    selected.classList.remove("active");
    const selectedIndex = (nonDisabled.indexOf(selected) + increment) % nonDisabled.length;
    nonDisabled.at(selectedIndex)?.classList.add("active");
  }

  /* -------------------------------------------- */
  /*  Rendering                                   */
  /* -------------------------------------------- */

  /**
   * Activate an auto-complete menu.
   * @param {HTMLElement} target                               The element the autocomplete is being rendered for.
   * @param {AutocompleteEntry[]} entries                      The entries to render.
   * @param {object} [options]
   * @param {string} [options.prefix]                          The matched prefix that triggered the autocomplete menu.
   * @param {Partial<ApplicationPosition>} [options.position]  Position the menu. If omitted, the menu is positioned
   *                                                           underneath the target.
   */
  activate(target, entries, { prefix, position }={}) {
    if ( !target || !entries?.length ) return;
    const document = target.ownerDocument;
    ui.autocomplete = this;
    if ( this.element ) document.body.append(this.element);
    else this.#createElement(document);
    this.element.classList.add("scrollable");
    this.element.classList.remove("themed", "theme-light", "theme-dark");
    const themed = target.closest(".themed") ?? document.body;
    const [, theme] = themed.className.match(/(?:^|\s)(theme-\w+)/) ?? [];
    if ( theme ) this.element.classList.add("themed", theme);
    position ??= {};
    const { bottom, left } = target.getBoundingClientRect();
    position.top ??= bottom;
    position.left ??= left;
    this.element.style.top = `${position.top}px`;
    this.element.style.left = `${position.left}px`;
    this.#render(entries);
    this.#prefix = prefix;
    this.element.showPopover();
  }

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

  /**
   * Insert the autocomplete element into the DOM.
   * @param {Document} document  The document to append the menu to.
   */
  #createElement(document) {
    const menu = document.createElement("menu");
    Object.assign(menu, { id: "autocomplete", popover: "manual" });
    menu.addEventListener("click", this.#onClickEntry.bind(this));
    menu.addEventListener("mouseover", this.#onMouseOverEntry.bind(this));
    document.body.append(menu);
  }

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

  /**
   * Render menu entries.
   * @param {AutocompleteEntry[]} entries  The entries.
   */
  #render(entries) {
    this.element.replaceChildren(...entries.map(({label, identifier, disabled}) => {
      const item = document.createElement("li");
      item.dataset.identifier = identifier;
      if ( disabled ) {
        item.dataset.disabled = "";
        item.classList.add("disabled");
      }
      item.append(label);
      return item;
    }));
  }

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

  /**
   * Handle clicking an entry.
   */
  #onClickEntry() {
    this.commit();
  }

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

  /**
   * Handle hovering an entry.
   * @param {PointerEvent} event  The originating event.
   */
  #onMouseOverEntry(event) {
    const target = event.target.closest("li");
    if ( !target ) return;
    for ( const item of this.element.children ) {
      item.classList.toggle("active", (item === target) && !("disabled" in item.dataset));
    }
  }

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

  /**
   * Handle keyboard navigation of the menu.
   * @param {KeyboardEvent} event  The originating event.
   */
  static #onKeyDown(event) {
    if ( !ui.autocomplete?.element ) return;

    switch ( event.key ) {
      case "Escape":
        ui.autocomplete.dismiss();
        break;

      case "Enter": case "Tab":
        ui.autocomplete.commit();
        break;

      case "ArrowDown": case "ArrowUp":
        ui.autocomplete.select(event.key === "ArrowDown" ? 1 : -1);
        break;

      default: return;
    }

    event.preventDefault();
    event.stopPropagation();
  }

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

  /**
   * Bind global autocomplete listeners to a given document.
   * @param {Document} document  The document.
   */
  static activateListeners(document) {
    document ??= window.document;
    document.addEventListener("keydown", Autocomplete.#onKeyDown, { capture: true });
  }

  /* -------------------------------------------- */
  /*  Factory Methods                             */
  /* -------------------------------------------- */

  /**
   * Retrieve the configured Autocomplete implementation.
   * @type {typeof Autocomplete}
   */
  static get implementation() {
    let Class = CONFIG.ux.Autocomplete;
    if ( !foundry.utils.isSubclass(Class, Autocomplete) ) {
      console.warn("Configured Autocomplete override must be a subclass of Autocomplete.");
      Class = Autocomplete;
    }
    return Class;
  }
}
