/**
 * auto-suggest.js
 *
 */

import ElementHelpers from '../helpers/element-helpers';

/**
 * Declare constants
 */
const attributeBase = 'data-rs-auto-suggest';

export class AutoSuggest {
  constructor(element) {
    this.element = element;
    this.minimumLength = 1;
    this.container = this.element;
    this.timeout;
    this.numberOfListItems = 0;
    this.selectedIndex;
    this.selectedValue;
    this.previousInput = '';
    this.resultList;

    // attributes
    this.input = ElementHelpers.getElementByAttributeWithinElement(
      this.element,
      this.attributes.input
    );
    this.list = ElementHelpers.getElementByAttributeWithinElement(
      this.element,
      this.attributes.list
    );
    this.noResultMessage = this.input.getAttribute(
      this.attributes.noResultMessage
    );
    // option to disable the input events RBD-9338
    this.preventInputEvent = this.input.getAttribute(
      this.attributes.preventInputEvent
    );
    // ignore default Orbit filter and display all returned data RBR-452
    this.useExactValues = this.input.getAttribute(
      this.attributes.useExactValues
    );
    this.separator = this.list.getAttribute(this.attributes.separator);
    this.threshold =
      Number(this.input.getAttribute(this.attributes.threshold)) || 300;

    // nodes
    this.submit = element.parentNode.querySelector('[type="submit"]');

    // functions
    this.dataSource = this.getDataSource();
    this.disableNativeAutoComplete();
    this.addEventHandlers();
  }

  /**
   * Declare attributes
   */
  get attributes() {
    return {
      autocomplete: 'autocomplete',
      container: `${attributeBase}-container`,
      index: `${attributeBase}-index`,
      input: `${attributeBase}-input`,
      list: `${attributeBase}-list`,
      listValue: `${attributeBase}-list-value`,
      noResultMessage: `${attributeBase}-no-result-message`,
      preselect: `${attributeBase}-preselect`,
      preventInputEvent: `${attributeBase}-prevent-input-event`,
      selected: `${attributeBase}-selected`,
      separator: `${attributeBase}-separator`,
      threshold: `${attributeBase}-threshold`,
      useExactValues: `${attributeBase}-use-exact-values`,
      values: `${attributeBase}-values`
    };
  }

  /**
   * Declare classes
   */
  get classes() {
    return {
      autosuggestOpen: 'select-menu--open',
      autosuggestItem: 'select-menu__item',
      autosuggestNoResult: 'select-menu__item select-menu__item--no-result',
      autosuggestPreselect: 'select-menu__item--preselect'
    };
  }

  /**
   * Declare key codes
   */
  get keyCodes() {
    return {
      Enter: 'Enter',
      Up: 'ArrowUp',
      Down: 'ArrowDown',
      Tab: 'Tab',
      Escape: 'Escape',
      PageUp: 'PageUp',
      PageDown: 'PageDown'
    };
  }

  /**
   * Disable native auto complete
   * Only Chrome does not support disabling native auto complete
   */
  disableNativeAutoComplete() {
    this.input.setAttribute(this.attributes.autocomplete, 'off');
  }

  /**
   * Add event handlers
   */
  addEventHandlers() {
    const events = ['focus', 'keydown'];

    if (this.preventInputEvent === null) {
      events.push('input');
    }

    // on input and focus, create list if user input length > minimum length
    events.forEach((eventType) => {
      this.element.addEventListener(eventType, this.onInput.bind(this), {
        capture: true
      });
    });
    document.addEventListener('click', this.clickOutside.bind(this));
  }

  onInput(event) {
    if (event.target.hasAttribute(this.attributes.input)) {
      if (event.type === 'input' || event.type === 'focus') {
        this.numberOfListItems = 0;
        // only process if input is equal to or longer then minimum length
        if (this.input.value.length >= this.minimumLength) {
          // if previous input length is less then minimum length OR if previous input does not contain current input -> reset the data source
          if (
            this.previousInput.length < this.minimumLength ||
            this.input.value
              .toLowerCase()
              .indexOf(this.previousInput.toLowerCase()) === -1
          ) {
            this.dataSource = this.getDataSource();
          }
          this.createList();
        } else {
          // Close the list if it contains the open class
          if (this.container.classList.contains(this.classes.autosuggestOpen)) {
            this.closeList();
          }
        }
        this.previousInput = this.input.value;
      } else if (event.type === 'keydown') {
        // if keydown is tab and item is preselected, select item and close list
        if (
          ElementHelpers.getKeyCodeOnKeyDownEvent(event) == this.keyCodes.Tab
        ) {
          if (this.selectedIndex > -1) {
            this.setSelectedValue(this.selectedIndex);
          }
          this.closeList();
        }
        // if keydown is escape, close list and do not select item
        if (
          ElementHelpers.getKeyCodeOnKeyDownEvent(event) == this.keyCodes.Escape
        ) {
          this.closeList();
        }
        this.handleButtonKeys(event);
      }
    }
  }

