/* 

use AJAX to suggest text completions in form-input el
extends an input text element and displays the suggestions in an associated div

FEATURES:
	- Highlight newly added portion
	- Maintain capitalization

*/

function Suggest(suggest_url, input_id, div_id) {
	// need to access this object from inside input handlers
	var me = this;

	// dom accessors
	// bind to
	this.input = document.getElementById(input_id);
	this.inputText = null;
	
	// dropdown display to
	this.div = document.getElementById(div_id);
	
	// a list of objects to display in the dropdown
	// might not display all
	this.matches = new Array();
	// only fetch new matches if the match cache is low
	this.match_cache_threshold = 1;
	
	// highlight which suggestion? -1=none
	this.highlighted = -1;
	
	// the key types
	var KEYUP = 38;
	var KEYDN = 40;
	var SPACE = 32;
	var ENTER = 13;
	var TAB = 9;
	var ESC = 3;
	
	this.url = suggest_url;
	
	// maximum number of suggestions to display
	// we can always override this
	this.max_suggest = 5;
	
	// remove browser autocomplete because it causes problems
	input.setAttribute("autocomplete","off");
	
	
	// handle new text in the input
	this.NewText = function() {
		var inputText = input.value;
		
		if( inputText.length > 0 ) {
			// break by space to grab only the last word for search
			var ta = inputText.split(" ");
			var matchText = ta[ta.length-1];
				
			if(this.matches.length <= this.match_cache_threshold)
				this.GetMatches(matchText);
				
			this.Filter(matchText);
		} else {
			this.Hide();
		}
	}
	
	
	// because there is a delay between submitting request and respose
	// this encapsulates the task after response
	this.NewMatches = function(new_matches) {
		this.matches = new_matches;
	
		this.Draw();
		this.Position();
		
		if(this.matches.length > 0)
			this.Show();	
		else
			this.Hide();
	}
	
	
	// because match response might take some time we dont want old matches to be displayed if they no longer match the input
	// this filters the current results until the next batch comes in
	this.Filter = function(str) {
		// go through the current matches and filter out non matches
		var still = new Array();
		
		for(var i=0; i<this.matches.length; i++) {
		
			var test_str = this.matches[i];
			
			if(test_str.length > 0 && test_str.indexOf(str) == 0)
				still.push(test_str);
		}
		
		NewMatches(still);
	}
	
	
	// accept the user selection and put the text in the input
	this.Accept = function() {
		if( highlighted > -1 ) {
			// replace only the last word
			var ta = input.value.split(" ");
			ta[ta.length-1] = matches[highlighted];
		
			input.value = ta.join(" ");
			this.Hide();
			
			input.focus();
		}
	}
	
	
	// move the highlight (e.g. keydn) to the prev match
	this.Prev = function() {
		if( highlighted > 0 ) {
			highlighted--;
			this.DrawHighlight();
		}			
	}
	
	
	// move the highlight (e.g. keydn) to the next match
	this.Next = function() {
		if( highlighted < (matches.length-1) ) {
			highlighted++;
			this.DrawHighlight();		
		}
	}
	
	
	// show the div
	this.Show = function() {		
		div.style.display = 'block';
	}
	
	
	// hide and reset the div
	this.Hide = function() {
		div.style.display = 'none';	
		highlighted = -1;	
	}
	
	
	// move the div below input
	this.Position = function() {
		var el = input;
		
		var y = input.offsetHeight;
		var x = 0;
		
		// find the offset of each element in the tree
		while(el.offsetParent && el.tagName.toLowerCase() != 'body') {
			y += el.offsetTop;
			x += el.offsetLeft;
			
			el = el.offsetParent;
		}			
		
		y += el.offsetTop;			
		x += el.offsetLeft;
	
		div.style.top = y + 'px';
		div.style.left = x + 'px';
	}
	
	
	// draw the div and contents
	this.Draw = function() {
	
		// set up the content table
		var tbl = document.createElement('table');
		var tblBody = document.createElement('tbody');
		tbl.appendChild(tblBody);
		
		for(i in matches) {
			if(i >= max_suggest)
				break;
		
			var match = matches[i];
			
			var tr = document.createElement('tr');
			tblBody.appendChild(tr);
			var td = document.createElement('td');
			tr.appendChild(td);
			
			var a = document.createElement('a');
			a.href='javascript: false';
			a.innerHTML = match;
			
			if(highlighted == i)
				this.classifyRow(tr,'selected');
			
			td.appendChild(a);
		}
		
		if(div.childNodes[0] == undefined)
			div.appendChild(tbl);
		else
			div.replaceChild(tbl, div.childNodes[0]);
		
		// move highlight with mouseover
		tbl.onmouseover = function(ev) {
			// find the tr that matches the event source by moving up DOM hierarchy
			var hit = me.getEventSource(ev);
			while( hit.parentNode && hit.tagName.toLowerCase() != 'tr' )
				hit = hit.parentNode;
				
			var rows = me.div.getElementsByTagName('tr');
			
			for(var i=0; i<rows.length; i++) {
				if(rows[i] == hit)
					me.highlighted = i;
			}			
			
			me.DrawHighlight();
		}
		
		
		// handle selection
		tbl.onclick = function(ev) {
			me.Accept();
			me.Hide();
			me.stopEvent(ev);
			
			return false;
		}
		
		div.style.position = 'absolute';
		div.className = 'suggestion_list';
	}
	
	
	this.DrawHighlight = function() {
		var rows = div.getElementsByTagName('tr');
		for(var i=0; i<rows.length; i++) {
			var row = rows[i];
			
			if( highlighted == i )
				this.classifyRow(row, 'selected');
			else
				this.classifyRow(row, '');
		}
	}
	
	
	// apply a class (c) to each td in a row
	this.classifyRow = function(row, c) {	
		var cols = row.getElementsByTagName('td');
		for(var i=0; i<cols.length; i++)
			cols[i].className = c;
	}
	
	// perform the actual request	
	this.GetMatches = function(str) {	
		if(str == '')
			return;
		var ajax = new AJAX();
		this.url="http://www.specialtysoftware.co.uk/ajax/search_suggest.php?q="+str;
		ajax.url = this.url;
//		ajax.AddGET('q', str);		
		ajax.ReadyState4 = this.HandleXMLResponse;		
		ajax.Go();
	}
	
	
	this.HandleXMLResponse = function(request) {

		var xml = request.responseXML;

		var XML_root = xml.getElementsByTagName('root')[0];
		var XML_response = XML_root.getElementsByTagName('response')[0];
		var XML_matches = XML_response.getElementsByTagName('matches')[0].getElementsByTagName('match');
		
		var matches = new Array();
		
		for(var i=0; i<XML_matches.length; i++) {
			var match = XML_matches[i].childNodes[0].data;
			matches.push(match);
		}
		
		NewMatches(matches);
	}
	
	
	// respond to user control sequences
	input.onkeydown = function(ev) {
		var key = me.getKeyCode(ev);
		
		switch(key) {
			case KEYUP:
				me.Prev();
				break;
			case KEYDN:
				me.Next();
				break;
			case SPACE:
			case ENTER:
				// dont submit if we are selecting a word
				submit = (me.highlighted > -1) ? false : true;
				me.Accept();
				
				if(! submit)
					return false;
				break;
			case ESC:
				me.Hide();
				break;
		}
	}
	
	
	// handle new text
	input.onkeyup = function(ev) {
		var key = me.getKeyCode(ev);
		
		switch(key) {
			case KEYUP:
			case KEYDN:
			case SPACE:
			case ENTER:
			case ESC:
				break;
			default:
				me.NewText();
		}
	}
	
	
	// return a platform independent keycode
	this.getKeyCode = function(ev) {
		if(ev)
			// Mozilla
			return ev.keyCode;
		if(window.event)
			// IE
			return window.event.keyCode;
	}
	
	
	// dont like to use this but...
	// returns the dom object where an event occured
	this.getEventSource = function(ev) {
		if(ev)
			// Mozilla
			return ev.target;
		if(window.event)
			// IE
			return window.event.srcElement;
	}
	
	
	// platform independent event bypass
	this.stopEvent = function(ev) {
		if(ev) {
			// Mozilla
			ev.preventDefault();
			ev.stopPropagation();
		}
		if(window.event) {
			// IE
			window.event.returnValue = false;
		}
	}
	
	return this;
}
