
/************************************************************************************************************/
/* suggestHandler																							*/
/************************************************************************************************************/

function suggestHandler()
{
	this.suggests = new Array();
	this.preloader;
}

suggestHandler.prototype.register = function(inputNode, url, requestParameter, noResultCallback, anyResultCallback)
{
	// input node has to have id attribute
	if (!inputNode.id)
	{
		throw new Error('Input node has to have "id" attribute');
	}
	
	// sugest object does not exist yet
	if (!this.suggests[inputNode.id])
	{
		this.suggests[inputNode.id] = new suggest(inputNode, url, requestParameter, noResultCallback, anyResultCallback);
		this.suggests[inputNode.id].ajaxRequest.setPreloader(this.preloader);
	}
}

suggestHandler.prototype.setPreloader = function(preloader)
{
	this.preloader = preloader;
}

suggestHandler.prototype.getSuggestById = function(id)
{
	return this.suggests[id];
}

/************************************************************************************************************/
/* suggestCache																								*/
/************************************************************************************************************/

function suggestCache()
{
	this.words = new Array();
	this.wordConverter;
}

suggestCache.prototype.exist = function(word)
{
	word = this.convertWord(word);
	
	if (this.words[word])
	{
		return true;
	}
	
	for (i = word.length - 2; i >= 0; i--)
	{
		var currentWord = word.substring(0, i + 1);
		
		if (this.words[currentWord])
		{
			var cacheResults = this.words[currentWord];
			var wordResults = new Array();
			var wordResultsIndex = 0;
			
			for (j = 0; j < cacheResults.length; j++)
			{
				if (this.convertWord(cacheResults[j]).indexOf(word) == 0)
				{
					wordResults[wordResultsIndex++] = cacheResults[j];
				}
			}
			
			this.set(word, wordResults);
			
			return true;
		}
	}
	
	return false;
}

suggestCache.prototype.set = function(word, words)
{
	this.words[this.convertWord(word)] = words;
}

suggestCache.prototype.get = function(word)
{
	return this.words[this.convertWord(word)];
}

suggestCache.prototype.convertWord = function(word)
{
	return this.getWordConverter().convert(word);
}

suggestCache.prototype.getWordConverter = function()
{
	if (!(this.wordConverter && this.wordConverter instanceof suggestCacheWordConverter))
	{
		this.wordConverter = new suggestCacheWordConverter();
	}
	
	return this.wordConverter;
}

suggestCache.prototype.setWordConverter = function(wordConverter)
{
	this.wordConverter = wordConverter;
}

/************************************************************************************************************/
/* suggestCacheWordConverter																				*/
/************************************************************************************************************/
function suggestCacheWordConverter()
{
	
}

suggestCacheWordConverter.prototype.convert = function(word)
{
	return word.toLowerCase();
}

/************************************************************************************************************/
/* suggest																									*/
/************************************************************************************************************/

function suggest(inputNode, url, requestParameter, noResultCallback, anyResultCallback)
{
	this.ajaxRequest = new ajax_request();
	this.cache = new suggestCache();
	this.inputNode = inputNode;
	this.url = url;
	this.requestParameter = requestParameter;
	this.noResultCallback = noResultCallback;
	this.anyResultCallback = anyResultCallback;
	this.suggestNode;
	this.suggestNodeHeight = 150;
	this.suggestNodeWidth = null;
	this.scrollNode;
	this.suggestDisplayed = false;
	this.hideSuggestNode = true;
	this.cellNodes = new Array();
	this.selectedCellIndex = -1;
	this.suppressMouseHandlers = false;
	this.lastWord = '';
	this.completedWord = '';
	this.ajaxRefreshTime = 500;
	this.ajaxTimeoutId;
	this.loadRefreshTime = 500;
	this.loadTimeoutId;
	this.completeRefreshTime = 1500;
	this.completeTimeoutId;
	this.ajaxBusy = false;
	
	this.init();
}

suggest.prototype.init = function()
{
	var self = this;
	
	this.inputNode.setAttribute('autocomplete', 'off');
	
	this.createSuggest();
	
	this.inputNode.onblur = function()
	{
		if (self.hideSuggestNode)
		{
			self.hideSuggest()
		}
		
		self.resetSelectedCell();
	};
	
	this.inputNode.onkeyup = function(event)
	{
		self.handle(event)
	};
	
	this.inputNode.onkeydown = function(event)
	{
		self.captureEvent(event)
	};
}

