/*
Searcher - Version 1.0 - by Rich
use like the following:

var mySearcher = new Search(xmlObject, idOfInputField, idOfResultHTMLObject, nodeNameWithinXml);
mySearcher.imageSize = 20;
mySearcher.typeLimit = 5;
//makewebsite...go

*/

//gets the xml

//adds new search
function Search(xml, searchInputId, searchResultsId, searchType)
{
	//adds object properties from supplied args
		//main ones
		this.xml = xml;
		this.searchInputId = searchInputId;
		this.searchInput = document.getElementById(searchInputId);
		this.searchResults = document.getElementById(searchResultsId);
		this.searchType = searchType;
		this.searchTopHit = new Array();
	
		//selected/total items
		this.thisSearchItemRef = -1;
		this.thisSearchItemLength = 0;
		
		//editable values
		this.imageSize = 20;
		this.typeLimit = 5;

		//key aliases
		this.ENTER = 13;
		this.ESC = 27;
		this.KEYUP = 38;
		this.KEYDOWN = 40;
		this.TAB = 9;
		
	//adds object methods
	this.searchByType = searchByType;
	this.search = search;
	this.outputItem = outputItem;
	this.searchKeyEsc = searchKeyEsc;
	this.searchKeyUp = searchKeyUp;
	this.searchKeyDown = searchKeyDown;
	this.searchKeyEnter = searchKeyEnter;
	this.searchKeyTab = searchKeyTab;
	this.getKeyCode = getKeyCode;
	this.showDOMObject = showDOMObject;
	this.hideDOMObject = hideDOMObject;
	this.highlightDOMObject = highlightDOMObject;
	this.lowlightDOMObject = lowlightDOMObject;
	this.scoreMultiple = scoreMultiple;
	this.scoreMultipleHighest = scoreMultipleHighest;
	this.scoreString = scoreString;

	//turns off autocomplete - should be set by default
	this.searchInput.setAttribute("autocomplete", "off");
	
	//positions results div
	//this.searchResults.style.left = this.searchInput.offsetLeft;
	this.searchResults.style.top = this.searchInput.offsetHeight + this.searchInput.style.height;

	//"side step" - see explanation in inputKeyAction() function
	this.searchInput.searchObject = this;
	
	//adds search key events
	this.searchInput.onkeyup = inputKeyAction;
	this.searchInput.onkeydown = keyDownCatcher; //enter and tab can only be caught on key down
	this.searchInput.onblur = function() { this.searchObject.searchKeyEsc(); };
}

//performs relevant function upon key press
function inputKeyAction(e)
{
	//gets the main search object via a pretty messy side step (ok. it's a fucking hack!)
	//'this', for this function, refers to the html input element instead of the search object
	var thisObject = this.searchObject;

	//gets character code
	var character = thisObject.getKeyCode(e);
	
	//gets relevant action
	if(character == thisObject.ESC)
	{
		thisObject.searchKeyEsc();
	}
	else if(character == thisObject.KEYUP)
	{
		thisObject.searchKeyUp();
	}
	else if(character == thisObject.KEYDOWN)
	{
		thisObject.searchKeyDown();
	}
	else if(character == thisObject.TAB)
	{
		return false;
	}
	//search used for everything else
	else
	{
		thisObject.search();
	}
}

function keyDownCatcher(e)
{
	var thisObject = this.searchObject;

	//gets character code
	var character = thisObject.getKeyCode(e);

	//processes enter
	if(character == thisObject.ENTER)
	{
		return thisObject.searchKeyEnter();
	}
	else if(character == thisObject.TAB)
	{
		return thisObject.searchKeyTab();
	}
	
	//returns false for up/down to stop text cursor moving side to side
	else if(character == thisObject.KEYUP)
	{
		return false;
	}
	else if(character == thisObject.KEYDOWN)
	{
		return false;
	}
}

//starts a new search
function search()
{
	//search variables
	this.searchItems = new Array();
	this.thisSearchItemRef = -1;
	this.searchItemsLength = 0;
	this.searchTopHit = new Array();
	
	//gets the search term
	this.searchString = this.searchInput.value;
	this.searchTerm = this.searchString.toLowerCase();
	
	//hides/shows the results
	if(this.searchString.length == 0)
	{
		this.hideDOMObject(this.searchResults);
		return false;
	}
	else
	{
		this.showDOMObject(this.searchResults);
	}
	
	//calls search function
	var output = '';
	var outputTypeCount = 0;
	if(typeof(this.searchType) == 'object') //everything
	{
		for(var i = 0; i < this.searchType.length; i++)
		{
			var thisItemOutput = this.searchByType(this.searchType[i]);
			if(thisItemOutput != '')
			{
				outputTypeCount++;
				output += thisItemOutput;
			}
		}
	}
	else //single type
	{
		output += this.searchByType(this.searchType);
	}
	
	//top hit output only if there's more than 1 type of results
	if(output != ''
		&& outputTypeCount > 1)
	{
		output =	'<li>'+
						'<ul>'+
							this.outputItem(this.searchTopHit, 0)+
						'</ul>'+
						'Top Hit'+
					'</li>'+
					output;
	}
	else if(output == '')
	{
		output = '<li>No results found</li>';
	}
	
	//puts everything in list container
	output =	'<ul>'+
					output+
				'</ul>'+
				'<div class="cl"></div>';
	
	//outputs results
	this.searchResults.innerHTML = output;
}

//searches genre
function searchByType(searchType)
{
	//gets relevent xml
	var searchTypeItemsXML = this.xml.getElementsByTagName(searchType)[0].childNodes;
	var searchTypePrependLink = this.xml.getElementsByTagName(searchType)[0].getAttribute('lD');
	
	//if there's actually content
	if(searchTypeItemsXML.length > 0)
	{
		//creates new type array
		this.searchItems[searchType] = new Array();
		
		//records scores
		for(var i = 0; i < searchTypeItemsXML.length; i++)
		{
			//shortcut for xml
			var thisItemXML = searchTypeItemsXML[i];
			
			//words to search within
			var thisItemSearchableStrings = new Array();
			
			//iterates through item children to find searchable nodes
			for(var j = 0; j < thisItemXML.childNodes.length; j++)
			{
				if(thisItemXML.childNodes[j].getAttribute('s') == 'y')
				{
					//adds words from searchable node to searchable strings list
					thisItemSearchableStrings[thisItemSearchableStrings.length] = thisItemXML.childNodes[j].childNodes[0].nodeValue.toLowerCase();
				}
			}

			//gets searchItem properties
			var thisItemScore = this.scoreMultiple(thisItemSearchableStrings);
			if(thisItemScore > 0)
			{
				//this item
				var thisItem = {
					xml:thisItemXML,
					score:thisItemScore,
					prependLink:searchTypePrependLink
				};
			
				//updates top hit data
				if(this.searchTopHit.length == 0
					|| thisItemScore > this.searchTopHit.score)
				{
					this.searchTopHit = thisItem;
				}
			
				//records this item
				this.searchItems[searchType][this.searchItems[searchType].length] = thisItem
			}
		}
		
		//if there are results for this section
		if(this.searchItems[searchType].length > 0)
		{
			//sorts the searchItems type array
			this.searchItems[searchType].sort(sortByScoreDesc);
			
			//creates output
			var output = 	'<li>'+
								'<ul>';
			
			var thisTypeLimit = this.typeLimit;
			if(this.searchItems[searchType].length < thisTypeLimit)
			{
				thisTypeLimit = this.searchItems[searchType].length;
			}
			
			//iterates through search items
			for(var i = 0; i < thisTypeLimit; i++)
			{
				//increases length of items on display - done here for top hit to squeeze in top
				this.searchItemsLength++;
			
				//output for this item
				output += this.outputItem(this.searchItems[searchType][i], this.searchItemsLength);
			}
			output +=			'</ul>' + searchTypeItemsXML[0].parentNode.getAttribute('title')+
							'</li>';
			
			return output;
		}
	}
	
	//no content
	return '';
}

//outputs html for search item
function outputItem(thisItem, thisItemId)
{
	var output =	'<li id="' + this.searchInputId + '_' + thisItemId + '">'+
						'<a href="' + thisItem.prependLink + thisItem.xml.getAttribute('l') + '" title="' + thisItem.xml.childNodes[0].childNodes[0].nodeValue + '">';
				
	//image
	if(thisItem.xml.getAttribute('js_image') != undefined)
	{
		output +=				'<img src="' + thisItem.xml.getAttribute('js_image') + '" width="' + this.imageSize + '" height="' + this.imageSize + '" />';
	}
	
	//xml elements
	for(var j = 0; j < thisItem.xml.childNodes.length; j++)
	{
		var thisItemXMLChild = thisItem.xml.childNodes[j];
		output +=				'<span class="' + this.searchInputId + '_detail_' + thisItemXMLChild.tagName + '">' + thisItemXMLChild.childNodes[0].nodeValue + '</span>';
	}
	
	output +=				'</a>'+
						'</li>';
	
	return output;
}

//sorts results by score
function sortByScoreDesc(a, b)
{
	var x = a.score;
	var y = b.score;
	return ((x < y) ? 1 : ((x > y) ? -1 : 0));
}

//gets key pressed - ie needs it to be an object, not an event
function getKeyCode(keyEvent)
{
	var code;
	
	if(keyEvent == undefined)
	{
		var keyEvent = window.event;
	}
	if(keyEvent.keyCode != undefined)
	{
		code = keyEvent.keyCode;
	}
	else if(keyEvent.which != undefined)
	{
		code = keyEvent.which;
	}
	
	return code;
}

//when enter key is pressed
function searchKeyEnter()
{
	//follow link if item is selected
	if(this.thisSearchItemRef > -1)
	{
		window.location = document.getElementById(this.searchInputId + '_' + this.thisSearchItemRef).getElementsByTagName('a')[0];
		return false;
	}
	
	//return true for default enter action if nothing is selected
	return true;
}

//when tab key is pressed
function searchKeyTab()
{
	//follow link if item is selected
	if(this.thisSearchItemRef > -1)
	{
		this.searchInput.value = document.getElementById(this.searchInputId + '_' + this.thisSearchItemRef).getElementsByTagName('span')[0].childNodes[0].nodeValue; //broken here
		this.searchKeyEsc();
		return false;
	}
	
	//return true for default enter action if nothing is selected
	return true;
}

//when escape key is pressed
function searchKeyEsc()
{
	//this.searchInput.value = '';
	this.hideDOMObject(this.searchResults);
}

//when down key is pressed
function searchKeyDown()
{
	if(this.thisSearchItemRef < this.searchItemsLength)
	{
		//lowlight previous item - only doesn't happen if one isn't selected
		if(this.thisSearchItemRef > -1)
		{
			this.lowlightDOMObject(document.getElementById(this.searchInputId + '_' + this.thisSearchItemRef));
		}
		
		//current item
		this.thisSearchItemRef++;
		
		//highlight current item
		this.highlightDOMObject(document.getElementById(this.searchInputId + '_' + this.thisSearchItemRef));
	}
}

//when up key is pressed
function searchKeyUp()
{
	if(this.thisSearchItemRef > -1)
	{
		//lowlight previous item
		this.lowlightDOMObject(document.getElementById(this.searchInputId + '_' + this.thisSearchItemRef));
		
		//current item
		this.thisSearchItemRef--;
		
		//highlight current item (if exists) - only happens if going off list at top
		if(this.thisSearchItemRef > -1)
		{
			this.highlightDOMObject(document.getElementById(this.searchInputId + '_' + this.thisSearchItemRef));
		}
	}
}


//DOM FUNCTIONS

//show html object
function showDOMObject(DOMObject)
{
	DOMObject.style.display = 'block';
}

//hide html object - on a delay in order for clicking links to work
function hideDOMObject(DOMObject)
{
	setTimeout('document.getElementById("' + DOMObject.id + '").style.display = "none";', 500);
}

//removes highlight class
function lowlightDOMObject(DOMObject)
{
	DOMObject.className = '';
}

//highlights
function highlightDOMObject(DOMObject)
{
	DOMObject.className = 'highlighted';
}


//SCORING FUNCTIONS

//allows scoring of multiple strings (highest wins)
function scoreMultipleHighest(searchObject)
{
	var score = 0;
	var highScore = 0;

	//iterates through strings
	for(var i = 0; i < searchObject.length; i++)
	{
		//current string score
		score = this.scoreString(searchObject[i]);
		
		//adds to highest if it is
		if(score > highScore)
		{
			highScore = score;
		}
	}
	
	return highScore;
}

//allows scoring of multiple strings (concatenates to one search term)
function scoreMultiple(searchObject)
{
	var searchString = searchObject[0];

	//iterates through strings
	if(searchObject.length > 1)
	{
		for(var i = 1; i < searchObject.length; i++)
		{
			searchString += ' ' + searchObject[i];
		}
	}
	
	return this.scoreString(searchString);
}

//applies qs-ish style score
function scoreString(searchString)
{
	//returns 1 if identical
	if(searchString == this.searchTerm)
	{
		return 1.0;
	}

	//if nothing is supplied then exit
	if(this.searchTerm.length == 0)
	{
		return 0.0;
	}
	
	//if search is longer than this length exit
	if(this.searchTerm.length > searchString.length)
	{
		return 0.0;
	}
	
	//new score
	var score = 0.0;
	
	//searches for them all together - 0.8 - 1.0
	var fullMatchIndex = searchString.indexOf(this.searchTerm);
	if(fullMatchIndex > -1)
	{
		return 0.2 + (searchString.substrCount(this.searchTerm) * this.searchTerm.length) / searchString.length * 0.7; //ratio score
	}
	
	//partial matches - will only run on multiple words! - 0 - 0.5
	var searchTermArray = this.searchTerm.explode(' ');
	if(searchTermArray.length > 0)
	{
		score = 0.2;
		for(var i = 0; i < searchTermArray.length; i++)
		{
			var thisWord = searchTermArray[i];
		
			//skip if word is blank
			if(thisWord == '')
			{
				continue;
			}
		
			//dies if word isn't in search string
			if(searchString.indexOf(thisWord) == -1)
			{
				return 0.0;
			}
		}
	}
	
	return score;
}
