/**
 * jQuery cem suggestion plugin
 *
 * (C) 2011 - Boxalino AG
 */

(function($) {
	$.fn.cemComplete = function(options) {
		this.each(
			function() {
				// update state
				var state = $(this).data('cemComplete.state') || {};

				// update options
				state.options = $.extend(
					state.options || {
						// options
						enabled: !$(this).attr('readonly'),
						debug: true,
						cache: true,
						alwaysSearch: true,
						fillOnHighlight: true,
						allowDefaultQuery: true,
						allowEmptyQuery: true,
						clearOnSearch: false,
						opacity: 1.0,
						minLength: 1,
						openDelay: 30000,
						sourceDelay: 100,
						fadeDelay: 250,
						closeDelay: 1000,
						appendTo: 'body',

						// demo handlers
						source: function(query, callback) {
							var task = setTimeout(
								function() {
									var list = [];

									for (var i = 0; i < 100; i++) {
										list.push(query + ' ' + i);
									}
									callback(list);
								},
								500
							);

							return function() { clearTimeout(task); };
						},
						render: function(query, data) {
							var suggestions = $('<ul></ul>').addClass('queries');
							var self = this;

							this.index = -1;
							this.size = data.length;
							for (var i = 0; i < data.length; i++) {
								$('<li></li>')
									.data('index', i)
									.data('value', data[i])
									.text(data[i])
									.hover(
										function() {
											self.highlight('mouse', $(this).data('index'), true);
										},
										function() {
											self.highlight('mouse', -1, true);
										}
									).click(
										function() {
											self.highlight('mouse', $(this).data('index'), true);
											if (self.select('mouse')) {
												self.complete();
											}
										}
									).appendTo(suggestions);
							}

							this.menu.empty()
								.css( {
									'top': this.input.offset().top + this.input.outerHeight() - 1,
									'left': this.input.offset().left + (this.input.outerWidth() - this.input.innerWidth()) / 2 - 1,
									'width': 'auto',
									'background-color': this.input.css('background-color')
								} );

							this.menu.append(suggestions)

							if (this.input.outerWidth() > this.menu.outerWidth()) {
								this.menu.css( {
									'width': this.input.outerWidth()
								} );
							} else if (this.input.outerWidth() < this.menu.outerWidth()) {
								$('<div class="cem-autocomplete-border"></div>')
									.append(
										$('<div></div>')
											.css( {
												'width': this.menu.outerWidth() - this.input.outerWidth()
											} )
									)
									.prependTo(this.menu);
							}
							return (this.size > 0);
						},
						scroll: function(offset) {
							var queries = $('ul.queries', this.menu);
							var rowHeight = $('li:first', queries).outerHeight();
							var pageSize = parseInt(queries.innerHeight() / rowHeight) + 1;

							this.highlight('keyboard', offset * pageSize);
						},
						highlight: function(source, offset, absolute) {
							var queries = $('ul.queries', this.menu);

							$('li.highlighted', queries).removeClass('highlighted');

							this.highlightSource = source;
							if (absolute) {
								this.index = offset;
							} else {
								this.index += offset;
							}
							if (this.index < 0) {
								if (absolute) {
									this.index = -1;
									this.input.focus();
								} else {
									this.index = 0;
								}
							} else if (this.index >= this.size) {
								this.index = this.size - 1;
							}
							if (this.index >= 0) {
								var selection = $('li:eq(' + this.index + ')', queries);

								if (selection.length == 0) {
									queries.scrollTop(0);
									return;
								}
								selection.addClass('highlighted');

								var scrollPosition = queries.scrollTop();
								var position = selection.position().top;

								if (position < 0) {
									queries.scrollTop(scrollPosition + position);
								} else if (position + selection.outerHeight() > queries.innerHeight()) {
									queries.scrollTop(scrollPosition + position + selection.outerHeight() - queries.innerHeight());
								}

								this.highlightCounter++;
								if (source == 'keyboard' && this.options.fillOnHighlight) {
									this.highlightSource = 'none';
									this.selectCounter++;
									this.input.val(selection.data('value'));
								}
							}
						},
						select: function(source) {
							var selection = $('ul.queries > li.highlighted', this.menu);

							if (selection.length > 0) {
								this.selectCounter++;
							}
							if (source == 'keyboard') {
								if (selection.length == 0 ||
									this.input.val() == selection.data('value') ||
									this.highlightSource == 'mouse' ||
									this.highlightSource == 'none'
								) {
									this.search(this.input.val());
									return false;
								}
							} else if (source == 'mouse') {
								if (selection.length == 0) {
									return false;
								}
								if (this.input.val() == selection.data('value')) {
									this.search(this.input.val());
									return false;
								}
							}
							this.input.val(selection.data('value'));

							if (this.options.alwaysSearch) {
								this.search(this.input.val());
								return false;
							}
							return true;
						},
						search: function(query) {
							this.close(true);

							if (!this.options.allowEmptyQuery && query.length == 0) {
								return;
							}
							if (!this.options.allowDefaultQuery && query.length > 0 && query == this.defaultValue) {
								return;
							}
							if (this.options.clearOnSearch) {
								this.input.val('');
							}

							// TODO: trigger search

							this.highlightCounter = 0;
							this.selectCounter = 0;
						}
					},
					options
				);

				// update internal variables
				if (state.options.cache) {
					state.cache = state.cache || {};
				} else {
					delete state.cache;
				}
				state.highlightCounter = state.highlightCounter || 0;
				state.selectCounter = state.selectCounter || 0;

				// update menu
				state.menu = state.menu || $('<div></div>')
					.addClass('cem-autocomplete-menu')
					.hide()
					.css( {
						'position': 'absolute',
						'opacity': state.options.opacity,
						'z-index': isNaN(parseInt($(this).css('z-index'))) ? 1 : parseInt($(this).css('z-index'))
					} ).hover(
						function() {
							state.hover = true;
							state.open.call(state);
						},
						function() {
							state.hover = false;
							state.close.call(state, false, true);
						}
					).appendTo($(state.options.appendTo));

				// update input element
				if (state.input == undefined) {
					state.defaultValue = $(this).val();
					state.input = $(this).addClass('cem-autocomplete-input')
						.attr( {
							'role': 'textbox',
							'autocomplete': 'off',
							'aria-autocomplete': 'list',
							'aria-haspopup': 'true'
						} ).hover(
							function() {
								state.hover = true;
								if (!state.focus) {
									state.open.call(state, true);
								}
							},
							function() {
								state.hover = false;
								state.close.call(state, false, true);
							}
						).bind(
							'focus',
							function(event) {
								state.focus = true;
								state.open.call(state);
							}
						).bind(
							'blur',
							function(event) {
								state.focus = false;
								state.close.call(state, false, true);
							}
						).bind(
							'click',
							function(event) {
								state.open.call(state);
							}
						).bind(
							'keydown',
							function(event) {
								if (state.options.enabled) {
									state._key.call(state, event, true);
								}
							}
						).bind(
							'keypress',
							function(event) {
								if (state.options.enabled) {
									switch(event.keyCode) {
//									case 9: // tab
									case 13:  // enter
//									case 27: // escape
									case 33: // page up
									case 34: // page down
									case 38: // up
									case 40: // down
//									case 108: // numpad enter
										event.preventDefault();
										return;
									}
								}
							}
						);
				}

				// open handler (public)
				state.open = state.open || function(delayed) {
					if (this.options.enabled) {
						if (this.closeTask) {
							clearTimeout(this.closeTask);
							this.closeTask = null;
						}
						if (!this.active) {
							if (this.openTask) {
								clearTimeout(this.openTask);
								this.openTask = null;
							}
							if (delayed) {
								this.openTask = setTimeout(function() { state._open.call(state); }, this.options.openDelay);
							} else {
								this._open();
							}
						}
					}
				};

				// complete handler (public)
				state.complete = state.complete || function(force) {
					this.abort();
					this.searchTask = setTimeout(function() { state._completeAsync.call(state, force); }, this.options.sourceDelay);
				};

				// abort handler (public)
				state.abort = state.abort || function() {
					if (this.sourceTaskAbort) {
						if (this.options.debug) {
							this.sourceTaskAbort();
						} else {
							try { this.sourceTaskAbort(); } catch (e) { }
						}
						this.sourceTaskAbort = null;
					}
					if (this.searchTask) {
						clearTimeout(this.searchTask);
						this.searchTask = null;
					}
				};

				// close handler (public)
				state.close = state.close || function(force, delayed) {
					if (this.options.enabled && (force || !(this.hover || this.focus))) {
						if (this.openTask) {
							clearTimeout(this.openTask);
							this.openTask = null;
						}
						if (this.active) {
							if (this.closeTask) {
								clearTimeout(this.closeTask);
								this.closeTask = null;
							}
							if (delayed) {
								this.closeTask = setTimeout(function() { state._close.call(state); }, this.options.closeDelay);
							} else {
								this._close();
							}
						}
					}
				};

				// update user-defined handlers
				var userHandlerWrapper = function(handler) {
					return function() {
						if (state.options.debug) {
							return handler.apply(state, arguments);
						}
						try { return handler.apply(state, arguments); } catch (e) { }
					}
				};

				state.source    = userHandlerWrapper(state.options.source);
				state.render    = userHandlerWrapper(state.options.render);
				state.scroll    = userHandlerWrapper(state.options.scroll);
				state.highlight = userHandlerWrapper(state.options.highlight);
				state.select    = userHandlerWrapper(state.options.select);
				state.search    = userHandlerWrapper(state.options.search);

				// private handlers
				state._key = state._key || function(event) {
					switch(event.keyCode) {
					case 9: // tab
						if (!this.active || !this.select('keyboard')) {
							return;
						}
						event.preventDefault();
						break;

					case 13:  // enter
						if (!this.active) {
							this.search(this.input.val());
						} else {
							this.select('keyboard');
						}
						event.preventDefault();
						break;

					case 27: // escape
						this.close(true);
						return;

					case 33: // page up
						this.open();
						this.scroll(-1);
						event.preventDefault();
						return;
					case 34: // page down
						this.open();
						this.scroll(1);
						event.preventDefault();
						return;

					case 38: // up
						this.open();
						this.highlight('keyboard', -1);
						event.preventDefault();
						return;
					case 40: // down
						this.open();
						this.highlight('keyboard', 1);
						event.preventDefault();
						return;
					}
					this.complete();
				};
				state._open = state._open || function() {
					if (!this.options.allowDefaultQuery && this.input.val().length > 0 && this.input.val() == this.defaultValue) {
						this.input.val('');
					}

					this.active = true;
					this.complete(true);
				};
				state._completeAsync = state._completeAsync || function(force) {
					this.searchTask = null;

					// check query
					var query = $.trim(this.input.val());

					if (query.length < this.options.minLength) {
						this._render(query);
						return;
					}
					if (!force && this.lastQuery == query) {
						this.highlight(null, -1, true);
						this.input.focus();
						return;
					}
					this.lastQuery = query;

					// cache lookup
					if (this.cache && this.cache[query.toLowerCase()]) {
						this._render(query, this.cache[query.toLowerCase()]);
						return;
					}

					// trigger source
					this.sourceTaskAbort = this.source(query, function(data) { state._completeCallback.call(state, query, data); } );
				}
				state._completeCallback = state._completeCallback || function(query, data) {
					this.sourceTaskAbort = null;
					if (this.cache) {
						this.cache[query.toLowerCase()] = data;
					}
					this._render(query, data);
				}
				state._render = state._render || function(query, data) {
					if (data && this.render(query, data)) {
						this.menu.fadeIn(this.options.fadeDelay);
						this.active = true;
						this.highlight('null', -1, true);
						this.input.focus();
					} else {
						this.close(true);
					}
				};
				state._close = state._close || function() {
					this.active = false;
					this.menu.fadeOut(this.options.fadeDelay);
				};

				// save state
				$(this).data('cemComplete.state', state)
			}
		);

		// wrap each widget
		var jq = $.sub();
		var self = this;

		jq.fn.cemSearch = function(query) {
			self.each(
				function() {
					var state = $(this).data('cemComplete.state');

					state.search(query ? query : state.input.val());
				}
			);
		};
		jq.fn.cemOpen = function() {
			self.each(
				function() {
					var state = $(this).data('cemComplete.state');

					state.open();
				}
			);
		};
		jq.fn.cemClose = function() {
			self.each(
				function() {
					var state = $(this).data('cemComplete.state');

					state.close();
				}
			);
		};
		return jq(this);
	};
})(jQuery);

