/**
--------------------------------------------------------------------
Copyright (c) 2008 Tim Jarrett tim@tim-jarrett.com
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


SearchList
This project creates a facebook style ajax driven textbox (see the "Compose To" page on 
facebook). 

Usage:
1. In your HTML, add a div with a class name of "searchlist_container" 

2. On page load (or at the bottom of the page) do something like:
   var searchlist = SearchListFactory.createList(
	   {
			area: <id of your div created in step #1>,
			url: <path to the url the ajax request should be made to>
			   
	   }
   );
   
   See "defaults" variable in SearchListFactory to see all possible options with their descriptions

3. Set up your ajax to return JSON that looks like this:
	{ 
		search_results: [ 
			{ value: "pbeesly@dundermifflin.com", text: "Pam Beesly", subtext: "pbeesly@dundermifflin.com", hidden: "" } , 
			{ value: "mscott@dundermifflin.com", text: "Michael Scott", subtext: "mscott@dundermifflin.com", hidden: "" }
		] 
	}
	
That's it.  Have fun

*/

/**
 * Factory for creating and tracking searchlists...
 */
var SearchListFactory = {
	version: '1.0.0',
	
	lists: {},
	
	defaults: { 
		//area,	//Most be provided, the id of the area to use for selected items, should be a div or span or something like that
		//url, //Most be provided - the url to send the Ajax request to
		preSelected: [],	//Array of anonymous objects with properties: value, text, subtext, hidden to be selected at list initialization
		showListElem: null, //Object that, when clicked, will span the search window
		inputVariableName: 'results', //Each item selected get's it's own <input type="hidden", they will all be named results[] (or whatever),
		minSearchLength: 3, //The minimum characters to kick off a search
		maxResults: 6, //Max results to be returned... passed to your server side script... is ok to ignore it server side if you want
		maxSelected: -1, //Max number of items that can be selected.  Default is -1, -1 == no limit
		variable: 'q', //Search variable passed to your script
		ajaxMethod: 'get', //Ajax method
		ajaxAdditionalParams: null, //Anything else you want sent (not currently honored)
		queue: true, //Whether to queue items to reduce ajax requests (not currently honored)
		itemSelectedCallback: null, //Callback function when an item is selected
		itemDeletedCallback: null //Callback function when an item is removed
						
	},
	
	/**
	 * Use this to create new lists... it takes care of many of the default handling items...
	 *
	 * @param options - An anonymous object ({}) 
	 */
	createList: function(options)
	{
		//Check for required options
		if ( options == null ) {
			throw new Error("No options provided.  Must provided at least area and url.");
			
		}
		
		if ( !options.area ) {
			throw new Error("No area div provided.");
			
		}
		
		if ( !options.url ) {
			throw new Error("No ajax url provided.");
			
		}
	
		//Fill in anything not provided
		for ( option in this.defaults ) {	
			if ( !options[option] ) {
				options[option] = this.defaults[option];
				
			}
		
		}//for
		
		//Now setup the list
		this.lists[options.area] = new SearchList(options);
	
	}//end createList
};

/**
 * SearchList definitation
 */