suggest.prototype.captureEvent = function(event)
{
	event = !event ? window.event : event;

	var code = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : (event.which ? event.which : 0));

	if (event.type == 'keydown')
	{
		// detect ARROW DOWN key
		if (code == 40)
		{
			if (this.cellNodes[this.selectedCellIndex])
			{
				this.unHighlightCellNode(this.cellNodes[this.selectedCellIndex]);
			}
			
			this.selectedCellIndex++;
			
			if (this.cellNodes[this.selectedCellIndex])
			{
				this.highlightCellNode(this.cellNodes[this.selectedCellIndex]);
				
				// scroll down if needed
				if (this.selectedCellIndex * this.cellNodes[this.selectedCellIndex].offsetHeight  > this.suggestNodeHeight - this.suggestNodeHeight * 0.33)
				{
					this.scrollNode.scrollTop += this.cellNodes[this.selectedCellIndex].offsetHeight;
				}
			}
			// select first one
			else if (this.selectedCellIndex > 1)
			{
				this.selectedCellIndex = 0;

				if (this.cellNodes[this.selectedCellIndex])
				{			
					this.highlightCellNode(this.cellNodes[this.selectedCellIndex]);
				}
				
				// scroll top
				this.scrollNode.scrollTop = 0;
			}
			
			this.completeWord();
			this.displaySuggest();
			this.suppressMouseHandlers = true;
			this.cancelEvent(event);
		}
		
		// detect ARROW UP key
		if (code == 38)
		{
			if (this.cellNodes[this.selectedCellIndex])
			{
				this.unHighlightCellNode(this.cellNodes[this.selectedCellIndex]);
			}
			
			this.selectedCellIndex--;
			
			if (this.cellNodes[this.selectedCellIndex])
			{
				this.highlightCellNode(this.cellNodes[this.selectedCellIndex]);
				
				// scroll up if needed
				if (this.selectedCellIndex * this.cellNodes[this.selectedCellIndex].offsetHeight <= this.scrollNode.scrollTop + this.suggestNodeHeight * 0.33)
				{
					this.scrollNode.scrollTop -= this.cellNodes[this.selectedCellIndex].offsetHeight;
				}
			}
			// select last one
			else if (this.selectedCellIndex < 0)
			{
				this.selectedCellIndex = this.cellNodes.length - 1;
				
				if (this.cellNodes[this.selectedCellIndex])
				{			
					this.highlightCellNode(this.cellNodes[this.selectedCellIndex]);
					
					// scroll bottom
					this.scrollNode.scrollTop = this.selectedCellIndex * this.cellNodes[this.selectedCellIndex].offsetHeight;
				}
			}
			
			this.completeWord();
			this.displaySuggest();
			this.suppressMouseHandlers = true;
			this.cancelEvent(event);
		}
		
		// detect ENTER key if suggest is displayed
		if (code == 13 && this.suggestDisplayed)
		{
			if (this.cellNodes[this.selectedCellIndex])
			{
				this.fillWord(this.cellNodes[this.selectedCellIndex]);
				this.cancelEvent(event);
			}
			
			this.resetSelectedCell();
		}
		
		// detect ESC key
		if (code == 27)
		{
			this.hideSuggest();
			
			if (this.cellNodes[this.selectedCellIndex])
			{
				this.unHighlightCellNode(this.cellNodes[this.selectedCellIndex]);
			}
			
			this.resetSelectedCell();
			this.cancelEvent(event);
		}
	}
}

suggest.prototype.suppressEvent = function(event)
{
	event = !event ? window.event : event;

	var code = event.charCode ? event.charCode : (event.keyCode ? event.keyCode : (event.which ? event.which : 0));
	
	if (event.type == 'keyup')
	{
		if (code == 40 || code == 38 || code == 13 || code == 27)
		{
			return true;
		}
	}
	
	return false;
}

suggest.prototype.handle = function(event)
{
	var self = this;
	
	this.hideSuggestNode = true;
	
	// was any event to suppress captured or was input node value changed
	if (!this.suppressEvent(event) && this.inputNode.value.length > 0
		&& this.inputNode.value.toLowerCase() != this.lastWord.toLowerCase()
		&& this.inputNode.value.toLowerCase() != this.completedWord.toLowerCase()
	)
	{
		this.lastWord = this.inputNode.value;
		
		// word is in cache
		if (this.cache.exist(this.inputNode.value))
		{
			this.clearWords();
			this.addWords(this.inputNode.value, this.cache.get(this.inputNode.value));
		}
		// word is not in cache
		else
		{
			// use timeout to limit ajax request
			if (this.loadTimeoutId)
			{
				clearTimeout(this.loadTimeoutId);
			}
			
			this.loadTimeoutId = setTimeout(function(){self.loadWords(self.inputNode.value);}, this.loadRefreshTime);
		}
	}
	
	// nothing in input node value
	if (this.inputNode.value.length == 0)
	{
		this.hideSuggest();	
		this.lastWord = '';
	}
}

