/* function testCallBack(data, status)
{
	alert(data);
	eval(data);
} */

/**
 * 
 * @author Jeremiah Metzen, University of Missouri
 * @copyright 2008, Curators of the University of Missouri 
 * @version 0.1.2008.09.08
 * 
 * @param {String} file The name of a file to pull dictionary terms and settings from.  Required.
 * @param {Object} xmlHandler An object to handle the XML used by the dictionary.  Required.
 * @param {String} displayType The way that each word's definitions should be displayed. Options include toolTip, before, after, and elemtn. Default is toolTip. Optional.
 * @param {Object} definitions An object that holds word/definition pairs.  Required.
 * @param {Array} searchElements A list of objects containing information about each element being searched for words to highlight. Required.
 * @param {String} horizontal If using a tooltip to display definitions, whether to show it to the right or the left of the cursor. Default is right. Optional.
 * @param {String} vertical If using a tooltip to display definitions, whether to show it above or below the cursor. Default is top. Optional.
 * @param {Integer} numTips The number of definitions that can be displayed on the screen at one time, 0 for infinite. Default is 1. Optional.
 * @param {Integer} tipCounter The number of definitions currently displayed. Default is 0. Required.
 * @param {String} markUp HTML to use as a template for displaying definitions. Default is a standard div. Required.
 * @param {String} file The name of a file to pull dictionary terms and settings from.  Required.
 * @param {String} eventName The event to bind to when showing word definitions. Default is click. Optional.
 * @param {String} xmlSource What method to use to get Dictionary terms and settings. For right now only 'ajax' should be used. Optional.
 * @param {String} searchClass The name of a CSS class to search for terms. Optional (Only if the class is specified in the XML file).
 * @param {Array} toolTipOptions Any array of options for the toolTip that holds word definitions. Optional.
 * @param {String} highlightClass A CSS class to highlight words with. Default is word. Optional.
 * @param {String} definitionClass A CSS class to use to format word definitions.  Default is definition. Optional.
 * @param {Boolean} matchFullWord Whether or not the regular expression used by the dictionary must match an entire word to highlight it. Default is true. Optional.
 * @param {Boolean} matchPlurals Whether or not the matched word can have an 's' at the end of it. Default is true. Optional.
 * @param {String} definitionSelector A jQuery selector to use to select elements to put definitions to if the dictionary displayType is set to 'element'. Optional.
 * @return void
 * 
 * @TODO Class still needs some optimizing/clean-up.
 * @TODO Break the tooltip functionality into a different class and add more features.
 * @TODO Add aditional features to this class and its sub-classes. Also add common getter/setter methods to the classes to get and set properties/info?
 */
 
 /**
 * @param {String} file
 * @param {String} eventName
 * @param {String} xmlSource
 * @param {String} cssDictionaryClassName
 * @param {Array} toolTipOptions
 * @param {String} cssHighlightClassName
 * @param {String} cssDefinitionClassName
 * @param {Boolean} matchFullWord
 * @param {Boolean} matchPlurals
 * @param {String} definitionSelector
 */
