var alphabet = [
	'a','b','c','d','e','f','g','h',
	'i','j','k','l','m','n','o','p',
	'q','r','s','t','u','v','w','x',
	'y','z',
	'0','1','2','3','4','5','6','7','8','9',
	'-','_',' '
];


// different browsers have different concepts of what offsetParent means
// but they're consistant enough to allow walking the tree up to absolute coordinates
function getAbsoluteLeft(elm)
{
	var offsetLeft = elm.offsetLeft;
	elm = elm.offsetParent;
	while (elm) {
		offsetLeft += elm.offsetLeft;
		elm = elm.offsetParent;
	}
	return offsetLeft;
}
function getAbsoluteTop(elm)
{
	var offsetTop = elm.offsetTop;
	elm = elm.offsetParent;
	while (elm) {
		offsetTop += elm.offsetTop;
		elm = elm.offsetParent;
	}
		
	return offsetTop;
}



function AutoComplete(inputFieldId, popupWindowId)
{
	this.inputFieldId = inputFieldId;
	this.popupWindowId = popupWindowId;
	this.cache = new Object();
	this.items = new Array();
	this.hoverId = null;
	this.previousText = ""; // text as of the last keypress; should the initial value be the field's current value?
	this.previousUpdateText = ""; // text as of the last update received
	this.sentRequestId = 0;
	this.firstNodeId = null;
	this.requireValid = false;
	this.isValid = false;
	this.selectedIndex = -1;
	this.autoSelectSingleMatches = false;
	this.enabled = true;
	//@ Min # chars before we start auto-completeing. also the # chars between update requests
	this.MIN_CHARS = 3;
	this.MAX_ITEM_DISPLAY = 9;

	var This = this; // closure
	
	var inputField = document.getElementById(this.inputFieldId);

	inputField.onkeydown = function(event) {
		This.onTextFieldKeyDown(window.event?window.event:event);
	};

	// keyup seems to be more consistant than keypress across browsers
	inputField.onkeyup = function(event) {
		This.onTextFieldKeyUp(window.event?window.event:event);
	};

	inputField.onblur = function(event) {
		This.onTextFieldBlur(window.event?window.event:event);
	};
	
	var popupWindow = document.getElementById(this.popupWindowId);
	
	popupWindow.onmousedown = function(event) {
		This.onWindowClick(window.event?window.event:event);
	};
	
	this.updateDisplay();
}
AutoComplete.idGen = 1;
// consts
AutoComplete.KEY_BACKSPACE = 8;
AutoComplete.KEY_UP = 38;
AutoComplete.KEY_DOWN = 40;
AutoComplete.KEY_ENTER = 13;
AutoComplete.KEY_HIDE = 27;
AutoComplete.getNodeIndex = function(node)
{
	if (!node.parentNode) return -1;
	var index = 0;
	while (node = node.previousSibling) ++index
	return index;
}
AutoComplete.getNodeId = function(node)
{
	var nodeId = node.id.substring('autocomplete-list-item-'.length);
	return nodeId;
}
AutoComplete.prototype.getNodeByIndex = function(index)
{
	if (index == -1)
		return null;
	var node = document.getElementById('autocomplete-list-item-'+this.firstNodeId);
	if (!node) return null;

	for (var i = 0; i < index && node; ++i)
		node = node.nextSibling;
	return node;
}
AutoComplete.prototype.toString = function() { return "[AutoComplete]"; }
AutoComplete.prototype.onWindowClick = function(event)
{
	if (!this.hoverId)
		return;
//	var hoverText = getTextContent(this.getHoverNode());
//	var inputField = document.getElementById(this.inputFieldId);
//	inputField.value = hoverText;
	this.setSelectedNode(this.getHoverNode());
	this.submit();
}

