/**
 * A helper to quickly search for and display results of a query without having
 * to leave the page.
 */
cw.provide("CW-QuickSearch", function(){

if (typeof jQuery == "undefined") {
    cw.error("jQuery is required for the Quick Search module");
}

// import the necessary dependencies
var Helpers = cw.require("CW-Helpers"),
    MakeNumberUserFriendly = Helpers.MakeNumberUserFriendly,
    GenerateRandomString = Helpers.GenerateRandomString,
    HighlightSearchTermsInText = Helpers.HighlightSearchTermsInText,
    TruncateString = Helpers.TruncateString,
    EscapeHtmlEntitiesInString = Helpers.EscapeHtmlEntitiesInString,
    StripHtmlFromString = Helpers.StripHtmlFromString,
    TrimString = Helpers.TrimString,
    FormatNumber = Helpers.FormatNumber,
    ResourceKeywordSearchData = Helpers.ResourceKeywordSearchData,
    MultipleValuesFieldData = Helpers.MultipleValuesFieldData,
    UserData = Helpers.UserData,
    Keyboard = cw.require("CW-Keyboard"),
    KeyCodes = Keyboard.KeyCodes,
    KeyPressStream = Keyboard.KeyPressStream,
    DefaultActionBlocker = Keyboard.DefaultActionBlocker,
    KeyCodeSequence = Keyboard.KeyCodeSequence,
    Emacs = Keyboard.StandardKeyCombos.Emacs,
    RouterUrl = cw.getRouterUrl();

// -----------------------------------------------------------------------------

/**
 * Provides a base class for quick searches, aka search-as-you-type or in-place
 * searching.
 * @see QuickSearch::addTriggerElements()
 * @see QuickSearch::addSearchListener()
 * @see QuickSearch::addSelectionListener()
 */
function QuickSearch() {
  // important: this method should be called first because much of this class
  // depends on having a unique identifier
  this.generateIdentifier();

  // finish initialization
  this.clearTriggerElements();
  this.clearSearchListeners();
  this.clearSelectionListeners();
  this.initKeyPressStream();
  this.initDefaultActionBlocker();
  this.initContainer();
  this.initDefaultValues();
}

/**
 * The default number of results to jump by when jumping forward or backward
 * through the list of results.
 * @var int DefaultJumpCount
 */
QuickSearch.DefaultJumpCount = 5;

/**
 * Add the given elements to the list of the elements that trigger searching.
 * @param jQuery elements elements that trigger searching
 * @see QuickSearch::removeTriggerElements()
 * @see QuickSearch::clearTriggerElements()
 */
QuickSearch.prototype.addTriggerElements = function(elements) {
  var eventSignatureNamespace = this.getEventSignatureNamespace();

  // remove the event listeners if already applied so that they don't fire
  // more than once
  elements.off(eventSignatureNamespace);

  // add the focus event listener
  elements.on(
    "focus" + this.getEventSignatureNamespace(),
    cw.bindObjectMethodCallback(this, this.focusListener));

  // add the blur event listenter
  elements.on(
    "blur" + this.getEventSignatureNamespace(),
    cw.bindObjectMethodCallback(this, this.blurListener));

  this._triggerElements = this._triggerElements.add(elements);
  this._keyPressStream.addTriggerElements(elements);
  this._defaultActionBlocker.addTriggerElements(elements);
};

/**
 * Remove the given elements from the list of the elements that trigger
 * searching.
 * @param jQuery elements elements that trigger searching
 * @see QuickSearch::addTriggerElements()
 * @see QuickSearch::clearTriggerElements()
 */
QuickSearch.prototype.removeTriggerElements = function(elements) {
  // remove the event listeners
  elements.off(this.getEventSignatureNamespace());

  this._triggerElements = this._triggerElements.filter(elements);
  this._keyPressStream.removeTriggerElements(elements);
  this._defaultActionBlocker.removeTriggerElements(elements);
};

/**
 * Remove all event listeners from all the elements that trigger focus and blur
 * events and then clear the list of elements.
 * @see QuickSearch::addTriggerElements()
 * @see QuickSearch::removeTriggerElements()
 */
QuickSearch.prototype.clearTriggerElements = function() {
  // remove event listeners if any elements have been bound
  if (this._triggerElements) {
    this._triggerElements.off(this.getEventSignatureNamespace());
  }

  // reset the list of bound elements
  this._triggerElements = jQuery();
};

/**
 * Add a search listener callback.
 * @param callback listener listener callback
 * @return the identifier of the listener callback
 * @see QuickSearch::removeSearchListener()
 * @see QuickSearch::clearSearchListeners()
 */
QuickSearch.prototype.addSearchListener = function(listener) {
  var identifier, placeholder;

  do {
    // generate a new, unused identifier for the listener
    identifier = this.generateListenerIdentifier();
  } while (this._searchListeners[identifier]);

  // add the listener to the list
  this._searchListeners[identifier] = listener;

  // create the placeholder for the listener to populate content
  placeholder = jQuery("<section>");
  placeholder.addClass("quicksearch-quicksearch-section");

  // add the placeholder to the container
  this._container.append(placeholder);

  // return the placeholder and listener identifier so that it can be removed
  return {"identifier": identifier, "placeholder": placeholder};
};

/**
 * Remove a search listener callback by its identifier.
 * @param string identifier the identifier of the listener callback
 * @see QuickSearch::addSearchListener()
 * @see QuickSearch::clearSearchListeners()
 */
QuickSearch.prototype.removeSearchListener = function(identifier) {
  delete this._searchListeners[identifier];
};

/**
 * Clear the array of search listener callbacks.
 * @see QuickSearch::addSearchListener()
 * @see QuickSearch::removeSearchListener()
 */
QuickSearch.prototype.clearSearchListeners = function() {
  this._searchListeners = {};
};

/**
 * Add a search result selection listener callback.
 * @param callback listener listener callback
 * @return the identifier of the listener callback
 * @see QuickSearch::removeSelectionListener()
 * @see QuickSearch::clearSelectionListeners()
 */
QuickSearch.prototype.addSelectionListener = function(listener) {
  var identifier;

  do {
    // generate a new, unused identifier for the listener
    identifier = this.generateListenerIdentifier();
  } while (this._selectionListeners[identifier]);

  // add the listener to the list
  this._selectionListeners[identifier] = listener;

  // return the listener identifier so that it can be removed
  return identifier;
};

/**
 * Remove a search result selection listener callback by its identifier.
 * @param string identifier the identifier of the listener callback
 * @see QuickSearch::addSelectionListener()
 * @see QuickSearch::clearSelectionListeners()
 */
QuickSearch.prototype.removeSelectionListener = function(identifier) {
  delete this._selectionListeners[identifier];
};

/**
 * Clear the array of search result selection listener callbacks.
 * @see QuickSearch::addSelectionListener()
 * @see QuickSearch::removeSelectionListener()
 */
QuickSearch.prototype.clearSelectionListeners = function() {
  this._selectionListeners = {};
};

/**
 * Set number of results to jump by when jumping forward or backward through
 * the list of results specific to this object.
 * @param int jumpCount new jump count value
 */
QuickSearch.prototype.setJumpCount = function(jumpCount) {
  this._jumpCount = jumpCount;
};

/**
 * Get the namespace used for event listener signatures.
 * @return the namespace for event listener signatures
 * @protected
 */
QuickSearch.prototype.getEventSignatureNamespace = function() {
  return ".quicksearch.quicksearch." + this.getIdentifier();
};

/**
 * Generate an identifier string for a search listener.
 * @return an identifier string for a search listener
 * @protected
 */
QuickSearch.prototype.generateListenerIdentifier = function() {
  return GenerateRandomString(5);
};

/**
 * Generates and sets the unique identifier for the object. The identifier is
 * used when creating event signatures.
 * @see QuickSearch::getIdentifier()
 * @protected
 */
QuickSearch.prototype.generateIdentifier = function() {
  this._identifier = GenerateRandomString(5);
};

/**
 * Fetch the identifier used to create event signatures.
 * @return the identifier used to create event signatures
 * @see QuickSearch::generateIdentifer()
 * @protected
 */
QuickSearch.prototype.getIdentifier = function() {
  return this._identifier;
};

/**
 * Initialize the key press stream object attached to this one, along with the
 * key code sequence listeners for it.
 * @protected
 */
QuickSearch.prototype.initKeyPressStream = function() {
  // initialize the key press stream
  this._keyPressStream = new KeyPressStream();

  // add a stream listener to check for changes to the input
  this._keyPressStream.addStreamListener(
    cw.bindObjectMethodCallback(this, this.keyPressStreamListener));

  // next item when the down arrow is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.goToNextItem),
    new KeyCodeSequence(KeyCodes.ARROW_DOWN));

  // next item when the next-line key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.goToNextItem),
    new KeyCodeSequence(Emacs.NEXT_LINE));

  // previous item when the up arrow is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.goToPreviousItem),
    new KeyCodeSequence(KeyCodes.ARROW_UP));

  // previous item when the previous-line key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.goToPreviousItem),
    new KeyCodeSequence(Emacs.PREVIOUS_LINE));

  // beginning of the list when the beginning-of-buffer key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.goToBeginningOfList),
    new KeyCodeSequence(Emacs.BEGINNING_OF_BUFFER));

  // end of the list when the end-of-buffer key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.goToEndOfList),
    new KeyCodeSequence(Emacs.END_OF_BUFFER));

  // jump forward through the list when the scroll-down key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.jumpBackwardThroughList),
    new KeyCodeSequence(Emacs.SCROLL_DOWN));

  // jump forward through the list when the scroll-up key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.jumpForwardThroughList),
    new KeyCodeSequence(Emacs.SCROLL_UP));

  // select the active result when pressing ENTER/RETURN
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.makeSelection),
    new KeyCodeSequence(KeyCodes.RETURN));

  // hide the container when the ESCAPE key is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.cancelSearching),
    new KeyCodeSequence(KeyCodes.ESCAPE));

  // recenter on the active result when the recenter key combo is pressed
  this._keyPressStream.addKeyCodeSequenceListener(
    cw.bindObjectMethodCallback(this, this.recenterOnActiveResult),
    new KeyCodeSequence(Emacs.RECENTER));
};

