/*
 * JQuery AutoSuggest
 *
 * Copyright 2009--2010 Drew Wilson
 * Copyright 2010--2012 Robert Katzki <robert@katzki.de>
 *
 * Based on the work of Drew Wilson. <code.drewwilson.com/entry/autosuggest-jquery-plugin>
 *
 * Changes:
 *     - Grouping of values
 *     - Fixed several bugs
 *
 * This plugin is dual-licensed under the GNU General Public License and the MIT License
 *
 */
if (jQuery) (function($) {
	$.fn.autoSuggest = function(data, userOptions) {
		var defaultOptions = {
			asHtmlID: false,                              // Specific ID for the Element
			startText: "Enter Name Here",
			emptyText: "No Results Found",
			preFill: {},
			limitText: "No More Selections Are Allowed",
			selectedItemProp: "value",                    //name of object property
			selectedValuesProp: "value",                  //name of object property
			searchObjProps: "value",                      //comma separated list of object property names
			resultGroups: false,
			resultGroupProp: "type",                      // object property which tells if it is a header
			resultGroupClass: "as-result-group-header",   // class name for the grouping
			queryParam: "q",                              // Param for the AJAX query
			retrieveLimit: false,                         // number for 'limit' param on ajax request
			extraParams: "",                              // Params to be added to the AJAX call
			extraParamsBefore: "",                        // Params to be added before the default stuff for the AJAX call
			matchCase: false,                             // Match case sensitivity?
			matchFirst: false,                            // Stop after the first occurence
			addNew: false,                                // If true, new values, that are not in the result list can be added via comma
			minChars: 1,                                  // Number of chars to enter before something happens
			keyDelay: 400,                                // Delay after entering something before loading starts
			resultsHighlight: true,                       // Shall the results be highlighted?
			neverSubmit: false,
			selectionLimit: false,
			showResultList: true,
			start: function(){},                          // Executed at the start of the AutoSuggest
			selectionClick: function(elem) {},            // Executed when clicking on a selection
			selectionAdded: function(elem) {},            // Executed when a selection is added
			selectionRemoved: function(elem) { elem.remove(); },                // Executed when a selection is removed
			formatList: false,                                                  //callback function
			beforeRetrieve: function(newInputValue) { return newInputValue; },  // Executed before calling the AJAX
			retrieveComplete: function(data) { return data; },                  // Executed at the retrieve of data
			resultClick: function(data) {},                                     // Executed at clicking on a result in the list
			resultsComplete: function() {}                                      // Executed when the results are all loaded
		};
		var options = $.extend(defaultOptions, userOptions);
		
		var dataType = "object";
		var dataCount = 0;
		if (typeof data == "string") {
			// Switch dataType and save URL for ajax request
			dataType = "string";
			var ajaxUrl = data;
		} else {
			// Save Object and count members
			var dataOriginal = data;
			$.each(data, function(index, value) {
				if (data.hasOwnProperty(index)) dataCount++;
			});
		}

		// Just when it is a object with data or an ajax URL
		if ((dataType == "object" && dataCount > 0) || dataType == "string") {
			return this.each(function(autoSuggestNumber) {
				var autoSuggestId;
				if (options.asHtmlID === false) {
					// This ensures there will be unique IDs on the page if autoSuggest() is called multiple times
					autoSuggestNumber = autoSuggestNumber+""+Math.floor(Math.random()*100);
					autoSuggestId = "as-input-"+autoSuggestNumber;
				} else {
					// Specified ID, so use that one
					autoSuggestNumber = options.asHtmlID;
					autoSuggestId = autoSuggestNumber;
				}

				// Call function to be executed at the start
				options.start.call(this);

				// Select Input Element und apply starting stuff to it
				var input = $(this);
				input.attr("autocomplete","off").addClass("as-input").attr("id",autoSuggestId).val(options.startText);
				var inputFocus = false;
				
				// Setup basic elements and render them to the DOM
				input.wrap('<ul class="as-selections" id="as-selections-'+autoSuggestNumber+'"></ul>')
					.wrap('<li class="as-original" id="as-original-'+autoSuggestNumber+'"></li>');
				var selectionsContainer   = $("#as-selections-"+autoSuggestNumber);
				var inputOriginal         = $("#as-original-"+autoSuggestNumber);
				var resultContainer       = $('<div class="as-results" id="as-results-'+autoSuggestNumber+'"></div>').hide();
				var resultList            =  $('<ul class="as-list"></ul>');
				var inputValues           = $('<input type="hidden" class="as-values" name="'+input.attr('name')+'" id="as-values-'+autoSuggestNumber+'" />');
				var preFillValue          = "";

				// Set name to empty, because the hidden field already has the name.
				input.attr('name', '');

				// Add values from preFill, if specified.
				if (typeof options.preFill == "string") {
					// Adding Elements from string
					var preFillValues = options.preFill.split(",");

					$.each(preFillValues, function(pfIndex, pfValue) {
						var preFillData = {};
						preFillData[options.selectedValuesProp] = pfValue;

						if (pfValue !== "") {
							add_selected_item(preFillData, "000"+pfIndex, true);
						}
					});
					
					preFillValue = options.preFill;
				} else {
					// Adding Elements from object or leave empty if empty object
					$.each(options.preFill, function(pfIndex, pfValue) {
						if (options.preFill.hasOwnProperty(pfIndex)) {
							var new_v = options.preFill[pfIndex][options.selectedValuesProp];
							if (new_v === undefined) {
								new_v = "";
							}

							preFillValue = preFillValue + new_v +",";
							
							if(new_v !== ""){
								add_selected_item(options.preFill[pfIndex], "000"+pfIndex, true);
							}
						}
					});
				}

				// Write preFill-values to the element
				if (preFillValue !== "") {
					input.val("");
					
					var lastChar = preFillValue.substring(preFillValue.length-1);
					
					if (lastChar === ",") {
						// Remove last ,
						preFillValue = preFillValue.substring(0,-1);
					}

					inputValues.val(preFillValue);
					
					$("li.as-selection-item", selectionsContainer).addClass("blur").removeClass("selected");
				}

				// Add the input with the values after the original Input
				input.after(inputValues);

				// Add SelectionsContainer after the resultContainer
				selectionsContainer.click(function(){
					inputFocus = true;
					input.focus();
				}).mousedown(function(){ inputFocus = false; }).after(resultContainer);

				// Define some values
				var timeout           = null;
				var prev              = "";
				var totalSelections   = 0;
				var tab_press         = false;
				
				// Handle input field events
				input.focus(function() {
					// Replace start text if set and visible
					if ($(this).val() === options.startText && inputValues.val() === "") {
						$(this).val("");
					// Input element is empty and has focus
					} else if (inputFocus) {
						$("li.as-selection-item", selectionsContainer).removeClass("blur");
						// Show result list if a value was entered
						if ($(this).val() !== "") {
							resultList.css("width",selectionsContainer.outerWidth());
							resultContainer.show();
						}
					}
					inputFocus = true;
					return true;
				// When leaving the Input
				}).blur(function() {
					// Show start text if empty
					if ($(this).val() === "" && inputValues.val() === "" && preFillValue === "") {
						$(this).val(options.startText);
					// Hide result list
					} else if (inputFocus) {
						$("li.as-selection-item", selectionsContainer).addClass("blur").removeClass("selected");
						resultContainer.hide();
					}
				// When entering something
				}).keydown(function(e) {
					// Track last key pressed
					lastKeyPressCode = e.keyCode;
					switch(e.keyCode) {
						// Go up or down in the result list
						case 38: // up
							e.preventDefault();
							moveSelection("up");
							break;
						case 40: // down
							e.preventDefault();
							moveSelection("down");
							break;

						// Deleting elements
						case 8:  // delete
							// Select last Element in list
							if (input.val() === "") {
								// Get last element
								var lastElement = inputValues.val().split(",");
								lastElement = lastElement[lastElement.length - 1];

								// Remove selection on all others
								selectionsContainer.children().not(inputOriginal.prev()).removeClass("selected");
								// Remove Element, when already selected, else select it.
								if (inputOriginal.prev().hasClass("selected")) {
									inputValues.val( inputValues.val().replace(lastElement,"") );

									// If last char is a comma, remove it.
									var lastChar = inputValues.val().substring(inputValues.val().length-1);
									if (lastChar === ",") {
										// Remove last ,
										inputValues.val(inputValues.val().substring(0,inputValues.val().length-1));
									}

									options.selectionRemoved.call(this, inputOriginal.prev());
								} else {
									options.selectionClick.call(this, inputOriginal.prev());
									inputOriginal.prev().addClass("selected");
								}
							}

							// When last Character is deleted, hide resultlist
							if(input.val().length == 1){
								resultContainer.hide();
								prev = "";
							}

							// When visible resultlist, set timeout for keyDelay
							if($(":visible",resultContainer).length > 0){
								if (timeout){ clearTimeout(timeout); }
								timeout = setTimeout(function(){ keyChange(); }, options.keyDelay);
							}
							break;

						// Comma handling
						case 188:  // comma
							tab_press = true;

							// Remove comma from value
							var newInputValue = input.val().replace(/(,)/g, "");
							
							// Reset the input value
							if (newInputValue !== "" && (inputValues.val()+",").search(newInputValue+",") < 0 && newInputValue.length >= options.minChars) {
								e.preventDefault();

								// If addNew is true, then add the entered string as value
								if (options.addNew === true) {
									var inputVal = input.val();
									var newData = {};
									newData[options.selectedItemProp] = inputVal;
									newData[options.selectedValuesProp] = inputVal;

									if (inputVal !== "") {
										add_selected_item(newData, "111"+Math.floor(Math.random()*100));
									}
								}

								input.val("");
								resultContainer.hide();
							}
							break;

						// Pressing tab or Enter
						case 9: case 13: // tab or return
							tab_press = false;

							// Select the first element from the result list
							var active = $("li.active:first", resultContainer);

							// Simulate click when there is an element
							if (active.length > 0) {
								active.click();
								resultContainer.hide();
							}

							// prevent default
							if (options.neverSubmit || active.length > 0) {
								e.preventDefault();
							}
							break;

						// All the other keys
						default:
							// Just when result list is wished
							if (options.showResultList) {
								// If limit of entrys is exceeded
								if (options.selectionLimit && $("li.as-selection-item", selectionsContainer).length >= options.selectionLimit) {
									resultList.html('<li class="as-message">'+options.limitText+'</li>');
									resultContainer.show();
								// Call delayed action
								} else {
									if (timeout){ clearTimeout(timeout); }
									timeout = setTimeout(function(){ keyChange(); }, options.keyDelay);
								}
							}
							break;
					}
				});
				
				function keyChange() {
					// ignore if the following keys are pressed: [del] [shift] [capslock]
					if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ){
						return resultContainer.hide();
					}

					// Get the new input value
					var newInputValue = input.val().replace(/[\\]+|[\/]+/g,"");
					if (newInputValue == prev) return;
					prev = newInputValue;

					// When minChars then show resultlist
					if (newInputValue.length >= options.minChars) {
						selectionsContainer.addClass("loading");

						// Get resultlist via AJAX
						if (dataType === "string") {
							var limit = "";

							// If a limit to of retrieving data is specified
							if(options.retrieveLimit){
								limit = "&limit="+encodeURIComponent(options.retrieveLimit);
							}

							// Call function to execute before retrieving the results
							if(options.beforeRetrieve){
								newInputValue = options.beforeRetrieve.call(this, newInputValue);
							}

							// Get resultlist via AJAX
							$.getJSON(ajaxUrl+"?"+options.extraParamsBefore+
											options.queryParam+"="+encodeURIComponent(newInputValue)+
											limit+options.extraParams, function(data){
								dataCount = 0;
								var newData = options.retrieveComplete.call(this, data);
								$.each(data, function(index, value) {
									if (newData.hasOwnProperty(index)) dataCount++;
								});
								processData(newData, newInputValue);
							});
						// Get resultlist from object
						} else {
							if(options.beforeRetrieve){
								newInputValue = options.beforeRetrieve.call(this, newInputValue);
							}
							processData(dataOriginal, newInputValue);
						}
					// Hide list when not minChars
					} else {
						selectionsContainer.removeClass("loading");
						resultContainer.hide();
					}
				}

				// Process the data
				function processData(data, query) {
					// If it should not be case sensitive
					if (!options.matchCase) {
						query = query.toLowerCase();
					}

					// Hide resultList
					var matchCount = 0;
					resultContainer.html(resultList.html("")).hide();

					// Each entry in the list
					$.each(data, function(suggestionIndex, suggestionValue) {
						var forward = false;
						var resultGroupHeader = false;
						var str = "";
						var sid = suggestionIndex+""+Math.floor(Math.random()*100);

						// Which fields to search on. Default is value.
						if (options.searchObjProps === "value") {
							str = suggestionValue.value;
						// When it's a different or multiple fields
						} else {
							var names = options.searchObjProps.split(",");
							$.each(names, function(index, name) {
								name = $.trim(name);
								str = str + suggestionValue[name] + " ";
							});
						}

						// In case that there is a string to match
						if (str) {
							if (!options.matchCase){ str = str.toLowerCase(); }

							// When the element is not yet added, then go forward
							if (str.search(query) !== -1 && (inputValues.val()+",").search(suggestionValue[options.selectedValuesProp]+",") === -1) {
								forward = true;
							}

							// Don't go forward, when it's a header.
							if (options.resultGroups && suggestionValue[options.resultGroupProp] === options.resultGroupClass){
								forward = false;
								resultGroupHeader = true;
							}
						}

						// Define the output
						var formatted = '';

						// In case that it is a resultGroupHeader
						if (resultGroupHeader) {
							formatted = $('<div class="'+options.resultGroupClass+'"></div>').mousedown(function() {
									inputFocus = false;
								}).click(function() {
									inputFocus = true;
									input.focus();
								}).mouseover(function() {
									$("li", resultList).removeClass("active");
								}).data("data",{attributes: suggestionValue, num: sid});

							//
							var suggestion = $.extend({},suggestionValue);
							formatted = formatted.html(suggestion[options.selectedItemProp]);

							// If last added element is another groupHeader, remove it, as it would be an empty header.
							if (resultList.children().last().hasClass(options.resultGroupClass)) {
								resultList.children().last().remove();
							}

							// Append the new Header
							resultList.append(formatted);
						}

						// if it's a regular suggestion, then append it
						if (forward) {
							formatted = $('<li class="as-result-item" id="as-result-item-'+sid+'"></li>').click(function() {
									var suggestionLiData = $(this).data("data");
									var number = suggestionLiData.num;

									//
									if ($("#as-selection-"+number, selectionsContainer).length <= 0 && !tab_press) {
										var data = suggestionLiData.attributes;
										input.val("").focus();
										prev = "";
										add_selected_item(data, number);
										options.resultClick.call(this, suggestionLiData);
										resultContainer.hide();
									}
									tab_press = false;
								}).mousedown(function() {
									inputFocus = false;
								}).mouseover(function() {
									// Highlight current entry
									$("li", resultList).removeClass("active");
									$(this).addClass("active");
								}).data("data",{attributes: suggestionValue, num: sid});
							var this_data = $.extend({},suggestionValue);
							
							// Match with regex
							var modifiers = "";
							if (!options.matchCase) { modifiers += "i"; }
							if (!options.matchFirst) { modifiers += "g"; }
							var regx = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + query + ")(?![^<>]*>)(?![^&;]+;)", modifiers);
							
							// Put element around for highlight
							if (options.resultsHighlight) {
								this_data[options.selectedItemProp] = this_data[options.selectedItemProp].replace(regx, "<em>$1</em>");
							}

							if(!options.formatList){
								formatted = formatted.html(this_data[options.selectedItemProp]);
							} else {
								formatted = options.formatList.call(this, this_data, formatted);
							}
							
							resultList.append(formatted);
							
							matchCount++;
							if (options.retrieveLimit && options.retrieveLimit === matchCount ) {
								return false;
							}
						}
					});

					// remove loading
					selectionsContainer.removeClass("loading");

					// Select first if none is active atm
					if ($('li.active', resultContainer).length <= 0) {
						$('li:first', resultContainer).addClass('active');
					}

					// Show warning
					if (matchCount <= 0) {
						resultList.html('<li class="as-message">'+options.emptyText+'</li>');
					}

					// Show container with results
					resultList.css("width", selectionsContainer.outerWidth());
					resultContainer.show();

					// Call function at the end of retrieving results
					options.resultsComplete.call(this);
				}
				
				function add_selected_item(data, num, preFill) {
					// When it's not the first element, then add comma
					if (inputValues.val() !== "") {
						inputValues.val(inputValues.val()+",");
					}
					// Add the new element
					inputValues.val(inputValues.val()+data[options.selectedValuesProp]);

					// Add item
					var item = $('<li class="as-selection-item" id="as-selection-'+num+'"></li>').click(function() {
							options.selectionClick.call(this, $(this));
							selectionsContainer.children().removeClass("selected");
							$(this).addClass("selected");
						}).mousedown(function() {
							inputFocus = false;
						});

					// Add close button
					var close = $('<a class="as-close">&times;</a>').click(function() {
							// Remove entry
							inputValues.val((inputValues.val()+",").replace(data[options.selectedValuesProp]+",",""));

							// If last char is a comma, remove it.
							var lastChar = inputValues.val().substring(inputValues.val().length-1);
							if (lastChar === ",") {
								// Remove last ,
								inputValues.val(inputValues.val().substring(0,inputValues.val().length-1));
							}

							options.selectionRemoved.call(this, item);
							inputFocus = true;
							input.focus();
							return false;
						});

					inputOriginal.before(item.html(data[options.selectedItemProp]).prepend(close));
					
					// Call function on added selection
					if (!preFill) {
						options.selectionAdded.call(this, inputOriginal.prev());
					}
				}
				
				function moveSelection(direction){
					// Just when it's visible
					if ($(":visible",resultContainer).length > 0) {
						// Get all suggestions
						var lis = $("li", resultContainer);
						var start;
						
						// Select first or last element
						if (direction == "down") {
							start = lis.eq(0);
						} else {
							start = lis.filter(":last");
						}
						
						// Set next / previous active
						var active = $("li.active:first", resultContainer);
						if (active.length > 0) {
							if(direction == "down"){
								start = active.nextAll("li").first();
							} else {
								start = active.prevAll("li").first();
							}
						}

						// Only the selected gets the class active
						lis.removeClass("active");
						start.addClass("active");
					}
				}
									
			});
		}
	};
})(jQuery);

