/**
 * Author and Version Information {{{ author: Antonio Ramirez
 * http://webeaters.blogspot.com
 *
 * class: AutoComplete for Prototype 1.6.0
 *
 * version: 1.2.1 - 2007-11-11 (based on AutoSuggest 2.1.3 - 2007-07-19)
 * version: 1.3.0 - 2008-01-03 by Andrew Nicols <andrew@nicols.co.uk> - Fixed
 * incorrect title-casing - CSS is Case Sensitive!!! - Adjusted the way in which
 * the Notifier images are loaded. - Changed json code to pass all json
 * variables back instead of just id, value and name - Fixed 'GMAIL' code such
 * that if valueSep is undefined, it is ignored - Changed the default for
 * valueSep to null - Fixed the resetTimeout function
 *
 * REFERENCES AND THANKS this class is based on the work in AutoSuggest.js of
 * Timothy Groves - http://www.brandspankingnew.net and adapted for use with
 * prototype 1.6.0
 *
 * UPDATED by R��da HADJOUTI GMAIL like AutoComplete (semicolon separator)
 * Update
 *
 * }}}
 */

var AutoComplete = Class.create();

AutoComplete.prototype = {
	Version : '1.3.0',
	REQUIRED_PROTOTYPE : '1.6.0',

	initialize : function(id, param) {
		// check whether we have the appropiate javascript libraries
	this.PROTOTYPE_CHECK();

	// Get the field we're watching.
	// It needs to be a valid field so throw an error if it's not valid or can't
	// be found.
	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

	// Parameter Handling {{{
	// Set the use specified options
	this.options = param ? param : {};
	// These are the default settings {{{
	var k, def = {
		valueSep : null,
		minchars : 2,
		meth : "get",
		varname : "input",
		className : "autocomplete",
		timeout : 30000,
		delay : 200,
		offsety : -5,
		shownoresults : true,
		noresults : 'Keine Treffer',
		maxheight : 250,
		cache : true,
		maxentries : 25,
		onAjaxError : null,
		setWidth : false,
		minWidth : 100,
		maxWidth : 300,
		useNotifier : true
	};

	// this.options.noresults = this.options.sNoResult;
	// }}}
	// 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];
	}
	// End of Parameter Handling }}}

	// Not everyone wants to use the Notifier. Give them the option
	if (this.options.useNotifier) {
		this.fld.addClassName('ac_field');
	}

	// set keyup handler for field
	// and prevent AutoComplete from client
	var p = this;

	// NOTE: not using addEventListener because UpArrow fired twice in Safari
	this.fld.onkeypress = function(ev) {
		return p.onKeyPress(ev);
	};
	this.fld.onkeyup = function(ev) {
		return p.onKeyUp(ev);
	};
	// ARN-DEBUG Chances are we want to reset the timeout when they lose focus,
	// at least that's what I prefer
	this.fld.onblur = function(ev) {
		p.resetTimeout();
		return true;
	};
	// ARN-DEBUG Not sure what this is about!
	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);
},

/*
 * set responses to keypress events in the field this allows the user to use the
 * arrow keys to scroll through the results ESCAPE clears the list RETURN sets
 * the current highlighted value UP/DOWN move around the list
 */

onKeyPress : function(e) {
	if (!e)
		e = window.event;
	var key = e.keyCode || e.wich;

	switch (key) {
	case Event.KEY_RETURN:
		this.setHighlightedValue();
		Event.stop(e);
		break;
	case Event.KEY_TAB:
		this.setHighlightedValue();
		break;
	case Event.KEY_ESC:
		this.clearSuggestions();
		break;
	}
	return true;
},

onKeyUp : function(e) {
	if (!e)
		e = window.event;

	var key = e.keyCode || e.wich;

	if (key == Event.KEY_UP || key == Event.KEY_DOWN) {
		this.changeHighlight(key);
		Event.stop(e);
	} else
		this.getSuggestions(this.fld.value);

	return true;
},

