/*
 * EdGEL: The University of Edinburgh Experience Language.
 * Autocomplete dropdown of suggestions for the University Search Engine service
 */

var EdGel = EdGel || {};
EdGel.searchApi = EdGel.searchApi || 'https://api.search.ed.ac.uk/s/suggest.json';

document.addEventListener('DOMContentLoaded', function () {
  'use strict';

  var unboundForEach = Array.prototype.forEach;
  var forEach = Function.prototype.call.bind(unboundForEach);
  var searchInputs = document.querySelectorAll('input[data-provide="uoe-search"]');
  var i = 0;

  forEach(searchInputs, function (el) {
    if (!el.id) {
      i++;
      el.id = 'uoe-search-' + i;
    }
    var listId = el.id + '-list';
    var statusId = el.id + '-status';
    var describedId = el.id + '-desc';

    // Use the input as our custom combobox element per ARIA 1.0 and draft 1.2
    // (which supersedes the 1.1 pattern that uses a wrapper to the input for
    // the combobox role)
    el.setAttribute('role', 'combobox')
    el.setAttribute('aria-autocomplete', 'list')
    el.setAttribute('aria-controls', listId)
    el.setAttribute('aria-haspopup', 'listbox')
    el.setAttribute('aria-owns', listId)
    el.setAttribute('aria-expanded', 'false')
    el.setAttribute('aria-describedby', describedId)
    el.setAttribute('aria-activedescendant', '')

    // Add instructions for use.  Also use this element for the Bootstrap
    // Dropdown component since you can't assign two Bootstrap components to
    // the same element.
    var db = document.createElement('div')
    db.id = describedId
    // We add the Boostrap Toggle selector somewhere so dismissing the dropdown
    // by clicking elsewhere works.
    db.className = 'sr-only'
    db.textContent = 'When search suggestions are available, use up and down arrows to review and Enter to select.'
    el.parentNode.classList.add('dropdown')
    el.insertAdjacentElement('afterend', db);

    // Add the drop-down menu.
    var lb = document.createElement('ul')
    lb.id = listId
    lb.className = 'dropdown-menu dropdown-uoe-search'
    lb.setAttribute('role', 'listbox')
    lb.setAttribute('aria-label', 'Suggested search terms')
    db.insertAdjacentElement('afterend', lb)

    // Add a status message container for ARIA live announcements.
    var st = document.createElement('div')
    st.id = statusId
    st.className = 'sr-only'
    st.setAttribute('aria-live', 'polite')
    st.textContent = ''
    lb.insertAdjacentElement('afterend', st)

    // Add event listeners bound to this custom combobox's context.
    var searchContext = {
      search: el,
      dropdown: lb,
      described: db,
      statusbox: st
    }

    el.addEventListener('input', function (ev) {
      var query = ev.target.value.trim();

      if (query.length > 1) {
        var params = {
          collection: 'edi-public-meta',
          fmt: 'json++',
          partial_query: query // jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
        }
        var qs = [];
        for (var p in params) {
          qs.push(p + '=' + encodeURIComponent(params[p]));
        }
        var url = EdGel.searchApi + '?' + qs.join('&');

        var request = new XMLHttpRequest();
        request.open('GET', url, true);
        request.addEventListener('load', function () {
          if (this.status === 200) {
            var data = JSON.parse(this.response);
            var suggestions = [];
            var value;

            forEach(data, function (datum) {
              switch (datum.action_t) {  // jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
                case 'Q':
                  value = datum.action;
                  break;
                case 'S':
                  value = datum.disp;
                  break;
                default:
                  value = datum.key;
              }

              if (value && value.trim().length) {
                suggestions.push(value);
              }
            });

            _buildListbox.bind(searchContext, suggestions, query)();
          }
        });

        request.send(params);
      }
      else {
        _buildListbox.bind(searchContext, [], query)();
      }
    }.bind(searchContext));

    // Up and down arrows open/close the combobox list.
    el.addEventListener('keydown', function (ev) {
      var isDisabled = this.search.disabled || this.search.classList.contains('disabled')

      if (isDisabled || !this.dropdown.childElementCount || /^ArrowUp|Up|Escape$/.test(ev.key)) {
        this.search.parentNode.classList.remove('open')
        this.search.setAttribute('aria-expanded', 'false');
        return
      }

      if (/^ArrowDown|Down$/.test(ev.key)) {
        ev.preventDefault()
        var itemsList = this.dropdown.querySelectorAll('.dropdown-item');
        // Convert items NodeList to an array (IE11 safe)
        var items = Array.prototype.slice.call(itemsList);
        if (items.length) {
          this.search.parentNode.classList.add('open')
          this.search.setAttribute('aria-expanded', 'true');
          items[0].focus()
          this.search.setAttribute('aria-activedescendant', items[0].parentNode.id)
          items[0].parentNode.setAttribute('aria-selected', 'true');
          for (var i = 1; i < items.length; i++) {
            items[i].parentNode.removeAttribute('aria-selected');
          }
        } else {
          this.search.setAttribute('aria-activedescendant', '')
        }
      }
    }.bind(searchContext));

    el.addEventListener('focus', function () {
      var itemsList = this.dropdown.querySelectorAll('.dropdown-item')
      // Convert items NodeList to an array (IE11 safe)
      var items = Array.prototype.slice.call(itemsList)
      if (items.length) {
        this.search.parentNode.classList.add('open')
        this.search.setAttribute('aria-expanded', 'true');
      }
    }.bind(searchContext));

    var _blur = function () {
      // We have to wait a tick for the activeElement to be updated with the new
      // focussed element.
      setTimeout(function () {
        var isActive = this.dropdown.contains(document.activeElement) || this.search === document.activeElement

        if (!isActive) {
          this.search.parentNode.classList.remove('open');
          this.search.setAttribute('aria-expanded', 'false');
          var itemsList = this.dropdown.querySelectorAll('.dropdown-item')
          // Convert items NodeList to an array (IE11 safe)
          var items = Array.prototype.slice.call(itemsList)
          for (var i = 0; i < items.length; i++) {
            items[i].parentNode.removeAttribute('aria-selected');
          }
        }
      }.bind(searchContext), 1);
    }

    el.addEventListener('blur', _blur, true);
    lb.addEventListener('focusout', _blur, true);

    // Disable default Enter action on form if it's coming from our
    // drop-down listbox otherwise IE will submit form before input value
    // is updated.
    lb.addEventListener('keydown', function (ev) {
      switch (ev.key) {
        case 'Enter':
          ev.preventDefault();
          break;
      }
    }.bind(searchContext));

    // Navigating listbox items.
    lb.addEventListener('keydown', function (ev) {
      var moveUp = /^ArrowUp|Up$/.test(event.key)
      var moveDown = /^ArrowDown|Down$/.test(event.key)
      var cancel = /^Escape|Esc$/.test(event.key)
      var accept = /^Spacebar| |Enter$/.test(event.key)

      if (!moveUp && !moveDown && !cancel && !accept) {
        return
      }

      ev.preventDefault()
      ev.stopPropagation()

      if (cancel) {
        this.search.setAttribute('aria-expanded', 'false');
        this.search.focus()
        this.search.setAttribute('aria-activedescendant', '')
        this.search.parentNode.classList.remove('open')
        return
      }

      if (accept) {
        if (ev.key === 'Enter') {
          this.search.value = ev.target.textContent
          this.search.form.submit()
        } else {
          this.search.value = ev.target.textContent + ' '
          this.search.focus()
          this.search.parentNode.classList.remove('open')
          this.search.setAttribute('aria-expanded', 'false');
          this.search.setAttribute('aria-activedescendant', '')
        }
      }

      var itemsList = this.dropdown.querySelectorAll('.dropdown-item')
      // Convert items NodeList to an array (IE11 safe)
      var items = Array.prototype.slice.call(itemsList)

      if (!items.length) {
        return
      }

      var index = items.indexOf(event.target)

      // Allow navigation back to the input using the up arrow.
      if (moveUp && index === 0) {
        this.search.focus()
        this.search.setAttribute('aria-activedescendant', '')
        items[0].parentNode.removeAttribute('aria-selected');
        return
      }

      if (moveUp && index > 0) {
        index--
      }

      if (moveDown && index < items.length - 1) {
        index++
      }

      index = index === -1 ? 0 : index
      items[index].focus()
      this.search.setAttribute('aria-activedescendant', items[index].parentNode.id)
      for (var i = 0; i < items.length; i++) {
        if (i === index) {
          items[i].parentNode.setAttribute('aria-selected', 'true');
        } else {
          items[i].parentNode.removeAttribute('aria-selected');
        }
      }
    }.bind(searchContext));

  });

  // Build the listbox.  Bind a search context object to provide values for
  // combobox, dropdown, search and statusbox nodes.
  function _buildListbox(suggestions, query) {
    /* jshint validthis: true */
    // If the input has changed since we ran this query, do nothing: the
    // list of options we've just got is out of date.
    if (query !== this.search.value.trim()) {
      return;
    }

    var limit = parseInt(Math.min(this.search.getAttribute('data-items') || 4, 10), 10);

    var isOpen = this.search.parentNode.classList.contains('open');
    // XOR operator
    if (!!isOpen ^ !!suggestions.length) {
      this.search.parentNode.classList.toggle('open');
      this.dropdown.setAttribute(
          'aria-expanded',
          this.search.parentNode.classList.contains('open') ? 'true' : 'false'
      );
    }

    this.dropdown.textContent = '';
    forEach(suggestions.slice(0, limit), function (suggestion, i) {
      if (!suggestion.trim()) {
        return;
      }
      var escaped = query.trim().replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&')
      var re = new RegExp(escaped, 'gi');
      var html = suggestion
          .replace(/&/g, '&amp;')
          .replace(/</g, '&lt;')
          .replace(/>/g, '&gt;')
          .replace(re, '<strong>$&</strong>')

      var item = document.createElement('li');
      item.setAttribute('role', 'option');
      item.id = this.dropdown.id + '-' + i

      var option = document.createElement('a');
      option.setAttribute('role', 'button');
      option.setAttribute('tabindex', '-1');
      option.className = 'dropdown-item';
      option.innerHTML = html;
      item.appendChild(option);
      this.dropdown.appendChild(item);

      option.addEventListener('click', function (ev) {
        ev.stopPropagation();
        ev.preventDefault();
        this.search.value = ev.target.textContent;
        this.search.form.submit();
      }.bind(this));

    }.bind(this));

    this.statusbox.textContent = Math.min(suggestions.length, limit) + ' suggestions available';
  }

});