/**
 * Initialize the default action blocker object attached to this one, along with
 * the key codes sequences it should block.
 * @protected
 */
QuickSearch.prototype.initDefaultActionBlocker = function() {
  this._defaultActionBlocker = new DefaultActionBlocker();

  // block the default browser action for the down arrow
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    KeyCodes.ARROW_DOWN));

  // block the default browser action for the next-line key combo
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    Emacs.NEXT_LINE));

  // block the default browser action for the up arrow
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    KeyCodes.ARROW_UP));

  // block the default browser action for the previous-line key combo
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    Emacs.PREVIOUS_LINE));

  // block the default browser action for the beginning-of-buffer key combo
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    Emacs.BEGINNING_OF_BUFFER));

  // block the default browser action for the end-of-buffer key combo
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    Emacs.END_OF_BUFFER));

  // block the default browser action for the scroll-down key combo
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    Emacs.SCROLL_DOWN));

  // block the default browser action for the scroll-up key combo
  // this will cause issues with Windows when trying to paste
  // this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
  //   Emacs.SCROLL_UP));

  // block the default browser action for the ENTER/RETURN key
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    KeyCodes.RETURN));

  // block the default browser action for the ESCAPE key
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    KeyCodes.ESCAPE));

  // block the default browser action for the recenter key combo
  this._defaultActionBlocker.addKeyCodeSequenceTrigger(new KeyCodeSequence(
    Emacs.RECENTER));
};

/**
 * Initialize the container that will hold the search results.
 * @protected
 */
QuickSearch.prototype.initContainer = function() {
  // initialize the container
  this._container = jQuery("<aside/>");
  this._container.addClass("quicksearch-quicksearch-container");

  // initially position the container off the screen
  this._container.css({
    "display": "block",
    "position": "absolute",
    "left": "-1000px"
  });

  this._container.mousedown(cw.bindObjectMethodCallback(
    this, this.containerMouseDownListener));
  this._container.mouseup(cw.bindObjectMethodCallback(
    this, this.containerMouseUpListener));

  // add the container to the document
  this._container.appendTo("body");
};

/**
 * Clear the contents of the container.
 * @protected
 */
QuickSearch.prototype.clearContainer = function() {
  this._container.empty();
};

/**
 * Initialize default scalar values.
 * @protected
 */
QuickSearch.prototype.initDefaultValues = function() {
  this._jumpCount = QuickSearch.DefaultJumpCount;
  this._lastValue = null;
};

/**
 * Listener callback for key code sequences that should go to the previous item.
 * @param jQuery trigger the element that triggered the sequence
 * @see QuickSearch::initKeyPressStream()
 * @see QuickSearch::goToNextItem()
 * @see QuickSearch::goToBeginningOfList()
 * @see QuickSearch::goToEndOfList()
 * @see QuickSearch::jumpBackwardThroughList()
 * @see QuickSearch::jumpForwardThroughList()
 * @protected
 */
QuickSearch.prototype.goToPreviousItem = function(trigger) {
  // activate the previous result, or the first result if there is no previous
  // result
  this.activateResult(this.getPreviousResult() || this.getFirstResult());
};

/**
 * Listener callback for key code sequences that should go to the next item.
 * @param jQuery trigger the element that triggered the sequence
 * @see QuickSearch::initKeyPressStream()
 * @see QuickSearch::goToPreviousItem()
 * @see QuickSearch::goToBeginningOfList()
 * @see QuickSearch::goToEndOfList()
 * @see QuickSearch::jumpBackwardThroughList()
 * @see QuickSearch::jumpForwardThroughList()
 * @protected
 */
QuickSearch.prototype.goToNextItem = function(trigger) {
  var activeResult = this.getActiveResult();

  // go to the first result only when there isn't an active result to prevent
  // looping
  if (!activeResult) {
    this.activateResult(this.getFirstResult());
  }

  // otherwise try going to the next result
  else {
    this.activateResult(this.getNextResult());
  }
};

/**
 * Listener callback for key code sequences that should go to the beginning of
 * the list.
 * @param jQuery trigger the element that triggered the sequence
 * @see QuickSearch::initKeyPressStream()
 * @see QuickSearch::goToNextItem()
 * @see QuickSearch::goToPreviousItem()
 * @see QuickSearch::goToEndOfList()
 * @see QuickSearch::jumpBackwardThroughList()
 * @see QuickSearch::jumpForwardThroughList()
 * @protected
 */
QuickSearch.prototype.goToBeginningOfList = function(trigger) {
  // go to the first result
  this.activateResult(this.getFirstResult());
};

/**
 * Listener callback for key code sequences that should go to the end of the
 * list.
 * @param jQuery trigger the element that triggered the sequence
 * @see QuickSearch::initKeyPressStream()
 * @see QuickSearch::goToNextItem()
 * @see QuickSearch::goToPreviousItem()
 * @see QuickSearch::goToBeginningOfList()
 * @see QuickSearch::jumpBackwardThroughList()
 * @see QuickSearch::jumpForwardThroughList()
 * @protected
 */
QuickSearch.prototype.goToEndOfList = function(trigger) {
  // go to the last result
  this.activateResult(this.getLastResult());
};

/**
 * Listener callback for key code sequences that should jump backward through
 * the list.
 * @param jQuery trigger the element that triggered the sequence
 * @see QuickSearch::initKeyPressStream()
 * @see QuickSearch::goToNextItem()
 * @see QuickSearch::goToPreviousItem()
 * @see QuickSearch::goToBeginningOfList()
 * @see QuickSearch::goToEndOfList()
 * @see QuickSearch::jumpForwardThroughList()
 * @protected
 */
QuickSearch.prototype.jumpBackwardThroughList = function(trigger) {
  // go to the previous result by jumping if one exists. otherwise go to the
  // first result
  this.activateResult(this.getPreviousResultByJumping() || this.getFirstResult());
};

/**
 * Listener callback for key code sequences that should jump backward through
 * the list.
 * @param jQuery trigger the element that triggered the sequence
 * @see QuickSearch::initKeyPressStream()
 * @see QuickSearch::goToNextItem()
 * @see QuickSearch::goToPreviousItem()
 * @see QuickSearch::goToBeginningOfList()
 * @see QuickSearch::goToEndOfList()
 * @see QuickSearch::jumpBackwardThroughList()
 * @protected
 */
QuickSearch.prototype.jumpForwardThroughList = function(trigger) {
  var targetResult = this.getNextResultByJumping();

  // there isn't a next result by jumping
  if (!targetResult) {
    // there are active results but there aren't enough results to do a full
    // jump, so use the last result
    if (this.getActiveResult()) {
      targetResult = this.getLastResult();
    }

    // otherwise, use the first available result offset by the jump count or
    // the last result if the jump count is greater than the number of results
    else {
      targetResult = this.getResultByIndex(this._jumpCount-1) || this.getLastResult();
    }
  }

  // go to the next result by jumping if one exists. otherwise go to the last
  // result
  this.activateResult(targetResult);
};

/**
 * Listener callback for key code sequences that should select a result.
 * @param jQuery trigger the element that triggered the sequence
 * @protected
 */
QuickSearch.prototype.makeSelection = function(trigger) {
  var activeResult = this.getActiveResult();

  for (var selectionListenerIdentifier in this._selectionListeners) {
    this._selectionListeners[selectionListenerIdentifier].call(
      this._selectionListeners[selectionListenerIdentifier],
      activeResult,
      trigger);
  }
};

/**
 * Callback executed when the searching should be canceled.
 * @param jQuery trigger the element that triggered the sequence
 * @protected
 */
QuickSearch.prototype.cancelSearching = function(trigger) {
  // make sure the trigger is visible in case the user scrolled and pushed it
  // out of the visible area
  this.makeItemVisible(trigger);

  this.hideContainer();
  this.revertTriggerOutlineFix();
};

/**
 * Listener callback for key code sequences that should recenter the window on
 * the active result.
 * @param jQuery trigger the element that triggered the sequence
 * @protected
 */
QuickSearch.prototype.recenterOnActiveResult = function(trigger) {
  var activeResult = this.getActiveResult();

  // center the window on the active result, if any
  if (activeResult) {
    this.scrollWithItemAsCenter(activeResult);
  }
};

/**
 * Scroll the window, if necessary, to make an entire item as visible as
 * possible.
 * @param jQuery item item
 * @protected
 */
QuickSearch.prototype.makeItemVisible = function(item, addExtraSpace) {
  var viewportOffset = jQuery(window).scrollTop(),
      viewportHeight = jQuery(window).height(),
      itemOffset = item.offset().top,
      itemHeight = item.outerHeight(),
      viewportDifference = viewportHeight - itemHeight,
      extraScroll = 0;

  // determine how much extra extra space to add if feasible and told to
  if (addExtraSpace !== false && viewportDifference > 0) {
    extraScroll = Math.min(viewportDifference, 125);
  }

  // if the viewport is too short to display an entire item or if the top of
  // the item is above the top edge of the viewport
  if (viewportHeight < itemHeight || itemOffset < viewportOffset) {
    this.scrollSmoothlyTo(itemOffset - extraScroll);
  }

  // the bottom of the item is below the bottom edge of the viewport
  else if (itemOffset + itemHeight > viewportOffset + viewportHeight) {
    // go to the equivalent of viewport offset + (item offset + item height
    // - viewport offset - viewport height)
    this.scrollSmoothlyTo(itemOffset + itemHeight - viewportHeight + extraScroll);
  }
};

/**
 * Scroll the window, if necessary, to make an entire item as visible as
 * possible and within the center as possible.
 * @param jQuery item item
 * @protected
 */
QuickSearch.prototype.makeItemVisibleAndCentered = function(item) {
  var viewportOffset = jQuery(window).scrollTop(),
      viewportHeight = jQuery(window).height(),
      itemOffset = item.offset().top,
      itemHeight = item.outerHeight();

  // if the viewport is too short to display an entire item or if the top of
  // the item is above the top edge of the viewport
  if (viewportHeight < itemHeight || itemOffset < viewportOffset) {
    this.scrollWithItemAsCenter(item);
  }

  // the bottom of the item is below the bottom edge of the viewport
  else if (itemOffset + itemHeight > viewportOffset + viewportHeight) {
    this.scrollWithItemAsCenter(item);
  }
};

/**
 * Scroll the window so that the item is as centered within it as possible.
 * @param jQuery item item
 * @protected
 */
QuickSearch.prototype.scrollWithItemAsCenter = function(item) {
  var viewportHeight = jQuery(window).height(),
      itemOffset = item.offset().top,
      itemHeight = item.outerHeight();

  this.scrollSmoothlyTo(itemOffset + (itemHeight / 2) - (viewportHeight / 2));
};

/**
 * Smoothly scroll to a new y/top position in the page.
 * @param int top new y/top position
 * @protected
 */
QuickSearch.prototype.scrollSmoothlyTo = function(top) {
  // use html and body for browser compatibility
  jQuery("html, body").animate({"scrollTop": top}, {"duration": 100});
};

/**
 * Deactivate the current trigger.
 * @protected
 */
QuickSearch.prototype.deactivateTrigger = function() {
  // the trigger has already been deactivated
  if (!this._currentTrigger) {
    return;
  }

  this.revertTriggerOutlineFix();
  this._currentTrigger = null;
};

/**
 * Callback function for a trigger element that receives focus.
 * @param object event standard JavaScript event object
 * @see QuickSearch::blurListener()
 * @protected
 */
QuickSearch.prototype.focusListener = function(event) {
  var trigger = jQuery(event.target);

  // there is a current trigger set, which means there was a blur but the user
  // had moused inside the container
  if (this._currentTrigger) {
    this.deactivateTrigger();
  }

  // set the new trigger
  this._currentTrigger = trigger;

  // set the new value
  this._lastValue = this.getElementValue(this._currentTrigger);

  // turn off browser autocompletion
  trigger.data("quicksearch.quicksearch.autocomplete", trigger.attr("autocomplete"));
  trigger.attr("autocomplete", "off");

  // populate the container from the content received from the search listeners
  this.populateContainerFromListeners(this._searchListeners);
};

/**
 * Callback function for a trigger element that loses focus.
 * @param object event standard JavaScript event object
 * @see QuickSearch::focusListener()
 * @protected
 */
QuickSearch.prototype.blurListener = function(event) {
  var trigger = jQuery(event.target);

  // only hide the results if the user hasn't mouse over them. otherwise the
  // user won't be able to interact with the results
  if (!this._mousedInContainer) {
    this.hideContainer();
    this.deactivateTrigger();
  }

  // revert the autocompletion fix
  trigger.attr("autocomplete", trigger.data("quicksearch.quicksearch.autocomplete"));
};

/**
 * Callback function executed when the user clicks somewhere inside the results
 * container.
 * @see QuickSearch::containerMouseUpListener()
 * @protected
 */
QuickSearch.prototype.containerMouseDownListener = function() {
  this._mousedInContainer = true;
};

/**
 * Callback function executed when the user lifts up the mouse button inside the
 * results container.
 * @see QuickSearch::containerMouseDownListener()
 * @protected
 */
QuickSearch.prototype.containerMouseUpListener = function() {
  this._mousedInContainer = false;
};

/**
 * Show the results container and apply any fixes.
 * @protected
 */
QuickSearch.prototype.showContainer = function() {
  // apply fixes
  this.fixTriggerOutline();

  // show/reposition the container on the trigger
  this.repositionOn(this._currentTrigger);
};

/**
 * Hide the results container off the side of the screen and revert any fixes
 * that were applied.
 * @protected
 */
QuickSearch.prototype.hideContainer = function() {
  // position the container off the screen
  this._container.css({"left": "-1000px"});
};

/**
 * Populate the containter with content from an array of listener callbacks.
 * @param array listeners listener callbacks
 * @protected
 */
QuickSearch.prototype.populateContainerFromListeners = function(listeners) {
  for (var identifier in listeners) {
    // execute the listener
    listeners[identifier].call(listeners[identifier], this._currentTrigger);
  }

  // show or hide the container depending on whether it's empty or not
  if (this._container.is(":empty") || !TrimString(this._container.text()).length) {
    this.hideContainer();
    this.revertTriggerOutlineFix();
  } else {
    this.showContainer();
  }
};

/**
 * Fix the outline property of a trigger so that it doesn't interfere with the
 * results container.
 * @see QuickSearch::revertTriggerOutlineFix()
 * @protected
 */
QuickSearch.prototype.fixTriggerOutline = function() {
  var style;
  var trigger = this._currentTrigger;

  // don't apply the fix twice
  if (trigger.data("quicksearch.quicksearch.outlinefixapplied")) {
    return;
  }

  style = trigger.attr("style");

  // if the trigger has an inline outline style
  if (style && style.search(/outline(-(style|width|color))?/i) != -1) {
    trigger.data("quicksearch.quicksearch.outline", trigger.css("outline"));
  }

  // remove the outline from the element
  trigger.css("outline", "none");

  // signal that the outline fix has been applied
  trigger.data("quicksearch.quicksearch.outlinefixapplied", true);
};

/**
 * Revert the outline property change fix applied so that the trigger's outline
 * doesn't interfere with the results container.
 * @see QuickSearch::fixTriggerOutline()
 * @protected
 */
QuickSearch.prototype.revertTriggerOutlineFix = function() {
  var trigger = this._currentTrigger;

  // can't revert the fix if it wasn't applied
  if (!trigger.data("quicksearch.quicksearch.outlinefixapplied")) {
    return;
  }

  // remove the outline fix
  trigger.attr("style", trigger.attr('style').replace(/outline(-(style|width|color)):[^;]*;/gi, ""));

  // revert the outline to the style attribute if necessary
  if (trigger.data("quicksearch.quicksearch.outline")) {
    trigger.css("outline", trigger.data("quicksearch.quicksearch.outline"));
  }

  // remove the associated data
  trigger.removeData("quicksearch.quicksearch.outlinefixapplied");
  trigger.removeData("quicksearch.quicksearch.outline");
};

/**
 * Listener that receives all key presses. Used to determine when a trigger's
 * value has changed.
 * @param KeyCodeSequence keyCodeSequence key code sequence
 * @protected
 */
QuickSearch.prototype.keyPressStreamListener = function(keyCodeSequence) {
  var value = this.getElementValue(this._currentTrigger);

  // do nothing if the element no longer has focus. this is to prevent the rest
  // of this method from executing when a key is used to switch focus to
  // something else, including, but not limited to, when the TAB key is pressed
  if (!this._currentTrigger || !this._currentTrigger.is(":focus")) {
    return;
  }

  // no changes have occurred
  if (this._lastValue == value) {
    return;
  }

  // fire off a search if a number of changes have occurred. this makes it so
  // that a search is always performed at least every time the length is evenly
  // divisible by three
  if (value.length % 3 === 0) {
    clearTimeout(this._triggerValueChangeTimeout);
    this.changeOccurred();
  }

  // otherwise fire off a search after a timeout (which may get cleared) by
  // another search
  else {
    clearTimeout(this._triggerValueChangeTimeout);
    this._triggerValueChangeTimeout = setTimeout(cw.bindObjectMethodCallback(
      this, this.changeOccurred), 250);
  }

  // update the last value seen
  this._lastValue = value;
};

/**
 * Get the value of input and textarea elements and the HTML of everything else.
 * @param jQuery element jQuery object
 * @return the value (or HTML) of the element
 * @protected
 */
QuickSearch.prototype.getElementValue = function(element) {
  return element.is("input, textarea") ? element.val() : element.text();
};

/**
 * Set the value of input and text area elements and the HTML of everything
 * else.
 * @param jQuery element jQuery object
 * @param string value the value to set
 * @protected
 */
QuickSearch.prototype.setElementValue = function(element, value) {
  return element.is("input, textarea") ? element.val(value) : element.text(value);
};

/**
 * Function to call when a change has occurred that should trigger the search
 * listeners.
 * @protected
 */
QuickSearch.prototype.changeOccurred = function() {
  // populate the container from the content received from the search listeners
  this.populateContainerFromListeners(this._searchListeners);
};

/**
 * Reposition the container on an element.
 * @param jQuery element element on which to position the container
 * @protected
 */
QuickSearch.prototype.repositionOn = function(element) {
  var pos = element.offset();
  var bwidth = parseInt(this._container.css("borderLeftWidth"), 10) +
        parseInt(this._container.css("borderRightWidth"), 10) +
        parseInt(this._container.css("paddingLeft"), 10) +
        parseInt(this._container.css("paddingRight"), 10);

  // position the container just below the element
  this._container.css({
    "top": (pos.top + element.outerHeight())+"px",
    "left": pos.left+"px",
    "width": (element.outerWidth()-bwidth)+"px"
  });
};

/**
 * Get the list of result items in the container.
 * @return the list of results (if any) as a jQuery object
 * @protected
 */