  clickOutside(event) {
    // if clicked outside input field or list, close the list and do not select item
    if (
      !this.list.contains(event.target) &&
      !this.input.contains(event.target)
    ) {
      if (this.container.classList.contains(this.classes.autosuggestOpen)) {
        this.closeList();
      }
    }
  }

  /**
   * Create list - get matched items from data source and put them in list with li elements
   */
  createList() {
    let list = '';
    let listItemIndex = 0;

    window.clearTimeout(this.timeout);

    // set result list equal to data source
    this.resultList = JSON.parse(JSON.stringify(this.dataSource));

    this.dataSource.forEach((listItem) => {
      // check for each list item in the data source if it matches with input from user
      if (
        listItem.toLowerCase().indexOf(this.input.value.toLowerCase()) > -1 ||
        this.useExactValues !== null
      ) {
        // escape special symbols RBD-8705
        const regexSafeValue = this.input.value.replace(
          /[-\[\]/{}()*+?.\\^$|]/g,
          '\\$&'
        );
        // if item matches, add mark to matched part to create highlighted text
        let regex = new RegExp(regexSafeValue, 'gi');
        let response = listItem.replace(regex, function (value) {
          return `<mark>${value}</mark>`;
        });
        // create li element for matched item
        let li = `<li class="${this.classes.autosuggestItem}" ${this.attributes.listValue} ${this.attributes.index}="${listItemIndex}" >${response}</li>`;

        // add li elements to list variable
        list = list + li;
        this.numberOfListItems++;
        listItemIndex++;
      } else {
        // if list item does not match input from user -> remove list item from result list
        let index = this.resultList.indexOf(listItem);
        if (index > -1) {
          this.resultList.splice(index, 1);
        }
      }
    });
    // set data source equal to result list
    this.dataSource = JSON.parse(JSON.stringify(this.resultList));
    // if number of list items > 0, bind result list
    if (this.numberOfListItems > 0) {
      this.timeout = setTimeout(
        () => this.bindResultList(list),
        this.threshold
      );
    } else {
      let noResults = `<li class="${this.classes.autosuggestNoResult}">${
        this.noResultMessage || 'no results'
      } </li>`;
      this.bindResultList(noResults);
    }
  }

  /**
   * bind result list - binds result to list, add eventlisteners for events 'click' and 'mouseenter' to list items
   *
   * @param {string} list - list with results
   */
  bindResultList(list) {
    this.list.innerHTML = list;
    this.container.classList.add(this.classes.autosuggestOpen);

    this.list
      .querySelectorAll(`[${this.attributes.listValue}]`)
      .forEach((element, index) => {
        if (index === 0) {
          // set first item as pre selected
          element.classList.add(this.classes.autosuggestPreselect);
          element.setAttribute(this.attributes.preselect, '');

          this.selectedIndex = 0;
          this.updatePreselectedItem(this.selectedIndex, null, true);
        }

        element.addEventListener('click', () => {
          this.setSelectedValue(this.selectedIndex);
        });

        element.addEventListener('mouseenter', (mouseEnterEvent) => {
          this.updatePreselectedItem(
            element.getAttribute(this.attributes.index),
            mouseEnterEvent,
            false
          );
        });
      });
  }

  /**
   * Update preselected item, triggers setScrollbar
   *
   * @param {number} index - index of preselected element
   * @param {Event} event - event triggered
   * @param {boolean} executeScroll - boolean to trigger scroll
   */
  updatePreselectedItem(index, event, executeScroll) {
    // get former preselected item, remove class and attribute
    let formerPreselectedItem = ElementHelpers.getElementByAttribute(
      this.attributes.preselect
    );
    if (formerPreselectedItem) {
      formerPreselectedItem.classList.remove(this.classes.autosuggestPreselect);
      formerPreselectedItem.removeAttribute(this.attributes.preselect);
    }
    // get preselected item, add class and attribute
    let preselectedItem = ElementHelpers.getElementByAttribute(
      this.attributes.index,
      index
    );
    if (preselectedItem) {
      preselectedItem.classList.add(this.classes.autosuggestPreselect);
      preselectedItem.setAttribute(this.attributes.preselect, '');
    }

    this.selectedIndex = index;
    this.selectedValue = preselectedItem.innerText;

    // only set scrollbar if event is keydown
    if (
      (typeof event !== 'undefined' &&
        event !== null &&
        event.type === 'keydown') ||
      executeScroll === true
    ) {
      this.setScrollbar(preselectedItem, event);
    }
  }

