/** 
* Author and Version Information
* author: Antonio Ramirez http://webeaters.blogspot.com
* author: Markus Knaup http://www.digitaleprodukte.de
* 
* @version 2009-09-29
*/

var AutoComplete = Class.create();

AutoComplete.prototype = {
	Version: '1.3.0',
	REQUIRED_PROTOTYPE: '1.6.0',
	
	initialize: function(id, param) {
		// Check for correct prototype version
		this.PROTOTYPE_CHECK();
		
		this.id = id;
		
		// Get the field we're watching, and throw an error if null
		this.fld = $(id);
		if (!this.fld) throw("AutoComplete requires a field id to initialize");
		
		// Init variables
		this.sInp = ""; // input value 
		this.nInpC = 0;	// input value length
		this.aSug = []; // suggestions array 
		this.iHigh = 0;	// level of list selection 
		this.enabled = true;
		
		// Set the options
		this.options = param ? param : {};
		// These are the default settings {{{
		var k, def = {
			valueSep:			null,
			minchars:			1,
			meth:				"get",
			varname:			"input",
			className:			"autocomplete",
			timeout:			3000,
			delay:				500,
			offsety:			-5,
			shownoresults:		true,
			noresults:			"No results were found.",
			maxheight:			250,
			cache:				true,
			maxentries:			25,
			onAjaxError:		null,
			setWidth:			false,
			minWidth:			100,
			maxWidth:			200,
			useNotifier:		true,
			id:					id,
			stopReturn:			true,
			highlightFirstItem:	true
		};
		
		// Overlay any values which weren't user specified.
		for (k in def) {
			if (typeof(this.options[k]) != typeof(def[k]))
			this.options[k] = def[k];
		}
		
		// Not everyone wants to use the Notifier. Give them the option	
		if (this.options.useNotifier) this.fld.addClassName('ac_field');
		
		// Set event handler
		var p = this;
		
		// Safari and Internet Explorer have to be handled different than Mozilla
		if (navigator.appVersion.indexOf('Safari') != -1 || navigator.appVersion.indexOf('MSIE') != -1 || navigator.appName == "Microsoft Internet Explorer") document.observe('keydown', function(ev) {return p.onKeyPress(ev);});
		else this.fld.observe('keypress', function(ev) {return p.onKeyPress(ev);});
		
		this.fld.observe('keyup', function(ev) {return p.onKeyUp(ev);});
		this.fld.observe('blur', function(ev){p.resetTimeout(); return true;});
		this.fld.setAttribute("AutoComplete","off");
	},
	
	convertVersionString: function(versionString) {
		var r = versionString.split('.');
		return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
	},
	
	PROTOTYPE_CHECK: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			(this.convertVersionString(Prototype.Version) < 
			this.convertVersionString(this.REQUIRED_PROTOTYPE))) throw("AutoComplete requires the Prototype JavaScript framework >= " + this.REQUIRED_PROTOTYPE);
	},
	
	// Handles the key press events
	onKeyPress: function(e) {
		if (!this.enabled) return false;
		
		this.caretPosition = null;
		
		if (!e) e = window.event;
		var key	= e.keyCode || e.wich;
		
		switch(key) {
			case Event.KEY_RETURN:
				this.fld.removeClassName('ac_field_busy');
				this.fld.addClassName('ac_field');
				if (this.ajaxRequest) this.ajaxRequest.abort();
				
				if ($(this.acID)) this.setHighlightedValue();
				if (this.options.stopReturn) Event.stop(e);
				break;
			case Event.KEY_TAB:
				if ($(this.acID)) this.setHighlightedValue();
				break;
			case Event.KEY_ESC:
				this.clearSuggestions();
				break;
			case Event.KEY_UP:
			case Event.KEY_DOWN:
				// Save the caret position
				this.caretPosition = this.getCaretPosition(this.fld);
				this.changeHighlight(key);
				break;
		}
		
		return true;
	},
	
	// Handles the key up events
	onKeyUp: function(e) {
		if (!this.enabled) return false;
		
		if (!e) e = window.event;
		
		var key = e.keyCode || e.wich;
		
		if (key != Event.KEY_UP && key != Event.KEY_DOWN) this.getSuggestions(this.fld.value);
		
		// Restore the caret position
		if (this.caretPosition != null) this.setCaretPosition(this.fld, this.caretPosition);
		
		return true;
	},
	
	// Returns the caret position
	getCaretPosition: function(ctrl) {
		var caretPos = 0;
		
		if (document.selection) {
			// IE Support
			ctrl.focus();
			var sel = document.selection.createRange ();
			sel.moveStart('character', -ctrl.value.length);
			caretPos = sel.text.length;
		} else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
			// Firefox support
			caretPos = ctrl.selectionStart;
		}
		
		return caretPos;
	},
	
	// Sets the caret position
	setCaretPosition: function(ctrl, pos){
		if (ctrl.setSelectionRange) {
			ctrl.focus();
			ctrl.setSelectionRange(pos,pos);
		} else if (ctrl.createTextRange) {
			var range = ctrl.createTextRange();
			range.collapse(true);
			range.moveEnd('character', pos);
			range.moveStart('character', pos);
			range.select();
		}
	},
	
	getSuggestions: function(val) {
		// Nothing changed
		if (val == this.sInp) return false;
		
		// Remove the old list
		if ($(this.acID)) $(this.acID).remove();
		
		this.sInp = val;
		
		// Minimum char count not reached
		if (val.length < this.options.minchars) {
			this.aSug 	= [];
			this.nInpC	= val.length; 
			return false;
		}
		
		// Here we will detect if there is a comma and the splitted value has a value to check
		// comma stars a new search and val is converted to the new value after the comma
		var ol = this.nInpC; // old length
		this.nInpC = val.length ? val.length : 0;
		
		// if caching enabled, and we didn't receive the maxentries value
		// and user is typing (ie. length of input is increasing)
		// filter results out of suggestions from last request
		var l = this.aSug.length;
		
		if( this.options.cache && (this.nInpC > ol) && l && (l < this.options.maxentries)) {
			var arr = new Array();
			for (var i=0;i<l;i++) {
				if (this.aSug[i].value.toLowerCase().indexOf(val.toLowerCase()) != -1) arr.push(this.aSug[i]);
			}
			this.aSug = arr;
			
			// Recreate the list
			this.createList(this.aSug);
		} else {
			var p = this;
			clearTimeout(this.ajID);
			this.ajID = setTimeout(function() {p.doAjaxRequest(p.sInp)}, this.options.delay);
		}
		
		document.helper = this;	
		
		return false;
	},
	
	getLastInput: function(str) {
		var ret = str;
		if (undefined != this.options.valueSep) {
			var idx = ret.lastIndexOf(this.options.valueSep);
			ret = idx == -1 ? ret : ret.substring(idx + 1, ret.length);
		}
		
		return ret;
	},
	
	doAjaxRequest: function(input) {
		// we have to check here if there is a new splitted value (, or ;)
		// always check against the last part of the comma and then check
		// saved input is still the value of the field
		if (input != this.fld.value) return false;
		
		// Gmail like : get only the last user's input
		this.sInp = this.getLastInput(this.sInp);
		
		// Create ajax request
		if (typeof this.options.script == 'function') var url = this.options.script(encodeURIComponent(this.sInp));
		else var url = this.options.script + this.options.varname + '=' + encodeURIComponent(this.sInp);
		
		if (!url) return false;
		
		var p = this;
		var m = this.options.meth;
		if (this.options.useNotifier) {
			this.fld.removeClassName('ac_field');
			this.fld.addClassName('ac_field_busy');
		};
		
		var options = {
			method: m,
			onSuccess: function (req) {
				p.setSuggestions(req, input);
				if (p.options.useNotifier && input == p.fld.value) {
					p.fld.removeClassName('ac_field_busy');
					p.fld.addClassName('ac_field');
				};
			},
			
			onFailure: (typeof p.options.onAjaxError == 'function') ? function (status) {
				if (p.options.useNotifier && input == p.fld.value) {
					p.fld.removeClassName('ac_field_busy');
					p.fld.addClassName('ac_field');
				}
				p.options.onAjaxError(status)
			} :
			
			function (status) {
				if (p.options.useNotifier && input == p.fld.value) {
					p.fld.removeClassName('ac_field_busy');
					p.fld.addClassName('ac_field');
				}
				
//				alert("AJAX error: " + status); 
			}
		}
		
		// Abort last AJAX request
		if (this.ajaxRequest) this.ajaxRequest.abort();
		
		// Execute AJAX request
		this.ajaxRequest = new Ajax.Request(url, options);
	},
	
	setSuggestions: function(req, input) {
		// if field input no longer matches what was passed to the request
		// don't show the suggestions
		// here we need to check against the splitted values if any (, or ;)
		if (input != this.fld.value) return false;
		
		this.aSug = [];
		
		if(this.options.json) {
			// response in json format?
			var jsondata = eval('(' + req.responseText + ')');
			this.aSug = jsondata.results;
		} else {
			// response in xml format?
			var results = req.responseXML.getElementsByTagName('results')[0].childNodes;
			
			for(var i=0; i<results.length; i++) {
				Element.extend(results[i]);
				if (results[i].hasChildNodes())
				this.aSug.push({'id': results[i].getAttribute('id'), 'value': results[i].childNodes[0].nodeValue, 'info': results[i].getAttribute('info')});
			}
		}
		
		this.acID = 'ac_' + this.fld.id;
		this.createList(this.aSug);
	},
	
	createDOMElement: function(type, attr, cont, html) {
		var ne = document.createElement(type);
		
		if (!ne) return 0;
		
		for (var a in attr) ne[a] = attr[a];
		
		var t = typeof(cont);
		
		if (t == "string" && !html) ne.appendChild( document.createTextNode(cont) );
		else if (t == "string" && html) ne.innerHTML = cont;
		else if (t == "object") ne.appendChild( cont );
		
		return ne;
	},
	
	createList:	function(arr) {
		isIE6 = /msie|MSIE 6/.test(navigator.userAgent);
		
		if (isIE6) {
			Element.extend(document.getElementsByTagName("body")[0]);
			document.getElementsByTagName("body")[0].select('select').each(function(s) {s.hide();});
		}
		
		// Remove the old list
		if ($(this.acID)) $(this.acID).remove();
		
		// Clear timeout
		this.killTimeout();
		
		// If no results, and showNoResults is false, do nothing
		if (arr.length == 0 && !this.options.shownoresults) return false;
		
		// Create frame div
		var div	= this.createDOMElement('div', {id:this.acID, className:this.options.className});
		
		// create div header
		var hcorner = this.createDOMElement('div', {className: 'ac_corner'});
		var hbar = this.createDOMElement('div', {className: 'ac_bar'});
		var header = this.createDOMElement('div', {className: 'ac_header'});
		header.appendChild(hcorner);
		header.appendChild(hbar);
		div.appendChild(header);
		
		// Create and populate ul
		var ul = this.createDOMElement('ul', {id:'ac_ul'});
		var p = this;
		
		// no results?
		if (arr.length == 0 && this.options.shownoresults) {
			var li = this.createDOMElement('li', {className: 'ac_warning'}, this.options.noresults );
			ul.appendChild(li);
		} else {
			// loop through arr of suggestions creating an LI element for each of them
			for (var i=0,l = arr.length; i<l; i++) {
				if (arr[i].type != "label") {
					// Format output with the input enclosed in a EM elementFromPoint
					// (as HTML not DOM)
					var val = arr[i].value;
					var st = val.toLowerCase().indexOf(this.sInp.toLowerCase()); // HERE WE CHECK AGAINST THE SPLITTED VALUE IF ANY***
					var output = val.substring(0,st) + '<em>' + val.substring(st,st+this.sInp.length) + '</em>' + val.substring(st+this.sInp.length);
					
					var span = this.createDOMElement('span',{},output,true); // type of, properties, output, isHTML?
					
					if (arr[i].info != '') {
						var br	= this.createDOMElement('br',{});
						span.appendChild(br);
						
						var small = this.createDOMElement('small',{}, arr[i].info);
						span.appendChild(small);
					}
					
					var a = this.createDOMElement('a',{href:'#'});
					
					var tl = this.createDOMElement('span',{className:'tl'},'&nbsp;',true);
					var tr = this.createDOMElement('span',{className:'tr'},'&nbsp;',true);
					
					a.appendChild(tl);
					a.appendChild(tr);
					a.appendChild(span); // add the object span into the link
					
					a.name = i + 1;
					
					a.onclick = function () {
						p.setHighlightedValue();
						return false; 
					};
					
					a.onmouseover = function () {
						p.setHighlight(this.name); 
					};
					
					var li = this.createDOMElement('li', {}, a);
					if (arr[i].type != null) {
						Element.extend(li);
						li.addClassName(arr[i].type);
					}
					
					// Finally add the newly created li element to the ul element
					ul.appendChild(li);
				} else {
					var span = this.createDOMElement('span', {}, arr[i].value, true);
					var tl = this.createDOMElement('span', {className:'tl'}, '&nbsp;', true);
					var tr = this.createDOMElement('span', {className:'tr'}, '&nbsp;', true);
					
					span.appendChild(tl);
					span.appendChild(tr);
					
					var li = this.createDOMElement('li', {}, span);
					if (arr[i].type != null) {
						Element.extend(li);
						li.addClassName(arr[i].type);
					}
					
					ul.appendChild(li);
				}
			}
		}
		
		div.appendChild(ul);
		
		// Create div footer
		var fcorner = this.createDOMElement('div', {className: 'ac_corner'});
		var fbar = this.createDOMElement('div', {className: 'ac_bar'});
		var footer = this.createDOMElement('div', {className: 'ac_footer'});
		footer.appendChild(fcorner);
		footer.appendChild(fbar);
		div.appendChild(footer);
		
		// Get position of target textfield
		// position holding div below it
		// set width of holding div to width of field
		
		var pos = this.fld.cumulativeOffset();
		div.style.left = pos[0] + "px";
		div.style.top = pos[1] + this.fld.offsetHeight + "px";
		
		var w = (this.options.setWidth && this.fld.offsetWidth < this.options.minWidth) ? this.options.minWidth : (this.options.setWidth && this.fld.offsetWidth > this.options.maxWidth) ? this.options.maxWidth : this.fld.offsetWidth;
		
		div.style.width = w + "px";
		
		// Set mouseover functions for div
		// when mouse pointer leaves div, set a timeout to remove the list after an interval
		// when mouse enters div, kill the timeout so the list won't be removed
		//
		div.onmouseover = function() {p.killTimeout();};
		div.onmouseout = function() {p.resetTimeout();};
		
		// Add DIV to document
		document.getElementsByTagName("body")[0].appendChild(div);
		
		// Highlight first item
		if (this.options.highlightFirstItem) {
			list = $('ac_ul');
			var n = 0;
			for(var i=0; i<list.childNodes.length; i++) {
				Element.extend(list.childNodes[i]);
				if (!list.childNodes[i].hasClassName('label')) {
					n = i + 1;
					break;
				}
			}
			this.iHigh = n;
			this.setHighlight(this.iHigh);
		} else {
			this.iHigh = 0;
		}
		
		// remove list after interval
		this.toID = setTimeout(function() {p.clearSuggestions()}, this.options.timeout);
	},
	
	changeHighlight: function(key) {
		var list = $("ac_ul");
		if (!list) return false;
		
		var n;
		
		if (key == Event.KEY_DOWN || key == Event.KEY_TAB) {
			n = this.iHigh + 1;
			
			if (n > list.childNodes.length) n = list.childNodes.length;
			
			Element.extend(list.childNodes[n - 1]);
			while(list.childNodes[n - 1].hasClassName('label')) {
				n++;
				if (n > list.childNodes.length) break;
			}
			
			if (n > list.childNodes.length) {
				for(var i=list.childNodes.length-1; i>=0; i--) {
					Element.extend(list.childNodes[i]);
					if (!list.childNodes[i].hasClassName('label')) {
						n = i + 1;
						break;
					}
				}
			}
		} else {
			n = this.iHigh - 1;
			
			if (n < 1) n = 1;
			
			Element.extend(list.childNodes[n - 1]);
			while(list.childNodes[n - 1].hasClassName('label')) {
				n--;
				
				if (n <= 0) break;
			}
			
			if (n <= 0) {
				for(var i=0; i<list.childNodes.length; i++) {
					Element.extend(list.childNodes[i]);
					if (!list.childNodes[i].hasClassName('label')) {
						n = i + 1;
						break;
					}
				}
			}
		}
		
		n = (n > list.childNodes.length) ? list.childNodes.length : ((n < 1) ? 1 : n);
		
		this.setHighlight(n);
	},
	
	setHighlight:		function(n) {
		var list = $('ac_ul');
		
		if (!list) return false;
		
		if (this.iHigh > 0) this.clearHighlight();
		
		this.iHigh = Number(n);
		
		Element.extend(list.childNodes[this.iHigh-1]);
		
		list.childNodes[this.iHigh-1].addClassName('ac_highlight');
		
		this.killTimeout();
	},
	
	clearHighlight:	function() {
		var list = $('ac_ul');
		
		if(!list) return false;
		
		if(this.iHigh > 0) {
			Element.extend(list.childNodes[this.iHigh-1]);
			list.childNodes[this.iHigh-1].removeClassName('ac_highlight');
			this.iHigh = 0;
		}
	},
	
	setHighlightedValue: function() {
		if (this.iHigh) {
			isIE6 = /msie|MSIE 6/.test(navigator.userAgent);
			
			if (isIE6) {
				Element.extend(document.getElementsByTagName("body")[0]);
				document.getElementsByTagName("body")[0].select('select').each(function(s) {s.show();});
			}
			
			// HERE WE NEED TO IMPLEMENT THE GMAIL LIKE SPLITTED VALUE
			if (!this.aSug[this.iHigh - 1]) return;
			
			// Gmail like
			if (undefined != this.options.valueSep) {
				var str = this.getLastInput(this.fld.value);
				var idx = this.fld.value.lastIndexOf(str);
				str = this.aSug[ this.iHigh -1 ].value + this.options.valueSep;
				this.sInp = this.fld.value = idx == -1 ? str : this.fld.value.substring(0, idx) + str;
			} else {
				var str = this.getLastInput(this.fld.value);
				var idx = this.fld.value.lastIndexOf(str);
				str = this.aSug[ this.iHigh -1 ].value;
				this.sInp = this.fld.value = idx == -1 ? str : this.fld.value.substring(0, idx) + str;
			}
			
			// move cursor to end of input (safari)
			this.fld.focus();
			if(this.fld.selectionStart)
			this.fld.setSelectionRange(this.sInp.length, this.sInp.length);
			
			this.clearSuggestions();
			
			// pass selected object to callback function, if exists
			if (typeof this.options.callback == 'function') this.options.callback(this.aSug[this.iHigh-1]);
		}
	},
	
	killTimeout: function() {
		clearTimeout(this.toID);
	},
	
	resetTimeout: function() {
		this.killTimeout();
		var p = this;
		this.toID = setTimeout(function() {p.clearSuggestions();}, p.options.timeout);
	},
	
	clearSuggestions:	function () {
		this.killTimeout();
		if ($(this.acID)) this.fadeOut(300, function() {$(this.acID).remove();});
	},
	
	fadeOut: function(milliseconds, callback) {
		this._fadeFrom = 1;
		this._fadeTo = 0;
		this._afterUpdateInternal = callback;
		
		this._fadeDuration	= milliseconds;
		this._fadeInterval = 50;
		this._fadeTime = 0;
		var p = this;
		this._fadeIntervalID = setInterval(function() {p._changeOpacity();}, this._fadeInterval);
	},
	
	_changeOpacity: function() {
		if (!$(this.acID)) {
			this._fadeIntervalID = clearInterval(this._fadeIntervalID);
			return;
		} 
		this._fadeTime += this._fadeInterval;
		
		var ieop = Math.round((this._fadeFrom + ((this._fadeTo - this._fadeFrom) * (this._fadeTime/this._fadeDuration))) * 100);
		var op = ieop / 100;
		
		var el = $(this.acID);
		if (el.filters) {
			// internet explorer
			try {
				el.filters.item("DXImageTransform.Microsoft.Alpha").opacity = ieop;
			} catch (e) { 
				// If it is not set initially, the browser will throw an error.
				// This will set it if it is not set yet.
				el.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + ieop + ')';
			}
		} else {
			el.style.opacity = op;
		}
		
		if (this._fadeTime >= this._fadeDuration) {
			clearInterval(this._fadeIntervalID);
			if (typeof this._afterUpdateInternal == 'function') this._afterUpdateInternal();
		}
	},
	
	enable: function(status) {
		this.enabled = status;
		
		if (this.enabled) {
			this.sInp = '';
			this.getSuggestions(this.fld.value);
		} else {
			this.clearSuggestions();
		}
	}
}

/**
* Ajax.Request.abort
* extend the prototype.js Ajax.Request object so that it supports an abort method
*/
Ajax.Request.prototype.abort = function() {
    // prevent and state change callbacks from being issued
    this.transport.onreadystatechange = Prototype.emptyFunction;
    // abort the XHR
    this.transport.abort();
    // update the request counter
    Ajax.activeRequestCount--;
};