function Dictionary(file, eventName, xmlSource, xmlElement, cssDictionaryClassName, toolTipOptions, cssDefinitionClassName, cssHighlightClassName, matchFullWord, matchPlurals, definitionSelector)
{
	
	// displayType, horizontal, vertical, markUp
	this.file = file;
	
	if(xmlSource) this.xmlSource = xmlSource;
	else this.xmlSource = "ajax";
	
	if(cssDictionaryClassName) this.searchClass = cssDictionaryClassName;
	if(cssHighlightClassName) this.highlightClass = cssHighlightClassName;
	else this.highlightClass = "word";
	if(cssDefinitionClassName) this.definitionClass = cssDefinitionClassName;
	else this.definitionClass = "definition";
	
	if(eventName) this.eventName = eventName;
	this.definitions = new Object();
	this.searchElements = new Array(); // NOTE: Array vs. Object was messing up each() loops...
	
	if(matchFullWord) this.matchFullWord = matchFullWord;
	else this.matchFullWord = true;
	
	if(matchPlurals) this.matchPlurals = matchPlurals;
	else this.matchPlurals = false;
	
	if(definitionSelector) this.definitionSelector = displaySelector;
	
	/* ToolTip stuff. */
	this.tipCounter = 0;
	this.toolTips = new Array();
	this.tipIDs = new Array();
	if(toolTipOptions)
	{
		// TODO: Force the class to use a div, but let the user customize what goes inside of it?
		if(toolTipOptions.markUp) this.markUp = toolTipOptions.markUp;
		else this.markUp = "<div class='" + this.definitionClass + "'>[%DEFINITION%]</div>";
		
		if(toolTipOptions.displayType) this.displayType = toolTipOptions.displayType;
		else this.displayType = "toolTip";
		
		if(toolTipOptions.numTips) this.numTips = toolTipOptions.numTips;
		else this.numTips = 1;
		
		if(toolTipOptions.cancelOnClick) this.cancelOnClick = toolTipOptions.cancelOnClick;
		else this.cancelOnClick = true;
		
		if(toolTipOptions.singleLine) this.singleLine = toolTipOptions.singleLine;
		else this.singleLine = false;
		
		if(toolTipOptions.horizontal) this.horizontal = toolTipOptions.horizontal;
		else this.horizontal = "right";
		if(toolTipOptions.vertical) this.vertical = toolTipOptions.vertical;
		else this.vertical = "top";
	}
	else
	{
		this.markUp = "<div class='" + this.definitionClass + "'>[%DEFINITION%]</div>";
		this.displayType = "toolTip";
		this.cancelOnClick = true;
		this.singleLine = false;
		this.numTips = 1;
		this.horizontal = "right";
		this.vertical = "top";
	}
	
	// $.getScript("classXMLHandler.js", testCallBack);
	
	if(this.xmlSource == "ajax")
		this.xmlHandler = new XMLHandler(this.xmlSource, file, this, xmlElement, "loadFromAjax");
	else
		this.xmlHandler = new XMLHandler(this.xmlSource, file, this, xmlElement);
	
	if(this.xmlSource == "element")
		this.loadFromAjax();
}
Dictionary.prototype.file;
Dictionary.prototype.xmlHandler; // An XML object to use to process the XML.
Dictionary.prototype.xmlSource; // Whether we get the XML to process from an Ajax call, or from another element, etc.
Dictionary.prototype.searchClass; // CSS class name in which to search.
Dictionary.prototype.highlightClass; // CSS class to use to highlight words.
Dictionary.prototype.definitionClass; // CSS class name to use for word definitions (if applicable).
Dictionary.prototype.eventName; // Which event to bind to...
Dictionary.prototype.displayType; // Whether to use a toolTip, another element, to prepend or append the definition.
Dictionary.prototype.toolTipManager; // Probably not going to use this unless I make a separate tooltip class.
Dictionary.prototype.definitions; // Associative array in which to store words => defs.
Dictionary.prototype.searchElements; // An array of elements to search, each element needs its own array of tooltips and other information.
Dictionary.prototype.matchFullWord; // Whether or not to match the full word in the regular expression.
Dictionary.prototype.matchPlurals;
Dictionary.prototype.definitionSelector; // Selector used to get elements to put the definition into if the display type is "element".

/* ToolTip stuff. */
Dictionary.prototype.toolTips; // An array containing tooltips.
Dictionary.prototype.tipIDs; // An array containing tooltip IDs.
Dictionary.prototype.horizontal; // left or right relative to cursor.
Dictionary.prototype.vertical; // top or bottom relative to cursor.
Dictionary.prototype.numTips; // How many tips to keep on-screen at a time.
Dictionary.prototype.tipCounter; // Count how many tooltips we have.
Dictionary.prototype.markUp; // HTML to use as a tooltip template.

/**
 * This function removes all definitions from the screen. It is designed to be used when the script has been
 * configured to cancel tips when the user clicks on an object in the document that is not a highlighted word.
 * @param {Object} e An object that holds information about the event that called this function. Required.
 * @return void
 */
Dictionary.prototype.cancelTips = function(e)
{
	thisClass = e.data.thisClass;
	
	if(!$(this).hasClass(thisClass.highlightClass))
	{
		$(thisClass.searchElements).each(function(index)
		{
			this.tipIDs = null;
			this.tipIDs = new Array();
		});
		
		if(thisClass.displayType != "element")
		{
			$(thisClass.toolTips).each(function()
			{
				$(this).remove();
			});
		}
		else
		{
			if(thisClass.definitionSelector)
			{
				$(thisClass.definitionSelector).empty();
			}
		}
		
		thisClass.toolTips = null;
		thisClass.toolTips = new Array();
		thisClass.tipCounter = 0;
	}
}

 /**
 * Binds the functionality to display the definition to an event.
 * @param {String} file
 * @return void
 */
Dictionary.prototype.bindEvent = function()
{
	if(this.eventName)
	{
		if(this.searchClass)
		{
			$("." + this.searchClass).bind(this.eventName, this.handleEvent);
		}
	}
}

/**
 * This function is used to display a definition for a given word when a given event occurs on the element containing the word.
 * @param {Object} e An object that holds information about the event that called this function. Required.
 * @return void
 */
Dictionary.prototype.handleEvent = function(e)
{
	var id = e.data.wordID;
	var eID = e.data.elementID;
	var thisClass = e.data.thisClass;
	var thisParent = this;
	var name = $(this).text().toLowerCase();
	var regEx = new RegExp("(\\[%DEFINITION%\\])", "g");
	var toolTipHTML = $(thisClass.markUp.replace(regEx, thisClass.definitions[name]));
	
	var tipIndex = jQuery.inArray(id, thisClass.searchElements[eID].tipIDs);
	if(tipIndex == -1)
	{
		if(thisClass.tipCounter >= thisClass.numTips && thisClass.numTips != 0)
		{
			// TODO: Allow the option to override the class-wide number of tips on a per-element basis?
			$(thisClass.searchElements).each(function(index, element)
			{
				$(this.tipObjects).each(function()
				{					
					$(this).remove();
				});
					
				this.tipIDs = null;
				this.tipIDs = new Array();
				this.tipObjects = null;
				this.tipObjects = new Array();
			});
			
			thisClass.toolTips = null;
			thisClass.toolTips = new Array();
			thisClass.tipCounter = 0;
			// thisClass.cancelTips();
		}
	
		if(thisClass.displayType == "after") toolTipHTML.insertAfter(this);
		else if(thisClass.displayType == "before") toolTipHTML.insertBefore(this);
		else if(thisClass.displayType == "element")
		{
			if(thisClass.definitionSelector)
			{
				$(thisClass.definitionSelector).html(toolTipHTML);
			}
		}
		else if(thisClass.displayType == "toolTip")
		{
			var tipX = e.pageX;
			var tipY = e.pageY;
			
			var htmlString = "<div style='";
			if(thisClass.singleLine == true) htmlString += "white-space: nowrap; ";
			htmlString += "position: absolute; z-index: 3000; top: " + tipY + "; left: " + tipX + ";'>" + $(toolTipHTML).parent().html() + "</div>";
			toolTipHTML = $(htmlString);
			
			toolTipHTML.insertAfter(this);
			if(thisClass.vertical == "top") tipY = tipY - $(toolTipHTML).height();
			var calc = tipX - $(toolTipHTML).width();
			
			if(thisClass.horizontal == "left")
			{
				if((calc >= 0 && thisClass.singleLine == true) || thisClass.singleLine != true)
				{
					tipX = calc;
					toolTipHTML.css("left", tipX);
				}
			}
			else
			{
				// NOTE: offset only works on VISIBLE elements, which means I can't start with the div hidden.
				if((toolTipHTML.offset().left + toolTipHTML.width()) > $("body").width() && thisClass.singleLine == true)
				{
					tipX = calc;
					toolTipHTML.css("left", tipX);
				}
			}
			// toolTipHTML.css("display", "block");
		}
		toolTipHTML.css("top", tipY);
		thisClass.toolTips.push(toolTipHTML);
		thisClass.searchElements[eID].tipIDs.push(id);
		thisClass.searchElements[eID].tipObjects.push(toolTipHTML);
		thisClass.tipCounter++;
	}
	else
	{
		// NOTE TO SELF: You do _not_ have a parent/child relationship!
		// alert("ID: " + thisParent.id + "\nDefinition: " + thisClass.definitions[name]);
		// alert("Parent: " + $(this).parent().html());
		var searchObj = $("a[id='" + thisParent.id + "'] + *:contains('" + thisClass.definitions[name] + "')", $(this).parent());
		// alert($(searchObj).length);
		if(thisClass.displayType == "before") searchObj = $(this).prev("*:contains('" + thisClass.definitions[name] + "')", $(this).parent());
		if(thisClass.displayType == "element") searchObj = $(thisClass.definitionSelector);
		
		if(thisClass.displayType != "element") searchObj.remove();
		else
		{
			searchObj.empty();
		}
		
		// thisClass.cancelTip();
		
		thisClass.tipCounter--;
		thisClass.searchElements[eID].tipIDs.splice(tipIndex, 1);
		thisClass.searchElements[eID].tipObjects.splice(tipIndex, 1);
	}
	
	if(thisClass.cancelOnClick == true)
		e.stopPropagation();
}