AutoComplete.prototype.onTextFieldKeyDown = function(event)
{
	var popupWindow = document.getElementById(this.popupWindowId);
	
	switch (event.keyCode)
	{
	case AutoComplete.KEY_ESC:
		this.hide();
		break;
	case AutoComplete.KEY_UP:
		this.hoverPrev();
		break;
	case AutoComplete.KEY_DOWN:
		this.hoverNext();
		break;
	}
}
AutoComplete.prototype.onTextFieldKeyUp = function(event)
{
	// don't update on nav keys
	switch (event.keyCode)
	{
	case AutoComplete.KEY_UP:
	case AutoComplete.KEY_DOWN:
		return;
	case AutoComplete.KEY_ENTER:
		this.submit();
		return;
	}

	var popupWindow = document.getElementById(this.popupWindowId);
	var inputField = document.getElementById(this.inputFieldId);
	var text = inputField.value;
	text = text.toUpperCase();
	text = Util.trim(text);
    if (text.length < 1) {
        this.clearItems();
        this.updateDisplay();
        return;
    }
	if (text==this.previousText)
		return;
	this.requestCompletions(text, event.keyCode);
	this.previousText = text;
}
AutoComplete.prototype.onTextFieldBlur = function(event)
{
	var popupWindow = document.getElementById(this.popupWindowId);
	this.hide();
}
AutoComplete.prototype.onItemHover = function(nodeId)
{
	if (nodeId.nodeType) {
		nodeId = AutoComplete.getNodeId(nodeId); // nodeId is actually a node
	}
	if (this.hoverId) {
		this.getHoverNode().className = 'autocomplete-list-item';
	}
	this.hoverId = nodeId;
	if (this.hoverId) {
		this.getHoverNode().className = 'autocomplete-list-item-hover';
	}
}
AutoComplete.prototype.hide = function()
{
	var popupWindow = document.getElementById(this.popupWindowId);
	popupWindow.style.visibility = 'hidden';
}
AutoComplete.prototype.show = function()
{
	if(!this.enabled) return;
	var inputField = document.getElementById(this.inputFieldId);
	var popupWindow = document.getElementById(this.popupWindowId);
	popupWindow.style.left = getAbsoluteLeft(inputField) + 'px';
	popupWindow.style.top = getAbsoluteTop(inputField) + inputField.offsetHeight + 'px';
	//I added the "- 2" to account for the difference in offsetWidth caused by the input field border
	//this may need to be customized for different browsers, as this applies in FireFox 2 and 
	//I haven't tested it elsewhere. 
	popupWindow.style.width = (inputField.offsetWidth - 2)+ 'px';
	popupWindow.style.height = 195 + 'px';
//	popupWindow.style.height = this.items.length*inputField.offsetHeight + 'px';
	if (this.items.length)
		popupWindow.style.visibility = 'visible';
}
AutoComplete.prototype.cacheItems = function(text, items)
{
//	var inputField = document.getElementById(this.inputFieldId);
	this.cache[text.toUpperCase()] = items;
}
AutoComplete.prototype.setItems = function(items)
{
	this.items = items;
	this.valid = false;
	this.setSelectedNode(null);
}
AutoComplete.prototype.clearItems = function()
{
	this.items = new Array();
	this.setSelectedNode(null);
	this.valid = false;
}
AutoComplete.prototype.getHoverNode = function()
{
	if (this.hoverId === null)
		return null;
	var node = document.getElementById('autocomplete-list-item-' + this.hoverId);
	if (node == null)
		alert('Hover node id='+this.hoverId+' is null');
	return node;
}
AutoComplete.prototype.getItemNode = function(id)
{
	return document.getElementById('autocomplete-list-item-' + id);
}
AutoComplete.prototype.createItemNode = function(id, item)
{
	var node = document.createElement('div');
	node.appendChild(document.createTextNode(item.toString()));
	node.setAttribute('class', 'autocomplete-list-item');
	node.setAttribute('id', 'autocomplete-list-item-' + id);
	var This = this;
	node.onmouseover = function(event) {
		This.onItemHover(id);
	};
	return node;
}