getSuggestions : function(val) {
	// input the same? do nothing
	if (val == this.sInp)
		return false;

	// kill the old list
	if ($(this.acID))
		$(this.acID).remove();

	this.sInp = val;

	// input length is less than the min required to trigger a request
	// do nothing
	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 {
		// do new request
		var p = this;
		// var input = this.sInp; // send the converted new value (comma)
		clearTimeout(this.ajID); // ajax id timer
		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
	// do we need to call a function to recreate the url?
	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; // get or post?
	if (this.options.useNotifier) {
		this.fld.removeClassName('ac_field');
		this.fld.addClassName('ac_field_busy');
	}
	;

	var options = {
		method : m,
		onSuccess : function(req) {
			if (p.options.useNotifier) {
				p.fld.removeClassName('ac_field_busy');
				p.fld.addClassName('ac_field');
			}
			;
			p.setSuggestions(req, input);
		},

		onFailure : (typeof p.options.onAjaxError == 'function') ? function(status) {
			if (p.options.useNotifier) {
				p.fld.removeClassName('ac_field_busy');
				p.fld.addClassName('ac_field');
			}
			p.options.onAjaxError(status)
		} :

		function(status) {
			if (p.options.useNotifier) {
				p.fld.removeClassName('ac_field_busy');
				p.fld.addClassName('ac_field');
			}
			alert("AJAX error: " + status);
		}
	}
	// make new ajax request
	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++) {
			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) {
	// get rid of the old list if any
	if ($(this.acID))
		$(this.acID).remove();

	// clear list removal timeout
	this.killTimeout();

	// if no results, and showNoResults is false, do nothing
	if (arr.length == 0 && !this.options.shownoresults)
		return false;

	// create holding div
	var div = this.createDOMElement('div', {
		id : this.acID,
		className : this.options.className
	});

	// create div header
	var header = this.createDOMElement('div', {
		className : 'ac_header'
	});
	div.appendChild(header);

	// create and populate ul
	var ul = this.createDOMElement('ul', {
		id : 'ac_ul'
	});
	var p = this; // pointer that we will need later on
	// 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++) {

			var wrap = this.createDOMElement('span', {
				className : 'wrap'
			}, '');

			// format output with the input enclosed in a EM
			// elementFromPoint
			// (as HTML not DOM)
			var val = arr[i].value;
			key = arr[i].value;
			var st = val.toLowerCase().indexOf(this.sInp.toLowerCase());
			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', {
				className : 'value'
			}, output, true); // type of, properties, output, isHTML?
			wrap.appendChild(span);

			if (arr[i].curr > 0) {
				var span = this.createDOMElement('span', {
					className : 'curr'
				}, arr[i].curr + ' ' + p.options.currency);
				wrap.appendChild(span);
			}

			// do we need to add extra info?
			if (arr[i].info != '') {
				var span = this.createDOMElement('span', {
					className : 'info'
				}, arr[i].info);
				wrap.appendChild(span);
			}

			if (arr[i].num != '') {
				var span = this.createDOMElement('span', {
					className : 'num'
				}, arr[i].num + ' ' + p.options.num_text);
				wrap.appendChild(span);
			}

			var a = this.createDOMElement('a', {
				href : '#'
			});
			a.appendChild(wrap); // 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); // add the link
			// element to a
			// li element

			// finally add the newly created li element to the ul element
			ul.appendChild(li);
		}
	}

	div.appendChild(ul); // add the newly created list to the div element

	// get position of target textfield
	// position holding div below it
	// set width of holding div to width of field
	// if

	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
	this.iHigh = 1;
	this.setHighlight(1);

	// 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;
	n = (key == Event.KEY_DOWN || key == Event.KEY_TAB) ? this.iHigh + 1 : this.iHigh - 1;
	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);
	list.childNodes[this.iHigh - 1].className = 'ac_highlight';
	this.killTimeout();
},

clearHighlight : function() {
	var list = $('ac_ul');
	if (!list)
		return false;
	if (this.iHigh > 0) {
		list.childNodes[this.iHigh - 1].className = '';
		this.iHigh = 0;
	}
},

setHighlightedValue : function() {
	if (this.iHigh) {
		// 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]); // the
	// object
	// has the
	// properties
	// we want,
	// it will
	// depend of
}
},

killTimeout : function() {
clearTimeout(this.toID);
},

resetTimeout : function() {
this.killTimeout();
var p = this;
this.toID = setTimeout(function() {
	p.clearSuggestions();
}, p.options.timeout);
// ARN-DEBUG Added p.options.timeout back :|
},

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();
}

}

}

// vim: set filetype=javascript foldmethod=marker foldlevel=5:

