/**
 * NS Autocomplete controller
 *
 * @version   1.10.100208
 * @author    LBI Lost Boys
 */
NS(function($){

	var ATTR_COMPLETE	= 'ns:complete';
	var ATTR_DEPENDENCY = 'ns:dependency';
	var ATTR_DEPENDNAME = 'ns:dependencyname';
	var TYPE_TEXT		= /text$/i;
	var TYPE_SELECT		= /select/i;
	var MODE_DEFAULT	= 'change';

	NS.Autocomplete = function() {
		this.modes = [];
		this.lists = [];
		this.minimal = 2;

		this.container = $('<select id="autoComplete" size="7"></select>');
		$('body').append(this.container);

		NS.subscribe('click', this.handleClick.bind(this));

		this.container.bind('blur', this.select.bind(this));
		this.container.bind('keydown', this.navigate.bind(this));
		this.parseNode(document);
	};

	NS.Autocomplete.prototype = {
		setMode:function(type, value){ this.modes[type] = value; },
		getMode:function(type) { return this.modes[type] || MODE_DEFAULT; },
		
		parseNode:function(node) {
			var forms = $('form', node);
				forms.attr('autocomplete', 'off');

			var root = forms[0]? forms : node;
			var inputs = $('input:text, select', root).filter(function(){
				return this.getAttribute(ATTR_COMPLETE)? true : false;
			});

			this.addElements(inputs);
		},
		
		// enables elements for autocompletion, may be a mixed array of inputs and selects
		addElements:function(inputs){
			inputs.each(function(index, input){
				var type = input.type;
				var completion = input.getAttribute(ATTR_COMPLETE);
				var dependency = input.getAttribute(ATTR_DEPENDENCY);

				if(dependency) {
					this.setMode(completion, MODE_DEFAULT);
				}

				if(TYPE_TEXT.test(type)) {
				// inputs are provided with a set of events
					var jInput = $(input);
					input.setAttribute('autocomplete', 'off');
					jInput.bind('keyup', this.keyup.bind(this));
					jInput.bind('keydown', this.keydown.bind(this));
					jInput.bind('keypress', this.keypress.bind(this));
					jInput.bind('focus', this.focus.bind(this));
				} else if(TYPE_SELECT.test(type)) {
				// selects are filled immediately
					this.getValues(input);
				}
			}.bind(this));
		},

		// main loader for autocomplete lists, either loads a response via ajax,
		// or uses a buffered list based on the first 2 chars of input.
		getValues:function(input){
			var value = input.value;
			var completion = input.getAttribute(ATTR_COMPLETE);
			var dependency = input.getAttribute(ATTR_DEPENDENCY);

			if(value.length < this.minimal){
				return;
			}

			var buffer = value.substring(0, this.minimal);
			var dependencyValue, depends = false;

			if(dependency) {
				depends = input.form.elements[dependency];
				dependencyValue = depends.value;
			}
			
			var defined = NS.Autocomplete.staticLists[completion];
			if(defined) {
				this.suggestValues(input, defined);			
			} else if(input.buffer != buffer || input.dependency != dependencyValue) {
				
				input.buffer = buffer;
				input.dependency = dependencyValue;
				
				var url = NS.getProperty('POST_AUTOCOMPLETE', input.form),
					post = 'type=' + completion + '&value=' + encodeURIComponent(value);

				if(depends) {
					var dependencyName = depends.getAttribute(ATTR_DEPENDNAME) || dependency;					
					post += '&' + dependencyName + '=' + encodeURIComponent(dependencyValue);
				}
				
				if(this.connection) {
					NS.XHR.abort(this.connection);
				}
				
				this.connection = NS.XHR.sendAndLoad(post, url, function(response, status){
					if (status !== 200) {
						var lang = NS.getLanguage();
						var msg = NS.getProperty(
							/nl/i.test(lang)? 'MSG_AUTOCOMPLETE' : 'MSG_AUTOCOMPLETE_EN'
						);
						if (this.usesAbbr(input)) {
							input.value = msg;
							input.setAttribute('disabled', 'disabled');
							input.blur();
						}
						return;
					}
					this.handleResponse(response, input);
				}.bind(this));
			
			} else {
				var list = this.lists[completion];
				if (list) {
					this.suggestValues(input, list);
				}
			}
		},

		handleResponse:function(response, input) {
			this.connection = false;
			var completion = input.getAttribute(ATTR_COMPLETE);
			var list = new AutoCompleteList(response, 'xml');
			
			this.lists[completion] = list;
			this.suggestValues(input, list);
		},
		
		suggestValues:function(input, list){
			var type = input.type;
			var select = this.container[0];
			var filtered = list.options;

			if(TYPE_TEXT.test(type)) {
			// for text inputs, the list is filtered on the current input's value
				filtered = list.filter(input.value);
				if(filtered.length === 0) {
					return;
				} else {
					select.innerHTML = '';
				}
			} else if(TYPE_SELECT.test(type)){
			// for selects, the select itself is used for (unfiltered) output
				select = input;
				var first = select.options[0].cloneNode(true);
				select.innerHTML = '';
				select.appendChild(first);
			}
			
			// build the new list, this may contain optgroups
			var optGroup, currentGroup = null;
			for(var i=0; i<filtered.length; i++) {
				var item = filtered[i], group = item.group;
				var opt = document.createElement('option');
				opt.value = item.value || item.label;
				opt.innerHTML = item.label;
				$(opt).attr('ns:abbr', item.abbr);

				if(group) {
					if(!optGroup || group != currentGroup) {
						optGroup = document.createElement('optgroup');
						optGroup.label = group;
						select.appendChild(optGroup);
					}
					optGroup.appendChild(opt);
					currentGroup = group;
				} else {
					optGroup = null;
					select.appendChild(opt);
				}
			}

			// only show the autocomplete for text inputs.
			if(TYPE_TEXT.test(type)) {
				this.open();
			}
		},

		handleClick:function(e) {
			var target = e.target;
			var select = $(target).closest('select')[0];

			if(select == this.container[0]) {
				this.select(e);
			} else {
				this.close();
			}
		},

		// selects the value, and fires an autocompleted event.
		select:function(e){
			var select = this.container[0];
			var index = Math.max(select.selectedIndex, 0);
			var option = select[index];
			
			if(option) {
				var value = select[index].text;
				if(this.currentInput.value != value) {
					e.preventDefault(); 
					this.currentInput.value = value;
					var hidden = this.usesAbbr(this.currentInput);
					if(hidden) {
						hidden.value = $(select[index]).attr('ns:abbr');
					}
					this.currentInput.focus();
					NS.Dispatcher.fire('change', this.currentInput);
				}
				this.close(); 
			}
		},
		
		usesAbbr:function(input){
			var prev = $(input).prev()[0];
			return (prev && prev.type === 'hidden') ? prev : false;
		},

		keyup:function(e) {
			var key = e.keyCode;
			if (key < 65 && (key !== 8)) {
				return;
			}

			this.getValues(e.target);
		},

		keydown:function(e) {
			var key = e.keyCode;
			switch (key) {
				case 40: this.open(e, true); break; // down
				case 38: this.close(); break; // up
				case 27: this.close(); break; // escape
				case 9:  this.select(e); break; // tab
			}
		},
		
		keypress:function(e) {
			var input = e.target,
				abbr = this.usesAbbr(input);
			if (abbr) {
				abbr.value = '';
			}
			$(input).unbind('keypress');
		},

		focus:function(e) {
			var input = e.target;
			if(input && input.getAttribute(ATTR_COMPLETE)) {				
				if(this.connection) {
					NS.XHR.abort(this.connection);
				}
				this.currentInput = input;
			}
		},
		
		navigate:function(e){
			var key = e.keyCode;
			switch (key) {
				case 27: this.close(); break;	// escape
				case 9:  this.select(e); break;	// tab
				case 13: this.select(e); break;	// enter
			}
		},

		open:function(e, focus){
			var input = this.currentInput;
			var offset = $(input).offset();
			var width = input.offsetWidth;
			var height = input.offsetHeight;

			this.container.css({
				left: offset.left + 'px',
				top: (offset.top + height) + 'px',
				width: Math.max(width, 200) + 'px'
			}).show();

			if(focus) {
				var val = input.value;
				input.value = '';
				this.container[0].focus();
				input.value = val;
			}
		},
		
		close:function(e){
			var related = e? e.relatedTarget : null;
			if(related == this.currentInput || related == this.container) {
				return;
			}

			this.container.hide();
			this.container[0].options.length = 0;
		}
	};

	/**
	 * Static method for predefining autocomplelists. An autocompleted field that
	 * uses a statically added list will not trigger an ajax request for a dynamic
	 * response. This is handy for relatively smaller lists.
	 */
	NS.Autocomplete.staticLists = {};
	NS.Autocomplete.define = function(type, data) {
		var list = new AutoCompleteList(data, 'array');
		this.staticLists[type] = list;
	};

	/**
	 * Private Autocomplete list, parses the xml into a more usable object format, and
	 * provides a filter method to reduce the amount of matches when typing.
	 */
	function AutoCompleteList(data, type) {
		this.options = [];
		this.error = null;

		switch (type) {
			case 'xml':
				this.parseXML(data);
			break;
			case 'array':
				this.parseArray(data);
			break;
			default:
				this.parseXML(data);
			break;
		}
	}

	AutoCompleteList.prototype = {
		parseXML:function(xml) {
			try { this.error = xml.getElementsByTagName("error")[0].firstChild.nodeValue; } catch(e){}
			try { this.subject = xml.getElementsByTagName("subject")[0].firstChild.nodeValue; } catch(s){}
			
			this.options = [];
			var items = xml.getElementsByTagName("item"),
				trueReg = /true/i,
				groupReg = /group/i;

			for(var i=0; i<items.length; i++) {
				var group = null, item = items[i],
					label = item.firstChild.nodeValue,
					value = item.getAttribute('value'),
					abbr = item.getAttribute('abbr');

				if(groupReg.test(item.parentNode.nodeName)) {
					group = item.parentNode.getAttribute('label');
				}
				
				var selected = trueReg.test(items[i].getAttribute('selected'));
				this.options.push({ 
					label:label, value:value, abbr:abbr, selected:selected, group:group
				});
			}
		},

		parseArray:function(data) {
			var l = data.length;
			for (var i=0; i<l; i++) {
				var value = data[i];
				this.options.push({
					label: value,
					value: value,
					abbr: abbr
				});
			}
		},

		filter:function(value) {
			var result = [];
			try {
				var reg = new RegExp('^'+value, 'i');
				result = this.filterOptions(value, reg);
				if(result.length === 0) {
					reg = new RegExp(value, 'i');
					result = this.filterOptions(value, reg);
				}
			} catch(e){}
			return result;
		},

		filterOptions:function(value, reg) {
			var result = [], options = this.options;
			for(var i=0; i<options.length; i++) {
				var option = options[i];
				if(reg.test(option.label)) {
					result[result.length] = option;
				}
			}
			return result;
		}
	};

});

