/* 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/filter/
 * 
 * Basic usage:
    <input type='text' id='someInput' />
    
    $('#someInput').simpleFilter({
      data: ['one', 'two', ...],  // Array REQUIRED
      maxListEntries: 10,         // Number If provided, result filter list will only contain up to this many entries
      caseSensitive: false,       // Boolean If true, must match case as well as text value
      useQuicksilver: true,       // Boolean Quicksilver matches entries by scoring characters, if set to false, simpleFilter will use a straight first-to-last character matching (in order) instead
      showAll: false,             // Boolean If true, when the input has focus and the up or down keys are pressed, all options are shown
      position: 0,                // Int (index) Where to place list (variable, above, below); see $.fn.simpleFilter.position at the end of this file for options
      sortBy: 0,                  // Int (index) How to sort results (none, score, alpha); see $.fn.simpleFilter.sortBy at the end of this file for options
      waitTime: 150,              // Int Milliseconds to wait after key up before filtering
      postFilter: null            // Function Callback function - called after every filtering
    });
 * 
 * If you want to know when a selection is made, bind to 
 * "input.simpleFilter" on the input:
   
    $('#someInput').bind('input.simpleFilter', function(jQEvent, text) {
      if (typeof(text) != 'undefined') {
        // handle it
      }
      // NOTE: this binding will also capture regular 'input' events,  
      // you will want to check the text argument. If it is undefined, 
      // then you do NOT process the event as a simpleFilter selection
    });
 * 
 * 
 * TODO:
 *   write testing suite
 *   cache list item nodes post-filter
 * 
 * REVISIONS
 *   0.5 Initial release to jQuery Plug-in site
 *   0.6 Fixes to make compatible with jQuery 1.2, FF3 bugs
 *   0.7 Fixed sorting,
 *       Removed postRender callback option (wasn't really working),
 *       Changed waitTime to start filtering after X ms, even if they are still typing,
 *       Add "escape" key handler for hiding list
 *       Force list above input if at bottom of viewport
 *       Add option to always show list above (or below), regardless of viewport size & input position
 *       Added event on input when filter option is selected (input.simpleFilter)
 *   0.8 Fixed type checking on various options
 *       Added 'showAll' option to show all items in list when no input is given on up/down key
 *       Added accessibility attributes to list and options (ARIA)
 *       Fixed issue in IE with non-Quicksilver scoring
 *       Fixed some issues with positioning fitlered list above input
 */
