Thoth.Admin = Thoth.Admin || {};

Thoth.Admin.TagComplete = function () {

// Shorthand.
var d   = document,
    Y   = YAHOO,
    yut = Y.util,
    yuc = yut.Connect,
    yud = yut.Dom,
    yue = yut.Event;

// -- Private Variables ------------------------------------------------------
var cache = {},
    conn,
    data;

// -- Private Methods --------------------------------------------------------

/**
 * Gets the last tag in the specified input element, or <i>null</i> if there
 * are no tags.
 *
 * @method getLastTag
 * @param {HTMLElement|String} el input element
 * @return {String|null} last tag or <i>null</i> if there are no tags
 * @private
 */
function getLastTag(el) {
  if (tag = getTags(el).pop()) {
    return tag;
  }

  return null;
}

/**
 * Returns a reference to the suggestion list element associated with the
 * specified tag input element. If the specified element is a suggestion list
 * element, it will be returned.
 *
 * @method getSuggestEl
 * @param {HTMLElement|String} el input element
 * @return {HTMLElement} suggestion list element or <i>null</i> if not found
 * @private
 */
function getSuggestEl(el) {
  el = yud.get(el);

  if (yud.hasClass(el, 'suggested-tags')) {
    return el;
  }

  return yud.getNextSiblingBy(el, function (node) {
    return yud.hasClass(node, 'suggested-tags');
  });
}

/**
 * Parses a comma-separated list of tags in the specified input element and
 * returns it as an Array.
 *
 * @method getTags
 * @param {HTMLElement|String} el tag input element
 * @return {Array} tags
 * @private
 */
function getTags(el) {
  var value = (el = yud.get(el)) ? el.value : null;

  if (value && value.length) {
    return value.split(/,\s*/);
  }

  return [];
}

/**
 * Replaces the last tag in the specified tag input element with the specified
 * tag.
 *
 * @method replaceLastTag
 * @param {HTMLElement|String} el tag input element
 * @param {String} tag tag text
 * @private
 */
function replaceLastTag(el, tag) {
  var tags = getTags(el = yud.get(el));

  tags.pop()
  tags.push(tag);

  el.value = tags.join(', ');
}

/**
 * Sends an Ajax request for tags matching the specified query.
 *
 * @method requestTags
 * @param {HTMLElement} el input element that triggered the request
 * @param {String} query query string
 * @param {Number} (optional) limit maximum number of tags to return
 * @private
 */
function requestTags(el, query, limit) {
  if (conn && yuc.isCallInProgress(conn)) {
    yuc.abort(conn);
  }

  var url = '/api/tag/suggest?q=' + encodeURIComponent(query);

  if (limit) {
    url += '&limit=' + encodeURIComponent(limit);
  }

  if (cache[url]) {
    handleResponse(cache[url]);
    return;
  }

  conn = yuc.asyncRequest('GET', url, {
    argument: [url, el],
    success : handleResponse,
    timeout : 1000
  });
}

// -- Private Event Handlers -------------------------------------------------

/**
 * Handles keydown events on tag input fields.
 *
 * @method handleKeyDown
 * @param {Event} e event object
 * @private
 */
function handleKeyDown(e) {
  var charCode = yue.getCharCode(e),
      el       = yue.getTarget(e),
      suggestEl;

  // Autocomplete the current tag when the tab key is pressed.
  if (charCode === 9 && !e.shiftKey && !e.ctrlKey && !e.altKey &&
        !e.metaKey && data.length && this.isVisible(el)) {
    yue.preventDefault(e);
    replaceLastTag(el, data[0][0]);
    this.hide(el);
  }
}

/**
 * Handles keyup events on tag input fields.
 *
 * @method handleKeyUp
 * @param {Event} e event object
 * @private
 */
function handleKeyUp(e) {
  var charCode = yue.getCharCode(e),
      el       = yue.getTarget(e),
      tag;

  if (charCode < 46 && charCode !== 8 && charCode !== 32) {
    return;
  }

  tag = getLastTag(el);

  if (tag) {
    requestTags(el, tag);
  } else {
    this.hide(el);
  }
}

/**
 * Handles Ajax responses.
 *
 * @method handleResponse
 * @param {Object} response response object
 * @private
 */
function handleResponse(response) {
  if (!cache[response.argument[0]]) {
    cache[response.argument[0]] = response;
  }

  try {
    data = Y.lang.JSON.parse(response.responseText);
    Thoth.Admin.TagComplete.refresh(response.argument[1], data);
  } catch (e) {}
}

/**
 * Handles clicks on the tag suggestions.
 *
 * @method handleTagClick
 * @param {Event} e event object
 * @param {HTMLElement} el tag input element
 * @private
 */
function handleTagClick(e, el) {
  var a = yue.getTarget(e);

  if (a.tagName.toLowerCase() !== 'a') {
    return;
  }

  yue.preventDefault(e);
  replaceLastTag(el, a.getAttribute('tagText'));
  el.focus();

  Thoth.Admin.TagComplete.hide(el);
}

return {
  // -- Public Methods -------------------------------------------------------

  /**
   * Initializes the TagComplete module on this page.
   *
   * @method init
   */
  init: function () {
    var self = this,
        list;

    yud.getElementsByClassName('tags-input', 'input', 'doc', function (el) {
      // Listen for keys on tag input fields.
      yue.on(el, 'keydown', handleKeyDown, self, true);
      yue.on(el, 'keyup', handleKeyUp, self, true);

      // Turn off browser autocomplete to avoid excess annoyingness.
      el.setAttribute('autocomplete', 'off');

      // Create a suggestion list element under the input element.
      list = d.createElement('ol');
      list.className = 'suggested-tags hidden';

      yud.insertAfter(list, el);
      yue.on(list, 'click', handleTagClick, el);
    });
  },

  /**
   * Clears the suggestions for the specified element.
   *
   * @method clear
   * @param {HTMLElement|String} el input element or suggestion list
   */
  clear: function (el) {
    if (el = getSuggestEl(el)) {
      el.innerHTML = '';
    }
  },

  /**
   * Hides the suggestions for the specified element.
   *
   * @method hide
   * @param {HTMLElement|String} el input element or suggestion list
   */
  hide: function (el) {
    if (el = getSuggestEl(el)) {
      yud.addClass(el, 'hidden');
    }
  },

  /**
   * Returns <i>true</i> if suggestions for the specified element are visible,
   * <i>false</i> otherwise.
   *
   * @method isVisible
   * @param {HTMLElement|String} el input element or suggestion list
   * @return <i>true</i> if suggestions are visible, <i>false</i> otherwise
   */
  isVisible: function (el) {
    return !yud.hasClass(getSuggestEl(el), 'hidden');
  },

  /**
   * Refreshes the tag suggestions for the specified element.
   *
   * @method refresh
   * @param {HTMLElement|String} el input element or suggestion list
   * @param {Array} (optional) tags suggested tags
   */
  refresh: function (el, tags) {
    var a, i, li, tag;

    if (!(el = getSuggestEl(el))) {
      return;
    }

    this.clear(el);

    if (!tags || tags.length === 0) {
      this.hide(el);
      return;
    }

    for (i = 0; i < tags.length; ++i) {
      tag = tags[i];
      li  = d.createElement('li');
      a   = d.createElement('a');

      a.href = '#';
      a.appendChild(d.createTextNode(tag[0] + ' (' + tag[1] + ')'));
      a.setAttribute('tagText', tag[0]);

      li.appendChild(a);
      el.appendChild(li);
    }

    this.show(el);
  },

  /**
   * Shows the suggestions for the specified element.
   *
   * @method show
   * @param {HTMLElement|String} el input element or suggestion list
   */
  show: function (el) {
    if (el = getSuggestEl(el)) {
      yud.removeClass(el, 'hidden');
    }
  }
};

}();

YAHOO.util.Event.onContentReady('doc', Thoth.Admin.TagComplete.init,

Thoth.Admin.TagComplete, true);