/**
* Copyright (c) White Whale Web Services (http://www.whitewhale.net/)
* but free for any use, with or without modification
*
* Version 1.3 (2009-10-29)
*
* Usage: $('.inputselector').quickaccess(settingsObject);
* 	where .inputselector selects the container for the search input (so when the element has no other function than quickaccess it isn't shown) OR the search input itself
*	settingsObject may contain:
*  links : the selector for the links that are to be searched (i.e. '#linkslist a' or 'a.quickaccess')
*    To organize results into categories, selector may be an array of objects (i.e. [{selector:'#mammals a',title:'Mammals',className:'mammals'},{selector:'#reptiles a',title:'Reptiles',className:'reptiles'}])
*  results : the container in which the results will be placed (default:they'll be placed in a #qa_results created immediately after the search box)
*  inlineLabel : instructional text that appears inside the search box (i.e. 'Just start typing what you're looking for...')
*	forceSelect : when true, an autocomplete option will always be selected; disable, for instance, if you'd like the quickaccess box to also function as a typical search box (default:true)
*	onSubmit(event,selected) : callback function for when the user hits the enter/return key; by default, this will take them to the selected link (args: the keypress event and the currently selected result)
*  maxResults : the maximum number of results to show at any one time (default : 10)
*  tooMany : the message to show when there are more matching results than maxResults (default : 'Keep typing...')
*  noneFound : the message to show when no results are found (default : 'No matches found.')
*  focus : true/false; should the search element grab focus upon page initialization? (default : false)
*  sort : true/false; should the search results be alphabetized? (default : false)
*  removeDuplicates : true/false; should duplicate URLs be allowed in the results? -- not currently supported by IE6 or 7 (default : false) 
* Example: $('#quicksearch').quickaccess({selector:'#offices li a', maxResults:10,noneFound:'Sorry, no matching links were found.'});
**/

