/*
 * jQuery Autocomplete plugin 1.1
 *
 * Copyright (c) 2009 Jörn Zaefferer
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $
 */

;
(function($) {

   $.fn.extend({
      autocomplete: function(urlOrData,options) {
         var isUrl = typeof urlOrData == "string";
         options = $.extend({},$.Autocompleter.defaults,{
            url: isUrl ? urlOrData : null,
            data: isUrl ? null : urlOrData,
            delay: isUrl ? $.Autocompleter.defaults.delay : 10,
            max: options && !options.scroll ? 10 : 150
         },options);

         // if highlight is set to false, replace it with a do-nothing function
         options.highlight = options.highlight || function(value) { return value; };

         // if the formatMatch option is not specified, then use formatItem for backwards compatibility
         options.formatMatch = options.formatMatch || options.formatItem;

         return this.each(function() {
            new $.Autocompleter(this,options);
         });
      },
      result: function(handler) {
         return this.bind("result",handler);
      },
      search: function(handler) {
         return this.trigger("search",[handler]);
      },
      flushCache: function() {
         return this.trigger("flushCache");
      },
      setOptions: function(options) {
         return this.trigger("setOptions",[options]);
      },
      unautocomplete: function() {
         return this.trigger("unautocomplete");
      }
   });

   $.Autocompleter = function(input,options) {

      var KEY = {
         UP: 38,
         DOWN: 40,
         DEL: 46,
         TAB: 9,
         RETURN: 13,
         ESC: 27,
         COMMA: 188,
         PAGEUP: 33,
         PAGEDOWN: 34,
         BACKSPACE: 8
      };

      // Create $ object for input element
      var $input = $(input).attr("autocomplete","off").addClass(options.inputClass);

      var timeout;
      var previousValue = "";
      var cache = $.Autocompleter.Cache(options);
      var hasFocus = 0;
      var lastKeyPressCode;
      var config = {
         mouseDownOnSelect: false
      };
      var select = $.Autocompleter.Select(options,input,selectCurrent,config);

      var blockSubmit;

      // prevent form submit in opera when selecting with return key
      $.browser.opera && $(input.form).bind("submit.autocomplete",function() {
         if(blockSubmit) {
            blockSubmit = false;
            return false;
         }
      });

      // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
      $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete",function(event) {
         // a keypress means the input has focus
         // avoids issue where input had focus before the autocomplete was applied
         hasFocus = 1;
         // track last key pressed
         lastKeyPressCode = event.keyCode;
         switch(event.keyCode) {

            case KEY.UP:
               event.preventDefault();
               if(select.visible()) {
                  select.prev();
               } else {
                  onChange(0,true);
               }
               break;

            case KEY.DOWN:
               event.preventDefault();
               if(select.visible()) {
                  select.next();
               } else {
                  onChange(0,true);
               }
               break;

            case KEY.PAGEUP:
               event.preventDefault();
               if(select.visible()) {
                  select.pageUp();
               } else {
                  onChange(0,true);
               }
               break;

            case KEY.PAGEDOWN:
               event.preventDefault();
               if(select.visible()) {
                  select.pageDown();
               } else {
                  onChange(0,true);
               }
               break;

            // matches also semicolon
            case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
            case KEY.TAB:
            case KEY.RETURN:
               if(selectCurrent()) {
                  // stop default to prevent a form submit, Opera needs special handling
                  event.preventDefault();
                  blockSubmit = true;
                  return false;
               }
               break;

            case KEY.ESC:
               select.hide();
               break;

            default:
               clearTimeout(timeout);
               timeout = setTimeout(onChange,options.delay);
               break;
         }
      }).focus(function() {
         // track whether the field has focus, we shouldn't process any
         // results if the field no longer has focus
         hasFocus++;
      }).blur(function() {
         hasFocus = 0;
         if(!config.mouseDownOnSelect) {
            hideResults();
         }
      }).click(function() {
         // show select when clicking in a focused field
         if(hasFocus++ > 1 && !select.visible()) {
            onChange(0,true);
         }
      }).bind("search",function() {
         // TODO why not just specifying both arguments?
         var fn = (arguments.length > 1) ? arguments[1] : null;

         function findValueCallback(q,data) {
            var result;
            if(data && data.length) {
               for(var i = 0; i < data.length; i++) {
                  if(data[i].result.toLowerCase() == q.toLowerCase()) {
                     result = data[i];
                     break;
                  }
               }
            }
            if(typeof fn == "function") fn(result);
            else $input.trigger("result",result && [result.data, result.value]);
         }

         $.each(trimWords($input.val()),function(i,value) {
            request(value,findValueCallback,findValueCallback);
         });
      }).bind("flushCache",function() {
         cache.flush();
      }).bind("setOptions",function() {
         $.extend(options,arguments[1]);
         // if we've updated the data, repopulate
         if("data" in arguments[1])
            cache.populate();
      }).bind("unautocomplete",function() {
         select.unbind();
         $input.unbind();
         $(input.form).unbind(".autocomplete");
      });


      function selectCurrent() {
         var selected = select.selected();
         if(!selected)
            return false;

         var v = selected.result;
         previousValue = v;

         if(options.multiple) {
            var words = trimWords($input.val());
            if(words.length > 1) {
               var seperator = options.multipleSeparator.length;
               var cursorAt = $(input).selection().start;
               var wordAt, progress = 0;
               $.each(words,function(i,word) {
                  progress += word.length;
                  if(cursorAt <= progress) {
                     wordAt = i;
                     return false;
                  }
                  progress += seperator;
               });
               words[wordAt] = v;
               // TODO this should set the cursor to the right position, but it gets overriden somewhere
               //$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
               v = words.join(options.multipleSeparator);
            }
            v += options.multipleSeparator;
         }

         $input.val(v);
         hideResultsNow();
         $input.trigger("result",[selected.data, selected.value]);
         return true;
      }

      function onChange(crap,skipPrevCheck) {
         if(lastKeyPressCode == KEY.DEL) {
            select.hide();
            return;
         }

         var currentValue = $input.val();

         if(!skipPrevCheck && currentValue == previousValue)
            return;

         previousValue = currentValue;

         currentValue = lastWord(currentValue);
         if(currentValue.length >= options.minChars) {
            $input.addClass(options.loadingClass);
            if(!options.matchCase)
               currentValue = currentValue.toLowerCase();
            request(currentValue,receiveData,hideResultsNow);
         } else {
            stopLoading();
            select.hide();
         }
      }

      ;

      function trimWords(value) {
         if(!value)
            return [""];
         if(!options.multiple)
            return [$.trim(value)];
         return $.map(value.split(options.multipleSeparator),function(word) {
            return $.trim(value).length ? $.trim(word) : null;
         });
      }

      function lastWord(value) {
         if(!options.multiple)
            return value;
         var words = trimWords(value);
         if(words.length == 1)
            return words[0];
         var cursorAt = $(input).selection().start;
         if(cursorAt == value.length) {
            words = trimWords(value)
         } else {
            words = trimWords(value.replace(value.substring(cursorAt),""));
         }
         return words[words.length - 1];
      }

      // fills in the input box w/the first match (assumed to be the best match)
      // q: the term entered
      // sValue: the first matching result
      function autoFill(q,sValue) {
         // autofill in the complete box w/the first match as long as the user hasn't entered in more data
         // if the last user key pressed was backspace, don't autofill
         var inputword = lastWord($input.val()).toLowerCase();
         if(options.autoFill && (inputword == q.toLowerCase()) && lastKeyPressCode
                 != KEY.BACKSPACE) {
            // fill in the value (keep the case the user has typed)
            $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
            // select the portion of the value not typed by the user (so the next character will erase)
            $(input).selection(previousValue.length,previousValue.length + sValue.length);
         }
      }

      ;

      function hideResults() {
         clearTimeout(timeout);
         timeout = setTimeout(hideResultsNow,200);
      }

      ;

      function hideResultsNow() {
         var wasVisible = select.visible();
         select.hide();
         clearTimeout(timeout);
         stopLoading();
         if(options.mustMatch) {
            // call search and run callback
            $input.search(
                    function (result) {
                       // if no value found, clear the input box
                       if(!result) {
                          if(options.multiple) {
                             var words = trimWords($input.val()).slice(0,-1);
                             $input.val(words.join(options.multipleSeparator) + (words.length
                                     ? options.multipleSeparator : ""));
                          }
                          else {
                             $input.val("");
                             $input.trigger("result",null);
                          }
                       }
                    }
                    );
         }
      }

      ;

      function receiveData(q,data) {
         if(data && data.length && hasFocus) {
            stopLoading();
            select.display(data,q);
            autoFill(q,data[0].value);
            select.show();
         } else {
            hideResultsNow();
         }
      }

      ;

      function request(term,success,failure) {
         if(!options.matchCase)
            term = term.toLowerCase();
         var data = cache.load(term);
         // recieve the cached data
         if(data && data.length) {
            success(term,data);
            // if an AJAX url has been supplied, try loading the data now
         } else if((typeof options.url == "string") && (options.url.length > 0)) {

            var extraParams = {
               timestamp: +new Date()
            };
            $.each(options.extraParams,function(key,param) {
               extraParams[key] = typeof param == "function" ? param() : param;
            });

            $.ajax({
               // try to leverage ajaxQueue plugin to abort previous requests
               mode: "abort",
               // limit abortion to this input
               port: "autocomplete" + input.name,
               dataType: options.dataType,
               url: options.url,
               data: $.extend({
                  q: lastWord(term),
                  limit: options.max
               },extraParams),
               success: function(data) {
                  var parsed = options.parse && options.parse(data) || parse(data);
                  cache.add(term,parsed);
                  success(term,parsed);
               }
            });
         } else {
            // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
            select.emptyList();
            failure(term);
         }
      }

      ;

      function parse(data) {
         var parsed = [];
         var rows = data.split("\n");
         for(var i = 0; i < rows.length; i++) {
            var row = $.trim(rows[i]);
            if(row) {
               row = row.split("|");
               parsed[parsed.length] = {
                  data: row,
                  value: row[0],
                  result: options.formatResult && options.formatResult(row,row[0]) || row[0]
               };
            }
         }
         return parsed;
      }

      ;

      function stopLoading() {
         $input.removeClass(options.loadingClass);
      }

      ;

   }

           ;

   $.Autocompleter.defaults = {
      inputClass: "ac_input",
      resultsClass: "ac_results",
      loadingClass: "ac_loading",
      minChars: 1,
      delay: 400,
      matchCase: false,
      matchSubset: true,
      matchContains: false,
      cacheLength: 10,
      max: 100,
      mustMatch: false,
      extraParams: {},
      selectFirst: true,
      formatItem: function(row) { return row[0]; },
      formatMatch: null,
      autoFill: false,
      width: 0,
      multiple: false,
      multipleSeparator: ", ",
      highlight: function(value,term) {
         return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("
                 + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")
                 + ")(?![^<>]*>)(?![^&;]+;)","gi"),"<strong>$1</strong>");
      },
      scroll: true,
      scrollHeight: 180
   };

   $.Autocompleter.Cache = function(options) {

      var data = {};
      var length = 0;

      function matchSubset(s,sub) {
         var withoutYo = s.toLowerCase().replace(/ё/g,"е");
         var subwithoutYo = sub.toLowerCase().replace(/ё/g,"е");

         var i = withoutYo.indexOf(subwithoutYo);
         if(options.matchContains == "word") {

            i = withoutYo.toLowerCase().search("\\b" + subwithoutYo.toLowerCase());
         }
         if(i == -1) return false;
         return i == 0 || options.matchContains;
      }

      ;

      function add(q,value) {
         if(length > options.cacheLength) {
            flush();
         }
         if(!data[q]) {
            length++;
         }
         data[q] = value;
      }

      function populate() {
         if(!options.data) return false;
         // track the matches
         var stMatchSets = {},
                 nullData = 0;

         // no url was specified, we need to adjust the cache length to make sure it fits the local data store
         if(!options.url) options.cacheLength = 1;

         // track all options for minChars = 0
         stMatchSets[""] = [];

         // loop through the array and create a lookup structure
         for(var i = 0, ol = options.data.length; i < ol; i++) {
            var rawValue = options.data[i];
            // if rawValue is a string, make an array otherwise just reference the array
            rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;

            var value = options.formatMatch(rawValue,i + 1,options.data.length);
            if(value === false)
               continue;

            var firstChar = value.charAt(0).toLowerCase();
            // if no lookup array for this character exists, look it up now
            if(!stMatchSets[firstChar])
               stMatchSets[firstChar] = [];

            // if the match is a string
            var row = {
               value: value,
               data: rawValue,
               result: options.formatResult && options.formatResult(rawValue) || value
            };

            // push the current match into the set list
            stMatchSets[firstChar].push(row);

            // keep track of minChars zero items
            if(nullData++ < options.max) {
               stMatchSets[""].push(row);
            }
         }
         ;

         // add the data items to the cache
         $.each(stMatchSets,function(i,value) {
            // increase the cache size
            options.cacheLength++;
            // add to the cache
            add(i,value);
         });
      }

      // populate any existing data
      setTimeout(populate,25);

      function flush() {
         data = {};
         length = 0;
      }

      return {
         flush: flush,
         add: add,
         populate: populate,
         load: function(q) {
            if(!options.cacheLength || !length)
               return null;
            /*
             * if dealing w/local data and matchContains than we must make sure
             * to loop through all the data collections looking for matches
             */
            if(!options.url && options.matchContains) {
               // track all matches
               var csub = [];
               // loop through all the data grids for matches
               for(var k in data) {
                  // don't search through the stMatchSets[""] (minChars: 0) cache
                  // this prevents duplicates
                  if(k.length > 0) {

                     var c = data[k];
                     $.each(c,function(i,x) {
                        // if we've got a match, add it to the array
                        if(matchSubset(x.value,q)) {
                           csub.push(x);
                        }
                     });
                  }
               }
               return csub;
            } else
            // if the exact item exists, use it
               if(data[q]) {
                  return data[q];
               } else
                  if(options.matchSubset) {
                     for(var i = q.length - 1; i >= options.minChars; i--) {
                        var sub = q.substr(0,i);
                        var c = data[q.substr(0,i)];
                        if(c) {
                           var csub = [];
                           $.each(c,function(i,x) {
                              if(matchSubset(x.value,q)) {
                                 csub[csub.length] = x;
                              }
                           });
                           return csub;
                        }
                     }
                  }
            return null;
         }
      };
   };

   $.Autocompleter.Select = function (options,input,select,config) {
      var CLASSES = {
         ACTIVE: "ac_over"
      };

      var listItems,
              active = -1,
              data,
              term = "",
              needsInit = true,
              element,
              list;

      // Create results
      function init() {
         if(!needsInit)
            return;
         element = $("<div/>")
                 .hide()
                 .addClass(options.resultsClass)
                 .css("position","absolute")
                 .appendTo(document.body);

         list = $("<ul/>").appendTo(element).mouseover(function(event) {
            if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
               active = $("li",list).removeClass(CLASSES.ACTIVE).index(target(event));
               $(target(event)).addClass(CLASSES.ACTIVE);
            }
         }).click(function(event) {
            $(target(event)).addClass(CLASSES.ACTIVE);
            select();
            // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
            input.focus();
            return false;
         }).mousedown(function() {
            config.mouseDownOnSelect = true;
         }).mouseup(function() {
            config.mouseDownOnSelect = false;
         });

         if(options.width > 0)
            element.css("width",options.width);

         needsInit = false;
      }

      function target(event) {
         var element = event.target;
         while(element && element.tagName != "LI")
            element = element.parentNode;
         // more fun with IE, sometimes event.target is empty, just ignore it then
         if(!element)
            return [];
         return element;
      }

      function moveSelect(step) {
         listItems.slice(active,active + 1).removeClass(CLASSES.ACTIVE);
         movePosition(step);
         var activeItem = listItems.slice(active,active + 1).addClass(CLASSES.ACTIVE);
         if(options.scroll) {
            var offset = 0;
            listItems.slice(0,active).each(function() {
               offset += this.offsetHeight;
            });
            if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
               list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
            } else if(offset < list.scrollTop()) {
               list.scrollTop(offset);
            }
         }
      }

      ;

      function movePosition(step) {
         active += step;
         if(active < 0) {
            active = listItems.size() - 1;
         } else if(active >= listItems.size()) {
            active = 0;
         }
      }

      function limitNumberOfItems(available) {
         return options.max && options.max < available
                 ? options.max
                 : available;
      }

      function fillList() {
         list.empty();
         var max = limitNumberOfItems(data.length);
         for(var i = 0; i < max; i++) {
            if(!data[i])
               continue;
            var formatted = options.formatItem(data[i].data,i + 1,max,data[i].value,term);
            if(formatted === false)
               continue;
            var li = $("<li/>").html(options.highlight(formatted,term)).addClass(i%2 == 0 ? "ac_even"
                    : "ac_odd").appendTo(list)[0];
            $.data(li,"ac_data",data[i]);
         }
         listItems = list.find("li");
         if(options.selectFirst) {
            listItems.slice(0,1).addClass(CLASSES.ACTIVE);
            active = 0;
         }
         // apply bgiframe if available
         if($.fn.bgiframe)
            list.bgiframe();
      }

      return {
         display: function(d,q) {
            init();
            data = d;
            term = q;
            fillList();
         },
         next: function() {
            moveSelect(1);
         },
         prev: function() {
            moveSelect(-1);
         },
         pageUp: function() {
            if(active != 0 && active - 8 < 0) {
               moveSelect(-active);
            } else {
               moveSelect(-8);
            }
         },
         pageDown: function() {
            if(active != listItems.size() - 1 && active + 8 > listItems.size()) {
               moveSelect(listItems.size() - 1 - active);
            } else {
               moveSelect(8);
            }
         },
         hide: function() {
            element && element.hide();
            listItems && listItems.removeClass(CLASSES.ACTIVE);
            active = -1;
         },
         visible : function() {
            return element && element.is(":visible");
         },
         current: function() {
            return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst
                    && listItems[0]);
         },
         show: function() {
            var offset = $(input).offset();
            element.css({
               width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
               top: offset.top + input.offsetHeight,
               left: offset.left
            }).show();
            if(options.scroll) {
               list.scrollTop(0);
               list.css({
                  maxHeight: options.scrollHeight,
                  overflow: 'auto'
               });

               if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
                  var listHeight = 0;
                  listItems.each(function() {
                     listHeight += this.offsetHeight;
                  });
                  var scrollbarsVisible = listHeight > options.scrollHeight;
                  list.css('height',scrollbarsVisible ? options.scrollHeight : listHeight);
                  if(!scrollbarsVisible) {
                     // IE doesn't recalculate width when scrollbar disappears
                     listItems.width(list.width() - parseInt(listItems.css("padding-left"))
                             - parseInt(listItems.css("padding-right")));
                  }
               }

            }
         },
         selected: function() {
            var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
            return selected && selected.length && $.data(selected[0],"ac_data");
         },
         emptyList: function () {
            list && list.empty();
         },
         unbind: function() {
            element && element.remove();
         }
      };
   };

   $.fn.selection = function(start,end) {
      if(start !== undefined) {
         return this.each(function() {
            if(this.createTextRange) {
               var selRange = this.createTextRange();
               if(end === undefined || start == end) {
                  selRange.move("character",start);
                  selRange.select();
               } else {
                  selRange.collapse(true);
                  selRange.moveStart("character",start);
                  selRange.moveEnd("character",end);
                  selRange.select();
               }
            } else if(this.setSelectionRange) {
               this.setSelectionRange(start,end);
            } else if(this.selectionStart) {
               this.selectionStart = start;
               this.selectionEnd = end;
            }
         });
      }
      var field = this[0];
      if(field.createTextRange) {
         var range = document.selection.createRange(),
                 orig = field.value,
                 teststring = "<->",
                 textLength = range.text.length;
         range.text = teststring;
         var caretAt = field.value.indexOf(teststring);
         field.value = orig;
         this.selection(caretAt,caretAt + textLength);
         return {
            start: caretAt,
            end: caretAt + textLength
         }
      } else if(field.selectionStart !== undefined) {
         return {
            start: field.selectionStart,
            end: field.selectionEnd
         }
      }
   };

}
        )
        (jQuery);
