/* Copyright (c) 2008 Jordan Kasper
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * Copyright notice and license must remain intact for legal use
 * Requires: jQuery 1.2+
 *           jQuery.quicksilver
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 * Fore more usage documentation and examples, visit:
 *          http://jkdesign.org/faq/
 * 
 * Basic usage:
    <div id='faqSearch'></div>
    <ul id='faqs'>
      <li>
        <p class='question'>...</p>
        <div class='answer'>...</div>
        <p class='tags'>...</p>
      </li>
      ...
    </ul>
    
    $('#faqs').simpleFAQ(); // Most simple form (all default options)
    // ----- OR ----- //
    $('#faqs').simpleFAQ({
      data: null,                // Array If provided, this data is used as the FAQ data with each array entry being an object with 'question', 'answer', and 'tags' properties, this will be used to build the list
      nodeType: 'li',            // String The type of node to look for (and use) for FAQs
      questionClass: 'question', // String The class that all questions will have (either you have to give them this class, or use the plugin to build the list)
      answerClass: 'answer',     // String The class that all answers will have (either you have to give them this class, or use the plugin to build the list)
      tagClass: 'tags',          // String The class for a node in the answer that contains tags specific to each answer. If this exists, it boosts the score for search terms that are in the tags
      showOnlyOne: true,         // Boolean If true, only one answer will be visible at a time
      allowSearch: true,         // Boolean If true, adds a search box (must provide searchNode)
      searchNode: '#faqSearch',  // jQ Node  Only required if allowSearch is true; it is the container for the search box (should be a node, the jQuery object, or a selector)
      minSearchScore: 0.5,       // Number The minimum score a FAQ must have in order to appear in search results. Should be a number between 0 and 1 (Quicksilver score)
      sortSearch: true,          // Boolean Whether or not to sort search results
      speed: 500                 // Number or String The speed to open and close FAQ answers. String values must be one of the three predefined speeds: "slow", "normal", or "fast"; numeric values are the number of milliseconds to run the animation (e.g. 1000).
      ignore: ['the', 'a', ...]  // Array A list of words to ignore when searching
    });
 *  Note that these are NOT necesarily the defaults! (Check $.fn.simpleFAQ.defaults at the bottom of this file.)
 * 
 * When using the 'data' option, the format should be:
    [
      {
        question: "The question...",
        answer: "The answer...",
        tags: "tag1, tag2, tag3"
      },
      ...
    ]
   Note that the 'tags' field is optional
 * 
 * If you want to know when the results are sorted, bind to the  
 * "sort.simpleFAQ" event on the list. The second argument to the handler
 * will be an array of the (sorted) result nodes.
    $('#faqs').bind('sort.simpleFAQ', function(jQEvent, results) {
      if (results.length > 0) {
        // do something
      }
    });
 * 
 * If you want to know when a result is shown (expanded), bind to the  
 * "show.simpleFAQ" event on the list. The second argument to the handler
 * will be the node that was shown (expanded).
    $('#faqs').bind('show.simpleFAQ', function(jQEvent, faqNode) {
      // do something
    });
 * 
 * If you want to know when a search is initiated, bind to the  
 * "searchStart.simpleFAQ" event on the list.
    $('#faqs').bind('searchStart.simpleFAQ', function(jQEvent) {
      // do something
    });
 * You can also bind to the "searchEnd.simpleFAQ" event to be notified
 * when the search is completed. This event is fired in parallel with 
 * "sort.simpleFAQ", so you will get both events. The second argument 
 * to the handler will be an array of the (sorted) result nodes.
    $('#faqs').bind('searchEnd.simpleFAQ', function(jQEvent, results) {
      // do something
    });
 * 
 * TODO:
 *   Full testing suite
 * 
 * REVISIONS:
 *   0.1 Initial release
 *   0.2 Added speed option (thanks to Ferenc Radius)
 *       Minor other changes (size, comments/notes)
 *   0.3 Added caseSensitive option for searches
 *       Added list of words to ignore when searching/scoring
 *       Added "searchStart" and "searchEnd" events
 *       Added more classes to FAQs found by searching
 */
;(function($) {

	// ----------- Public methods ----------- //

	$.fn.simpleFAQ = function(o) {
		var n = this;
		if (n.length < 1) { return n; }
		n.addClass('simpleFAQList');

		// Set up options (and defaults)
		o = (o) ? o : {};
		o = auditOptions($.extend({}, $.fn.simpleFAQ.defaults, o));

		// Are we building the FAQs from data? (audited above)
		if (o.data != null) {
			n.html('');
			for (var i = 0, l = o.data.length; i < l; ++i) {
				n.append(
          "<" + o.nodeType + " class='simpleFAQ'>" +
          " <p class='" + o.questionClass + "'>" + o.data[i].question + "</p>" +
          " <div class='" + o.answerClass + "'>" +
          o.data[i].answer +
          "<p class='" + o.tagClass + "'>" + o.data[i].tags + "</p>" +
          "</div>" +
          "</" + o.nodeType + ">"
        );
			}
		}

		// Cache all FAQ nodes
		var faqs = n.find(o.nodeType);

		// Show answers when question clicked
		faqs
      .find('.' + o.questionClass)
        .css({ cursor: 'pointer' })
        .hover(
          function() { $(this).addClass('simplefaqhover'); },
          function() { $(this).removeClass('simplefaqhover'); }
        )
        .bind('click.simpleFAQ', function(e) {
        	var faq = $(this).parent();
        	faq.addClass('visible');
        	faq.find('.' + o.questionClass).hide();
        	if (o.showOnlyOne) {
        		// Hide all others
        		n.find(o.nodeType)
              .not(faq)
                .find('.' + o.answerClass)
                  .slideUp(o.speed, function() {
                  	$(this).removeClass('simplefaqshowing');
                  });
        	}
        	$(this)
            .siblings('.' + o.answerClass)
              .toggle(o.speed, function() {
              	if ($(this).is(':visible')) {
              		faq
                    .find('.' + o.answerClass)
                      .addClass('simplefaqshowing');
              		n.trigger('show.simpleFAQ', [faq[0]]);
              	} else {
              		faq
                    .find('.' + o.answerClass)
                      .removeClass('simplefaqshowing');
              		faq.removeClass('visible');
              	}
              	faq.find('.' + o.questionClass).show();
              	$('li.visible').fadeIn(200);
              });
        });

		// Hide all answers by default
		faqs.find('.' + o.answerClass).hide();

		// Searching is enabled
		if (o.allowSearch) {
			// Helper for hiding FAQs when not in search results
			var hideFAQ = function(node) {
				$(node)
          .hide()
          .removeClass('simpleFAQResult')
          .find('.' + o.answerClass)
            .hide()
            .removeClass('simplefaqshowing');
			}

			// create input node
			var iNode = $(o.searchNode);
			if (iNode.length > 0 && typeof $.score == 'function') {
				// Hide all FAQs, they'll be shown when found in a search
				hideFAQ(n.find(o.nodeType));

				iNode
          .append("<input type='text' id='simpleFAQSearch' />")
          .find('#simpleFAQSearch')
            .keyup(function(e) {
            	clearTimeout($.fn.simpleFAQ.keyTimeoutHandle);
            	var input = this;
            	if (input.value.length < 1) {
            		hideFAQ(n.find(o.nodeType));
            		return;
            	}

            	// add a slight delay to wait for more input
            	$.fn.simpleFAQ.keyTimeoutHandle = setTimeout(function() {
            		n.trigger('searchStart.simpleFAQ', []);
            		// Score the input
            		var scores = [];
            		faqs.each(function(i) {
            			var faq = $(this);
            			var tags = faq.find('.' + o.tagClass).text();
            			tags = (o.caseSensitive) ? tags : tags.toLowerCase();
            			var text = faq.text();
            			text = (o.caseSensitive) ? text : text.toLowerCase();
            			var q = getQuery(input.value, o);
            			var s = 0;

            			if (q.length > 0) {
            				s = $.score(text, q);
            				s += scoreTags(q, tags);
            			}
            			if (s > o.minSearchScore) {
            				scores.push([s, faq]);
            			} else {
            				hideFAQ($(this));
            			}
            		});

            		if (o.sortSearch) {
            			// Sort results
            			scores.sort(function(a, b) {
            				return b[0] - a[0];
            			});
            		}

            		// Show the relevant questions
            		var results = [];
            		$.each(scores, function() {
            			n.append(this[1].show().addClass('simpleFAQResult'));
            			results.push(this[1][0]);
            		});

            		n.trigger('sort.simpleFAQ', [results]);
            		n.trigger('searchEnd.simpleFAQ', [results]);

            	}, $.fn.simpleFAQ.keyTimeout);
            });
			}
		}

		var scoreTags = function(input, tags) {
			var s = 0;
			if (tags.length < 1) { return s; }
			var w = input.split(' ');
			for (var i = 0, l = w.length; i < l; ++i) {
				if (w[i].length < 1) { continue; }
				if (tags.indexOf(w[i]) > -1) {
					s += $.fn.simpleFAQ.tagMatchScore;
				}
			}
			return s;
		}

		var getQuery = function(t, o) {
			var q = '';
			t = (o.caseSensitive) ? t : t.toLowerCase();
			if (o.ignore.length > 0) {
				var w = t.split(' ');
				for (var i = 0; i < w.length; ++i) {
					if (w[i].length > 0) {
						if (typeof o.ignore.indexOf == 'function') {
							if (o.ignore.indexOf(w[i]) < 0) {
								q += w[i] + ' ';
							}
						} else {
							var f = false;
							for (var j = 0; j < o.ignore.length; ++j) {
								if (o.ignore[j] == w[i]) {
									f = true;
									break;
								}
							}
							if (!f) { q += w[i] + ' '; }
						}
					}
				}
				if (q.length > 0) { q = q.substr(0, q.length - 1); }
			} else {
				q = t;
			}
			return q;
		}

		// Return original node to continue jQuery chain
		return n;
	};

	// Defined outside simpleFAQ to allow for usage during construction
	var auditOptions = function(o) {
		if (!o.data || !o.data.length || typeof o.data.splice != 'function') {
			o.data = $.fn.simpleFAQ.defaults.data;
		}
		if (typeof (o.nodeType) != 'string') { o.nodeType = $.fn.simpleFAQ.defaults.nodeType; }
		if (typeof (o.questionClass) != 'string') { o.questionClass = $.fn.simpleFAQ.defaults.questionClass; }
		if (typeof (o.answerClass) != 'string') { o.answerClass = $.fn.simpleFAQ.defaults.answerClass; }
		if (typeof (o.tagClass) != 'string') { o.tagClass = $.fn.simpleFAQ.defaults.tagClass; }

		if (typeof (o.showOnlyOne) != 'boolean') { o.showOnlyOne = $.fn.simpleFAQ.defaults.showOnlyOne; }
		if (typeof (o.allowSearch) != 'boolean') { o.allowSearch = $.fn.simpleFAQ.defaults.allowSearch; }
		if (typeof (o.minSearchScore) != 'number') { o.minSearchScore = $.fn.simpleFAQ.defaults.minSearchScore; }
		if (typeof (o.sortSearch) != 'boolean') { o.sortSearch = $.fn.simpleFAQ.defaults.sortSearch; }
		if (typeof (o.caseSensitive) != 'boolean') { o.caseSensitive = $.fn.simpleFAQ.defaults.caseSensitive; }
		if (typeof (o.speed) != 'number') { o.speed = $.fn.simpleFAQ.defaults.speed; }

		if (!o.ignore || !o.ignore.length || typeof o.ignore.splice != 'function') {
			o.ignore = $.fn.simpleFAQ.defaults.ignore;
		}

		return o;
	}


	// ----------- Static properties ----------- //

	$.fn.simpleFAQ.keyTimeout = 400;
	$.fn.simpleFAQ.tagMatchScore = 0.1;

	// options for simpleFAQ instances...
	$.fn.simpleFAQ.defaults = {
		data: null,                // Array If provided, this data is used as the FAQ data with each array entry being an object with 'question', 'answer', and 'tags' properties, this will be used to build the list
		nodeType: 'li',            // String The type of node to look for (and use) for FAQs
		questionClass: 'question', // String The class that all questions will have (either you have to give them this class, or use the plugin to build the list)
		answerClass: 'answer',     // String The class that all answers will have (either you have to give them this class, or use the plugin to build the list)
		tagClass: 'tags',          // String The class for a node in the answer that contains tags specific to each answer. If this exists, it boosts the score for search terms that are in the tags
		showOnlyOne: false,        // Boolean If true, only one answer will be visible at a time
		allowSearch: false,        // Boolean If true, adds a search box (must provide searchNode)
		searchNode: null,          // jQ Node  Only required if allowSearch is true; it is the container for the search box (should be a node, the jQuery object, or a selector)
		minSearchScore: 0,         // Number The minimum score a FAQ must have in order to appear in search results. Should be a number between 0 and 1 (Quicksilver score)
		sortSearch: false,         // Boolean Whether or not to sort search results
		caseSensitive: false,      // boolean Whether or not the search is case sensitive
		speed: 0,                // Number or String The speed to open and close FAQ answers. String values must be one of the three predefined speeds: "slow", "normal", or "fast"; numeric values are the number of milliseconds to run the animation (e.g. 1000).
		ignore: ['the', 'a', 'an', 'i', 'we', 'you', 'it', 'that', 'those', 'these', 'them', 'to', 'and', 'or', 'as', 'at', 'by', 'for', 'of', 'so']
		// Array A list of words to ignore when searching
	};

})(jQuery);
