var Autocomplete = function(contextInput, options) {
	if (typeof options != "object") return null;
	var optionDefaults =
		{minLength:3,highlight:true,maxResults:5,autoHide:true,width:0,
		 trackInputMovement:true,css:null,delayInit:false,
		 onResultClear:function(){},onResultEnter:function(){},
		 onSearchFailed:function(){},onSearchInitiated:function(){},
		 onSearchSuccess:function(){}};
	var requiredOptions = ['callback'];
	this.options = options;
	for (var i in optionDefaults)
		if (typeof this.options[i] == 'undefined')
			this.options[i] = optionDefaults[i];
	for (var i in requiredOptions)
		if (typeof this.options[requiredOptions[i]] == 'undefined')
			return null;

	// Helper autocomplete div hiding function.
	var _this = this;
	this.hideBox = function() {
		if (_this.box) {
			_this.box.css('display','none');
			if (_this.boxDownHandler) {
				try {
				$(document).unbind('keydown', 'down')
						   .unbind('keydown', 'up')
						   .unbind('keydown', 'return');
				} catch(e) { };
			}
		}
	};

	// If the autocomplete box has not been initialized,
	// construct the autocomplete DIV and set some variables
	// that will be used later.
	this.initialize = function() {

	if (!this.initialized) {
		this.contextInput = contextInput;

		this.$ = $(this.contextInput);
		this.offset = this.$.offset();
		this.dimension = {
			width: contextInput.offsetWidth || contextInput.clientWidth,
			height: contextInput.offsetHeight || contextInput.clientHeight
		};

		// Construct the autocomplete DIV.
		this.box = $(document.createElement('DIV'));

		// The autocomplete DIV will also have an "autocomplete" class
		// for styling, but will be a DIV, not an input.
		this.box.addClass('autocomplete');

		var width = this.options.width || this.dimension.width;
		this.box.css({
			display: 'none', // to be altered when dropdown appears
			padding: 0, margin: 0,
			width: width + 'px',
			maxWidth: width + 'px',
			overflow: 'hidden',
			position: 'absolute',
			left: this.offset.left + 'px',
			top: (this.offset.top + this.dimension.height) + 'px'
			// all other styles are decorative and can be in stylesheet
		});

		if (this.options.css) this.box.css(this.options.css);

		// Everything in the autocomplete DIV will be contained in
		// another DIV. This is so we can simply replace that DIV
		// when displaying new results. This guarantees the screen
		// won't flicker, since we will be able to construct results
		// in the background and then replace the DIV, rather than
		// construct them in front of the user's view.
		this.box.append($(document.createElement('DIV')));

		this.box.appendTo($(document.body));

		// Hide the box when the user is not using the input field.
		if (this.options.autoHide) {
            var thisAC = this;
			$(this.contextInput).blur(function(){
				setTimeout(function() { thisAC.hideBox(); thisAC.contextInput.value='';},250)
			});
		}

		this.doAutocomplete = true;
		this.initialized = true;
	} // end autocomplete initialization

	this.initialize = null;

	}; //end initialization function

	if (!this.options.delayInit) this.initialize.call(this);

	var context = this;

	$(contextInput).keyup(function(e) {
	if (e.keyCode == 27) {
		context.hideBox();
		$(contextInput).val('');
	}
	else
		if (context.invalid) {
			return;
		}

	// Now we can start the actual work.
	// Make the AJAX call to see if we have any completions.
	// The target URL that sends back autocompletion info was
	// stored in this.target during initialization.

    // If the callback is a function then call it otherwise (it is a string)
	// the target still has a %1 where the autocomplete string should
	// be, so store the string-specific target:
	var callback = $.isFunction(context.options.callback) ?
                    context.options.callback() :
                    context.options.callback.replace('%1',contextInput.value);

	// Now we can make the AJAX call.
	// Store "this" as a variable so we can refer to it in the
	// AJAX result function.
	var autocompleteString = contextInput.value;


	// Ignore keypresses that don't enter or erase text.
	if (context.previousValue && context.previousValue == contextInput.value) return;
	context.previousValue = contextInput.value;

	// Give a lower limit for autocompletion.
	if (contextInput.value.length < context.options.minLength) {
		// If the autocomplete dropdown is showing, hide it.
		context.hideBox();

		context.options.onResultClear.call(context);

		// The variable below specifies if an autocomplete AJAX return
		// should fill its results in.
		context.doAutocomplete = false;

		return false;
	} else {
		context.doAutocomplete = true;
	}

	$.ajax({ type: "GET", url: callback, dataType: "json", success:
		function(results) {
			if (!context.initialized && context.options.delayInit) {
				context.initialize.call(context);
			}

			// If autocomplete is turned off because the user cleared
			// the field (or some similar action), do nothing.
			if (!context.doAutocomplete) return;

			// The AJAX can lag behind the autocomplete results.
			// In this case, the prudent thing to do is not to show
			// anything until the results match up. If we show the
			// results indiscriminantly, we risk the scenario where
			// a user enters two characters, and the former's
			// AJAX request returns later than the latter's,
			// resulting in invalid results.
			if (contextInput.value != autocompleteString) return;

			results = results.results;
			// results is now an array of objects with members "id",
			// "description", and "name".

			// If there are no results, hide the autocomplete.
			var numResults = 0;
			for (var i in results) numResults++;
			if (numResults == 0) {
				context.hideBox();

				context.options.onSearchFailed
					.call(context, 'AC_SearchFailed', '');
				return;
			} else {
				context.options.onSearchSuccess.call(context);
			}

			// Construct the results in a DIV.
			var resultBox = $(document.createElement('DIV'));

			var count = 0;
			for(var i in results) {
				if (++count > context.options.maxResults)
					break;
				var result = results[i];
				// Autocomplete results will be hyperlinks so we can
				// style hovering over them with only CSS.
				var resultNode = $(document.createElement('A'));

				// A little hack so we can style the first result.
				if (i == 0) resultNode.addClass('first');

				// Store the ID and name.
				$.data(resultNode[0], 'data', result);

				// When we click a result, populate the input field.
				resultNode.click(function(){
					var data = $.data(this, 'data');

					contextInput.value = data.name || data;

					/*if (contextInput.resultIDInputNode)
						contextInput.resultIDInputNode.val(
							$(this).attr('autocompleteID'));
					*/
					context.options.onResultEnter
						.call(context, 'AC_ResultChosen', data);

					// Hide the autocomplete box.
					context.hideBox();
					context.doAutocomplete = false;

					// Prevent autocomplete from popping up for this.
					context.previousValue = contextInput.value;

					contextInput.focus();
				});

				resultNode.mouseover(function() {
					if (context.boxSelected) {
						context.boxSelected.removeClass('selected');
					}
					context.boxSelected = $(this);
					context.boxSelected.addClass('selected');
				});

				var resultHeader = $(document.createElement('H2'));
				var headerText = result.name || result;
				if (result.akaNames && result.akaNames.length) {
					// append the akanames to headerText, with
					// the first initial of each name capitalized
					// and the akanames separated by commas.

                    // Chris - this doesn't work in IE - the first char bit is failing
//					headerText += ' ~ ' +
//						(function(a){ for(var i in a)
//						   a[i] = a[i][0].toUpperCase()+a[i].substr(1);
//						return a;})(result.akaNames).join(', ');
					headerText += ' ~ ' +
						(function(a) {
                            var names = [];
                            for(var i in a) {
                                names.push(a[i].name ? a[i].name.toLowerCase() : a[i].toLowerCase());
                            }
                            return names;
                        })(result.akaNames).join(', ');

				}
				if (context.options.highlight) {
					/// TODO: Maybe clean/filter any HTML in the name?
					var pattern =
					   new RegExp('('+autocompleteString+')','i');
					var replacement =
					   '<span class="highlight">$1</span>';
					var header =
					   headerText.replace(pattern, replacement);
					resultHeader.html(header);
				} else {
					resultHeader.text(headerText);
				}

				resultNode.append(resultHeader);
                /*
				.append(
					$(document.createElement('P'))
					   .text(result.description)
				);
				*/

				resultBox.append(resultNode);
			} // end constructing results in a DIV

			// Populate the autocomplete box with the results.
			$(context.box.children().get(0)).replaceWith(resultBox);

			// Let the user go up and down through the results.
			context.boxSelected = null;
			if (!context.boxDownHandler) {
				context.boxReturnHandler = function(evt) {
					if (context.boxSelected) {
						evt.stopPropagation();
						evt.preventDefault();
						context.boxSelected.click();
					}
				};
				context.boxUpHandler = function(evt) {
					if (!context.boxSelected) return false;
					if (context.boxSelected.prev().length == 0) {
						context.boxSelected
							.removeClass('selected');
						context.boxSelected = null;
						return false;
					}
					context.boxSelected =
						context.boxSelected
							.removeClass('selected')
							.prev().addClass('selected');
				};
				context.boxDownHandler = function(evt) {
					if (!context.boxSelected) {
						context.boxSelected =
							context.box.find('a:first');
						context.boxSelected.addClass('selected');
						return false;
					}
					if (context.boxSelected.next().length == 0)
						return false;
					context.boxSelected =
						context.boxSelected
							.removeClass('selected')
							.next().addClass('selected');
				};
			}
			var boxHandlers = ['Down','Up','Return'];
			for(var i in boxHandlers) {
				var handlerName = boxHandlers[i];

				try {
					$(document).unbind('keydown',
						handlerName.toLowerCase());
				} catch(e) {}
				try {
				$(document).bind('keydown', handlerName.toLowerCase(),
							context['box'+handlerName+'Handler']);
				} catch(e) {}
			}

			if (context.options.trackInputMovement) {
				context.offset = context.$.offset();
				context.box.css({
					left: context.offset.left + 'px',
					top: (context.offset.top + context.dimension.height) + 'px'
				});
			}

            context.box.attr('id', contextInput.id + '-autocompleteResults');
			context.box.css('display','block');
		}
	}); // end AJAX call

	context.options.onSearchInitiated.call(this);

	}); // End keydown handler

};