suggest.prototype.displaySuggest = function()
{
	this.suggestDisplayed = true;
	this.suggestNode.style.display = 'block';
}

suggest.prototype.hideSuggest = function()
{
	this.suggestDisplayed = false;
	
	if (!this.suggestNode)
	{
		return;
	}
	
	this.suggestNode.style.display = 'none';
}

suggest.prototype.createSuggest = function()
{
	var self = this;
	
	function computePositionLeft()
	{
		return computePosition('offsetLeft');
	}
	
	function computePositionTop()
	{
		return computePosition('offsetTop');
	}
	
	function computePosition(attribute)
	{
		var position = 0;
		var inputNode = self.inputNode;
		
		while (inputNode)
		{
			position += inputNode[attribute];
			inputNode = inputNode.offsetParent;
			
		}
		
		return position;
	}
	
	// suggest note
	this.suggestNode = document.createElement('div');
	this.suggestNode.id = 'suggest-'+this.inputNode.id;
	this.suggestNode.style.display = 'none';
	this.suggestNode.style.position = 'absolute';
	this.suggestNode.style.backgroundColor = 'White';
	this.suggestNode.style.left = computePositionLeft()+'px';
	this.suggestNode.style.top = computePositionTop() + this.inputNode.offsetHeight+'px';
	this.suggestNode.style.width = this.suggestNodeWidth != null ? this.suggestNodeWidth+'px' : this.inputNode.offsetWidth+'px';
	
	// scroll node
	this.scrollNode = document.createElement('div');
	this.scrollNode.style.border = '1px solid Black';
	this.scrollNode.style.height = this.suggestNodeHeight+'px';
	this.scrollNode.style.overflow = 'auto';
	this.scrollNode.onmousedown = function(){self.hideSuggestNode = false;};
	this.suggestNode.appendChild(this.scrollNode);
	
	// table node
	var tableNode = document.createElement('table');
	var tBodyNode = document.createElement('tbody');
	tableNode.style.width = '100%';
	tableNode.style.borderCollapse = 'collapse';
	tableNode.appendChild(tBodyNode);
	this.scrollNode.appendChild(tableNode);
	
	this.inputNode.parentNode.insertBefore(this.suggestNode, this.inputNode);
}

suggest.prototype.addWords = function(word, words)
{
	var self = this;
	var tBodyNode = this.suggestNode.getElementsByTagName('tbody')[0];
	var totalWords = words.length;
	
	this.cellNodes = new Array();
	
	if (totalWords == 0)
	{
		this.hideSuggest();
		
		// call no result callback if any
		if (this.noResultCallback && this.inputNode.value.length > 0)
		{
			this.noResultCallback();
		}
		
		return;
	}
	else
	{
		if (this.anyResultCallback)
		{
			this.anyResultCallback();
		}
	}
	
	this.displaySuggest();
	
	for (i = 0; i < totalWords; i++)
	{
		var rowNode = document.createElement('tr');
		var cellNode = document.createElement('td');
		var cellNodeFragment = document.createDocumentFragment();
		var textNode = document.createTextNode(words[i].substring(word.length));
		var strongNode = document.createElement('strong');
		
		strongNode.appendChild(document.createTextNode(words[i].substring(0, word.length)));
		cellNodeFragment.appendChild(strongNode);
		cellNodeFragment.appendChild(textNode);
		
		cellNode.style.padding = '1px 3px';
		cellNode.style.whiteSpace = 'nowrap';
		this.cellNodes.push(cellNode);
		
		cellNode.onclick = function()
		{
			self.fillWord(this);
		}
		
		cellNode.onmouseover = function()
		{
			if (self.suppressMouseHandlers)
			{
				self.suppressMouseHandlers = false;
				
				return;
			}
			
			self.highlightCellNode(this);
			
			// select cell index
			for(var index in self.cellNodes)
			{
				if(self.cellNodes[index] == this)
				{
					self.selectedCellIndex = index;
				}
			}
		}
		
		cellNode.onmouseout = function()
		{
			if (self.suppressMouseHandlers)
			{
				return;
			}
			
			for(var index in self.cellNodes)
			{
				self.unHighlightCellNode(self.cellNodes[index]);
			}
		}
		
		cellNode.appendChild(cellNodeFragment);
		rowNode.appendChild(cellNode);
		tBodyNode.appendChild(rowNode);
	}
	
	// reset selected cell
	this.resetSelectedCell();
	this.scrollNode.scrollTop = 0;
	
	if (!this.cellNodes[this.selectedCellIndex])
	{
		return;
	}
	
	this.highlightCellNode(this.cellNodes[this.selectedCellIndex]);
	
	// complete first word but using timeout to give some time to user to type
	if (this.completeTimeoutId)
	{
		// abort it
		clearTimeout(this.completeTimeoutId);
	}
	
	this.completeTimeoutId = setTimeout(function(){if (self.inputNode.value.length > 0){self.completeWord();}}, this.completeRefreshTime);
}