function SearchList(options)
{	
	var searchlist = this;
	var area = $(options.area);
	var selected = { };
	var selected_count = 0;
	var win = null;
	var searchbox = null;
	var results = null;
	var visible = false;
	var charwait = 500; //time to wait for additional characters
	var charwait_stop = null;
	
	/**
	 * Initialize -- show the default selected, hook stuff up, etc
	 */
	this.init = function()
	{			
		//Configure and add the search box
		var input = document.createElement('input');
		input.type = 'text';
		Element.addClassName(input, 'searchlist_searchfield');
		area.appendChild(input);
		searchbox = input;
		searchbox.onkeyup = searchlist.eventSearchBoxKeyUp.bindAsEventListener(this);
		searchbox.onkeypress = searchlist.eventSearchBoxKeyPress.bindAsEventListener(this);
		searchbox.last_value = '';
		area.onclick = function(event) {
			var event = ( !event ) ? window.event : event;
			Event.stop(event);
			searchbox.focus();
			
		}//end onclick
		
		//Initialize the selected values
		var max = ( options.preSelected.length > options.maxSelected && options.maxSelected != -1 ) ? options.maxSelected : options.preSelected.length;
		for ( var i=0; i<max; i++ ) {
			this.selectItem(options.preSelected[i]); 
			
		}//for i
		
		//Create popup window -- append to end of document
		win = document.createElement('div');
		win._searchlist = searchlist;
		Element.addClassName(win, 'searchlist_window');		
		
		win.innerHTML = '<table cellspacing="0" cellpadding="0">' + 
						'<tr><td class="ptr" colspan="3"></td></tr>' + 
						'<tr><td class="nw"></td><td class="n"><!-- <img src="" width="1" height="1" /> --></td><td class="ne"></td></tr>' + 
						'<tr><td class="w"><!-- <img src="" width="10" height="1" /> --></td><td class="searchlist_searcharea"></td><td class="e"><!-- <img src="" width="10" height="1" /> --></td></tr>' + 
						'<tr><td class="sw"></td><td class="s"><!-- <img src="" width="1" height="1" /> --></td><td class="se"></td></tr>' + 
						'</table>';		
		
		var win_dims = Element.getDimensions(win);
		Element.hide(win);
		document.body.appendChild(win);
		
		//Store a reference to the "searcharea" cell in the table
		var td = document.getElementsByClassName('searchlist_searcharea', win);
		td = td[0];
		win._contentarea = td;
		
		//Populate with form controls
		var input = document.createElement('input');
		input.type = 'button';
		input.value = "Close";
		input.onclick = function(event) { if ( !event ) { var event = window.event; } Event.stop(event); searchlist.hide(); searchbox.value = ''; };
		td.appendChild(input);
		
		var div = document.createElement('div');
		Element.addClassName(div, 'searchlist_results');
		td.appendChild(div);
		results = div;
		results.style.overflow = "hidden";
		results.searchlist_default_text = 'Use the search box above to search';
		results.innerHTML = results.searchlist_default_text;
	
	}//end init
	
	/**
	 * Add an item to the selected list.  Info is an anynomous object with properties: value, text, subtext, and hidden
	 *
	 * @return boolean
	 */
	this.selectItem = function(info)
	{		
		if ( options.maxSelected != -1 && selected_count >= options.maxSelected ) {
			alert("Sorry, you have already selected " + options.maxSelected + " items.  Please remove one of the items already selected before chosing a different item.");
			return false;
			
		}
	
		var item = document.createElement('span');
		var input = document.createElement('input');
		var close = document.createElement('span');
		
		item._info = input._info = close._info = info;
		item._searchlist = input._searchlist = close._searchlist = searchlist;
		
		//Finish setting up item
		item.innerHTML = info.text;
		Element.addClassName(item, 'searchlist_selecteditem');
		if ( searchbox ) {
			area.insertBefore(item, searchbox);
			
		} else { 
			area.appendChild(item);
		
		}
		
		item.onmouseover = function(event)
		{
			if ( !event ) {
				var event = window.event;
			}
			
			if ( event.currentTarget ) {
				var elem = event.currentTarget;
					
			} else {
				var elem = event.srcElement;
			}
			
			Element.addClassName(elem, 'searchlist_selecteditem_highlight');
			
		}
		
		item.onmouseout = function(event)
		{
			if ( !event ) {
				var event = window.event;
			}
			
			if ( event.currentTarget ) {
				var elem = event.currentTarget;
					
			} else {
				var elem = event.srcElement;
			}
			
			Element.removeClassName(elem, 'searchlist_selecteditem_highlight');
		
		}//end onmouseout
		
		//Finish setting up input
		input.type = 'hidden';
		input.name = options.inputVariableName;
		if ( options.maxSelected != 1 ) {
			input.name += '[]';
		}
		input.value = info.value;
		item.appendChild(input);
		
		//Finish setting up close
		close.innerHTML = '&nbsp;';
		Element.addClassName(close, 'close');
		item.appendChild(close);
		close.onclick = function() { if ( !searchlist.isVisible() ) { searchlist.removeItem(info.value); } }; 
		
		info.elem = item;
		selected[info.value] = info;
		
		if ( options.itemSelectedCallback != null ) {
			options.itemSelectedCallback(searchlist, info);
			
		}
		
		selected_count++;
		
		this.doWrap();
		
		return true;
		
	}//end selectItem
	
	/**
	 * Remove an item from the selected list
	 * value is the id item of the value to remove
	 */
	this.removeItem = function(value)
	{
		if ( selected[value] ) {
			var elem = selected[value].elem;
			//elem.parentNode.removeChild(elem);
			selected[value].elem = null;
			selected[value] = null;
			selected_count--;
			
			new Effect.DropOut(
				elem, 
				{
					queue: {position: 'end', scope: 'searchlistitem', limit: 2}, 
					afterFinish: function() { 
						elem.parentNode.removeChild(elem);  
						if ( options.itemDeletedCallback != null ) {
							options.itemDeletedCallback(searchlist, value);
						}
						
						searchlist.doWrap();
							
					}
					 
				});
			
		}
	
	}//end removeItem
	
	/**
	 * Handles wrapping of selected items
	 */
	this.doWrap = function()
	{
		//First remove all the brs
		var brs = area.getElementsByTagName('BR');
		for ( var i=brs.length-1; i>=0; i-- ) {
			area.removeChild(brs[i]);
			
		}//for i
	
		//Setup the variables
		var fudge_factor = 15;
		var area_dims = Element.getDimensions(area);
		var max_width = area_dims.width;
		var line_width = 0;
		
		//Loop through and figure out where to put in brs
		for ( var i = 0; i<area.childNodes.length; i++ ) {
			var child = area.childNodes[i];			
			if ( child.tagName && ( child.tagName == 'SPAN' ) ) {
				var child_dims = Element.getDimensions(child);
				line_width += child_dims.width + fudge_factor;
				
				if ( line_width >= max_width ) {
					console.log('adding br...');
					area.insertBefore(document.createElement('BR'), child);
					line_width = 0;
					
				}
					
			}
		
		}//for i	
		
		//Size the search box
		var new_width = max_width - line_width - 3;
		if ( new_width < 0 ) {
			line_width = 0;
			new_width = max_width - 3;
			area.insertBefore(document.createElement('BR'), searchbox);
			console.log('adding br for input box...');
			
		}
		Element.setStyle(searchbox, {width: new_width + "px"});
	
	}//end doWrap
	
	/**
	 * Returns true if this list is currently visible, false otherwise
	 */
	this.isVisible = function()
	{
		return visible;
	
	}//end isVisible
	
	/**
	 * Kick off the search with the given search term
	 */
	this.search = function(search_term)
	{
		//Make sure we have min chars
		if ( search_term.length < options.minSearchLength ) {
			results.innerHTML = "Please enter at least " + options.minSearchLength + " characters.";
			return;
			
		}
		
		//Delay execution incase more characters are incoming
		if ( charwait_stop != null ) {
			clearTimeout(charwait_stop);
			
		}
		
		charwait_stop = setTimeout(function() { searchlist.ajaxLookup(search_term); }, charwait);
		
	}//end search
	
	/**
	 * Monitor key's being released into the search field
	 */
	this.eventSearchBoxKeyUp = function(event)
	{		
		if ( !event ) {
			var event = window.event;
		}
		
		//Prevent modifier keys from triggering ajax
		var ignore = [ 224, 17, 16, 18, 38, 37, 39, 40, 35, 36, 33, 34, 91 ];
		if ( ignore.contains(event.keyCode) || event.ctrlKey || event.altKey || event.metaKey ) {
			Event.stop(event);
			return;
			
		}
		
		/*if ( searchbox.value.length < options.minSearchLength && searchlist.isVisible() ) {
			searchlist.hide();
			return;
			
		}*/
		
		if ( searchbox.last_value != searchbox.value ) {			
			this.search(searchbox.value);
			
		}
		
		searchbox.last_value = searchbox.value;
	
	}//end eventSearchBoxKeyUp
	
	/**
	 * Handle keypress
	 */
	this.eventSearchBoxKeyPress = function(event)
	{
		if ( !event ) {
			var event = window.event;
		}
		
		//If return, search immediately
		if ( event.keyCode == Event.KEY_RETURN ) {
			this.search(Event.element(event).value);
			Event.stop(event);
			return false;
			
		}
		
		//If escape key, close list
		if ( event.keyCode == Event.KEY_ESC ) {
			searchlist.hide();
			Event.stop(event);
			return false;
			
		}
	
	}//end eventSearchBoxKeyPress
	
	/**
	 * Start off the ajax request
	 */
	this.ajaxLookup = function(search_term)
	{
		if ( !this.isVisible() ) {
			this.show();
			
		}
	
		var params = options.variable + "=" + search_term + "&maxResults=" + options.maxResults;
		
		var ajax = new Ajax.Request(
			options.url,
			{
				method: options.ajaxMethod,
				parameters: params,
				onComplete: function(result) { searchlist.ajaxComplete(result); }
			}
		);
		
	}//end ajaxLookup
	
	/**
	 * Handle the result of the ajax request
	 */
	this.ajaxComplete = function(requestResponse)
	{
		var items = eval(requestResponse.responseText); 
		this.updateList(items);
	
	}//end ajaxComplete
	
	/**
	 * Update the list of results
	 */
	this.updateList = function(items)
	{
		results.innerHTML = "";
		var color = 'odd';
		
		if ( items.length == 0 ) {
			results.innerHTML = 'No results found';
		}
	
		for ( var it=0; it<items.length; it++ ) {
			var info = items[it];
			
			//Only show items not already selected
			if ( !selected[info.value] ) {						
				var color = ( color == 'odd' ) ? 'even' : 'odd';
				
				var div = document.createElement('div');
				results.appendChild(div);
				div.info = info;
				div.innerHTML += '<div class="searchlist_result_text">' + info.text + '&nbsp;' + '</div>' + 
				   '<div class="searchlist_result_subtext">' + info.subtext + '&nbsp;' + '</div>';
				
				Element.addClassName(div, 'searchlist_result');
				Element.addClassName(div, color);
				
				/**
				 * IE only... fires only when div (or any of it's children) is first moused over... 
				 * MS did something right!!  The way FF (and everyone else) does it is stupid
				 */
				div.onmouseenter = function(event)
				{
					var event = ( window.event ) ? window.event : event;
					Element.addClassName(event.srcElement, 'highlight');	
					
				}//end onmouseenter 
				
				/**
				 * IE only... fires only when div is entirely moused out of (not for each child element
				 * in the div.  MS did something right!!  The ay FF (and everyone else does it is stupid
				 */
				div.onmouseleave = function(event)
				{
					var event = ( window.event ) ? window.event : event;
					Element.removeClassName(event.srcElement, 'highlight');						
					
				}//end onmouseleave
				
				/**
				 * onMouseOver - fires when the mouse is over the div or any of it's children
				 */
				div.onmouseover = function(event) 
				{
					if ( !event ) {
						var event = window.event;
					}
					
					//Get rid of IE -- we'll let onmouseenter
					if ( !event.target ) {
						return;
						
					}
					
					var elem = Event.element(event);
					
					//This will fire not only on the div we apply it to, but all of that div's children.  SO F'in stupd...
					//Anyways... if we mouse over the div or any of it's children... apply the highlight style
					var parent = elem;
					while ( !Element.hasClassName(parent, 'searchlist_result') && parent.nodeName != 'BODY' ) {
						parent = parent.parentNode;
						
					}//while
					
					if ( parent.nodeName != 'BODY' && !Element.hasClassName(parent, 'highlight') ) {
						Element.addClassName(parent, 'highlight');
						
					}
					
				}//end onmouseover
				
				/**
				 * Fires when mousing out of this div or mousing into any children of this div... or out of any 
				 * children of this div... so stupid
				 */
				div.onmouseout = function(event)
				{
					if ( !event ) {
						var event = window.event;
					}
					
					//Get rid of IE -- we'll let onmouseleave handle it
					if ( !event.target ) {
						return;
						
					}
					
					var elem = ( event.currentTarget ) ? event.currentTarget : event.srcElement;
					
					Element.removeClassName(elem, 'highlight');
					
				}//end onmouseout		
				
				/**
				 * Works just like mouseover...
				 */				
				div.onclick = function(event)
				{
					var event = ( !event ) ? window.event : event;
					var elem = Event.element(event);
					
					//This will fire not only on the div we apply it to, but all of that div's children.  SO F'in stupd...
					//Anyways... if we mouse over the div or any of it's children... apply the highlight style
					var parent = elem;
					while ( !Element.hasClassName(parent, 'searchlist_result') && parent.nodeName != 'BODY' ) {
						parent = parent.parentNode;
						
					}//while
					
					if ( parent.nodeName != 'BODY' ) {
						searchlist.selectResultItem(parent);
						
					}
					
				}//end onclick
					
				div.remove = function()
				{
					if ( !Effect.SwitchOff ) {
						this.parentNode.removeChild(this); 
						
					}
					
					new Effect.SwitchOff(this, {queue: {position: 'end', scope: 'searchlistitem', limit: 2}});
				
				}//end remove
				
				//Hook up the select link
				var as = div.getElementsByTagName('a');
				for ( var j=0; j<as.length; j++ ) {
					if ( Element.hasClassName(as[j], 'searchlist_select') ) {
						as[j].searchlist_parent = div;
						as[j].info = info;
						as[j].onclick = searchlist.selectResultItem.bindAsEventListener(this);
					}
					
				}//for j 
				
			}
			
		}//for i
		
	}//end updateList
	
	/**
	 * Select an item who's select link has been clicked
	 */
	this.selectResultItem = function(elem)
	{		
		if ( this.selectItem(elem.info) ) {
			elem.remove();
			
		}		
		
		searchbox.focus();
	
	}//end selectResultItem
			 
	/**
	 * Show the list if it's hidden
	 */
	this.show = function()
	{
		//Calculate where it should show up... 
		var win_dims = Element.getDimensions(win);
		var dims = Element.getDimensions(area);
		var pos = Position.cumulativeOffset(area);
		win.style.top = ( pos[1] + dims.height + 5 ) + "px";
		win.style.left = pos[0] + parseInt(( dims.width - win_dims.width ) / 2) + "px";
	
		if ( visible ) {
			return;
			
		}
	
		new Effect.Grow(win, {queue: {position: 'end', scope: 'searchlist', limit: 2}, afterFinish: function() { results.style.overflow = "auto"; } });
		visible = true;
		
	}//end show
	
	/**
	 * Hide the list, if it's visible
	 */
	this.hide = function()
	{
		if ( !visible ) {
			return;
			
		}
		
		new Effect.Shrink(
			win, 
			{
				queue: {
					position: 'end', 
					scope: 'searchlist',
					limit: 2
				},
				
				beforeStart: function() { results.style.overflow = "hidden"; results.innerHTML = ''; }
				
			});
				
		visible = false;
	
	}//end hide
	
	this.init();
	
}//end SearchList 