/**
 * This function gets a list of words to highlight and display definitions for from the XML configuration.
 * @return void
 */
Dictionary.prototype.getWordList = function()
{
	// TODO: Abstract this:
	var t = this.xmlHandler.getElements("word");
	var thisClass = this;
	if($(t).length)
	{
		var i = 0;
		$(t).each(function()
		{
			var name = $("name", this).text().toLowerCase();
			var def = $("definition", this).text().replace(/\s{2,}/, " "); // NOTE: The regex is an IE fix for the jQuery contains() bug.
			thisClass.definitions[name] = def;
			i++;
		});
		return t;
	}
	else return null;
}

Dictionary.prototype.start = function()
{
	thisClass = this;
	if(this.searchClass)
	{
		var elements = $("." + this.searchClass);
		var $finalString = "";
		var i = 0;
		if($(elements).length)
		{
			var words = thisClass.getWordList();
			$(elements).each(function(index)
			{
				thisClass.searchElements[index] = new Object();
				thisClass.searchElements[index].tipIDs = new Array();
				thisClass.searchElements[index].tipObjects = new Array();
				finalString = $(this).html();
				// Get a list of words to search for from the XML (this needs to be abstracted so that it doesn't necessarily have to be an XML doc later).
				var thisElement = this;
				if(words)
				{
					$(words).each(function()
					{
						var name = $("name", this);
						if(name.length)
						{
							var regExString = "";
							if(thisClass.matchFullWord == true) regExString += "\\b";
							regExString += "(" + $(name[0]).text();
							regExString += ")";
							if(thisClass.matchPlurals) regExString += "(s{0,1})";
							// if(thisClass.matchPlurals == true) regExString += "|(" + $(name[0]).text() + ")s{1,1}?";
							if(thisClass.matchFullWord == true) regExString += "\\b";
							
							var regEx = new RegExp(regExString, "gi");
							var t = finalString.match(regEx);
							finalString = finalString.replace(regEx, "<a class=\"" + thisClass.highlightClass + "\">$1</a>$2");
						}
					});
					
					$(thisElement).html(finalString);
					
					if(thisClass.highlightClass && thisClass.eventName)
					{
						$("." + thisClass.highlightClass, thisElement).each(function(index)
						{
							this.id = "word_" + index;
							$(this).bind(thisClass.eventName, {elementID: i, wordID: index, thisClass: thisClass}, thisClass.handleEvent);
						});
					}
				}
				i++;
			});
		}
	}

	if(thisClass.cancelOnClick == true)
		$("body").bind("click", {thisClass: thisClass}, thisClass.cancelTips);
	
	// this.bindEvent();
}

/**
 * This function is called when the ajax call to get the configuration XML is completed.
 * It then reads configuration information from the XML and begins searching elements for the given words.
 * @return void
 */
Dictionary.prototype.loadFromAjax = function()
{
	if(this.xmlHandler)
	{
		var classFields = new Array("searchClass", "definitionClass", "highlightClass", "eventName", "horizontal", "vertical", "numTips", "cancelOnClick", "displayType", "singleLine", "matchFullWord", "definitionSelector", "matchPlurals");
		var xmlFields = new Array("searchClass", "definitionClass", "highlightClass", "event", "toolTipX", "toolTipY", "numDefinitions", "cancelOnClick", "displayType", "singleLine", "matchFullWord", "definitionSelector", "matchPlurals");
		
		for(var i=0;i<classFields.length;i++)
		{
			var t =  this.xmlHandler.getElements("setting[name='" + xmlFields[i] + "']");
			if(t[0])
			{
				var val = jQuery.trim(($(t[0]).text()));
				var type = $(t[0]).attr("type")
				if(!type) type = "string";
				
				if(type == "int") val = parseInt(val);
				if(type == "bool")
				{
					if(val == "true") val = true;
					else val = false;
				}
				this[(classFields[i])] = val;
			}
		}
	
		this.start();
	}
}