QuickSearch.prototype.getResultsList = function() {
  return jQuery(".quicksearch-quicksearch-result", this._container);
};

/**
 * Get the active result from the container.
 * @return the active result as a jQuery object or undefined if there is none
 * @protected
 */
QuickSearch.prototype.getActiveResult = function() {
  var activeResult = jQuery(".quicksearch-quicksearch-active", this._container);

  // if an active result isn't found
  if (!activeResult.length) {
    return undefined;
  }

  return activeResult;
};

/**
* Get a result relative to the active result.
* @param int offset the offset relative to the active result
* @return result (jQuery) relative to the active one or undefined if none
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResultByJumping()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getLastResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getResultRelativeToActiveResult = function(offset) {
  var results, index;
  var activeResult = this.getActiveResult();

  // there's no active result so it's impossible to get a result relative to it
  if (!activeResult) {
    return undefined;
  }

  results = this.getResultsList();
  index = results.index(activeResult) + offset;

  // if the index falls within the range of results
  if (index >= 0 && index < results.length) {
    return jQuery(results.get(index));
  }

  return undefined;
};

/**
* Get the result immediately preceding the active result.
* @return the result (jQuery) immediately preceding the active result or
*         undefined if none
* @see QuickSearch::getResultRelativeToActiveResult()
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResultByJumping()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getLastResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getPreviousResult = function() {
  return this.getResultRelativeToActiveResult(-1);
};

/**
* Get the result immediately after the active result.
* @return the result (jQuery) immediately after the active result or undefined
* @see QuickSearch::getResultRelativeToActiveResult()
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getPreviousResultByJumping()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getLastResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getNextResult = function() {
  return this.getResultRelativeToActiveResult(1);
};

/**
* Get the result after the active result that is the jump count away from it.
* @return the result (jQuery) after the active result or undefined if none
* @see QuickSearch::getResultRelativeToActiveResult()
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getLastResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getPreviousResultByJumping = function() {
  return this.getResultRelativeToActiveResult(-this._jumpCount);
};

/**
* Get the result before the active result that is the jump count away from it.
* @return the result (jQuery) before the active result or undefined if none
* @see QuickSearch::getResultRelativeToActiveResult()
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getLastResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getNextResultByJumping = function() {
  return this.getResultRelativeToActiveResult(this._jumpCount);
};

/**
* Get the first result in the list of results.
* @return the first result (jQuery) or undefined if none
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getPreviousResultByJumping()
* @see QuickSearch::getLastResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getFirstResult = function() {
  return this.getResultByIndex(0);
};

/**
* Get the last result in the list of results.
* @return the last result (jQuery) or undefined if none
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getPreviousResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getResultByIndex()
* @protected
*/
QuickSearch.prototype.getLastResult = function() {
  var lastResult = this.getResultsList().last().get(0);

  return lastResult ? jQuery(lastResult) : lastResult;
};

/**
* Get a result by its index in the list of results.
* @param int index index withint the list of results
* @return the first result (jQuery) or undefined if none
* @see QuickSearch::getNextResult()
* @see QuickSearch::getPreviousResult()
* @see QuickSearch::getNextResultByJumping()
* @see QuickSearch::getPreviousResultByJumping()
* @see QuickSearch::getFirstResult()
* @see QuickSearch::getLastResult()
* @protected
*/
QuickSearch.prototype.getResultByIndex = function(index) {
  var result = this.getResultsList().get(index);

  return result ? jQuery(result) : result;
};

/**
 * Activate the given result and deactive the currently active result, if any.
 * @param jQuery result result
 * @protected
 */
QuickSearch.prototype.activateResult = function(result) {
  var activeResult;

  // don't activate the result if it isn't valid
  if (!result) {
    return;
  }

  activeResult = this.getActiveResult();

  // deactivate the active result, if any
  if (activeResult) {
    activeResult.removeClass("quicksearch-quicksearch-active");
  }

  // activate the new result and make it visible if necessary
  result.addClass("quicksearch-quicksearch-active");
  this.makeItemVisible(result);
};

/**
 * Format a string to be inserted into a search result. This strips HTML from
 * the text, optionally truncates the text, and then highlights search terms
 * within the text.
 * @param string text string to format
 * @param string searchTerms search terms
 * @param int truncate (optional) max length of the string before highlighting
 *        search terms in it
 * @protected
 */
QuickSearch.prototype.formatText = function(text, searchTerms, truncate) {
  var formattedText = StripHtmlFromString(text);

  // truncate the string if necessary
  if (truncate) {
    formattedText = TruncateString(formattedText);
  }

  return HighlightSearchTermsInText(searchTerms, formattedText);
};

/**
 * Generate a message for no search results for use in returning in a search
 * listener callback.
 * @return the no results message section
 * @protected
 */
QuickSearch.prototype.getMessageForNoSearchResults = function(value) {
  var escapedValue = EscapeHtmlEntitiesInString(value);

  return "<div class='quicksearch-quicksearch-message'>" +
           "No search results were found for <i>"+escapedValue+"</i>." +
         "</div>";
};

/**
 * The unique identifier for the constructed object. This is used to create
 * unique event signatures for the focus and blur event listeners on a
 * per-object basis.
 * @var string _identifier
 * @protected
 */
QuickSearch.prototype._identifier;

/**
 * Elements that trigger the focus and blur events that are used to activate and
 * deactivate quick searching.
 * @var jQuery _triggerElements
 * @protected
 */
QuickSearch.prototype._triggerElements;

/**
 * Element that is currently triggering searching.
 * @var jQuery _currentTrigger
 * @protected
 */
QuickSearch.prototype._currentTrigger;

/**
 * List of search listener callbacks.
 * @var object _searchListeners
 * @protected
 */
QuickSearch.prototype._searchListeners;

/**
 * List of search result selection listener callbacks.
 * @var object _selectionListeners
 * @protected
 */
QuickSearch.prototype._selectionListeners;

/**
 * Key press stream used to determine when input is entered and for key
 * combinations.
 * @var KeyPressStream _keyPressStream
 * @protected
 */
QuickSearch.prototype._keyPressStream;

/**
 * Default action blocker used to prevent the browser from taking action when
 * certain keys are pressed.
 * @protected
 */
QuickSearch.prototype._defaultActionBlocker;

/**
 * Element used to contain the results of a quick search.
 * @var jQuery _container
 * @protected
 */
QuickSearch.prototype._container;

/**
 * The number of results to jump by when jumping forward or backward through
 * the list of results specific to this object.
 * @var int _jumpCount
 * @protected
 */
QuickSearch.prototype._jumpCount;

/**
 * Whether or not the user has moused over/into the container. This is primarily
 * used to keep the results displayed when a trigger element loses focus.
 * @protected
 */
QuickSearch._mousedInContainer;

/**
 * Holds the last value entered into the trigger element and is used to
 * determine if the value of the trigger has changed.
 * @var string lastValue
 * @protected
 */
QuickSearch._lastValue;

/**
 * Holds the value from setTimeout() and is used to set and clear timeouts for
 * timeout-based search triggering.
 * @var timeout _triggerValueChangeTimeout
 * @protected
 */
QuickSearch.prototype._triggerValueChangeTimeout;

// -----------------------------------------------------------------------------

/**
 * Quick searching functionality that uses a RemoteData object as its data
 * source.
 * @see QuickSearch::addTriggerElements()
 * @see QuickSearch::addSearchListener()
 * @see QuickSearch::addSelectionListener()
 */
function RemoteDataQuickSearch() {
  RemoteDataQuickSearch.base.call(this);

  // finish initialization
  this.initSearchListeners();
  this.initRemoteData();

  // initialize data members
  this._loadingCount = 0;
  this._lastContentPopulationTime = 0;
  this._seqNo = 0;
  this._highestReturnedSeqNo = 0;
} cw.extend(RemoteDataQuickSearch, QuickSearch);

/**
 * Extend the QuickSearch::keyPressStreamListener() method to also clear
 * previous results when the key pressed isn't a character key.
 * @param KeyCodeSequence keyCodeSequence key code sequence
 * @see QuickSearch::keyPressStreamListener()
 * @protected
 */
RemoteDataQuickSearch.prototype.keyPressStreamListener = function(keyCodeSequence) {
  var value;

  // do nothing if the element no longer has focus. this is to prevent the rest
  // of this method from executing when a key is used to switch focus to
  // something else, including, but not limited to, when the TAB key is pressed
  if (!this._currentTrigger || !this._currentTrigger.is(":focus")) {
    return;
  }

  value = this.getElementValue(this._currentTrigger);

  // remove loading styles
  if (!TrimString(value).length) {
    this.removeLoadingStyles(this._currentTrigger);
  }

  // reset blocking if the value has changed
  if (this._lastValue != value) {
    this._blockLatentContentPopulation = false;
  }

  RemoteDataQuickSearch.base.prototype.keyPressStreamListener.call(this, keyCodeSequence);
};

/**
 * Initialize the RemoteData object.
 * @protected
 */
RemoteDataQuickSearch.prototype.initRemoteData = function() {
  this._remoteData = new RemoteData();
};

/**
 * Initialize search listener callbacks.
 * @see QuickSearch::addSearchListener()
 * @protected
 */
RemoteDataQuickSearch.prototype.initSearchListeners = function() {
  var listenerData;

  // add a listener that will fetch data when a search is triggered
  listenerData = this.addSearchListener(cw.bindObjectMethodCallback(
    this, this.searchListener));

  // save the placeholder
  this._placeholder = listenerData.placeholder;
};

/**
 * Callback function for a trigger element that loses focus.
 * @param object event standard JavaScript event object
 * @see QuickSearch::blurListener()
 * @protected
 */