;(function($) {
  
  // ----------- Public methods ----------- //
  
  $.fn.simpleFilter = function(options) {
    var n = $(this);
    if (n.length < 1) { return n; }
    n.attr('autocomplete', 'off');
    
    // Set up options and cached filter instance
    options = (options)?options:{};
    $.fn.simpleFilter.instances[n.attr('id')] = {
      input: this,
      itemFocus: 0,
      options: $.fn.simpleFilter.auditOptions($.extend({}, $.fn.simpleFilter.defaults, options))
    };
    $.fn.simpleFilter.instances.count++;
    if ($.fn.simpleFilter.instances.count == 1) {
      $.fn.simpleFilter.setupWindowBindings();
    }
    var self = $.fn.simpleFilter.instances[n.attr('id')];
    
    // Cache the data to filter in the instance
    self.cache = [];
    if (self.options.data.length > 0) {
      $.each(self.options.data, function(i) {
        self.cache.push(this.toString());
      });
    } else {
      return n;
    }
    
    // create list element with some styles
    var pos = n.offset();
    self.list = $('body')
                  .append('<ul id="'+n.attr('id')+'_simpleFilterInput" class="filterList" style="display: none;" role="listbox" aria-hidden="true" aria-live="assertive" aria-relevant="additions removals"></ul>')
                  .find('ul#'+n.attr('id')+'_simpleFilterInput')
                    .css({
                      position: 'absolute',
                      //top: pos.top + n.height() + 5,
                      left: pos.left,
                      width: n.width() + 2,  // border
                      zIndex: 990
                    })
                    .get(0);
    var l = $(self.list); // just for ease of use and slight efficiency
    $(self.input).attr('aria-controls', n.attr('id')+'_simpleFilterInput');
    
    // Set up fitler handling
    function doFilter() {
      self.waitHandle = null;
      var input = $.trim(n.val());
      if (!self.options.caseSensitive) { input = input.toLowerCase(); }
      if (input.length < 1) {
        if (!self.options.showAll) {
          hideList();
        }
      } else {
        l.html('')  // kill the prev. entries  TODO: cache these?
          .show();   // then show list (empty)
       
        // filter the data
        var scores = [];
        $.each(self.cache, function(i) {
          var w = this;
          if (!self.options.caseSensitive) { w = w.toLowerCase(); }
          if (self.options.useQuicksilver) {
            var s = $.score(w, input);
          } else {
            var s = $.fn.simpleFilter.score(w, input);
          }
          if (s > 0) { scores.push([s, i, this]); }
        });
        
        // sort results if necessary
        if ($.fn.simpleFilter.sortBy[self.options.sortBy] != 'none' && scores.length > 0) {
          scores.sort(function(a, b){
            switch ($.fn.simpleFilter.sortBy[self.options.sortBy]) {
              case 'score':
                return b[0] - a[0];
              case 'alpha':
                if (a[2] > b[2]) { return 1; } else
                if (b[2] > a[2]) { return -1; } else
                { return 0; }
              default:
                return 0;
            }
          });
        }
        
        if ($.isFunction(self.options.postFilter)) {
          self.options.postFilter.apply(self.input, [input, scores]);
        }
        
        // show results
        if (scores.length > 0) {
          if (self.options.maxListEntries && self.options.maxListEntries > 0) {
            scores = scores.slice(0, self.options.maxListEntries);
          }
          $.each(scores, function() {
            addListItem(this[2]);
          });
          if (scores[0][2].toString().toLowerCase() == n.val().toLowerCase()) {
            // exact match to only entry so hide filter list
            hideList();
            return;
          }
        } else {
          // no matches, hide fitler list
          hideList();
          return;
        }
        
        // Put the list where it needs to be
        $.fn.simpleFilter.repositionList(self);
        // Add click and hover handlers
        addListItemHandler();
      }
    }
    
    function addListItem(text) {
      l.append('<li role="option" datatype="string">'+text+'</li>');
    }
    
    function showList() {
      // Clear out prev entries and show list
      l.html('').show().attr('aria-hidden', 'false');
    }
    
    function hideList() {
      l.hide();
      self.itemFocus = 0;
    }
    
    function showAll() {
      showList()
      $.each(self.cache, function() {
        addListItem(this);
      });
      $.fn.simpleFilter.repositionList(self);
      addListItemHandler();
    }
    
    function addListItemHandler() {
      l.find('li')
        .click(function(e) {
          $(self.input)
            .val(this.innerHTML)
            .trigger('input.simpleFilter', [this.innerHTML]);
          hideList();
        })
        .hover(
          function() { $(this).addClass('activeFilterOption'); },
          function() { $(this).removeClass('activeFilterOption'); }
         )
    }
    
    function handleInput() {
      if (self.waitHandle === null) {
        self.waitHandle = setTimeout(function() {
          doFilter();
        }, self.options.waitTime);
      }
    }
    
    // Bind filtering to keyup
    self.waitHandle = null;
    n.bind('keyup.simpleFilter', function(e) {
      if ($.trim(self.input.value).length < 1 && self.options.showAll && l.css('display') == 'none') {
        if (e.keyCode == 40 || e.which == 40 || e.keyCode == 38 || e.which == 38) {
          showAll();
        }
      }
      // using keys to navigate filter list
      if (e.keyCode == 40 || e.which == 40) { // down
        $.fn.simpleFilter.handleKeyControl(self, 40);
        if (l.css('display') == 'none') {
          handleInput();
        }
        
      } else if (e.keyCode == 38 || e.which == 38) { // up
        $.fn.simpleFilter.handleKeyControl(self, 38);
        if (l.css('display') == 'none') {
          handleInput();
        }
        
      } else if (e.keyCode == 13 || e.which == 13) { // enter
        if ($.fn.simpleFilter.handleKeyControl(self, 13)) {
          e.preventDefault(); // stop enter-submit if selecting list item
        }
        
      } else if (e.keyCode == 27 || e.which == 27) { // escape
        hideList();
        
      } else {
        // typing input
        handleInput();
      }
    });
    // Bind hiding to blur
    n.bind('blur.simpleFilter', function() {
      // use a timeout to allow time for click event above to trigger
      setTimeout(function() {
        hideList();
      }, 100);
    });
    
    return n;
  };
  
  $.fn.removeSimpleFilter = function() {
      var n = $(this);
      if (n.length < 1) { return n; }
      n.unbind('.simpleFilter');
      if ($.fn.simpleFilter.instances[n.attr('id')]) {
        $($.fn.simpleFilter.instances[n.attr('id')].list).remove();
        $.fn.simpleFilter.instances[n.attr('id')] = null;
        $.fn.simpleFilter.instances.count --;
      }
      if ($.fn.simpleFilter.instances.count < 1) {
        $(window).unbind('resize.simpleFilter');
      }
      return n;
  }
  
  
  // ----------- Private Helper Methods ----------- //
  
  // add binding to reposition all filter lists on window resize
  $.fn.simpleFilter.setupWindowBindings = function() {
    $(window).bind('resize.simpleFilter', function() {
      $.each($.fn.simpleFilter.instances, function(key) {
        if (key != 'count') {
          $.fn.simpleFilter.repositionList(this);
        }
      });
    });
  }
  
  $.fn.simpleFilter.repositionList = function(self) {
    var n = $(self.input);
    var l = $(self.list);
    if (!n || n.length < 1) { return; }
    var pos = n.offset();
    var t = pos.top + n.height() + 5;
    var placeAbove = false;
    var vh  = (window.innerHeight)?window.innerHeight:$(window).height();
    var st = (document.documentElement.scrollTop)?document.documentElement.scrollTop:document.body.scrollTop;
    //alert('vh='+vh+'; t='+t+'; st='+st+'; l.h()='+l.height()+'; p.t='+pos.top);
    if ($.fn.simpleFilter.position[self.options.position] == 'above' ||
        ($.fn.simpleFilter.position[self.options.position] != 'below' &&
         ((t - st) + l.height()) > vh && 
         ((pos.top - st) - l.height()) > 0)) {
      placeAbove = true;
      if ($.browser && $.browser.msie && $.browser.version < 7) {
        var b = $(document).height() - (pos.top + 2);
      } else {
        var b = vh - (pos.top + 1);
      }
    }
    l.css({
      left: pos.left,
      width: n.width() + 2  // border
    });
    if (placeAbove) {
      //alert('vh='+vh+'; pos.top='+pos.top+'; b='+b+'; l.css(top)='+l.css('top')+'; l.css(bot)='+l.css('bottom'));
      l.css({'bottom': b+'px', 'top': 'auto'});
    } else {
      l.css({'bottom': 'auto', 'top': t});
    }
  }
  
  $.fn.simpleFilter.handleKeyControl = function(self, key) {
    if (key == 38) {
      self.itemFocus--;
      if (self.itemFocus < 1) {
        self.itemFocus = $(self.list).children('li').length;
      }
      
    } else if (key == 40) {
      self.itemFocus++;
      if (self.itemFocus > $(self.list).children('li').length) {
        self.itemFocus = 1;
      }
      
    } else if (key == 13) {
      if (self.itemFocus > 0) {
        $(self.list)
          .children('li:eq('+(self.itemFocus-1)+')')
          .click();
        return true;
      }
    }
    $(self.list).children('li').removeClass('activeFilterOption').attr('aria-selected', 'false');
    $(self.list).children('li:eq('+(self.itemFocus-1)+')').addClass('activeFilterOption').attr('aria-selected', 'true');
  }
  
  $.fn.simpleFilter.score = function(text, input) {
    for (var i=0; i<input.length; ++i) {
      if (input.substr(i,1) != text.substr(i,1)) { return 0; }
    }
    return ( input.length / text.length );
  }
  
  $.fn.simpleFilter.auditOptions = function(options) {
    if (!options.data || !options.data.length || typeof(options.data) != "object") { options.data = $.fn.simpleFilter.defaults.data; }
    if (!options.maxListEntries || typeof options.maxListEntries != 'number' || options.maxListEntries < 0) { options.maxListEntries = $.fn.simpleFilter.defaults.maxListEntries; }
    if (typeof(options.caseSensitive) != 'boolean') { options.caseSensitive = $.fn.simpleFilter.defaults.caseSensitive; }
    if (typeof(options.useQuicksilver) != 'boolean') { options.useQuicksilver = $.fn.simpleFilter.defaults.useQuicksilver; }
    if (typeof(options.showAll) != 'boolean') { options.showAll = $.fn.simpleFilter.defaults.showAll; }
    if (typeof(options.position) != 'number' || options.position < 0 || options.position >= $.fn.simpleFilter.position.length) { options.position = $.fn.simpleFilter.defaults.position; }
    if (typeof(options.sortBy) != 'number' || options.sortBy < 0 || options.sortBy >= $.fn.simpleFilter.sortBy.length) { options.sortBy = $.fn.simpleFilter.defaults.sortBy; }
    if (typeof(options.waitTime) != 'number' || typeof options.waitTime != 'number' || options.waitTime < 0) { options.waitTime = $.fn.simpleFilter.defaults.waitTime; }
    return options;
  }
  
  
  // ----------- Static properties ----------- //
  
  $.fn.simpleFilter.instances = {};
  $.fn.simpleFilter.instances.count = 0;
  
  // options for simpleFilter instances...
  $.fn.simpleFilter.sortBy = ['score', 'alpha', 'none'];
  $.fn.simpleFilter.position = ['variable', 'above', 'below'];
  $.fn.simpleFilter.defaults = {
    data: [],              // Array REQUIRED
    maxListEntries: null,  // Number If provided, result filter list will only contain up to this many entries
    caseSensitive: false,  // Boolean If true, must match case as well as text value
    useQuicksilver: true,  // Boolean Quicksilver matches entries by scoring characters, if set to false, simpleFilter will use a straight first-to-last character matching (in order) instead
    showAll: false,        // Boolean If true, when the input has focus and the up or down keys are pressed, all options are shown
    position: 0,           // Int (index) Where to place list (variable, above, below); see $.fn.simpleFilter.position above for options
    sortBy: 0,             // Int (index) How to sort results (none, score, alpha); see $.fn.simpleFilter.sortBy above for options
    waitTime: 150,         // Int Milliseconds to wait after key up before filtering
    postFilter: null       // Function Callback function - called after every filtering
  };

})(jQuery);

