import { Component, ComponentConfig } from '../Component';
import { EventDispatcher, Event } from '../../EventDispatcher';
import { ArrayUtils } from '../../utils/ArrayUtils';
import { LocalizableText, i18n } from '../../localization/i18n';

/**
 * A map of items (key/value -> label} for a {@link ListSelector} in a {@link ListSelectorConfig}.
 */
export interface ListItem {
  key: string;
  label: LocalizableText;
  sortedInsert?: boolean;
  ariaLabel?: string;
}

/**
 * Filter function that can be used to filter out list items added through {@link ListSelector.addItem}.
 *
 * This is intended to be used in conjunction with subclasses that populate themselves automatically
 * via the player API, e.g. {@link SubtitleSelectBox}.
 */
export interface ListItemFilter {
  /**
   * Takes a list item and decides whether it should pass or be discarded.
   * @param {ListItem} listItem the item to apply the filter to
   * @returns {boolean} true to let the item pass through the filter, false to discard the item
   */
  (listItem: ListItem): boolean;
}

/**
 * Translator function to translate labels of list items added through {@link ListSelector.addItem}.
 *
 * This is intended to be used in conjunction with subclasses that populate themselves automatically
 * via the player API, e.g. {@link SubtitleSelectBox}.
 */
export interface ListItemLabelTranslator {
  /**
   * Takes a list item, optionally changes the label, and returns the new label.
   * @param {ListItem} listItem the item to translate
   * @returns {string} the translated or original label
   */
  (listItem: ListItem): string;
}

/**
 * Configuration interface for a {@link ListSelector}.
 *
 * @category Configs
 */
export interface ListSelectorConfig extends ComponentConfig {
  items?: ListItem[];
  filter?: ListItemFilter;
  translator?: ListItemLabelTranslator;
}

export abstract class ListSelector<Config extends ListSelectorConfig> extends Component<ListSelectorConfig> {
  protected items: ListItem[];
  protected selectedItem: string | null = null;

  private listSelectorEvents = {
    onItemAdded: new EventDispatcher<ListSelector<Config>, string>(),
    onItemRemoved: new EventDispatcher<ListSelector<Config>, string>(),
    onItemSelected: new EventDispatcher<ListSelector<Config>, string>(),
    onItemSelectionChanged: new EventDispatcher<ListSelector<Config>, string>(),
  };

  constructor(config: ListSelectorConfig = {}) {
    super(config);

    this.config = this.mergeConfig(
      config,
      {
        items: [],
        cssClass: 'ui-listselector',
      },
      this.config,
    );

    this.items = this.config.items;
  }

  private getItemIndex(key: string): number {
    for (let i = 0; i < this.items.length; i++) {
      if (this.items[i].key === key) {
        return i;
      }
    }

    return -1;
  }

  /**
   * Returns all current items of this selector.
   * * @returns {ListItem[]}
   */
  getItems(): ListItem[] {
    return this.items;
  }

  /**
   * Checks if the specified item is part of this selector.
   * @param key the key of the item to check
   * @returns {boolean} true if the item is part of this selector, else false
   */
  hasItem(key: string): boolean {
    return this.getItemIndex(key) > -1;
  }

  /**
   * Adds an item to this selector by doing a sorted insert or by appending the element to the end of the list of items.
   * If an item with the specified key already exists, it is replaced.
   * @param key the key of the item to add
   * @param label the (human-readable) label of the item to add
   * @param sortedInsert whether the item should be added respecting the order of keys
   * @param ariaLabel custom aria label for the listItem
   */
  addItem(key: string | null, label: LocalizableText, sortedInsert = false, ariaLabel = '') {
    const listItem = { key: key, label: i18n.performLocalization(label), ...(ariaLabel && { ariaLabel }) };

    // Apply filter function
    if (this.config.filter && !this.config.filter(listItem)) {
      return;
    }

    // Apply translator function
    if (this.config.translator) {
      listItem.label = this.config.translator(listItem);
    }

    // Try to remove key first to get overwrite behavior and avoid duplicate keys
    this.removeItem(key); // This will trigger an ItemRemoved and an ItemAdded event

    // Add the item to the list
    if (sortedInsert) {
      const index = this.items.findIndex(entry => entry.key > key);
      if (index < 0) {
        this.items.push(listItem);
      } else {
        this.items.splice(index, 0, listItem);
      }
    } else {
      this.items.push(listItem);
    }
    this.onItemAddedEvent(key);
  }

  /**
   * Removes an item from this selector.
   * @param key the key of the item to remove
   * @returns {boolean} true if removal was successful, false if the item is not part of this selector
   */
  removeItem(key: string): boolean {
    const index = this.getItemIndex(key);
    if (index > -1) {
      ArrayUtils.remove(this.items, this.items[index]);
      this.onItemRemovedEvent(key);
      return true;
    }

    return false;
  }