// doesn't effect visibility, just repopulates the popupwindow
AutoComplete.prototype.updateDisplay = function()
{
	var popupWindow = document.getElementById(this.popupWindowId);
	while (popupWindow.childNodes.length)
		popupWindow.removeChild(popupWindow.firstChild);
	var items_length = this.items.length;//Math.min(this.items.length, this.MAX_ITEM_DISPLAY);
	this.firstNodeId = null;
	for (var i = 0; i < items_length; ++i)
	{
		var id = ++AutoComplete.idGen;
		if (!this.firstNodeId)
			this.firstNodeId = id;
		var itemNode = this.createItemNode(id, this.items[i]);
		// it seems like we should just skip nodes past this point
		// but i want to have item indices in the model match up to those in the view
		if (i > this.MAX_ITEM_DISPLAY)
			itemNode.style.display = 'none';
		popupWindow.appendChild(itemNode);
	}
	this.hoverId = null;
}
AutoComplete.prototype.hoverNext = function()
{
	var popupWindow = document.getElementById(this.popupWindowId);
	if (!this.hoverId)
		this.onItemHover(popupWindow.firstChild);
	else {
		if (this.getHoverNode().nextSibling)
			this.onItemHover(this.getHoverNode().nextSibling);
		else
			this.onItemHover(popupWindow.firstChild);
	}
	if (this.hoverId) {
//		var hoverText = getTextContent(this.getHoverNode());
//		var inputField = document.getElementById(this.inputFieldId);
//		inputField.value = hoverText;
		this.setSelectedNode(this.getHoverNode());
	}
}
AutoComplete.prototype.hoverPrev = function()
{
	var popupWindow = document.getElementById(this.popupWindowId);
	if (!this.hoverId)
		this.onItemHover(popupWindow.lastChild);
	else {
		if (this.getHoverNode().previousSibling)
			this.onItemHover(this.getHoverNode().previousSibling);
		else
			this.onItemHover(popupWindow.lastChild);
	}
	if (this.hoverId) {
//		var hoverText = getTextContent(this.getHoverNode());
//		var inputField = document.getElementById(this.inputFieldId);
//		inputField.value = hoverText;
		this.setSelectedNode(this.getHoverNode());
	}
}
AutoComplete.prototype.setSelectedIndex = function(index)
{
	var node = this.getNodeByIndex(index);
	this.setSelectedNode(node);
}
AutoComplete.prototype.setSelectedNode = function(node)
{
	var nodeIndex = -1;
	var inputField = document.getElementById(this.inputFieldId);
	if (node) {
		var nodeId = AutoComplete.getNodeId(node);
		nodeIndex = AutoComplete.getNodeIndex(node);
		var item = this.items[nodeIndex]; // at the moment items and their nodes share indices
		if (!item)
			item = null;
		else {
		var text = item;
		inputField.value = text;
		}
	}
	this.selectedIndex = nodeIndex;
	this.validate();
	if (this.requireValid) {
		if (this.isValid)
//			inputField.style.backgroundColor = null; // style default;
			inputField.style.color = null; // style default;
		else
//			inputField.style.backgroundColor = '#dedede';
			inputField.style.color = 'red';
	}
}
AutoComplete.prototype.getSelectedItem = function()
{
	return this.items[this.selectedIndex];
}
AutoComplete.prototype.getSelectedValue = function()
{
	var item = this.getSelectedItem();
	if (!item) return null;
	return item.value;
}
AutoComplete.prototype.requestCompletions = function(text, keyCode)
{
// || (text.length-this.previousText.length < this.MIN_CHARS && this.cache[text]!=null)

	// attempt to update cache from current items list
//	if (text.length - this.previousUpdateText.length < 3)
 {

		var key = text;
		if (key.length>=this.MIN_CHARS && this.cache[key] == null) {
			var regex = new RegExp('^'+text, 'i');
			var matches = new Array();
			for (var i = 0; i < this.items.length; ++i) {
				if (this.items[i].toString().search(regex) == 0) {
					matches.push(this.items[i]);
				}
			}
			if (matches.length) {
				this.cache[key] = matches;
				//alert(key + " => " + matches);
            }
		}
	}

	if (this.cache[text]) {
	//alert(text + " <= " + this.cache[text]);
		this.setItems(this.cache[text]);
		this.updateDisplay(); // this hsould perhaps be implicit in setItems
		var matchIndex = this.validate();
		if (this.autoSelectSingleMatches && this.items.length == 1 && keyCode != AutoComplete.KEY_BACKSPACE)
		  matchIndex = 0;
		this.setSelectedIndex(matchIndex);
		this.show();
	} else {
		if (text.indexOf(this.previousText) == -1) {
			this.clearItems();
		}
		this.isValid = false;
		this.setSelectedNode(null);
		this.updateDisplay();
		if (text.length >= this.MIN_CHARS && keyCode != AutoComplete.KEY_BACKSPACE)
			this.onRequestCompletions(text);
	}
}
AutoComplete.prototype.onRequestCompletions = function(){} // override me
// call this in response to onRequestCompletions
AutoComplete.prototype.updateCompletions = function(text, items)
{
	this.previousUpdateText = text;
	this.setItems(items);
	this.updateDisplay(); // this should perhaps be implicit in setItems
	this.cacheItems(text, items);
	var matchIndex = this.validate();
    if (this.autoSelectSingleMatches && this.items.length == 1)
      matchIndex = 0;
	this.setSelectedIndex(matchIndex);
	this.show();
}
AutoComplete.prototype.submit = function()
{
	//if (this.requireValid && !this.isValid)
		//return;
	var inputField = document.getElementById(this.inputFieldId);
	var text = inputField.value;
	text = Util.trim(text);
//	alert('about to onSubmit: ' + text);
	this.hide();
	this.onSubmit(text);
}
AutoComplete.prototype.onSubmit = function(){} // override me
AutoComplete.prototype.validate = function()
{
//window.document.documentElement.innerHTML+=('<span>stuff='+this.items.length+'</span><br />');
	var inputField = document.getElementById(this.inputFieldId);
	var text = inputField.value.toUpperCase();
	text = Util.trim(text);
	this.isValid = false;
	for (var i = 0; i < this.items.length; ++i) {
		//alert(text);
		//alert(this.items[i]);
		if (text == this.items[i].toString().toUpperCase()) {
			
			this.isValid = true;
			return i;
		}
	}
	return -1;
}

AutoComplete.prototype.requestHandler = function(text, items, requestId)
{
//alert("" + text + "; " + requestId + " - " + this.sentRequestId);
    if (requestId < this.sentRequestId) {
        this.cacheItems(text, items);
        return;
    } else {
        this.updateCompletions(text, items);
    }
};


AutoComplete.Item = function(value)
{
	this.value = value.toString().toUpperCase();
}
AutoComplete.Item.prototype.toString = function()
{
	return this.value;
}