suggest.prototype.loadWords = function(word)
{
	var self = this;
	
	// is ajax request not busy
	if (this.ajaxRequest.xmlhttp.readyState == 4 || this.ajaxRequest.xmlhttp.readyState == 0)
	{
		this.ajaxBusy = false;
		
		var url = this.url+'&'+this.requestParameter+'='+escape(word);
		
		this.ajaxRequest.setUrl(url);
		this.ajaxRequest.setHandler(function(xml){self.xhrCallback(xml, word);});
		this.ajaxRequest.send();
	}
	// ajax request is busy
	else
	{
		this.ajaxBusy = true;
		
		if (this.ajaxTimeoutId)
		{
			clearTimeout(this.ajaxTimeoutId);
		}
		
		// try again later
		this.ajaxTimeoutId = setTimeout(function(){self.loadWords(word);}, this.ajaxRefreshTime);
	}
}

suggest.prototype.xhrCallback = function(xmlhttp, word)
{
	var words = new Array();
	var xml = xmlhttp.responseXML;
	var xmlWords = xml.getElementsByTagName('word');
	
	for (var i = 0; i < xmlWords.length; i++)
	{
		words.push(xmlWords[i].firstChild.nodeValue);
	}
	
	// save to cache
	this.cache.set(word, words);
	
	// ajax is not busy now
	if (!this.ajaxBusy)
	{
		// add words
		this.clearWords();
		this.addWords(word, words);
	}
}

suggest.prototype.clearWords = function()
{
	var tBodyNode = this.suggestNode.getElementsByTagName('tbody')[0];
	
	while (tBodyNode.hasChildNodes())
	{
		tBodyNode.removeChild(tBodyNode.firstChild);
	}
	
	this.resetSelectedCell();
}

suggest.prototype.fillWord = function(cellNode)
{
	var cellText = cellNode.innerHTML.replace(/(<([^>]+)>)/ig, '');
	
	this.inputNode.value = cellText;
	this.lastWord = cellText;
	
	if (this.cache.exist(this.inputNode.value))
	{
		this.clearWords();
		this.addWords(cellText, this.cache.get(cellText));
	}
	else
	{
		this.unHighlightCellNode(cellNode);
		this.resetSelectedCell();
	}
	
	this.hideSuggest();
}

suggest.prototype.completeWord = function()
{
	var cellNode = this.cellNodes[this.selectedCellIndex];
	
	if (!cellNode)
	{
		return;
	}
	
	var cellText = cellNode.innerHTML.replace(/(<([^>]+)>)/ig, '');
	
	this.inputNode.value = cellText;
	this.completedWord = cellText;
	this.selectText(this.lastWord.length, cellText.length);
}

suggest.prototype.highlightCellNode = function(cellNode)
{
	cellNode.className = 'highlighted';
	cellNode.style.backgroundColor = '#96989b';
	cellNode.style.color = 'White';
}

suggest.prototype.unHighlightCellNode = function(cellNode)
{
	cellNode.className = '';
	cellNode.style.backgroundColor = 'White';
	cellNode.style.color = '#494d52';
}

suggest.prototype.resetSelectedCell = function()
{
	this.selectedCellIndex = -1;
}

suggest.prototype.cancelEvent = function(event)
{
	event.cancelBubble = true;
	event.returnValue = false;
	
	if (event.stopPropagation)
	{
		event.stopPropagation();
	}
	
	if (event.preventDefault)
	{
		event.preventDefault();
	}
}

suggest.prototype.selectText = function(start, length)
{
	// IE
	if (this.inputNode.createTextRange)
	{
		var range = this.inputNode.createTextRange();
		
		range.moveStart('character', start);
		range.moveEnd('character', length - this.inputNode.value.length);
		range.select();
	}
	// FF
	else if (this.inputNode.setSelectionRange)
	{
		this.inputNode.setSelectionRange(start, length);
	}
}

/************************************************************************************************************/
/* suggestCacheNull																							*/
/************************************************************************************************************/
function suggestCacheNull()
{
	
}

/**
 * @extends suggestCache
 */
suggestCacheNull.prototype = new suggestCache();

suggestCacheNull.prototype.exist = function(word){}
suggestCacheNull.prototype.set = function(word, words){}
suggestCacheNull.prototype.get = function(word){}
suggestCacheNull.prototype.setWordConverter = function(wordConverter){}

/************************************************************************************************************/
/* init																										*/
/************************************************************************************************************/

var suggestHandler = new suggestHandler();

