/*
 * jQuery Plugin: Tokenizing Autocomplete Text Entry
 * Version 1.1
 *
 * Copyright (c) 2009 James Smith (http://loopj.com)
 * Licensed jointly under the GPL and MIT licenses,
 * choose which one suits your project best!
 *
 */

/* 
 * http://loopj.com/2009/04/25/jquery-plugin-tokenizing-autocomplete-text-entry/
 * 2009-10-14: Extended and modified for use on Kidsdata by David Mills
 */


(function($) {

	$.fn.tokenInput = function(url, options) {

		//private variables
		var list;

		var settings = $.extend({
			url: url,
			hintText: "Type in a search term",
			noResultsText: "No results",
			searchingText: "Searching...",
			searchDelay: 300,
			minChars: 1,
			tokenLimit: null,
			jsonContainer: null,
			method: "GET",
			contentType: "json",
			queryParam: "q",
			onResult: null,
			indicatorId: -1
		}, 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",
			loading: "token-input-loading",
			ghostText: "token-input-ghost-text"
		}, options.classes);

		//public functions
		this.addToken = function(text, value) {
			list.addToken(text, value);
		}
		this.addTokens = function(tokens) {
			list.addTokens(tokens);
		}
		this.clearTokens = function() {
			list.clearTokens();
		}

		//initialize the TokenList and keep a reference to it for later
		return this.each(function() {
			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
		};

		// Save the tokens
		var saved_tokens = [];

		// Keep track of the number of tokens in the list
		var token_count = 0;

		// Basic cache to save on db hits
		var cache = new $.TokenList.Cache();

		// Keep track of the timeout
		var timeout;

		// Create a new text input an attach keyup events
		var input_box = $("<input type=\"text\">")
        .css({
        	outline: "none"
        })
        .focus(function() {
        	if ($(this).val() && $(this).val().length > 0) {
        		setTimeout(function() { do_search(true); }, 5);
        	}
        	else if (settings.tokenLimit == null || settings.tokenLimit != token_count) {
        		show_dropdown_hint();
        	}

        	if (ghostText) {
        		ghostText.remove();
        	}
        })
        .blur(function() {
        	hide_dropdown();

        	if (ghostText && token_count == 0) {
        		ghostText.insertBefore(input_box);
        	}
        })
        .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();

        					//wrap-around to the top if we're at the bottom
        					var listItems = $(dropdown).find("ul:first");
        					previousIndex = listItems.children().index(selected_dropdown_item);
        					if (previousIndex == listItems.children().size() - 1) {
        						//wrap to the top
        						dropdown_item = $(listItems.children()[0]);
        					}

        				} else {
        					dropdown_item = $(selected_dropdown_item).prev();

        					//wrap-around to the bottom if we're at the top
        					var listItems = $(dropdown).find("ul:first");
        					previousIndex = listItems.children().index(selected_dropdown_item);
        					if (previousIndex == 0) {
        						//wrap to the bottom
        						dropdown_item = $(listItems.children()[listItems.children().size() - 1]);
        					}
        				}

        				if (dropdown_item.length) {
        					select_dropdown_item(dropdown_item);

        					//scroll the view if necessary
        					var listItems = $(dropdown).find("ul:first");

        					//find the currently selected index
        					activeIndex = listItems.children().index(dropdown_item);

        					//figure out the offset to scroll to
        					var offset = 0;
        					listItems.children().slice(0, activeIndex).each(function() {
        						offset += this.offsetHeight;
        					});

        					//scroll the dropdown so it shows our newly selected item
        					if ((offset + dropdown_item.get()[0].offsetHeight - $(dropdown).scrollTop()) > $(dropdown).get()[0].clientHeight) {
        						var scrollTo = offset + dropdown_item.get()[0].offsetHeight - $(dropdown).innerHeight();
        						$(dropdown).attr({ scrollTop: scrollTo });
        					} else if (offset < $(dropdown).scrollTop()) {
        						$(dropdown).attr({ scrollTop: offset });
        					}
        				}

        				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); }, 5);
        			}
        			break;

        		case KEY.TAB:
        		case KEY.RETURN:
        		case KEY.COMMA:
        			if (selected_dropdown_item) {
        				add_token($(selected_dropdown_item));
        			}
        			return false; //supress form submission
        			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.
        				setTimeout(function() { do_search(false); }, 5);
        			}
        			break;
        	}
        });

		// Keep a reference to the original input box
		var hidden_input = $(input)
                           .hide()
                           .focus(function() {
                           	input_box.focus();
                           })
                           .blur(function() {
                           	input_box.blur();
                           });

		// 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);


		init_list();

		//ghost text to display on the main box on load
		var ghostText = $("<p />").addClass(settings.classes.ghostText).text(settings.hintText);
		if (token_count == 0) {
			ghostText.insertBefore(input_box);
		}

		//
		// public functions
		//

		///
		/// Invoked by an external caller to add an item to the selected tokens list
		///
		this.addToken = function(text, value, selectNewToken) {

			if (selectNewToken === undefined) {
				selectNewToken = true; //default
			}

			//deselect any selected token
			if (selected_token) {
				deselect_token($(selected_token), POSITION.END, false);
			}

			if (tokenExists(value)) {
				//don't add the same value twice, instead just highlight the existing token
				var token = findTokenByText(text);
				if (token && selectNewToken) {
					select_token(token);
				}
				return;
			}

			//else add a new token to the box
			var newToken = insert_token(value, text);

			//add the item to the hidden value list
			var id_string = value + ",";
			hidden_input.val(hidden_input.val() + id_string);

			//we added a token, increment the count
			token_count++;

			if (selectNewToken) {
				//select the newly added token (also scrolls it into view)
				select_token(newToken);
			}

			if (ghostText) {
				ghostText.remove();
			}
		}

		///
		/// Invoked by an external caller to add a list of items to the selected tokens list
		///
		this.addTokens = function(tokens) {
			if (tokens && tokens.length) {
				for (var i = 0; i < tokens.length; i++) {
					var token = tokens[i];
					this.addToken(token.name, token.id, false);
				}
			}

			//make sure the tokens we just added are visible
			var lastToken = findTokenByText(token.name);
			select_token(lastToken);
		}

		///
		/// Invoked by an external caller to clear the list of selected tokens
		///
		this.clearTokens = function() {
			token_list.children("li.token-input-token").remove();
			hidden_input.val("");
			token_count = 0;
			input_box.focus();
		}

		//
		// private functions
		//

		function findTokenByText(text) {
			var listItems = token_list.children("li.token-input-token");
			for (var i = 0; i < listItems.length; i++) {
				var item = $(listItems[i]);
				var itemText = $(item.children("span:first")[0]).text();
				if (itemText == text) {
					return item;
				}
			}
			return null;
		}

		function tokenExists(value) {
			var ids = hidden_input.val().split(',');
			for (var i = 0; i < ids.length; i++) {
				var id = ids[i];
				if (id == value) {
					return true;
				}
			}
			return false;
		}

		function scrollTokenIntoView(token) {

			//token's offset
			var offset = token.get()[0].offsetTop;

			//is the token currently visible?
			var visibleMin = token_list.scrollTop();
			var visibleMax = visibleMin + token_list.innerHeight();
			var tokenHeight = token.get()[0].offsetHeight;

			if (offset < visibleMin) { //we need to scroll up
				//put the token at the top with a couple pixels padding
				token_list.attr({ scrollTop: (offset - 4) });
			}
			else if (offset > visibleMax - tokenHeight) { //scroll down
				//put the token at the bottom with a couple pixels padding
				var scrollTo = offset - token_list.innerHeight() + tokenHeight + 4;
				token_list.attr({ scrollTop: scrollTo }); ;
			}
		}

		// 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) {
					var this_token = $("<li><span>" + li_data[i].name + "</span> </li>")
                    .addClass(settings.classes.token)
                    .insertBefore(input_token);

					$("<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": li_data[i].id, "name": li_data[i].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();

					// Save this token id
					var id_string = li_data[i].id + ",";
					hidden_input.val(hidden_input.val() + id_string);

					//we added a token, increment the count
					token_count++;
				}
			}
		}

		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 this_token = $("<li><span>" + value + "</span> </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 });

			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");

			if (tokenExists(li_data.id)) {
				//don't add the same value twice, instead just highlight the existing token
				var token = findTokenByText(li_data.originalName);
				if (token) {
					select_token(token);
					return;
				}
			}

			var this_token = insert_token(li_data.id, li_data.originalName);

			//hide the ghost text
			if (ghostText) {
				ghostText.remove();
			}

			// 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();

			// Save this token id
			var id_string = li_data.id + ","
			hidden_input.val(hidden_input.val() + id_string);

			token_count++;

			if (settings.tokenLimit != null && settings.tokenLimit >= token_count) {
				input_box.hide();
				hide_dropdown();
			}

			//select the newly added token
			select_token(this_token);
		}

		// 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();

			//make sure the token we just selected is visible
			scrollTokenIntoView(token);
		}

		// Deselect a token in the token list
		function deselect_token(token, position, focusOnInput) {

			if (focusOnInput === undefined) {
				focusOnInput = true;
			}

			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);
			}

			if (focusOnInput) {
				// 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");

			// 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));
			}

			token_count--;

			if (settings.tokenLimit != null) {
				input_box
                .show()
                .val("")
                .focus();
			}
		}

		// Hide and clear the results dropdown
		function hide_dropdown() {
			dropdown.hide().empty();
			selected_dropdown_item = null;
		}

		function show_dropdown_searching() {
			var html = "<p class=\"" + settings.classes.loading + "\">" + settings.searchingText + "</p>";
			if (dropdown.html() != html) {
				dropdown.html(html).show();
			}
		}

		function show_dropdown_hint() {
			var html = "<p>" + settings.hintText + "</p>";
			if (dropdown.html() != html) {
				dropdown.html(html).show();
			}
		}

		// Highlight the query part of the search term
		function highlight_term(value, term) {
			//don't highlight to search term
			//return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
			return value;
		}

		// Populate the results dropdown with some results
		function populate_dropdown(query, results) {
			if (results.length) {
				dropdown.empty();
				var dropdown_ul = $("<ul>")
                .appendTo(dropdown)
                .mouseover(function(event) {
                	select_dropdown_item(get_element_from_event(event, "li"));
                })
                .mousedown(function(event) {
                	add_token(get_element_from_event(event, "li"));
                	// 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, "originalName": results[i].originalName });
					}
				}

				dropdown.show();
				dropdown_ul.show();

			} else {
				var text = settings.noResultsText;

				//do we have a callback?
				if ($.isFunction(settings.noResultsText)) {
					var query = input_box.val().toLowerCase();
					text = settings.noResultsText.call(this, query);
				}

				dropdown
                .html("<p>" + text + "</p>")
                .show();
			}
		}

		// 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);
			if (cached_results) {
				populate_dropdown(query, cached_results);
			} else {
				var queryStringDelimiter = settings.url.indexOf("?") < 0 ? "?" : "&";
				var callback = function(results) {
					if ($.isFunction(settings.onResult)) {
						results = settings.onResult.call(this, results);
					}
					cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
					populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
				};

				var jsonText = JSON.stringify({
					q: query,
					indicatorId: settings.indicatorId
				});

				$.ajax({
					type: settings.method,
					url: settings.url,
					data: jsonText,
					contentType: "application/json; charset=utf-8",
					dataType: "json",
					success: callback
				});
			}
		}
	};

	// Really basic cache for the results
	$.TokenList.Cache = function(options) {
		var settings = $.extend({
			max_size: 50
		}, options);

		var data = {};
		var size = 0;

		var flush = function() {
			data = {};
			size = 0;
		};

		this.add = function(query, results) {
			if (size > settings.max_size) {
				flush();
			}

			if (!data[query]) {
				size++;
			}

			data[query] = results;
		};

		this.get = function(query) {
			return data[query];
		};
	};

})(jQuery);