  /**
   * Set selected value
   *
   * @param {number} index - index of selected item
   */
  setSelectedValue(index) {
    // get preselected item with index, set value to input field and close the list
    let preSelectedItem = ElementHelpers.getElementByAttribute(
      this.attributes.index,
      index
    );

    if (preSelectedItem != null) {
      this.input.value = preSelectedItem.innerText;
      this.closeList();
    }
  }

  /**
   * Handle button keys - handle key presses down, up, enter, page down and page up
   *
   * @param {Event} event - event triggered
   */
  handleButtonKeys(event) {
    if (event != null && this.selectedIndex > -1) {
      switch (ElementHelpers.getKeyCodeOnKeyDownEvent(event)) {
        case this.keyCodes.Down:
          event.preventDefault();
          // if preselected item is not last of the list, preselect next item
          if (this.selectedIndex < this.numberOfListItems - 1) {
            this.selectedIndex++;
            this.updatePreselectedItem(this.selectedIndex, event, false);
          }
          break;

        case this.keyCodes.Up:
          event.preventDefault();
          // if preselected item is not first of the list, preselect previous item
          if (this.selectedIndex > 0) {
            this.selectedIndex--;
            this.updatePreselectedItem(this.selectedIndex, event, false);
          }
          break;

        case this.keyCodes.PageDown:
          event.preventDefault();
          // if preselected item is not last of the list, preselect last item
          if (this.selectedIndex < this.numberOfListItems - 1) {
            this.updatePreselectedItem(
              Number(this.numberOfListItems) - 1,
              event,
              false
            );
          }
          break;

        case this.keyCodes.PageUp:
          event.preventDefault();
          // if preselected item is not first of the list, preselect first item
          if (this.selectedIndex > 0) {
            this.updatePreselectedItem(0, event, false);
          }
          break;

        case this.keyCodes.Enter:
          event.preventDefault();
          this.setSelectedValue(this.selectedIndex);
          this.closeList();
          this.input.blur();

          // If there is a submit button after enter focus on the submit button
          if (this.submit) {
            this.submit.focus();
          }
          break;
      }
    }
  }

  /**
   * Set scroll bar to correct position
   *
   * @param {HTMLElement} element - selected element from the list
   * @param {Event} event - event triggered
   */
  setScrollbar(element, event) {
    let elementHeight = element.offsetHeight;
    let elementOffsetTop = element.offsetTop;
    let offsetTopFirstElement = this.list.children[0].offsetTop;

    // set scrollbar on moving down
    if (
      event != null &&
      ElementHelpers.getKeyCodeOnKeyDownEvent(event) == this.keyCodes.Down
    ) {
      // if bottom of preselected element (elementHeight + elementOffsetTop) > bottom of visible list (this.list.offsetHeight + this.list.scrollTop),
      // set scrollTop to difference between bottom of preselected element and the height of the list
      if (
        elementHeight + elementOffsetTop >
        this.list.offsetHeight + this.list.scrollTop
      ) {
        this.list.scrollTop =
          elementHeight + elementOffsetTop - this.list.offsetHeight;
      }
    }
    // set scrollbar on  moving up
    else {
      // if top of preselected element - margin top) < scroll position OR bottom of preselected element (elementHeight + elementOffsetTop) > bottom of visible list,
      // set scrollTop to top op preselected element - margin top
      if (
        elementOffsetTop - offsetTopFirstElement < this.list.scrollTop ||
        elementOffsetTop + elementHeight >
          this.list.scrollTop + this.list.offsetHeight
      ) {
        this.list.scrollTop = elementOffsetTop - offsetTopFirstElement;
      }
    }
  }

  /**
   * Close list - closes the list with suggestions and resets selected index and selected value
   */
  closeList() {
    // Add custom event to be able to add custom events on the autosuggest close dropdown
    let event = new CustomEvent('autoSuggestCloseDropdown');
    window.dispatchEvent(event);

    this.container.classList.remove(this.classes.autosuggestOpen);
    this.list.innerHTML = '';

    this.selectedValue = '';
    this.selectedIndex = -1;
  }

  /**
   * Get data source
   */
  getDataSource() {
    let autoSuggestValues = this.list.getAttribute(this.attributes.values);
    if (
      typeof autoSuggestValues !== 'undefined' &&
      autoSuggestValues !== null
    ) {
      return autoSuggestValues.split(this.separator);
    } else {
      return [];
    }
  }

  /**
   * Get selector
   */
  static getSelector() {
    return `[${attributeBase}]`;
  }
}
