/**
* Quicksearch functionality for tags.
*/
cw.provide("Tags", function(){

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

// import the necessary dependencies
var QuickSearch = cw.require("CW-QuickSearch"),
    Helpers = cw.require("CW-Helpers"),
    Keyboard = cw.require("CW-Keyboard"),
    EscapeHtmlEntitiesInString = Helpers.EscapeHtmlEntitiesInString,
    TrimString = Helpers.TrimString,
    BaseUrl = cw.getBaseUrl(),
    RouterUrl = cw.getRouterUrl();

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

/**
* Quick searching functionality for tags.
*/
function TagsQuickSearch() {
  TagsQuickSearch.base.apply(this, arguments);

  // finish initialization
  this._blacklistExpressions = [];
  this._tagsInUse = [];
} cw.extend(TagsQuickSearch, QuickSearch.MultipleValuesFieldQuickSearch);

/**
* Get the ID of the resource to which to assign tags.
* @return Returns the ID of the resource to which to assign tags.
*/
TagsQuickSearch.prototype.getResourceId = function() {
  return this._resourceId;
};

/**
* Set the ID of the resource to which to assign tags.
* @param int resourceId ID of the resource to which to assign tags.
*/
TagsQuickSearch.prototype.setResourceId = function(resourceId) {
  this._resourceId = resourceId;
};

/**
* Set the blacklist to prevent certain tags from being added.
* @param array blacklist List of items that are blacklisted.
*/
TagsQuickSearch.prototype.setBlacklist = function(blacklist) {
  // clear the current list of blacklist expressions
  this._blacklistExpressions = [];

  // create a blacklist expression for each item in the blacklist
  for (var i = 0; i < blacklist.length; i++) {
    // skip blank entries
    if (!TrimString(blacklist[i]).length) {
      continue;
    }

    // don't let errors in the blacklist format mess up execution
    try {
      this._blacklistExpressions.push(new RegExp('^'+blacklist[i]+'$', "m"));
    } catch(e) {}
  }
};

/**
* Set the list of tags already in use.
* @param array tagsInUse List of tags already in use.
*/
TagsQuickSearch.prototype.setTagsInUse = function(tagsInUse) {
  this._tagsInUse = tagsInUse;
  this._remoteData.setValueExclusions(tagsInUse);
};

/**
* Callback for key presses. Normalize the user input when necessary.
* @param KeyCodeSequence keyCodeSequence Key code sequence pressed.
* @protected
*/
TagsQuickSearch.prototype.keyPressStreamListener = function(keyCodeSequence) {
  var normalizedValue;
  var currentValue = this.getElementValue(this._currentTrigger);

  // normalize the input value if the user typed in a character and the value
  // isn't blank
  if (!keyCodeSequence.isKeyCombo() && currentValue.length) {
    normalizedValue = this.normalizeTag(currentValue);
    this.setElementValue(this._currentTrigger, normalizedValue);
  }

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

/**
* Callback function for a trigger element that receives focus. Sets the field ID
* and resource ID if they are set on the trigger.
* @param object event Standard JavaScript event object.
* @protected
*/
TagsQuickSearch.prototype.focusListener = function(event) {
  var trigger = jQuery(event.target);

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

  // set the resource ID, if available
  if (trigger.attr("data-resourceid")) {
    this.setResourceId(trigger.attr("data-resourceid"));
  }

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

/**
* The listener callback executed when a selection is made. Adds tags when a
* result or creation result is selected.
* @param jQuery result the selected result
* @protected
*/
TagsQuickSearch.prototype.selectionListener = function(result) {
  var tag;

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

  tag = this.getTagValueFromResult(result);

  // only add a tag if it's not blacklisted and it's not already in use
  if (!this.isTagInUse(tag) && !this.isTagInUse(tag)) {
    this.addTag(tag);
    this._blockLatentContentPopulation = true;
  }
};

/**
* Listener callback executed whenever a search is triggered. Add a message for
* blacklisted tags as necessary.
* @protected
*/
TagsQuickSearch.prototype.searchListener = function() {
  var tag = this.getTagValue(this._currentTrigger);

  // add a message about how the tag is blacklisted and don't search
  if (this.isTagBlacklisted(tag)) {
    this.removeLoadingStyles(this._currentTrigger);
    this.setPlaceholderContent(this.getMessageForBlacklistedTag());
    return;
  }

  TagsQuickSearch.base.prototype.searchListener.apply(this, arguments);
};

/**
* Format search results for adding to the results container.
* @param string value The original search string.
* @param object data Search results data.
* @return Returns the formatted search results.
*/
TagsQuickSearch.prototype.formatSearchResults = function(value, data) {
  var results = jQuery(TagsQuickSearch.base.prototype.formatSearchResults.apply(this, arguments)),
      tag = this.getTagValue(this._currentTrigger),
      tagInResults = false,
      self = this;

  // see if the tag is in any of the results
  jQuery(".quicksearch-quicksearch-result", results).each(function(){
    if (jQuery(this).text() == tag) {
      tagInResults = true;
      return false;
    }
  });

  // add the option to add the tag since it's not in use and it's not in the
  // results. this might add the "Create <tag>" message when the tag already
  // exists, but only when there are more results to display that contain the
  // value in question. since this is unlikely for tags and that "creating" a
  // tag is exactly the same operation, don't worry about it
  if (!this.isTagInUse(tag) && !tagInResults) {
    // create the list if necessary
    if (!results.is(".quicksearch-multiplevaluesfieldquicksearch-results")) {
      // create the list
      results = jQuery("<ul/>");

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

    // the the "Create <tag>" message at the beginning
    results.prepend(this.createTagResultItem(tag));
  }

  // if there are no tags to display
  if (!jQuery(".quicksearch-quicksearch-result", results).length) {
    return this.getMessageForNoMoreTags(tag);
  }

  return results;
};

/**
* Get the value of the element and then normalize it.
* @param jQuery element jQuery element.
* @return Returns the tag value from the element.
* @protected
*/
TagsQuickSearch.prototype.getTagValue = function(element) {
  return this.normalizeTag(this.getElementValue(element));
};

/**
* Get the tag ID of a result.
* @param jQuery element jQuery element.
* @return Returns the tag ID from the result, if any.
* @protected
*/
TagsQuickSearch.prototype.getTagValueFromResult = function(result) {
  return result.attr("data-result-id");
};

/**
* Get the tag value of a result.
* @param jQuery element jQuery element.
* @return Returns the tag value from the result.
* @protected
*/
TagsQuickSearch.prototype.getTagValueFromResult = function(result) {
  return result.attr("data-result-title");
};

/**
* Determine if a tag is blacklisted.
* @param string tag Tag value to check.
* @return Returns true if the tag is blacklisted and false otherwise.
* @protected
*/
TagsQuickSearch.prototype.isTagBlacklisted = function(tag) {
  // match the tag against each blacklist expression
  for (var i = 0; i < this._blacklistExpressions.length; i++) {
    // the tag is blacklisted if it matches a blacklist expression
    if (this._blacklistExpressions[i].exec(tag) !== null) {
      return true;
    }
  }

  // it didn't match a blacklist expression, so it's not blacklisted
  return false;
};

/**
* Determine if a tag is already in use.
* @param string tag Tag value to check.
* @return Returns true if the tag is already in use and false otherwise.
* @protected
*/
TagsQuickSearch.prototype.isTagInUse = function(tag) {
  return this._tagsInUse.indexOf(tag) != -1;
};

/**
* Normalize a tag name. This converts uppercase letters to lowercase ones.
* @param string tag Tag to normalize.
* @return Returns the normalized tag.
* @protected
*/
TagsQuickSearch.prototype.normalizeTag = function(tag) {
  return (tag+"").toLowerCase();
};

/**
* Add a tag to the resource.
* @param string tag Tag to add to the resource.
* @protected
*/
TagsQuickSearch.prototype.addTag = function(tag) {
  // send a request to the server to add the tag to the resource
  this.addTagRequest(this._resourceId, tag);

  // add it to the list of tags in use
  this._tagsInUse.push(tag);

  // add an exclusion for the tag
  this._remoteData.addValueExclusion(tag);
};

/**
* Remove a tag from the resource.
* @param string tag Tag to remove from the resource.
* @protected
*/
TagsQuickSearch.prototype.removeTag = function(tag) {
  // send a request to the server to remove the tag from the resource
  this.removeTagRequest(this._resourceId, tag);

  // remove the tag from the tags in use
  delete this._tagsInUse[this._tagsInUse.indexOf(tag)];

  // remove the tag from the exclusions
  this._remoteData.removeValueExclusion(tag);
};

/**
* Perform a server request to add a tag to a resource.
* @param int resourceId ID of the resource to which to add the tag.
* @param string tag Tag to add to the resource.
* @protected
*/
TagsQuickSearch.prototype.addTagRequest = function(resourceId, tag) {
  jQuery.post(RouterUrl + "?P=P_Tags_AddTag", {
    "ResourceId": resourceId,
    "Tag": tag
  });
};

/**
* Perform a server request to remove a tag from a resource.
* @param int resourceId ID of the resource to which to remove the tag.
* @param string tag Tag to remove from the resource.
* @protected
*/
TagsQuickSearch.prototype.removeTagRequest = function(resourceId, tag) {
  jQuery.post(RouterUrl + "?P=P_Tags_RemoveTag", {
    "ResourceId": resourceId,
    "Tag": tag
  });
};

/**
* Create a "Create <tag>" result item for use in formatted results.
* @param string tag Tag value.
* @return Returns a jQuery list item to add to the results.
* @protected
*/
TagsQuickSearch.prototype.createTagResultItem = function(tag) {
  var item = jQuery("<li/>"),
      link = jQuery("<a/>"),
      self = this;

  // create the result list item
  item.addClass("quicksearch-quicksearch-result");
  item.attr("data-result-title", tag);

  // create the clickable result area
  link.attr("href", this.getSearchUrlForValue(tag));
  link.html("Create <i>"+this.formatText(tag, tag)+"</i>");

  // select the result when clicked
  link.click(function(){
    self.activateResult(jQuery(this).parent());
    self.makeSelection(self._currentTrigger);
    return false;
  });

  // add the clickable area to the result item
  item.append(link);

  return item;
};

/**
* Generate a message for a blacklisted tag.
* @return Returns a string containing a message for blacklisted tags.
* @protected
*/
TagsQuickSearch.prototype.getMessageForBlacklistedTag = function() {
  return "<div class='quicksearch-quicksearch-message'>" +
           "That tag is not allowed." +
         "</div>";
};

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

  return "<div class='quicksearch-quicksearch-message'>" +
           "The resource is already tagged with <i>"+escapedValue+"</i> and no additional results were found." +
         "</div>";
};

/**
* The ID of the resource to which to assign tags.
* @var int _resourceId
* @protected
*/
TagsQuickSearch.prototype._resourceId;

/**
* The list of blacklist regular expression objects.
* @var array _blacklistExpressions
* @protected
*/
TagsQuickSearch.prototype._blacklistExpressions;

/**
* The list of tags already in use.
* @var array _tagsInUse
* @protected
*/
TagsQuickSearch.prototype._tagsInUse;

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

/**
* Quick searching functionality for tags on the full record page.
*/
function FullRecordTagsQuickSearch() {
  FullRecordTagsQuickSearch.base.apply(this, arguments);

  // finish initialization
  this._editableTags = [];
  this._tagList = jQuery();
} cw.extend(FullRecordTagsQuickSearch, TagsQuickSearch);

/**
* Set the blacklist to prevent certain tags from being added. Each item in the
* blacklist element should be separated by a newline (\n).
* @param jQuery blacklist Element containing newline-separated list of items
*      that are blacklisted.
*/
FullRecordTagsQuickSearch.prototype.setBlacklist = function(blacklist) {
  // split the blacklist string by the new lines and set the resulting array
  FullRecordTagsQuickSearch.base.prototype.setBlacklist.call(
    this,
    this.getElementValue(blacklist).split("\n"));
};

/**
* Set the HTML list of editable tags. This should be called before calling
* setTagList() because setTagList() uses editable tag information to add remover
* buttons.
* @param jQuery tagList HTML list of editable tags.
*/
FullRecordTagsQuickSearch.prototype.setEditableTags = function(editableTagList) {
  var editableTags = this.getElementValue(editableTagList).split("\n");

  // reset the current list
  this._editableTags = [];

  // add each editable tag to the list
  for (var i = 0; i < editableTags.length; i++) {
    this._editableTags.push(editableTags[i]);
  }
};

/**
* Set the HTML list of existing tags.
* @param jQuery tagList HTML list of tags.
*/
FullRecordTagsQuickSearch.prototype.setTagList = function(tagList) {
  var tagsInUse = [],
      self = this;

  // set the tag list element
  this._tagList = tagList;

  // add the tags in the tag list to the list of tags in use
  this._tagList.children("li").each(function(){
    var item = jQuery(this),
        link = jQuery("a", item),
        tag = self.getElementValue(link);

    // add a tag remover if the tag isn't uneditable
    if (!self.isTagUneditable(tag)) {
      item.append(self.createTagRemover(link));
    }

    // add the tag to the temporary list of those in use
    tagsInUse.push(tag);
  });

  // set the tags in use use the data from the element
  this.setTagsInUse(tagsInUse);
};

/**
* The listener callback executed when a selection is made. Adds tags to the tag
* list as necessary
* @param jQuery result the selected result
* @protected
*/
FullRecordTagsQuickSearch.prototype.selectionListener = function(result) {
  var tag;

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

  tag = this.getTagValueFromResult(result);

  // only add a tag if it's not blacklisted and it's not already in use
  if (!this.isTagInUse(tag)) {
    FullRecordTagsQuickSearch.base.prototype.selectionListener.apply(this, arguments);
    this._tagList.append(this.createTagListItem(tag));
  }

  // clear the input element and reorient on it
  this.setElementValue(this._currentTrigger, "");
  this._currentTrigger.focus();
  this.repositionOn(this._currentTrigger);
};

/**
* Create an element to append after a tag which will allow it to be removed.
* @param jQuery link Link that the remover will be applied to.
* @return Returns a jQuery object for the new remover.
* @protected
*/
FullRecordTagsQuickSearch.prototype.createTagRemover = function(link) {
  var image = jQuery("<img/>"),
      self = this;

  // set up the attributes of the image
  image.addClass("tags-tag_remover");
  image.attr({
    "src": BaseUrl + "plugins/Tags/interface/default/images/cross.png",
    "title": "Remove this tag from the resource.",
    "alt": "x"
  });

  // remove the tag when the image is clicked
  image.click(function(){
    self.removeTag(self.getElementValue(link));

    link.parent().fadeOut(400, function(){
      jQuery(this).remove();
    });
  });

  return image;
};

/**
* Create a new tag list item. Used after the user selects a result to add a tag
* to the list.
* @param string tag The tag value of the result.
* @return Returns the new tag list item as a jQuery object.
* @protected
*/
FullRecordTagsQuickSearch.prototype.createTagListItem = function(tag) {
  var item = jQuery("<li/>"),
      link = this.createTagLink(tag);

  // add the link and the tag remover
  item.append(link);
  item.append(this.createTagRemover(link));

  return item;
};

/**
* Create a tag link that links to the advanced search page for the tag value.
* @param string tag The tag value to link to.
* @returns Returns the new tag link as a jQuery object.
* @protected
*/
FullRecordTagsQuickSearch.prototype.createTagLink = function(tag) {
  var link = jQuery("<a/>"),
      escapedTag = EscapeHtmlEntitiesInString(tag);

  // set the necessary attributes of the link
  link.attr({
    "href": RouterUrl + "?P=AdvancedSearch&Q=Y&F"+
             escape(this.getFieldId())+"="+escape("="+tag),
    "title": "Search for all resources tagged with \""+escapedTag+"\""
  });

  // set the text of the link
  link.text(tag);

  return link;
};

/**
* Determine if the user is not permitted to edit the tag.
* @param string tag The tag to check.
* @return Returns true if the user is not permitted to edit the tag.
* @protected
*/
FullRecordTagsQuickSearch.prototype.isTagUneditable = function(tag) {
  return this._editableTags.indexOf(tag) === -1;
};

/**
* The HTML list containing the tags.
* @var jQuery _tagList
* @protected
*/
FullRecordTagsQuickSearch.prototype._tagList;

/**
* The list of tags that are editable.
* @var jQuery _editableTags
* @protected
*/
FullRecordTagsQuickSearch.prototype._editableTags;

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

// exported items
this.TagsQuickSearch = TagsQuickSearch;
this.FullRecordTagsQuickSearch = FullRecordTagsQuickSearch;

});
