/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

// @ts-ignore-next-line
import Bloodhound from 'typeahead.js';

/**
 * This component is an overlay of typeahead it allows to have a single config input (since
 * typeahead weirdly uses two different configs). It also provides some default rendering
 * functions which are, of course, overridable.
 */

type DisplayFunction = (item: any) => string;

export interface TypeaheadJQueryDataset extends Twitter.Typeahead.Dataset<any> {
  display: string | DisplayFunction;
  value: string;
  limit: number;
  dataLimit: number;
  templates: any;
}

export interface TypeaheadJQueryOptions extends Twitter.Typeahead.Options {
  minLength: number,
  highlight: boolean,
  hint: boolean,
  onSelect: (
    selectedItem: any,
    event: JQueryEventObject,
    searchInput: JQuery
  ) => boolean;
  onClose: (event: JQueryEventObject, searchInput: JQuery) => void;
}

export type AutoCompleteSearchConfig = {
  minLength: number;
  highlight: boolean;
  hint: boolean;
  source: Bloodhound<Record<string, any>> | (
    (query: string, syncResults: (result: any[]) => void, asyncResults?: (result: any[]) => void
  ) => void);
  onSelect: (
    selectedItem: any,
    event: JQueryEventObject,
    searchInput: JQuery
  ) => boolean;
  onClose: (event: JQueryEventObject, searchInput: JQuery) => void;
  suggestionLimit: number;
  dataLimit: number;
  display: string | DisplayFunction;
  value: string;
  templates: any;
}
export type InputAutoCompleteSearchConfig = Partial<AutoCompleteSearchConfig> & {
  source: Bloodhound<Record<string, any>> | (
    (query: string, syncResults: (result: any[]) => void, asyncResults?: (result: any[]) => void
  ) => void); // source is mandatory option
};

export default class AutoCompleteSearch {
  private $searchInput: JQuery;

  private searchInputId: string;

  private config: AutoCompleteSearchConfig;

  constructor($searchInput: JQuery, inputConfig: Partial<InputAutoCompleteSearchConfig>) {
    this.$searchInput = $searchInput;
    this.searchInputId = this.$searchInput.prop('id');

    // Merging object works fine on one level, but on two it erases sub elements even if not present, so
    // we handle templates separately, these are the default rendering functions which can be overridden
    const defaultTemplates = {
      // Be careful that your rendering function must return HTML node not pure text so always include the
      // content in a div at least
      suggestion: (item: Record<string, string>) => {
        let displaySuggestion: Record<string, string> | string = item;

        if (typeof this.config.display === 'function') {
          displaySuggestion = this.config.display(item);
        } else if (
          Object.prototype.hasOwnProperty.call(
            item,
            <string> this.config.display,
          )
        ) {
          displaySuggestion = item[<string> this.config.display];
        }

        return `<div class="px-2">${displaySuggestion}</div>`;
      },
      pending(query: Record<string, string>) {
        return `<div class="px-2">Searching for "${query.query}"</div>`;
      },
      notFound(query: Record<string, string>) {
        return `<div class="px-2">No results found for "${query.query}"</div>`;
      },
    };

    // Merge default and input config
    this.config = <AutoCompleteSearchConfig>{
      minLength: 2,
      highlight: true,
      hint: false,
      onSelect: (
        selectedItem: any,
        event: JQueryEventObject,
        searchInput: JQuery,
      ): boolean => {
        searchInput.typeahead('val', selectedItem[this.config.value]);
        return true;
      },
      onClose(
        event: Event,
        searchInput: JQuery,
      ) {
        searchInput.typeahead('val', '');
        return true;
      },
      suggestionLimit: 30,
      dataLimit: 0,
      display: 'name',
      value: 'id',
      templates: defaultTemplates,
      ...inputConfig,
    };

    // If input has templates override me merge them with default ones
    if (Object.prototype.hasOwnProperty.call(inputConfig, 'templates')) {
      this.config.templates = {
        ...defaultTemplates,
        ...(<Record<string, unknown>>inputConfig.templates),
      };
    }

    this.buildTypeahead();
  }

  /**
   * Build the typeahead component based on provided configuration.
   */
  private buildTypeahead(): void {
    // Create the two config object for typeahead based on the full config
    const typeaheadOptions = {
      minLength: this.config.minLength,
      highlight: this.config.highlight,
      hint: this.config.hint,
      onSelect: this.config.onSelect,
      onClose: this.config.onClose,
    };

    const dataSetConfig = {
      source: this.config.source,
      display: this.config.display,
      value: this.config.value,
      limit: this.config.suggestionLimit,
      dataLimit: this.config.dataLimit,
      templates: this.config.templates,
    };

    /* eslint-disable */
    this.$searchInput
      .typeahead(<TypeaheadJQueryOptions>typeaheadOptions, <TypeaheadJQueryDataset>dataSetConfig)
      .on('typeahead:select', (e: any, selectedItem: any) =>
        this.config.onSelect(selectedItem, e, this.$searchInput)
      )
      .on('typeahead:close', (e: any) => {
        this.config.onClose(e, this.$searchInput);
      });
    /* eslint-enable */
  }
}