RemoteDataQuickSearch.prototype.blurListener = function(event) {
  // make sure loading styles are removed when moving away from the element
  this.removeLoadingStylesIfFullyLoaded(jQuery(event.target));

  RemoteDataQuickSearch.base.prototype.blurListener.call(this, event);
};

/**
 * Deactivate the current trigger.
 * @protected
 */
RemoteDataQuickSearch.prototype.deactivateTrigger = function() {
  this._blockLatentContentPopulation = false;

  // only if the trigger hasn't already been deactivated
  if (this._currentTrigger) {
    this.removeLoadingStylesIfFullyLoaded(this._currentTrigger);
  }

  RemoteDataQuickSearch.base.prototype.deactivateTrigger.call(this);
};

/**
 * Listener callback executed whenever a search is triggered.
 * @protected
 */
RemoteDataQuickSearch.prototype.searchListener = function() {
  var value = this.getElementValue(this._currentTrigger);

  // show nothing if there are no search terms
  if (!TrimString(value).length) {
    this.clearPlaceholder();
    this.removeLoadingStyles(this._currentTrigger);
    return;
  }

  // the data has already been fetched before, so add the results to the
  // placeholder after they've been formatted
  if (this._remoteData.isDataCached(value)) {
    // save the latest content population time
    this.setLastPopulationTime();

    // add the formatted search results to the placeholder
    this.setPlaceholderContent(this.formatSearchResults(
      value, this._remoteData.getData(value)));

    // remove loading styles even if there are callbacks that haven't returned
    // because the results for this query have been found
    this.removeLoadingStyles(this._currentTrigger);
  }

  // the data needs to be fetched
  else {
    // there are no previous results, so use a placeholder message
    if (!this.placeholderHasResults()) {
      this.setPlaceholderContent(this.generateSearchMessage());
    }

    // add loading styles
    this.addLoadingStyles(this._currentTrigger);

    // fetch the data
    this._remoteData.getData(value, cw.bindObjectMethodCallback(
      this, this.remoteDataCallback, value, this._currentTrigger, this._seqNo));

    // increment the sequence number
    this._seqNo += 1;
  }
};

/**
* Clear the contents of the placeholder.
* @protected
*/
RemoteDataQuickSearch.prototype.clearPlaceholder = function() {
  this._placeholder.empty();
};

/**
* Add some content to the placeholder.
* @param jQuery|string content content to add to the placeholder
* @protected
*/
RemoteDataQuickSearch.prototype.addContentToPlaceholder = function(content) {
  this._placeholder.append(content);
};

/**
* Set the content of the placeholder to the given content, clearing any
* existing content.
* @param jQuery|string content content to add to the placeholder
* @protected
*/
RemoteDataQuickSearch.prototype.setPlaceholderContent = function(content) {
  this.clearPlaceholder();
  this.addContentToPlaceholder(content);
};

/**
* Determine if the placeholder contains any results.
* @return Returns true if the placeholder contains at least one result.
* @protected
*/
RemoteDataQuickSearch.prototype.placeholderHasResults = function(content) {
  return this._placeholder.has(".quicksearch-quicksearch-result").length > 0;
};

/**
 * Callback used when fetching remote data.
 * @param string value search string
 * @param string key search string
 * @param object data remote data
 * @protected
 */
RemoteDataQuickSearch.prototype.remoteDataCallback = function(value, trigger, seqNo, key, data) {
  var currentValue = this.getElementValue(trigger);

  // ignore results from lower sequence numbers
  if (seqNo < this._highestReturnedSeqNo) {
    return;
  }

  // updated the highest returned sequence number
  this._highestReturnedSeqNo = seqNo;

  // return if the current trigger is no longer available, which might happen
  // if the trigger loses focus before the remote data becomes available
  if (!this._currentTrigger) {
    return;
  }

  // return if the trigger has changed
  if (!this._currentTrigger.is(trigger)) {
    return;
  }

  // only replace the data if the user hasn't started to navigate through the
  // results
  if (!this._blockLatentContentPopulation) {
    // if the results are for the current search string
    if (currentValue == value || this.timeSinceLastPopulation() > 700) {
      this.setLastPopulationTime();
      this.setPlaceholderContent(this.formatSearchResults(value, data));
      this.showContainer();
    }
  }

  // remove loading styles even if there are callbacks that haven't returned if
  // displaying the results for the current search
  if (this.getElementValue(trigger) == value) {
    this.removeLoadingStyles(this._currentTrigger);
  }

  // otherwise, only remove the loading styles from the trigger if everything
  // has fully loaded
  else {
    this.removeLoadingStylesIfFullyLoaded(this._currentTrigger);
  }
};

/**
 * This is a stub method and should be overridden in subclasses.
 * @param string value search string
 * @param object data search results data
 */
RemoteDataQuickSearch.prototype.formatSearchResults = function(value, data) {
  return this.generateMessageForError("The formatSearchResults method has not been extended.");
};

/**
 * Generate an error section using the given error message for use in returning
 * in a search listener callback.
 * @param string message error message
 * @return the error section
 * @see RemoteDataQuickSearch::generateMessageForNoSearchResults()
 * @protected
 */
RemoteDataQuickSearch.prototype.generateMessageForError = function(message) {
  return "<div class='quicksearch-quicksearch-message quicksearch-quicksearch-message-error'>" +
           StripHtmlFromString(message) +
         "</div>";
};

/**
* Generate a message for what is currently being searched for.
* @return the message HTML
* @see RemoteDataQuickSearch::generateMessageForError()
* @see RemoteDataQuickSearch::generateMessageForNoSearchResults()
* @protected
*/
RemoteDataQuickSearch.prototype.generateSearchMessage = function() {
  return "<div class='quicksearch-quicksearch-message quicksearch-remotedataquicksearch-placeholder'>" +
           "Searching for <i>"+StripHtmlFromString(this.getElementValue(this._currentTrigger))+"</i>." +
         "</div>";
};

/**
 * Add loading styles to a trigger element.
 * @param jQuery trigger element that triggered searching
 * @protected
 */
RemoteDataQuickSearch.prototype.addLoadingStyles = function(trigger) {
  // increment how many remote data calls are loading
  this._loadingCount += 1;

  // add the styles
  trigger.addClass("quicksearch-remotedataquicksearch-loading");
};

/**
 * Remove loading styles if there are no more remote data calls loading.
 * @param jQuery trigger element that triggered searching
 * @protected
 */
RemoteDataQuickSearch.prototype.removeLoadingStylesIfFullyLoaded = function(trigger) {
  // prevent the loading count from going below zero. this is done becase this
  // method is potentially called more times than addLoadingStyles() because we
  // can't know if addLoadingStyles() was called because of some methods are
  // called asynchronously
  if (this._loadingCount > 0) {
    this._loadingCount -= 1;
  }

  // only remove the style if it's the last item to load
  if (this._loadingCount === 0) {
    this.removeLoadingStyles(trigger);
  }
};

/**
* Remove loading styles from the trigger.
* @param jQuery trigger element that triggered searching
* @protected
*/
RemoteDataQuickSearch.prototype.removeLoadingStyles = function(trigger) {
  this._loadingCount = 0;
  trigger.removeClass("quicksearch-remotedataquicksearch-loading");
};

/**
 * Listener callback for key code sequences that should go to the previous item.
 * @param jQuery trigger the element that triggered the sequence
 * @see RemoteDataQuickSearch::initKeyPressStream()
 * @see RemoteDataQuickSearch::goToNextItem()
 * @see RemoteDataQuickSearch::goToBeginningOfList()
 * @see RemoteDataQuickSearch::goToEndOfList()
 * @see RemoteDataQuickSearch::jumpBackwardThroughList()
 * @see RemoteDataQuickSearch::jumpForwardThroughList()
 * @protected
 */
RemoteDataQuickSearch.prototype.goToPreviousItem = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.goToPreviousItem.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should go to the next item.
 * @param jQuery trigger the element that triggered the sequence
 * @see RemoteDataQuickSearch::initKeyPressStream()
 * @see RemoteDataQuickSearch::goToPreviousItem()
 * @see RemoteDataQuickSearch::goToBeginningOfList()
 * @see RemoteDataQuickSearch::goToEndOfList()
 * @see RemoteDataQuickSearch::jumpBackwardThroughList()
 * @see RemoteDataQuickSearch::jumpForwardThroughList()
 * @protected
 */
RemoteDataQuickSearch.prototype.goToNextItem = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.goToNextItem.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should go to the beginning of
 * the list.
 * @param jQuery trigger the element that triggered the sequence
 * @see RemoteDataQuickSearch::initKeyPressStream()
 * @see RemoteDataQuickSearch::goToNextItem()
 * @see RemoteDataQuickSearch::goToPreviousItem()
 * @see RemoteDataQuickSearch::goToEndOfList()
 * @see RemoteDataQuickSearch::jumpBackwardThroughList()
 * @see RemoteDataQuickSearch::jumpForwardThroughList()
 * @protected
 */
RemoteDataQuickSearch.prototype.goToBeginningOfList = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.goToBeginningOfList.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should go to the end of the
 * list.
 * @param jQuery trigger the element that triggered the sequence
 * @see RemoteDataQuickSearch::initKeyPressStream()
 * @see RemoteDataQuickSearch::goToNextItem()
 * @see RemoteDataQuickSearch::goToPreviousItem()
 * @see RemoteDataQuickSearch::goToBeginningOfList()
 * @see RemoteDataQuickSearch::jumpBackwardThroughList()
 * @see RemoteDataQuickSearch::jumpForwardThroughList()
 * @protected
 */