  /**
   * Selects an item from the items in this selector.
   *
   * This represents an actual value change in the UI state. It should be used when the
   * selection is updated based on the current player/component state (e.g. from a player event),
   * not as a user-intent signal.
   *
   * @param key the key of the item to select
   * @returns {boolean} true is the selection was successful, false if the selected item is not part of the selector
   */
  selectItem(key: string): boolean {
    if (key === this.selectedItem) {
      // itemConfig is already selected, suppress any further action
      return true;
    }

    const index = this.getItemIndex(key);

    if (index > -1) {
      this.selectedItem = key;
      this.onItemSelectedEvent(key);
      return true;
    }

    return false;
  }

  /**
   * Returns the key of the selected item.
   * @returns {string} the key of the selected item or null if no item is selected
   */
  getSelectedItem(): string | null {
    return this.selectedItem;
  }

  /**
   * Returns the items for the given key or undefined if no item with the given key exists.
   * @param key the key of the item to return
   * @returns {ListItem} the item with the requested key. Undefined if no item with the given key exists.
   */
  getItemForKey(key: string): ListItem | null {
    return this.items.find(item => item.key === key);
  }

  /**
   * Synchronize the current items of this selector with the given ones. This will remove and add items selectively.
   * For each removed item the ItemRemovedEvent and for each added item the ItemAddedEvent will be triggered. Favour
   * this method over using clearItems and adding all items again afterwards.
   * @param newItems
   */
  synchronizeItems(newItems: ListItem[]): void {
    newItems
      .filter(item => !this.hasItem(item.key))
      .forEach(item => this.addItem(item.key, item.label, item.sortedInsert, item.ariaLabel));

    this.items
      .filter(item => newItems.filter(i => i.key === item.key).length === 0)
      .forEach(item => this.removeItem(item.key));
  }

  /**
   * Removes all items from this selector.
   */
  clearItems() {
    // local copy for iteration after clear
    const items = this.items;
    // clear items
    this.items = [];

    // clear the selection as the selected item is also removed
    this.selectedItem = null;

    // fire events
    for (const item of items) {
      this.onItemRemovedEvent(item.key);
    }
  }

  /**
   * Returns the number of items in this selector.
   * @returns {number}
   */
  itemCount(): number {
    return Object.keys(this.items).length;
  }

  protected onItemAddedEvent(key: string) {
    this.listSelectorEvents.onItemAdded.dispatch(this, key);
  }

  protected onItemRemovedEvent(key: string) {
    this.listSelectorEvents.onItemRemoved.dispatch(this, key);
  }

  protected onItemSelectedEvent(key: string) {
    this.listSelectorEvents.onItemSelected.dispatch(this, key);
  }

  protected onItemSelectionChangedEvent(key: string) {
    this.listSelectorEvents.onItemSelectionChanged.dispatch(this, key);
  }

  /**
   * Dispatches a selection-changed event and optionally updates the selected item.
   *
   * This is the entry point for user-driven interactions. It exists separately from {@link selectItem}
   * so we can distinguish intent (user interaction that should call into the e.g. player or other components)
   * from actual value changes (state updates originating from the player).
   *
   * @param key the key of the item to select
   * @param updateSelectedItem when true, updates the selected item
   */
  dispatchItemSelectionChanged(key: string, updateSelectedItem: boolean = true): void {
    if (updateSelectedItem) {
      this.selectItem(key);
    }
    this.onItemSelectionChangedEvent(key);
  }

  /**
   * Gets the event that is fired when an item is added to the list of items.
   * @returns {Event<ListSelector<Config>, string>}
   */
  get onItemAdded(): Event<ListSelector<Config>, string> {
    return this.listSelectorEvents.onItemAdded.getEvent();
  }

  /**
   * Gets the event that is fired when an item is removed from the list of items.
   * @returns {Event<ListSelector<Config>, string>}
   */
  get onItemRemoved(): Event<ListSelector<Config>, string> {
    return this.listSelectorEvents.onItemRemoved.getEvent();
  }

  /**
   * Gets the event that is fired when the selected item value changes.
   *
   * Use this to react to actual value changes (e.g. player state updates). This should not
   * trigger new player calls to avoid feedback loops.
   *
   * @returns {Event<ListSelector<Config>, string>}
   */
  get onItemSelected(): Event<ListSelector<Config>, string> {
    return this.listSelectorEvents.onItemSelected.getEvent();
  }

  /**
   * Gets the event that is fired when a selection change is requested.
   *
   * Use this to react to user interaction and call into the player or other components.
   * It intentionally does not represent a confirmed value change.
   *
   * @returns {Event<ListSelector<Config>, string>}
   */
  get onItemSelectionChanged(): Event<ListSelector<Config>, string> {
    return this.listSelectorEvents.onItemSelectionChanged.getEvent();
  }
}
