/**
 * http://loopj.com/2009/04/25/jquery-plugin-tokenizing-autocomplete-text-entry/
 */
(function($) {

	$.fn.tokenInput = function (urlOrData, options) {
		var isUrl = typeof urlOrData == "string";
		var settings = $.extend({
			url: isUrl ? urlOrData : null,
			data: isUrl ? null : urlOrData,
			hintText: "Type in a search term",
			noResultsText: "No results",
			searchingText: "Searching...",
			searchDelay: 300,
			showHint: true,
			trigger: 100,
			minChars: 1,
			input_box_class: false,
			input_box_validation_msg: '',
			input_box_error_cont: false,
			matchContains: true,
			matchSubset: true,
			prePopulate: []
		}, options);

		settings.classes = $.extend({
			tokenList: "token-input-list",
			token: "token-input-token",
			tokenDelete: "token-input-delete-token",
			selectedToken: "token-input-selected-token",
			highlightedToken: "token-input-highlighted-token",
			dropdown: "token-input-dropdown",
			dropdownItem: "token-input-dropdown-item",
			dropdownItem2: "token-input-dropdown-item2",
			selectedDropdownItem: "token-input-selected-dropdown-item",
			inputToken: "token-input-input-token"
		}, options.classes);

		return this.each(function () {
			var list = new $.TokenList(this, settings);
		});
	};

	$.TokenList = function (input, settings) {
		//
		// Variables
		//

		// Input box position "enum"
		var POSITION = {
			BEFORE: 0,
			AFTER: 1,
			END: 2
		};

		// Keys "enum"
		var KEY = {
			BACKSPACE: 8,
			TAB: 9,
			RETURN: 13,
			ESC: 27,
			LEFT: 37,
			UP: 38,
			RIGHT: 39,
			DOWN: 40,
			COMMA: 188,
			SEMICOLON: 59
		};

		// Save the tokens
		var saved_tokens = [];

		// Basic cache to save on db hits
		var cache = new $.TokenList.Cache(settings);

		// Keep track of the timeout
		var timeout;
    
		// Currently searching
		var searching = false;
    
		// input_box has focus
		var hasFocus = false;
    
		// Create a new text input an attach keyup events
		var input_box = $("<input type=\"text\">")
		.attr('class',settings.input_box_class ? settings.input_box_class : '')
		.attr('error',settings.input_box_error_cont ? settings.input_box_error_cont : '')
		.attr('alt',settings.input_box_class ? '{"'+settings.input_box_class+'":"'+settings.input_box_validation_msg+'"}' : '')
		.attr('messages',settings.input_box_class ? '{"'+settings.input_box_class+'":"'+settings.input_box_validation_msg+'"}' : '')
		.css({
			outline: "none"
		})
		.focus(function () {
			hasFocus = true;
			show_dropdown_hint();
		})
		.blur(function () {
			if(selected_dropdown_item) {
				add_token($(selected_dropdown_item));
				return true;
			}
            
			hasFocus = false;
			hide_dropdown();
			oriValue = $(this).val();
			if(oriValue == ''){
				return;
			}
			if(!insert_email()){
				if(searching){
					setTimeout(function(){
						check_separator_value(oriValue,",",true);
					}, 0);
				} else {
					check_separator_value(oriValue,",",true);
				}
			}
			return true;
		})
		.change(function(){
			oriValue = $(this).val();
			if(!insert_email()){
				if(searching){
					setTimeout(function(){
						check_separator_value(oriValue,",",true);
					}, 0);
				} else {
					check_separator_value(oriValue,",",true);
				}
			}
			return true;
		})
		.keydown(function (event) {
			var previous_token;
			var next_token;

			switch(event.keyCode) {
				case KEY.LEFT:
				case KEY.RIGHT:
				case KEY.UP:
				case KEY.DOWN:
					if(!$(this).val()) {
						previous_token = input_token.prev();
						next_token = input_token.next();

						if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
							// Check if there is a previous/next token and it is selected
							if(event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) {
								deselect_token($(selected_token), POSITION.BEFORE);
							} else {
								deselect_token($(selected_token), POSITION.AFTER);
							}
						} else if((event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) && previous_token.length) {
							// We are moving left, select the previous token if it exists
							select_token($(previous_token.get(0)));
						} else if((event.keyCode == KEY.RIGHT || event.keyCode == KEY.DOWN) && next_token.length) {
							// We are moving right, select the next token if it exists
							select_token($(next_token.get(0)));
						}
					} else {
						var dropdown_item = null;

						if(event.keyCode == KEY.DOWN || event.keyCode == KEY.RIGHT) {
							dropdown_item = $(selected_dropdown_item).next();
						} else {
							dropdown_item = $(selected_dropdown_item).prev();
						}

						if(dropdown_item.length) {
							select_dropdown_item(dropdown_item);
						}
						return false;
					}
					break;

				case KEY.BACKSPACE:
					previous_token = input_token.prev();

					if(!$(this).val().length) {
						if(selected_token) {
							delete_token($(selected_token));
						} else if(previous_token.length) {
							select_token($(previous_token.get(0)));
						}

						return false;
					} else if($(this).val().length == 1) {
						hide_dropdown();
					} else {
						// set a timeout just long enough to let this function finish.
						setTimeout(function(){
							do_search(false);
						}, 0);
					}
					break;

				case KEY.RETURN:
					if(selected_dropdown_item) {
						add_token($(selected_dropdown_item));
						return false;
					}
					break;
				case KEY.COMMA:
				case KEY.SEMICOLON:
					insert_email();
					break;

				case KEY.TAB:
					if(selected_dropdown_item) {
						add_token($(selected_dropdown_item));
						return true;
					}
					break;

				case KEY.ESC:
					hide_dropdown();
					return true;

				default:
					if(is_printable_character(event.keyCode)) {
						// set a timeout just long enough to let this function finish.
						searching = true;
						setTimeout(function(){
							do_search(false);
						}, 0);
					}
					break;
			}
		})
		.keyup(function(event){
			switch(event.keyCode) {
				case KEY.COMMA:
				case KEY.SEMICOLON:
					var oriValue = $(this).val();
					var sep = event.keyCode == KEY.COMMA ? ',' : ';';
					if(searching){
						setTimeout(function(){
							check_separator_value(oriValue,sep);
						}, 0);
					} else {
						check_separator_value(oriValue,sep);
					}
					break;
			}
		});

		// Keep a reference to the original input box
		var hidden_input = $(input)
		.hide()
		.focus(function () {
			input_box.focus();
		})
		.blur(function () {
			input_box.blur();
		}).val("");

		// Keep a reference to the selected token and dropdown item
		var selected_token = null;
		var selected_dropdown_item = null;

		// The list to store the token items in
		var token_list = $("<ul />")
		.addClass(settings.classes.tokenList)
		.insertAfter(hidden_input)
		.click(function (event) {
			var li = get_element_from_event(event, "li");
			if(li && li.get(0) != input_token.get(0)) {
				toggle_select_token(li);
				return false;
			} else {
				input_box.focus();

				if(selected_token) {
					deselect_token($(selected_token), POSITION.END);
				}
			}
		})
		.mouseover(function (event) {
			var li = get_element_from_event(event, "li");
			if(li && selected_token !== this) {
				li.addClass(settings.classes.highlightedToken);
			}
		})
		.mouseout(function (event) {
			var li = get_element_from_event(event, "li");
			if(li && selected_token !== this) {
				li.removeClass(settings.classes.highlightedToken);
			}
		})
		.mousedown(function (event) {
			// Stop user selecting text on tokens
			var li = get_element_from_event(event, "li");
			if(li){
				return false;
			}
		});


		// The list to store the dropdown items in
		var dropdown = $("<div>")
		.addClass(settings.classes.dropdown)
		.insertAfter(token_list)
		.hide();

		// The token holding the input box
		var input_token = $("<li />")
		.addClass(settings.classes.inputToken)
		.appendTo(token_list)
		.append(input_box);

		// If the input had any values initially, make tokens for them
		if (hidden_input.val()) {
			var original_vals = hidden_input.val().
			replace(/[, ]+/g, ' ').replace(/^\s+|\s+$/g, '').split(/ /);
			for ( var i = 0; i < original_vals.length; i++) {
				// TODO consider calling the server or checking the local cache to
				// find the correct "name" values for the ids.
				insert_token(original_vals[i], original_vals[i]);
			}
		// Canonicalize the values in the hidden input
		//hidden_input.val(original_vals.join(',') + ',');
		}
    
		init_list();

		//
		// Functions
		//
    
		//pre-populate list if items exist
		function init_list () {
			li_data = settings.prePopulate;
			if(li_data && li_data.length) {
				for(var i in li_data) {
					insert_token(li_data[i].id,li_data[i].name);
				}
			}
		}

		function is_printable_character(keycode) {
			if((keycode >= 48 && keycode <= 90) ||      // 0-1a-z
				(keycode >= 96 && keycode <= 111) ||     // numpad 0-9 + - / * .
				(keycode >= 186 && keycode <= 192) ||    // ; = , - . / ^
				(keycode >= 219 && keycode <= 222)       // ( \ ) '
				) {
				return true;
			} else {
				return false;
			}
		}

		// Get an element of a particular type from an event (click/mouseover etc)
		function get_element_from_event (event, element_type) {
			var target = $(event.target);
			var element = null;

			if(target.is(element_type)) {
				element = target;
			} else if(target.parent(element_type).length) {
				element = target.parent(element_type+":first");
			}

			return element;
		}

		// Inner function to a token to the list
		function insert_token(id, value) {
			var current_tokens = hidden_input.val().split(/,/);
			if($.inArray(id,current_tokens) > -1){
				return;
			}
	
			var this_token = $("<li><p>"+ value +"</p> </li>")
			.addClass(settings.classes.token)
			.insertBefore(input_token);
		
			// The 'delete token' button
			$("<span>x</span>")
			.addClass(settings.classes.tokenDelete)
			.appendTo(this_token)
			.click(function () {
				delete_token($(this).parent());
				return false;
			});
		
			$.data(this_token.get(0), "tokeninput", {
				"id": id,
				"name": value
			});
		
			// Save this token id
			var id_string = id + ",";
			hidden_input.val(hidden_input.val() + id_string);
		  
			return this_token;
		}

		// Add a token to the token list based on user input
		function add_token (item) {
			var li_data = $.data(item.get(0), "tokeninput");
			var this_token = insert_token(li_data.id, li_data.name);

			// Clear input box and make sure it keeps focus
			input_box
			.val("")
			.focus();

			// Don't show the help dropdown, they've got the idea
			hide_dropdown();
		}

		// Select a token in the token list
		function select_token (token) {
			token.addClass(settings.classes.selectedToken);
			selected_token = token.get(0);

			// Hide input box
			input_box.val("");

			// Hide dropdown if it is visible (eg if we clicked to select token)
			hide_dropdown();
		}

		// Deselect a token in the token list
		function deselect_token (token, position) {
			token.removeClass(settings.classes.selectedToken);
			selected_token = null;

			if(position == POSITION.BEFORE) {
				input_token.insertBefore(token);
			} else if(position == POSITION.AFTER) {
				input_token.insertAfter(token);
			} else {
				input_token.appendTo(token_list);
			}

			// Show the input box and give it focus again
			input_box.focus();
		}

		// Toggle selection of a token in the token list
		function toggle_select_token (token) {
			if(selected_token == token.get(0)) {
				deselect_token(token, POSITION.END);
			} else {
				if(selected_token) {
					deselect_token($(selected_token), POSITION.END);
				}
				select_token(token);
			}
		}

		// Delete a token from the token list
		function delete_token (token) {
			// Remove the id from the saved list
			var token_data = $.data(token.get(0), "tokeninput");
			//saved_tokens.splice($.inArray(saved_tokens, token_data.id), 1);

			// Delete the token
			token.remove();
			selected_token = null;

			// Show the input box and give it focus again
			input_box.focus();

			// Delete this token's id from hidden input
			var str = hidden_input.val()
			var start = str.indexOf(token_data.id+",");
			var end = str.indexOf(",", start) + 1;

			if(end >= str.length) {
				hidden_input.val(str.slice(0, start));
			} else {
				hidden_input.val(str.slice(0, start) + str.slice(end, str.length));
			}
		}

		// Hide and clear the results dropdown
		function hide_dropdown () {
			dropdown.hide().empty();
			selected_dropdown_item = null;
		}

		function show_dropdown_searching () {
			if(settings.showHint){
				dropdown
				.html("<p>"+settings.searchingText+"</p>")
				.show();
			}

		}

		function show_dropdown_hint () {
			if(settings.showHint){
				dropdown
				.html("<p>"+settings.hintText+"</p>")
				.show();
			}
		}

		// Highlight the query part of the search term
		function highlight_term(value, term) {
			return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
		}

		// Populate the results dropdown with some results
		function populate_dropdown (query, results) {
			if(hasFocus && results.length) {
				dropdown.empty();
				var dropdown_ul = $("<ul>")
				.appendTo(dropdown)
				.mouseover(function (event) {
					select_dropdown_item(get_element_from_event(event, "li"));
				})
				.click(function (event) {
					add_token(get_element_from_event(event, "li"));
				})
				.mousedown(function (event) {
					// Stop user selecting text on tokens
					return false;
				})
				.hide();

				for(var i in results) {
					if (results.hasOwnProperty(i)) {
						var this_li = $("<li>"+highlight_term(results[i].name, query)+"</li>")
						.appendTo(dropdown_ul);

						if(i%2) {
							this_li.addClass(settings.classes.dropdownItem);
						} else {
							this_li.addClass(settings.classes.dropdownItem2);
						}

						if(i == 0) {
							select_dropdown_item(this_li);
						}

						$.data(this_li.get(0), "tokeninput", {
							"id": results[i].id,
							"name": results[i].name
							});
					}
				}

				dropdown.show();
				dropdown_ul.slideDown("fast");
			} else {
				if(settings.showHint){
					dropdown
					.html("<p>"+settings.noResultsText+"</p>")
					.show();
				} else {
					dropdown.empty();
					hide_dropdown();
				}
			}
			searching = false;
		}

		// Highlight an item in the results dropdown
		function select_dropdown_item (item) {
			if(item) {
				if(selected_dropdown_item) {
					deselect_dropdown_item($(selected_dropdown_item));
				}

				item.addClass(settings.classes.selectedDropdownItem);
				selected_dropdown_item = item.get(0);
			}
		}

		// Remove highlighting from an item in the results dropdown
		function deselect_dropdown_item (item) {
			item.removeClass(settings.classes.selectedDropdownItem);
			selected_dropdown_item = null;
		}

		// Do a search and show the "searching" dropdown if the input is longer
		// than settings.minChars
		function do_search(immediate) {
			var query = input_box.val().toLowerCase();

			if (query && query.length) {
				if(selected_token) {
					deselect_token($(selected_token), POSITION.AFTER);
				}
				if (query.length >= settings.minChars) {
					show_dropdown_searching();
					if (immediate) {
						run_search(query);
					} else {
						clearTimeout(timeout);
						timeout = setTimeout(function(){
							run_search(query);
						}, settings.searchDelay);
					}
				} else {
					hide_dropdown();
				}
			}
		}

		// Do the actual search
		function run_search(query) {
			var cached_results = cache.get(query);
			searching = true;
			if(cached_results) {
				populate_dropdown(query, cached_results);
			} else {
				$.get(settings.url, {
					"q": query
				}, function (results) {
					cache.add(query, results);
					populate_dropdown(query, results);
				}, "json");
			}
		}
    
		function check_separator_value(value,sep,blur){
			if(searching) {
				clearTimeout(timeout);
				timeout = setTimeout(function(){
					check_separator_value(value,sep,blur);
				}, 0);
			} else {
				var regexp = new RegExp(sep,"g");
				var replace = "";
				value = value.replace(regexp,"");
				if(value && value.length){
					var cached_results = cache.get(value);
					if(cached_results && cached_results.length) {
						if(cached_results.length == 1 || blur) {
							insert_token(cached_results[0].id,cached_results[0].name);
							input_box.val("");
							hide_dropdown();
						} else {
							replace = " ";
						}
					} else if(blur && settings.trigger != 100){
						insert_token(value,value);
					} else if(!blur) {
						input_box.val("");
					}
				}
				input_box.val((input_box.val()).replace(regexp,replace));
			}
		}
    
		function insert_email(){
			var oriValue = input_box.val();
			var value = oriValue.replace(/\s+/g," ");
			value = jQuery.trim(value);
			if(value.length && is_email(value)){
				insert_token(value,value);
				var regexp = new RegExp(oriValue,"g")
				input_box.val((input_box.val()).replace(regexp,""));
				return true;
			}
			return false;
		}
    
		function is_email(value){
			return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value);
		}
	};

	// Really basic cache for the results
	$.TokenList.Cache = function (options) {
		var settings = $.extend({
			max_size: 10
		}, options);
		var data = {};
		var size = 0;

		var flush = function () {
			data = {};
			size = 0;
		};
    
		function matchSubset(s, sub) {
			if (!settings.matchCase)
				s = s.toLowerCase();
			var i = s.indexOf(sub);
			if (i == -1) return false;
			return i == 0 || settings.matchContains;
		};
	
    
		function add(query, results){
			if(size > settings.max_size) {
				flush();
			}
			if(!data[query]) {
				size++;
			}
			data[query] = results;
		}
	
		this.add = function (query, results) {
			add(query,results);
		};

		this.get = function (query) {
			/*
		 * if dealing w/local data and matchContains than we must make sure
		 * to loop through all the data collections looking for matches
		 */
			if( !settings.url && settings.matchContains ){
				// track all matches
				var csub = [];
				var q = $.trim(query);
				// 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.name, q)) {
								csub.push(x);
							}
						});
					}
				}
				return csub;
			} else if (data[query]){
				return data[query];
			} else if (settings.matchSubset) {
				for (var i = query.length - 1; i >= settings.minChars; i--) {
					var c = data[query.substr(0, i)];
					if (c) {
						var csub = [];
						$.each(c, function(i, x) {
							if (matchSubset(x.name, query)) {
								csub[csub.length] = x;
							}
						});
						return csub;
					}
				}
			}
			return null;
		};
    
		var formatMatch = function(row) {
			return row[0];
		}
    
		this.populate = function (){
			if( !settings.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( !settings.url ) settings.max_size = 1;
		
			// track all settings for minChars = 0
			stMatchSets[""] = [];
		
			// loop through the array and create a lookup structure
			for ( var i = 0, ol = settings.data.length; i < ol; i++ ) {
				var rawValue = settings.data[i];
				// if rawValue is a string, make an array otherwise just reference the array
				rawValue = (typeof rawValue == "string") ? {
					id: rawValue,
					name: rawValue
				} : rawValue;
			
				if ( typeof rawValue['name'] == 'undefined' || rawValue['name'] === false )
					continue;
				
				var firstChar = rawValue['name'].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 = {
					id: rawValue.id,
					name: rawValue.name,
					data: rawValue,
					result: settings.formatResult && settings.formatResult(rawValue) || rawValue['name']
				};
			
				// push the current match into the set list
				stMatchSets[firstChar].push(row);

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

			// add the data items to the cache
			$.each(stMatchSets, function(i, item) {
				// increase the cache size
				settings.max_size++;
				// add to the cache
				add(i, item);
			});
		}
	
		// populate any existing data
		setTimeout(this.populate, 25);
	};

})(jQuery);