RemoteDataQuickSearch.prototype.goToEndOfList = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.goToEndOfList.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should jump backward through
 * the list.
 * @param jQuery trigger the element that triggered the sequence
 * @see RemoteDataQuickSearch::initKeyPressStream()
 * @see RemoteDataQuickSearch::goToNextItem()
 * @see RemoteDataQuickSearch::goToPreviousItem()
 * @see RemoteDataQuickSearch::goToBeginningOfList()
 * @see RemoteDataQuickSearch::goToEndOfList()
 * @see RemoteDataQuickSearch::jumpForwardThroughList()
 * @protected
 */
RemoteDataQuickSearch.prototype.jumpBackwardThroughList = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.jumpBackwardThroughList.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should jump backward through
 * the list.
 * @param jQuery trigger the element that triggered the sequence
 * @see RemoteDataQuickSearch::initKeyPressStream()
 * @see RemoteDataQuickSearch::goToNextItem()
 * @see RemoteDataQuickSearch::goToPreviousItem()
 * @see RemoteDataQuickSearch::goToBeginningOfList()
 * @see RemoteDataQuickSearch::goToEndOfList()
 * @see RemoteDataQuickSearch::jumpBackwardThroughList()
 * @protected
 */
RemoteDataQuickSearch.prototype.jumpForwardThroughList = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.jumpForwardThroughList.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should select a result.
 * @param jQuery trigger the element that triggered the sequence
 * @protected
 */
RemoteDataQuickSearch.prototype.makeSelection = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.makeSelection.call(this, trigger);
};

/**
 * Listener callback for key code sequences that should recenter the window on
 * the active result.
 * @param jQuery trigger the element that triggered the sequence
 * @protected
 */
RemoteDataQuickSearch.prototype.recenterOnActiveResult = function(trigger) {
  // block latent content population when the user starts navigating the list so
  // that the resources displayed don't get replaced after a long load
  this._blockLatentContentPopulation = true;

  RemoteDataQuickSearch.base.prototype.recenterOnActiveResult.call(this, trigger);
};

/**
* Set the last time content population took place.
* @protected
*/
RemoteDataQuickSearch.prototype.setLastPopulationTime = function() {
  this._lastContentPopulationTime = new Date().getTime();
};

/**
* Get the time in milliseconds since the last time content population took
* place.
* @return the time since the last time content population took place
* @protected
*/
RemoteDataQuickSearch.prototype.timeSinceLastPopulation = function() {
  var now = new Date().getTime();

  return now - this._lastContentPopulationTime;
};

/**
 * Holds the RemoteData object for this particular object.
 * @var RemoteData _remoteData
 * @protected
 */
RemoteDataQuickSearch.prototype._remoteData;

/**
* Holds the placeholder to populate with content.
* @var jQuery _placeholder
* @protected
*/
RemoteDataQuickSearch.prototype._placeholder;

/**
 * Determines whether or not to block content population that is happening after
 * the initial search. This is used so that the container doesn't change when
 * results become available after a user has begun navigating through the list.
 * @var bool _blockLatentContentPopulation
 * @protected
 */
RemoteDataQuickSearch.prototype._blockLatentContentPopulation;

/**
 * Used to determine if there are still remote data calls loading.
 * @var int _loadingCount
 * @protected
 */
RemoteDataQuickSearch.prototype._loadingCount;

/**
* The last time (millisecond timestamp) since the container was populated with
* results from the object.
* @var int _lastContentPopulationTime
* @protected
*/
RemoteDataQuickSearch.prototype._lastContentPopulationTime;

/**
* The current sequence number used to make use out-of-date results aren't used.
* @var int _seqNo
* @protected
*/
RemoteDataQuickSearch.prototype._seqNo;

/**
* The highest returned sequence number from the server used to make use
* out-of-date results aren't used.
* @var int _highestReturnedSeqNo
* @protected
*/
RemoteDataQuickSearch.prototype._highestReturnedSeqNo;

// -----------------------------------------------------------------------------

/**
 * Quick searching functionality for resource.
 * @see QuickSearch::addTriggerElements()
 */
function ResourceQuickSearch() {
  ResourceQuickSearch.base.call(this);

  // finish initialization
  this.initSelectionListeners();
  this.setJumpCount(2);
} cw.extend(ResourceQuickSearch, RemoteDataQuickSearch);

/**
 * Default maximum number of search results to display.
 * @var int DefaultMaxNumSearchResults
 */
ResourceQuickSearch.DefaultMaxNumSearchResults = 5;

/**
 * Get the maximum number of search results to display.
 * @return the maximum number of search results to display
 */
ResourceQuickSearch.prototype.getMaxNumSearchResults = function() {
  return this._remoteData.getMaxNumSearchResults();
};

/**
 * Set the maximum number of search results to display.
 * @param int maxNumSearchResults maximum number of search results to display
 */
ResourceQuickSearch.prototype.setMaxNumSearchResults = function(maxNumSearchResults) {
  this._remoteData.setMaxNumSearchResults(maxNumSearchResults);
};

/**
 * Override RemoteDataQuickSearch::initRemoteData() to use a keyword searching
 * remote data object.
 * @protected
 */
ResourceQuickSearch.prototype.initRemoteData = function() {
  this._remoteData = new ResourceKeywordSearchData();
  this.setMaxNumSearchResults(ResourceQuickSearch.DefaultMaxNumSearchResults);
};

/**
 * Initialize listeners that are executed whenever a result is selected via the
 * keyboard.
 * @protected
 */
ResourceQuickSearch.prototype.initSelectionListeners = function() {
  this.addSelectionListener(cw.bindObjectMethodCallback(this, this.resultSelected));
};

/**
 * Listener callback executed when a result is selected.
 * @param jQuery result selected result
 * @protected
 */
ResourceQuickSearch.prototype.resultSelected = function(result) {
  // go the resource full record page if a result is selected
  if (result) {
    window.location = jQuery("a", result).attr("href");
  }

  // otherwise, trigger the first form the trigger belongs to
  else {
    this._currentTrigger.closest("form").submit();
  }
};

/**
 * Format search results for adding to the results container.
 * @param string value search string
 * @param object data search results data
 */
ResourceQuickSearch.prototype.formatSearchResults = function(value, data) {
  var searchResults, numSearchResults, numAdditionalSearchResults, listItem;
  var anchor, subtitle, formattedResults;

  // there was an error returned by the server
  if (data.status.state == "ERROR") {
    return this.generateMessageForError(data.status.message);
  }

  // extract some data
  searchResults = data.data.SearchResults;
  numSearchResults = data.data.NumSearchResults;
  numAdditionalSearchResults = data.data.NumAdditionalSearchResults;

  // no search results for the value, so use a generic message
  if (numSearchResults < 1) {
    return this.getMessageForNoSearchResults(value);
  }

  // create the list
  formattedResults = jQuery("<ul/>");

  // add classes to the list
  formattedResults.addClass("cw-list cw-list-unmarked cw-list-dematte");
  formattedResults.addClass("quicksearch-resourcequicksearch-results");

  for (var i in searchResults) {
    // start with a list item to wrap the link
    listItem = jQuery("<li/>");
    listItem.addClass("quicksearch-quicksearch-result");
    listItem.attr("data-result-id", searchResults[i]["_Id"]);
    listItem.attr("data-result-title", searchResults[i]["_Title"]);
    listItem.attr("data-result-uri", searchResults[i]["_Uri"]);

    // then create the link to the resource
    anchor = jQuery("<a/>");
    anchor.attr("href", searchResults[i]["_URI"]);
    anchor.html(this.formatText(searchResults[i]["_Title"], value));

    subtitle = "";

    // only add a subtitle if the width of the container is big enough so that
    // the content does not become crowded
    if (this._currentTrigger.outerWidth() > 175) {
      subtitle = jQuery("<small/>");
      subtitle.html(this.formatText(
        searchResults[i]["_Description"],
        value,
        120));
    }

    // at the item, link, and subtitle to the list
    formattedResults.append(listItem.append(anchor.append(subtitle)));
  }

  // add an additional message if there are more search results
  if (numAdditionalSearchResults > 0) {
    formattedResults = formattedResults.add(this.getMessageForMoreSearchResults(
      value, numAdditionalSearchResults));
  }

  return formattedResults;
};

/**
 * Generate a message for alerting the user that there are some additional
 * search results that were not displayed.
 * @param string searchingString search string
 * @param int numAdditionalSearchResults number of additional search results
 * @return message pertaining to more search results
 * @protected
 */
ResourceQuickSearch.prototype.getMessageForMoreSearchResults = function(searchString, numAdditionalSearchResults) {
  var escapedSearchString = EscapeHtmlEntitiesInString(searchString),
      formattedNumAdditionalSearchResults = FormatNumber(numAdditionalSearchResults),
      verb = numAdditionalSearchResults == 1 ? "is" : "are",
      plural = numAdditionalSearchResults == 1 ? "" : "s";

  return "<div class='quicksearch-quicksearch-message quicksearch-quicksearch-message-fancy quicksearch-resourcequicksearch-moreresults'>" +
           "There "+verb+" <b>"+formattedNumAdditionalSearchResults+" additional result"+plural+"</b>. " +
           "<a class='cw-button cw-button-elegant' href='"+RouterUrl+"?P=AdvancedSearch&amp;Q=Y&ampST=Quick&amp;FK="+escapedSearchString+"'>View All</a>" +
         "</div>";
};

// -----------------------------------------------------------------------------

/**
 * Quick searching functionality for metadata field values.
 */
function MetadataFieldQuickSearch() {
  MetadataFieldQuickSearch.base.apply(this, arguments);

  // finish initialization
  this.initSelectionListeners();
} cw.extend(MetadataFieldQuickSearch, RemoteDataQuickSearch);

