/*
---
name: Autocomplete
description: Autocomplete, autocompletion/autosuggestion for text input fields.
license: MIT license
authors: [Harald Kirschner]
requires: [Core/*]
provides: [Autocomplete]
...
*/

var Autocomplete = new Class({

	Implements: [Options, Events],

	name: 'autocomplete',

	options: {/*
		indicator: null,
		onOver: function(){},
		onSelect: function(){},
		onSelection: function(){},
		onShow: function(){},
		onHide: function(){},
		onBlur: function(){},
		onFocus: function(){},
		onRequest: function(){},
		onComplete: function(){},*/

		minLength: 1,
		maxChoices: 10,
		injectChoice: null,

		delay: 400,
		autoSubmit: false,
		cache: true,

		selectFirst: false,
		forceSelect: false,

		caseSensitive: false,
		matchSubset: true,

		multiple: false,
		separator: ', ',
		separatorSplit: /\s*[,;]\s*/,

		url: window.location + '',
		postData: {},
		requestOptions: {},
		postVar: 'value',

		className: 'autocomplete',
		indicatorClass: 'loading',
		choicesVisible: true,
		overflow: false,
		overflowMargin: 25,
		zIndex: 500,
		fxOptions: {}
	},

	initialize: function(element, options){
		this.element = document.id(element);
		this.form = this.element.getParent('form');
		this.setOptions(options);
		this.build();
		this.element.addEvent('keyup', this.onInputChange.bind(this));
		this.queryValue = null;
		this.cached = [];
		this.request = new Request.JSON(Object.merge({
			'url': this.options.url,
			'link': 'cancel',
			'onComplete': this.queryResponse.bind(this)
		}, this.options.requestOptions));
	},

	build: function(){
		this.choices = new Element('ul', {
			'class': this.options.className,
			'styles': {
				'zIndex': this.options.zIndex
			}
		}).inject(this.element, 'after');
		this.relative = this.element.getOffsetParent();
		this.shim = new IframeShim(this.choices);

		if (!this.options.separator.test(this.options.separatorSplit)){
			this.options.separatorSplit = this.options.separator;
		}
		this.fx = new Fx.Tween(this.choices, Object.merge({
			'property': 'opacity',
			'link': 'cancel',
			'duration': 200
		}, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0);
		this.element.setProperty('autocomplete', 'off')
			.addEvent((Browser.ie || Browser.chrome || Browser.safari) ? 'keydown' : 'keypress', this.onCommand.bind(this))
			.addEvents({
				'click': this.onCommand.bind(this, false),
				'focus': function(){
					this.toggleFocus.delay(100, this, true);
				}.bind(this),
				'blur': function(){
					this.toggleFocus.delay(100, this, false);
				}.bind(this)
			});
	},

	destroy: function(){
		if (this.shim) this.shim.destroy();
		this.choices = this.selected = this.choices.destroy();
	},

	toggleFocus: function(state){
		this.focussed = state;
		if (!state) this.hideChoices(true);
		this.fireEvent((state) ? 'focus' : 'blur', [this.element]);
	},

	onCommand: function(e){
		if (!e && this.focussed) return this.prefetch();
		if (e && e.key && !e.shift){
			switch (e.key){
				case 'enter':
					if (this.element.value != this.opted) return true;
					if (this.selected && this.visible){
						this.choiceSelect(this.selected);
						return !!(this.options.autoSubmit);
					}
					break;
				case 'up':
				case 'down':
					if (!this.prefetch() && this.queryValue !== null){
						var up = (e.key == 'up');
						this.choiceOver((this.selected || this.choices)[
							(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')
						](), true);
					}
					return false;
				case 'esc':
				case 'tab':
					this.hideChoices(true);
					break;
			}
		}
		return true;
	},

	setSelection: function(finish){
		var input = this.selected.inputValue, value = input;
		var start = this.queryValue.length, end = input.length;
		if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0;
		if (this.options.multiple){
			var split = this.options.separatorSplit;
			value = this.element.value;
			start += this.queryIndex;
			end += this.queryIndex;
			var old = value.substr(this.queryIndex).split(split, 1)[0];
			value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length);
			if (finish){
				var tokens = value.split(this.options.separatorSplit).filter(function(entry){
					return this.test(entry);
				}, /[^\s,]+/);
				var sep = this.options.separator;
				value = tokens.join(sep) + sep;
				end = value.length;
			}
		}
		this.element.setProperty('value', value);
		this.opted = value;
		if (finish) start = end;
		this.selectRange(this.element, start, end);
		this.fireEvent('selection', [this.element, this.selected, value, input]);
	},

	showChoices: function(){
		var first = this.choices.getFirst();
		this.selected = this.selectedValue = null;
		if (this.shim){
			var pos = this.element.getCoordinates(this.relative);
			var borders = this.choices.getStyles('border-left-width', 'border-right-width');
			this.choices.setStyles({
				'left': pos.left,
				'top': pos.bottom,
				'min-width': pos.width - borders['border-left-width'].toInt() - borders['border-right-width'].toInt()
			});
		}
		if (!first) return;
		if (!this.visible){
			this.visible = true;
			this.choices.setStyle('display', null);
			this.fx.start(1);
			this.fireEvent('show', [this.element, this.choices]);
		}
		if (this.options.selectFirst || first.inputValue == this.queryValue) this.choiceOver(first, true);
		var items = this.choices.getChildren(), max = this.options.maxChoices;
		var styles = {'overflowY': 'hidden', 'height': ''};
		this.overflown = false;
		if (items.length > max){
			var item = items[max - 1];
			styles.overflowY = 'scroll';
			styles.height = item.getCoordinates(this.choices).bottom;
			this.overflown = true;
		};
		this.choices.setStyles(styles);
		this.shim.show();
		if (this.options.choicesVisible){
			var scroll = document.getScroll(),
			size = document.getSize(),
			coords = this.choices.getCoordinates();
			if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x;
			if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y;
			window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top));
		}
	},

	hideChoices: function(clear){
		if (clear){
			var value = this.element.value;
			if (this.options.forceSelect) value = this.opted;
			this.element.setProperty('value', value);
		}
		if (!this.visible) return;
		this.visible = false;
		if (this.selected) this.selected.removeClass('selected');
		clearTimeout(this.timeout || null);
		var hide = function(){
			this.choices.setStyle('display', 'none');
			this.shim.hide();
		}.bind(this);
		this.fx.start(0).chain(hide);
		this.fireEvent('hide', [this.element, this.choices]);
	},

	onInputChange: function(){
		clearTimeout(this.timeout || null);
		this.timeout = this.prefetch.delay(this.options.delay, this);
	},

	prefetch: function(){
		var value = this.element.value, query = value;
		if (this.options.multiple){
			var split = this.options.separatorSplit;
			var values = value.split(split);
			var index = this.getSelectedRange(this.element).start;
			var toIndex = value.substr(0, index).split(split);
			var last = toIndex.length - 1;
			index -= toIndex[last].length;
			query = values[last];
		}
		if (query.length < this.options.minLength){
			this.hideChoices();
		} else {
			if (query === this.queryValue || (this.visible && query == this.selectedValue)){
				if (this.visible) return false;
				this.showChoices();
			} else {
				this.queryValue = query;
				this.queryIndex = index;
				if (!this.fetchCached()) this.query();
			}
		}
		return true;
	},

	fetchCached: function(){
		return false;
		if (!this.options.cache
			|| !this.cached
			|| !this.cached.length
			|| this.cached.length >= this.options.maxChoices
			|| this.queryValue) return false;
		this.update(this.filter(this.cached));
		return true;
	},

	update: function(tokens){
		this.choices.empty();
		this.cached = tokens;
		var type = tokens && typeOf(tokens);
		if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())){
			this.hideChoices();
		} else {
			if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices;
			tokens.each(this.options.injectChoice || function(token){
				var choice = new Element('li', {'html': this.markQueryValue(token)});
				choice.inputValue = token;
				this.addChoiceEvents(choice).inject(this.choices);
			}, this);
			this.showChoices();
		}
	},

	choiceOver: function(choice, selection){
		if (!choice || choice == this.selected) return;
		if (this.selected) this.selected.removeClass('selected');
		this.selected = choice.addClass('selected');
		this.fireEvent('select', [this.element, this.selected, selection]);
		if (!selection) return;
		this.selectedValue = this.selected.inputValue;
		if (this.overflown){
			var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin,
				top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height;
			if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0);
			else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom);
		}
		this.setSelection();
	},

	choiceSelect: function(choice){
		if (choice) this.choiceOver(choice);
		this.setSelection(true);
		this.queryValue = false;
		this.hideChoices();
		if (this.form && this.options.autoSubmit) this.form.submit();
	},

	filter: function(tokens){
		return (tokens || this.tokens).filter(function(token){
			return this.test(token);
		}, new RegExp(((this.options.matchSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.caseSensitive) ? '' : 'i'));
	},

	markQueryValue: function(str){
		return str.replace(new RegExp('(' + ((this.options.matchSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.caseSensitive) ? '' : 'i'), '<span class="query">$1</span>');
	},

	addChoiceEvents: function(el){
		return el.addEvents({
			'mouseover': this.choiceOver.bind(this, el),
			'click': this.choiceSelect.bind(this, el)
		});
	},

	query: function(){
		var data = Object.clone(this.options.postData) || {};
		data[this.options.postVar] = this.queryValue;
		var indicator = document.id(this.options.indicator);
		if (indicator) indicator.setStyle('display', null);
		var cls = this.options.indicatorClass;
		if (cls) this.element.addClass(cls);
		this.fireEvent('request', [this.element, this.request, data, this.queryValue]);
		this.request.send({'data': data});
	},

	queryResponse: function(response){
		var indicator = document.id(this.options.indicator);
		if (indicator) indicator.setStyle('display', 'none');
		var cls = this.options.indicatorClass;
		if (cls) this.element.removeClass(cls);
		this.update(response);
		return this.fireEvent('complete', [this.element, this.request]);
	},

	getSelectedRange: function(el){
		if (!Browser.ie) return {start: el.selectionStart, end: el.selectionEnd};
		var pos = {start: 0, end: 0};
		var range = el.getDocument().selection.createRange();
		if (!range || range.parentElement() != el) return pos;
		var dup = range.duplicate();
		if (el.type == 'text'){
			pos.start = 0 - dup.moveStart('character', -100000);
			pos.end = pos.start + range.text.length;
		} else {
			var value = el.value;
			var offset = value.length - value.match(/[\n\r]*$/)[0].length;
			dup.moveToElementText(el);
			dup.setEndPoint('StartToEnd', range);
			pos.end = offset - dup.text.length;
			dup.setEndPoint('StartToStart', range);
			pos.start = offset - dup.text.length;
		}
		return pos;
	},

	selectRange: function(el, start, end){
		if (Browser.ie){
			var diff = el.value.substr(start, end - start).replace(/\r/g, '').length;
			start = el.value.substr(0, start).replace(/\r/g, '').length;
			var range = el.createTextRange();
			range.collapse(true);
			range.moveEnd('character', start + diff);
			range.moveStart('character', start);
			range.select();
		} else {
			el.focus();
			el.setSelectionRange(start, end);
		}
		return el;
	}

});