(function($) {
    $.fn.extend({
        quickaccess: function(s) {
            var self = this;

            // initialize the settings with their defaults
            s = $.extend({
                links: '.qa_links a',
                results: null,
                forceSelect: true,
                onSubmit: function(e, selected) { if (selected.length) { e.preventDefault(); window.location = selected.eq(0).find('a').attr('href'); } },
                maxResults: 10,
                tooMany: 'Keep typing...',
                noneFound: 'No matches found.',
                focus: true,
                sort: false,
                removeDuplicates: false
            }, s);

            qa = {};

            var blur = function() { // blur function (that we can call without dispatching onBlur events)
                if (s.inlineLabel && !$.trim(qa.search.val())) { // if an inlineLabel is specified and there's no query
                    qa.search.val(s.inlineLabel).addClass('qa_inline_label'); // set the value to the inlineLabel and add the class
                }
            };

            if (self.is('input[type=text],textarea')) qa.search = self; // if the specified item is an input element, use it
            else qa.search = self.prepend('<input type="text" id="qa_search_query"/>').children().eq(0); // otherwise, add an input inside of it

            if (s.results) qa.results = $(s.results).eq(0); // select the specified container
            else qa.results = qa.search.after('<div id="qa_results"></div>').next(); // otherwise, create a new container

            qa.results.addClass('qa_blur'); // initialize the blurred state

            qa.message = $('<div class="qa_message"></div>').appendTo(qa.results).hide(); // create a message div

            var toSpace = new RegExp('[,\-_\/\\\s&]+', 'g'), // regexp for elements that should become spaces
				toRemove = new RegExp('[^a-zA-Z 0-9]+', 'g'); // regexp for elements that should be removed

            if (typeof s.links == 'string') { // if the selector is not an array of categories
                qa.categories = [{ selector: s.links}]; // make it one
            } else { // otherwise
                qa.categories = s.links; // use the selectors as categories
            }

            $.each(qa.categories, function(index, category) { // with each category
                qa.categories[index].results = $('<div id="qa_category' + index + '" class="qa_category' + (category.className ? ' ' + category.className : '') + '">' + (category.title ? '<div class="qa_category_title">' + category.title + '</div>' : '') + '</div>').appendTo(qa.results); // append the category div
                qa.categories[index].results_list = $('<ul class="qa_results_list"><li></li></ul>').appendTo(qa.categories[index].results); // and its results list
                qa.categories[index].links = new Array(); // array to store all links
                $(category.selector).each(function() { // with each item
                    qa.categories[index].links.push({ // add it to the array
                        title: $(this).text(), // the "title"
                        keywords: (this.innerHTML).toLowerCase().replace('&amp;', 'and').replace(toSpace, ' ').replace(toRemove, ''), // keyword for matching against
                        href: $(this).attr('href'),
                        category: index
                    });
                });
                if (s.sort) qa.categories[index].links.sort(function(a, b) { // alphabetize
                    var la = a.title.toLowerCase(), // toLowerCase for case-insensitive sort; we use a new variable name due to IE bug
						lb = b.title.toLowerCase(); // ditto
                    return (la == lb ? 0 : (la > lb ? 1 : -1));
                });
                qa.categories[index].matches = qa.categories[index].links; // initialize the set as containing all links
            });

            qa.category_divs = qa.results.find('.qa_category'); // store the set of all the category divs
            qa.results_lists = qa.results.find('.qa_results_list'); // store the set of all results lists

            qa.results_items = $([]); // create an empty jQuery object for the results items

            qa.search.attr('autocomplete', 'off').keyup(function() { // on each keypress, filter the links
                var raw = $(this).val(),
					query = $.trim(raw.toLowerCase().replace(toSpace, ' ').replace(toRemove, '')), subquery; // grab query, sanitize it
                if (query == qa.lastquery) return; // do nothing if the query is unchanged
                if (!query.length) {
                    qa.lastquery = '';
                    qa.results_lists.html('<li></li>');
                    qa.results.addClass('qa_noquery');
                    return;
                } else qa.results.removeClass('qa_noquery');
                if (!qa.lastquery || query.indexOf(qa.lastquery) != 0) {  // if this query is NOT a subset of the last query
                    $.each(qa.categories, function(index) { // for each category
                        qa.categories[index].matches = qa.categories[index].links; // reinitialize the matches and search on all terms
                    });
                    subquery = query;
                } else subquery = query.substring(query.lastIndexOf(' ') + 1, query.length); // otherwise, since this query IS a subset of the last query, no need to search the last query's terms
                qa.lastquery = query;
                var count = 0;
                $.each(subquery.split(' '), function() { // filter the result for each word in the query
                    var search = this;
                    $.each(qa.categories, function(index) { // for each category
                        qa.categories[index].matches = $.grep(qa.categories[index].matches, function(item) { return (' ' + item.keywords).indexOf(' ' + search) >= 0; }); // filter out the non-matching links
                        count += qa.categories[index].matches.length;
                    });
                });
                var query_exp = new RegExp('(\\b' + raw.replace(toRemove, '.').replace(/\s/g, '|\\b') + ')', 'ig'); // for highlighting the query terms (we compile this before emptying the list for performance)

                qa.message.empty().hide(); // hide the message
                qa.results_lists.empty(); // empty all the results lists
                qa.category_divs.removeClass('qa_nonefound'); // remove none found classes from the categories
                qa.results.removeClass('qa_toomany').removeClass('qa_nonefound'); // and the too many none found class from the main list

                if (count > s.maxResults) qa.message.html(s.tooMany).show().parent().addClass('qa_toomany'); // if there are too many matches, add the message and class
                else if (count == 0) {
                    qa.message.html(s.noneFound).show().parent().addClass('qa_nonefound'); // if there are no matches, add the message and class
                    if (qa.categories.length > 1) qa.results_lists.html('<li class="qa_message">' + s.noneFound + '</li>').parent().addClass('qa_nonefound'); // if there's more than one category, add an individual message for each
                } else { // if the matches are just right
                    $.each(qa.categories, function(index) { // for each category
                        if (!qa.categories[index].matches.length) qa.categories[index].results_list.html('<li class="qa_message">' + s.noneFound + '</li>').parent().addClass('qa_nonefound'); // if there are no matches for this list, add the message and class
                        else { // otherwise, if there are matches
                            $.each(qa.categories[index].matches, function(i, item) { // with each item
                                if (!s.removeDuplicates || !qa.categories[index].results_list.find('a[href=' + item.href + ']').length) { // if duplicates are okay or if there are no duplicates
                                    qa.categories[index].results_list.append('<li><a href="' + item.href + '">' + (' ' + item.title).replace(query_exp, '<span class="qa_highlight">$1</span>') + '</a></li>'); // list the match, with highlighting
                                }
                            });
                        }
                    });
                    qa.results_items = qa.results.find('.qa_results_list li:not(.qa_message)'); // store all the items
                    if (s.forceSelect) qa.results_items.eq(0).addClass('qa_selected'); // force select the first match
                }
            }).keydown(function(e) { // capture special keys on keydown
                switch (e.keyCode) {
                    case 38: // up arrow
                        e.preventDefault();
                        var selected = qa.results.find('.qa_selected'),
							index = qa.results_items.index(selected);
                        selected.removeClass('qa_selected');
                        if (index > 0) qa.results_items.eq(index - 1).addClass('qa_selected');
                        else if (s.forceSelect) qa.results_items.eq(qa.results_items.length - 1).addClass('qa_selected');
                        break;
                    case 40: // down arrow
                        e.preventDefault();
                        var selected = qa.results.find('.qa_selected'),
							index = qa.results_items.index(selected);
                        selected.removeClass('qa_selected');
                        if (index < qa.results_items.length - 1) qa.results_items.eq(index + 1).addClass('qa_selected');
                        else if (s.forceSelect) qa.results_items.eq(0).addClass('qa_selected');
                        break;
                    case 13: // enter/return
                        var selected = qa.results.find('.qa_selected');
                        s.onSubmit.apply(self, [e, selected]);
                        break;
                }
            }).focus(function() {
                qa.results.removeClass('qa_blur');
                if ($.trim(qa.search.val()) == s.inlineLabel) { // if the function is the inlineLabel
                    qa.search.val('').removeClass('qa_inline_label');
                }
            }).blur(function() {
                setTimeout(function() { qa.results.addClass('qa_blur'); }, 200); // after 200ms, add the blur class; we use a delay in case the user has clicked in that area
                blur();
            });

            qa.search.keyup(); // run search in case field is pre-populated (e.g. in Firefox)

            blur(); // call the blur function

            if (s.focus) qa.search.focus(); // put cursor in search box

            return this; // return original element for chaining
        }
    });

})(jQuery);