/**
 * Add the given elements to the list of the elements that trigger searching.
 * @param jQuery elements elements that trigger searching
 * @see QuickSearch::removeTriggerElements()
 * @see QuickSearch::clearTriggerElements()
 */
MetadataFieldQuickSearch.prototype.addTriggerElements = function(elements) {
  var self = this;

  // pass them to the superclass method
  MetadataFieldQuickSearch.base.prototype.addTriggerElements.apply(this, arguments);

  // adjust the elements to their contents
  elements.doautoheight({"duration": 1});

  // make sure the elements adjust to their contents automatically. this part
  // cannot be undone when removing triggers
  elements.autoheight({
    "duration": 1,
    "onComplete": function(){
      // reposition the container when the height changes
      if (self._currentTrigger) {
        self.repositionOn(self._currentTrigger);
      }
    }
  });
};

/**
 * Get the maximum number of values to return per search.
 * @return the maximum number of values returned per search
 */
MetadataFieldQuickSearch.prototype.getMaxNumSearchResults = function() {
  return this._remoteData.getMaxNumSearchResults();
};

/**
 * Set the maximum number of values to return per search.
 * @param int maxNumSearchResults maximum number of values to return per search
 */
MetadataFieldQuickSearch.prototype.setMaxNumSearchResults = function(maxNumSearchResults) {
  this._remoteData.setMaxNumSearchResults(maxNumSearchResults);
};

/**
 * Override the default selection listener.
 * @protected
 */
MetadataFieldQuickSearch.prototype.initSelectionListeners = function() {
  this.addSelectionListener(cw.bindObjectMethodCallback(this, this.selectionListener));
};

/**
 * Callback function for a trigger element that receives focus.
 * @param object event standard JavaScript event object
 * @see QuickSearch::blurListener()
 * @protected
 */
MetadataFieldQuickSearch.prototype.focusListener = function(event) {
  var trigger = jQuery(event.target);

  // set the max results if available
  if (trigger.attr("data-maxnumsearchresults")) {
    this.setMaxNumSearchResults(trigger.attr("data-maxnumsearchresults"));
  }

  MetadataFieldQuickSearch.base.prototype.focusListener.apply(this, arguments);
};

/**
 * Callback function for a trigger element that is blurred.
 * @param object event standard JavaScript event object
 * @see QuickSearch::focusListener()
 * @protected
 */
MetadataFieldQuickSearch.prototype.blurListener = function(event) {
  var trigger = jQuery(event.target);

  // clear the input if blurred without a selection
  if (!trigger.prev().val()) {
    trigger.val("");
  }

  MetadataFieldQuickSearch.base.prototype.blurListener.apply(this, arguments);
};

/**
 * Listener callback executed whenever a search is triggered.
 * @protected
 */
MetadataFieldQuickSearch.prototype.searchListener = function() {
  var value = this.getElementValue(this._currentTrigger);

  // show instructions if there isn't any input
  if (!TrimString(value).length) {
    this.setPlaceholderContent(this.getInstructionsMessage());
    return;
  }

  // use the superclass method
  MetadataFieldQuickSearch.base.prototype.searchListener.apply(this, arguments);

  // if it's the first search, fill the generic placeholder with instructions
  if (value.length == 1 && this._placeholder.has(".quicksearch-remotedataquicksearch-placeholder").length) {
    this.setPlaceholderContent(this.getInstructionsMessage());
  }
};

/**
 * The listener callback executed when a selection is made.
 * @param jQuery result the selected result
 * @protected
 */
MetadataFieldQuickSearch.prototype.selectionListener = function(result) {
  var input, textarea, self;

  // no active result selected, so return
  if (!result) {
    return;
  }

  self = this;

  // set the item ID and title
  this._currentTrigger.prev().val(result.attr("data-result-id"));
  this._currentTrigger.val(StripHtmlFromString(result.attr("data-result-title")));

  // hide the container
  this.hideContainer();
  this.revertTriggerOutlineFix();

  // if it's the last reference field in the group, then create a new field
  if (!this._currentTrigger.nextAll(".cw-resourceeditor-metadatafield").length) {
    // create the new input elements
    input = this.createNewInput(this._currentTrigger.prev());
    textarea = this.createNewTextarea(this._currentTrigger);

    // add them after the current trigger
    this._currentTrigger.after(input);
    input.after(textarea);

    // add the textarea to the list of trigger elements
    this.addTriggerElements(textarea);

    // reposition on the textarea
    this.repositionOn(textarea);

    // automatically trigger the blur and focus functions for both the new and
    // old trigger
    this._currentTrigger.blur();
    textarea.focus();

    // make the textarea visible
    this.makeItemVisibleAndCentered(textarea);
  }

  // make sure the trigger is visible and blur it
  else {
    this.makeItemVisibleAndCentered(this._currentTrigger);
    this._currentTrigger.blur();
  }
};

/**
 * Override the superclass method to clear the item ID input value when the
 * trigger value changes.
 * @param KeyCodeSequence keyCodeSequence key code sequence
 * @protected
 */
MetadataFieldQuickSearch.prototype.keyPressStreamListener = function(keyCodeSequence) {
  var firstKeyCode = keyCodeSequence.getKeyCodeAt(0);

  // do nothing if the element no longer has focus. this is to prevent the rest
  // of this method from executing when a key is used to switch focus to
  // something else, including, but not limited to, when the TAB key is pressed
  if (!this._currentTrigger || !this._currentTrigger.is(":focus")) {
    return;
  }

  // clear the resource ID input if the value changes
  if (KeyCodes.isCharacterKey(firstKeyCode) ||
      (KeyCodes.isRemovalKey(firstKeyCode) &&
       this._lastValue != this.getElementValue(this._currentTrigger))) {
    this._currentTrigger.prev().val("");
  }

  MetadataFieldQuickSearch.base.prototype.keyPressStreamListener.apply(this, arguments);
};

/**
 * Create a new item ID input element.
 * @param jQuery clone the item the clone properties from
 * @return the item ID input element (jQuery)
 * @protected
 */
MetadataFieldQuickSearch.prototype.createNewInput = function(clone) {
  return jQuery("<input/>").attr({"type": "hidden", "name": clone.attr("name")});
};

/**
 * Create a new item title input element.
 * @param jQuery clone the item the clone properties from
 * @return the item title input element (jQuery)
 * @protected
 */
MetadataFieldQuickSearch.prototype.createNewTextarea = function(clone) {
  return jQuery("<textarea/>").addClass("cw-resourceeditor-metadatafield").attr({
    "data-maxnumsearchresults": clone.attr("data-maxnumsearchresults")
  });
};

/**
 * Override the superclass method to automatically activate the first result
 * whenever it's available.
 * @param array listeners listener callbacks
 * @protected
 */
MetadataFieldQuickSearch.prototype.populateContainerFromListeners = function(listeners) {
  MetadataFieldQuickSearch.base.prototype.populateContainerFromListeners.apply(this, arguments);

  // activate the first result if there isn't an already active result
  if (!this.getActiveResult()) {
    this.activateResult(this.getFirstResult());
  }
};

/**
 * Override the superclass method to automatically activate the first result
 * whenever it's available.
 * @param string value search string
 * @param string key search string
 * @param object data remote data
 * @protected
 */
MetadataFieldQuickSearch.prototype.remoteDataCallback = function(value, trigger, seqNo, key, data) {
  MetadataFieldQuickSearch.base.prototype.remoteDataCallback.apply(this, arguments);

  // activate the first result if there isn't an already active result
  if (!this.getActiveResult()) {
    this.activateResult(this.getFirstResult());
  }
};

/**
 * Generate a message giving instructions to the user.
 * @return instructions message
 * @protected
 */
MetadataFieldQuickSearch.prototype.getInstructionsMessage = function() {
  return "<div class='quicksearch-quicksearch-message'>" +
           "Type some keywords to begin searching." +
         "</div>";
};

/**
 * Generate a message for alerting the user that there are some additional
 * search results that were not displayed.
 * @param string value search string
 * @param int numAdditionalSearchResults number of additional search results
 * @return message pertaining to more search results
 * @protected
 */
MetadataFieldQuickSearch.prototype.getMessageForMoreSearchResults = function(value, numAdditionalSearchResults) {
  var formattedNumAdditionalSearchResults = FormatNumber(numAdditionalSearchResults),
      verb = numAdditionalSearchResults == 1 ? "is" : "are",
      plural = numAdditionalSearchResults == 1 ? "" : "s";

  return "<div class='quicksearch-quicksearch-message quicksearch-quicksearch-message-fancy quicksearch-metadatafieldquicksearch-moreresults'>" +
           "There "+verb+" <b>"+formattedNumAdditionalSearchResults+" additional result"+plural+"</b> that "+verb+" not displayed. " +
           "Add additional search terms if you do not see what you're trying to find." +
         "</div>";
};

// -----------------------------------------------------------------------------

/**
 * Quick searching functionality for resource reference fields.
 * @see QuickSearch::addTriggerElements()
 */
function ReferenceFieldQuickSearch() {
  ReferenceFieldQuickSearch.base.call(this);
} cw.extend(ReferenceFieldQuickSearch, MetadataFieldQuickSearch);

/**
* Get the ID of the field from which to fetch values.
* @return the ID of the field from which to fetch values
*/
ReferenceFieldQuickSearch.prototype.getFieldId = function() {
  return this._remoteData.getFieldId();
};

/**
* Set the ID of the field from which to fetch values.
* @param int fieldId ID of the field from which to fetch values
*/
ReferenceFieldQuickSearch.prototype.setFieldId = function(fieldId) {
  this._remoteData.setFieldId(fieldId);
};

/**
* Callback function for a trigger element that receives focus.
* @param object event standard JavaScript event object
* @see QuickSearch::blurListener()
* @protected
*/
ReferenceFieldQuickSearch.prototype.focusListener = function(event) {
  var trigger = jQuery(event.target);

  // set the field ID if set
  if (trigger.attr("data-fieldid")) {
    this.setFieldId(trigger.attr("data-fieldid"));
  }

  ReferenceFieldQuickSearch.base.prototype.focusListener.apply(this, arguments);
};

/**
 * Override ResourceQuickSearch::initRemoteData() to set a different default
 * maximum number of search results.
 * @protected
 */
ReferenceFieldQuickSearch.prototype.initRemoteData = function() {
  this._remoteData = new ResourceKeywordSearchData();
};

/**
 * Format search results for adding to the results container.
 * @param string value search string
 * @param object data search results data
 */
ReferenceFieldQuickSearch.prototype.formatSearchResults = function(value, data) {
  var results = jQuery(ResourceQuickSearch.prototype.formatSearchResults.apply(this, arguments)),
      links = jQuery(".quicksearch-quicksearch-result a", results),
      self = this;

  // make the results do the select action instead of going to a resource
  links.off("click");
  links.click(function(){
    self.activateResult(jQuery(this).parent());
    self.makeSelection(self._currentTrigger);
    return false;
  });

  return results;
};

// -----------------------------------------------------------------------------

/**
 * Quick searching functionality for resource.
 * @see QuickSearch::addTriggerElements()
 */
function MultipleValuesFieldQuickSearch() {
  MultipleValuesFieldQuickSearch.base.apply(this, arguments);
} cw.extend(MultipleValuesFieldQuickSearch, MetadataFieldQuickSearch);

/**
 * Override RemoteDataQuickSearch::initRemoteData() to use a remote data object
 * that fetches data from metadata fields with multiple values, like controlled
 * names, classifications, etc.
 * @protected
 */
MultipleValuesFieldQuickSearch.prototype.initRemoteData = function() {
  this._remoteData = new MultipleValuesFieldData();
};

/**
 * Get the ID of the field from which to fetch values.
 * @return the ID of the field from which to fetch values
 */
MultipleValuesFieldQuickSearch.prototype.getFieldId = function() {
  return this._remoteData.getFieldId();
};

/**
 * Set the ID of the field from which to fetch values.
 * @param int fieldId ID of the field from which to fetch values
 */
MultipleValuesFieldQuickSearch.prototype.setFieldId = function(fieldId) {
  this._remoteData.setFieldId(fieldId);
};

/**
 * Callback function for a trigger element that receives focus.
 * @param object event standard JavaScript event object
 * @see QuickSearch::blurListener()
 * @protected
 */
MultipleValuesFieldQuickSearch.prototype.focusListener = function(event) {
  var trigger = jQuery(event.target);

  // set the field ID if set
  if (trigger.attr("data-fieldid")) {
    this.setFieldId(trigger.attr("data-fieldid"));
  }

  MultipleValuesFieldQuickSearch.base.prototype.focusListener.apply(this, arguments);
};

/**
 * Format search results for adding to the results container.
 * @param string value search string
 * @param object data search results data
 */
MultipleValuesFieldQuickSearch.prototype.formatSearchResults = function(value, data) {
  var searchResults, numSearchResults, numAdditionalSearchResults, listItem;
  var anchor, formattedResults;
  var self = this;

  // there was an error returned by the server
  if (data.status.state == "ERROR") {
    return this.generateMessageForError(data.status.message);
  }

  // extract some data
  searchResults = data.data.SearchResults;
  numSearchResults = data.data.NumSearchResults;
  numAdditionalSearchResults = data.data.NumAdditionalSearchResults;

  // no search results for the value so use a generic message
  if (numSearchResults < 1) {
    return this.getMessageForNoSearchResults(value);
  }

  // create the list
  formattedResults = jQuery("<ul/>");

  // add classes to the list
  formattedResults.addClass("cw-list cw-list-unmarked cw-list-dematte");
  formattedResults.addClass("quicksearch-multiplevaluesfieldquicksearch-results");

  for (var i in searchResults) {
    // start with a list item to wrap the link
    listItem = jQuery("<li/>");
    listItem.addClass("quicksearch-quicksearch-result");
    listItem.attr("data-result-id", i);
    listItem.attr("data-result-title", searchResults[i]);

    // then create the link to the resource
    anchor = jQuery("<a/>");
    anchor.attr("href", this.getSearchUrlForValue(searchResults[i]));
    anchor.html(this.formatText(searchResults[i], value));
    anchor.click(function(){
      self.activateResult(jQuery(this).parent());
      self.makeSelection(self._currentTrigger);
      return false;
    });

    // at the item, link, and subtitle to the list
    formattedResults.append(listItem.append(anchor));
  }

  // add an additional message if there are more search results
  if (numAdditionalSearchResults > 0) {
    formattedResults = formattedResults.add(this.getMessageForMoreSearchResults(
      value, numAdditionalSearchResults));
  }

  return formattedResults;
};

/**
* Get the search URL for a field that is set to a value.
* @param string value Value to use in the search URL.
* @return Returns the search URL for a field that is set to a value.
* @protected
*/
MultipleValuesFieldQuickSearch.prototype.getSearchUrlForValue = function(value) {
  return RouterUrl + "?P=AdvancedSearch&Q=Y&F" + escape(this.getFieldId()) +
    "=" + escape("="+value);
};

/**
 * Create a new item ID input element.
 * @param jQuery clone the item the clone properties from
 * @return the item ID input element (jQuery)
 * @protected
 */
MultipleValuesFieldQuickSearch.prototype.createNewInput = function(clone) {
  return jQuery("<input/>").attr({
    "type": "hidden",
    "name": clone.attr("name"),
    "data-fieldid": clone.attr("data-fieldid")
  });
};

/**
 * Create a new item title input element.
 * @return the item title input element (jQuery)
 * @protected
 */
MultipleValuesFieldQuickSearch.prototype.createNewTextarea = function(clone) {
  return jQuery("<textarea/>").addClass("cw-resourceeditor-metadatafield").attr({
    "data-maxnumsearchresults": clone.attr("data-maxnumsearchresults"),
    "data-fieldid": clone.attr("data-fieldid")
  });
};

// -----------------------------------------------------------------------------

/**
 * Quick searching functionality for resource reference fields.
 * @see QuickSearch::addTriggerElements()
 */
function UserFieldQuickSearch() {
  UserFieldQuickSearch.base.call(this);
} cw.extend(UserFieldQuickSearch, MetadataFieldQuickSearch);

/**
 * Override MetadataFieldQuickSearch::initRemoteData() to use a different remote
 * data object.
 * @protected
 */
UserFieldQuickSearch.prototype.initRemoteData = function() {
  this._remoteData = new UserData();
};

/**
 * The listener callback executed when a selection is made.
 * @param jQuery result the selected result
 * @protected
 */
UserFieldQuickSearch.prototype.selectionListener = function(result) {
  var input, textarea, self;

  // no active result selected, so return
  if (!result) {
    return;
  }

  // set the item ID and title
  this._currentTrigger.val(StripHtmlFromString(result.attr("data-result-title")));

  // hide the container
  this.hideContainer();
  this.revertTriggerOutlineFix();

  // make sure the trigger is visible and blur it
  this.makeItemVisibleAndCentered(this._currentTrigger);
  this._currentTrigger.blur();
};

/**
 * Format search results for adding to the results container.
 * @param string value search string
 * @param object data search results data
 */
UserFieldQuickSearch.prototype.formatSearchResults = function(value, data) {
  var searchResults, numSearchResults, listItem, anchor, formattedResults;
  var self = this;

  // there was an error returned by the server
  if (data.status.state == "ERROR") {
    return this.generateMessageForError(data.status.message);
  }

  // extract some data
  searchResults = data.data.SearchResults;
  numSearchResults = data.data.NumSearchResults;

  // no search results for the value so use a generic message
  if (numSearchResults < 1) {
    return this.getMessageForNoSearchResults(value);
  }

  // create the list
  formattedResults = jQuery("<ul/>");

  // add classes to the list
  formattedResults.addClass("cw-list cw-list-unmarked cw-list-dematte");
  formattedResults.addClass("quicksearch-multiplevaluesfieldquicksearch-results");

  for (var i in searchResults) {
    // start with a list item to wrap the link
    listItem = jQuery("<li/>");
    listItem.addClass("quicksearch-quicksearch-result");
    listItem.attr("data-result-id", i);
    listItem.attr("data-result-title", searchResults[i]);

    // then create the link to the resource
    anchor = jQuery("<a/>");
    anchor.attr("href", this.getUrlForUser(i));
    anchor.html(this.formatText(searchResults[i], value));
    anchor.click(function(){
      self.activateResult(jQuery(this).parent());
      self.makeSelection(self._currentTrigger);
      return false;
    });

    // at the item, link, and subtitle to the list
    formattedResults.append(listItem.append(anchor));
  }

  return formattedResults;
};

/**
* Get the URL for the user with the given ID.
* @param int userId User ID to use in the URL.
* @return Returns the URL for the user with the given ID.
* @protected
*/
UserFieldQuickSearch.prototype.getUrlForUser = function(userId) {
  return RouterUrl + "?P=EditUser&ID=" + escape(userId);
};

// -----------------------------------------------------------------------------

// exported items
this.QuickSearch = QuickSearch;
this.RemoteDataQuickSearch = RemoteDataQuickSearch;
this.ResourceQuickSearch = ResourceQuickSearch;
this.MetadataFieldQuickSearch = MetadataFieldQuickSearch;
this.ReferenceFieldQuickSearch = ReferenceFieldQuickSearch;
this.MultipleValuesFieldQuickSearch = MultipleValuesFieldQuickSearch;
this.UserFieldQuickSearch = UserFieldQuickSearch;

});
