/* dynamic_select_v2.js
	Author:	Joe Savona, Rita Ho
	Updated Date: 04/05/2007
	Date:	4/10/06
	Summary:

	This code attaches event listeners to all select lists on the current page. It uses two hashes that should be defined in a separate .js file to determine, for a given select list, what other fields are linked, and what values are allowed in the 'child' field for a given value in the 'parent' field.

	The following variables should be defined in global scope for this script to work:
		var lookupPairs is a hash with associations of 
			parent field -> child field -> lookup list

		var lookupValues is a hash with associations of 
			lookup list -> parent value -> child value

		var fieldLists is a hash of 
			field -> list name 
		it is used to determine what list is stored in each field (these three fields have the state list, these two have country, etc)

		var lists is initially empty (could be full, doesn't need to be). it is a hash of 
			list name -> Array of Options
		Each list name corresponds to one of the values in the fieldLists hash - and the array is a a bunch of Option objects that can be copied and used to create a new options list for a select node.

	The ASP file needs only the following (or similar) two lines at the top:
		<script type="text/javascript" src="dynamic/umich_ugrad_lookups.js"></script>
		<script type="text/javascript" src="dynamic/dynamic_select.js"></script>
	The first is app specific, containing definitions of the 4 global variables described above. The second is to link to this file.
*/

//next two are for debugging only - can remove
var POPUP;
var POPUP_DIV = "debug";

var ALERT_CLASS = "dynamicAlertMessage";


/* 
	attach an onload event to the page
*/
window.onload = doOnLoad;


/* doOnLoad()
	Summary:
		Verify that the browser supports some basic JS functions, if not return immediately (preventing any of the other code from running - including onchange handlers). After confirming browser support, it then adds select handlers to all the select lists and then updates the select lists values based on existing values in other fields.
*/
function doOnLoad()
{
	if (!document.getElementById || ! document.getElementsByTagName)
	{
		return;
	}
	openDebug();
	addSelectHandlers();
	updateChildrenByExistingValues();
}



/* addSelectHandlers()
	Summary:
		Finds all select fields in the form and attaches an onchange event handler
*/
function addSelectHandlers()
{
	var selects = document.getElementsByTagName("select");
	var select;
	for (var i = 0; i < selects.length; i++)
	{
		select = selects[i];
		/* TODO: fix the below line - it does NOT need to be a closure. should write the selectChanged() function so it references 'this' for the select fields, instead of having the select field passed in as parameter */
		//select.onchange = function() { selectChanged(this) };
		select.onchange = selectChanged;
		//writeFieldDescription(select);
		debug("added onchange handler to select: " + select.name + " " + select.onchange);
	}

}

/* update all select list values based on the existing parent value */
/* updateChildrenByExistingValues()
	Summary:
		This function is meant to be used when the page first loads, resetting all the select lists to the list of allowed values based on existing values of other fields on the page. for instance if country had previously been selected as USA, this would immediately update the state list to just US states as soon as the page loaded.
	Params:
		none
	Returns:
		none
*/
function updateChildrenByExistingValues()
{
	var selects = document.getElementsByTagName("select");
	for (var i = 0; i < selects.length; i++)
	{
		parentChanged(selects[i]);
	}
}

/*	run whenever a select list value is changed. updates the available options in the child field
 and may alerts the user if their chosen child value is invalid */
/* parentChanged(parentNode)
	Summary:
		Called whenever a node that may be a 'parent' node changes. A parent node is defined as a node whose current value determines the available/allowed values in some other field. Whenever the parent value changes, the child field(s) value must be updated to reflect the new list of allowed values.
	Params:
		parentNode: the parent node
	Returns:
		nothing
*/
function parentChanged(parentNode)
{
	/* loop over all the children of this node (may be none) and then update their list of available values. if the current value of any of them is invalid, alert the user. whenver a current value is ok, remove any previous alerts. */
	for (var child in lookupPairs[idOf(parentNode.name)])
	{		
		var childNode = nodeForId(child);
			
		if ( ! updateChildForParent(parentNode, childNode))
		{
			if (childNode) valueInvalid(childNode);
		}
		else
		{
			if (childNode) valueValid(childNode);
		}
	}
}

/*	saveList(node)
	Summary: 
		Saves the option display and value pairs for the node's options list. only saves the values if they have not been saved before: it checks the name of list for the given field id, and then determines if that list has been saved already.
	Params:
		node: the node (of type select) whose list should be saved
	Returns:
		true if the list needed to be saved
		false if the list had already been saved
*/
function saveList(node)
{
	var listName = fieldLists[idOf(node.name)];
	if (typeof lists[listName] != "undefined") return false;
	lists[listName] = new Array(node.length);
	for (var i = 0; i < node.length; i++)
	{
		lists[listName][i] = new Option(node.options[i].text, node.options[i].value);
	}
	return true;
}

/*  updateChildForParent(parentNode,childNode)

	Summary: for a given (parent, child) pair, update the available options list in the child.
	Params:
		parentNode: the node that determines the available options in the child node
		childNode: the node whose value is determined by parentNode
	Returns:
		true if the existing child value was ok
		false if the existing child value was not allowed
*/
function updateChildForParent(parentNode, childNode)
{
	try {

	if (! childNode || ! parentNode) 
	{	
		return false;
	}
	if (! childNode.options) 
	{
		//child node not a select list
		return false;
	}
	
	var lookupID = lookupPairs[idOf(parentNode.name)][idOf(childNode.name)];	
	var currentOK = false;
	var errorFlag = false;

	if (typeof lookupID == "undefined") return false;
	var currentValue = childNode.value;
	var selectedIndex = childNode.selectedIndex;
	var oldSelectedIndex = selectedIndex;

	/* save the list and then reset it to be empty */
	saveList(childNode);
	childNode.options.length = 0;
	
	var optionIndex = 0;
	var option;
	var list = fieldLists[idOf(childNode.name)];
	
	for (var o = 0; o < lists[list].length; o++)
	{
		option = lists[list][o];
		/* 
			add the item to the new list if 
			a) the parent value is blank (nothing selected)
			b) lookupValues doesn't contain a key for parent value (also indicates 'nothing' or blank selected)
			c) there exists a pair of lookupID->parentValue->childValue
		*/
		if (parentNode.value == "" || typeof lookupValues[lookupID][parentNode.value] == "undefined" || typeof lookupValues[lookupID][parentNode.value][option.value] != "undefined")
		{
			if (option.value == currentValue)
			{
				currentOK = true;
				selectedIndex = optionIndex;				
			}
		}
		else if (option.value != currentValue)
		{
			continue;
		}
		else
		{	
			selectedIndex = 0;
			continue;
			// RitaH 04.12.2007: Commented out so that the value will clear when selected wrong combinations.
			//selectedIndex = optionIndex;
		}
		childNode.options[optionIndex] = new Option(option.text, option.value);
		optionIndex++;

	}
	if (childNode.options.length == 2 && ! currentOK)
	{		
		childNode.remove(selectedIndex);
		childNode.selectedIndex = 0;
		currentOK = true;
		parentChanged(childNode);
	}
	else if (selectedIndex > childNode.options.length)
	{
		childNode.selectedIndex = 0;
	}
	else
	{
		childNode.selectedIndex = selectedIndex;
	}
	return currentOK;

	} catch(error) {
		alert("Error in updateChildForParent() parent: " + parentNode.name + " parentValue: " + parentNode.value + " child: " + childNode.name + " childValue: " + childNode.value);
	}
}

/* childChanged(childNode)
	Summary: 
		run whenever a select list that is listed as a child node in lookupPairs has its value changed. the point is to fix the fact that the user may have had an invalid option on the list, which we should now eliminate since they obviously changed away from it. 
	Params:
		childNode: the node that changed
	Returns:
		true if the node was a child node
		false otherwise
*/
function childChanged(childNode)
{
	for (var parent in lookupPairs)
	{
		if (typeof lookupPairs[parent][idOf(childNode.name)] != "undefined")
		{
			//TODO: old way: var parentNode = document.WebApps[parent];
			var parentNode = nodeForId(parent);
			if (!parentNode || typeof parentNode == "undefined")
			{
				debug("childChanged(): cannot find parent node '" + parent + "'");
				return true;
			}
			if ( ! updateChildForParent(parentNode, childNode))
			{
				valueInvalid(childNode);
			}
			else
			{
				valueValid(childNode);
			}
			return true;
		}
	}
	return false;
}

/* selectChanged()
	Summary: set as the onchange handler for all select lists
	Params: none (attached to select node)
	Returns: false
*/
function selectChanged()
{
	parentChanged(this);
	childChanged(this);
	return false;
}

/* valueInvalid(node)
	Summary:
		Called when the value of the input field has an incorrect value. The function will ensure that the user is alerted in some manner.
	Params:
		the input field (at this point it would only be a select list but that could change)
	Returns:
		nothing
*/
function valueInvalid(node)
{
	debug("valueInvalid(" + node.name + "): value=" + node.value);
	var parent = node.parentNode;
	var nextSibling = node.nextSibling;
	if (nextSibling && typeof nextSibling != "undefined" && nextSibling.nodeType == 1 && nextSibling.getAttribute("class") == ALERT_CLASS)
	{
		nextSibling.style.display = "inline";
	}
	else
	{
		// the next node is not an alert message, so we still need to add the alert text
		/*var newNode = document.createElement("div");
		newNode.setAttribute("class",ALERT_CLASS);
		var newTextNode = document.createTextNode("*Note: The value previously select is invalid based on your other selections.");
		newNode.style.color = "red";
		newNode.style.fontSize = "75%";
		newNode.appendChild(document.createElement("BR"));
		newNode.appendChild(newTextNode);
		parent.insertBefore(newNode, nextSibling);*/
	}
}

/* valueValid(node)
	Summary:
		Called when an input field has a valid value - ensure that any alert for a previous invalid value is removed.
	Params:
		input field (generally a select list, this may change)
	Returns:
		nothing
*/
function valueValid(node)
{
	debug("valueValid(" + node.name + "): value=" + node.value);
	var parent = node.parentNode;
	var nextSibling = node.nextSibling;
	if (nextSibling && typeof nextSibling != "undefined" && nextSibling.nodeType == 1 && nextSibling.getAttribute("class") == ALERT_CLASS)
	{
		nextSibling.style.display = "none";
	}
}

/* idOf(nodeName)
	Summary:
		Returns just the id portion of an input field name. For example, given 'S12345' it would return '12345'.
	Params:
		A string, should be the name of a node
	Returns:
		A string, without the original leading character (which is always a letter signifying type of field)
*/
function idOf(nodeName)
{
	return nodeName
	/* return nodeName.substring(1); Removed by RitaH*/
}

/* find the DOM node for the given numeric ID - tries the select and text version. returns null if node not found */
/* nodeForId(nodeId)
	Summary:
		Attempts to find the DOM input element for the given id number. This input should be something like '12345', and it will return the input node with the name 'T12345' or 'S12345'
	Params:
		A string containing a number and no whitespace
	Returns:
		A DOM element from the page's form with the input field name matching the id given
*/
function nodeForId(nodeId)
{
	var node = document.getElementsByName(nodeId)[0];
	
	return node
	/*
	var possibleNames = Array( "S" + nodeId, "T" + nodeId);
	for (var i = 0; i < possibleNames.length; i++)
	{
		var node = document.WebApps[possibleNames[i]];
		if (  node && typeof node != "undefined")
		{
			return node;
		}
	}
	return null; Removed by RitaH*/
}

/* updateRelatedSelects(changedSelect)
	Summary:
		Updates the necessary select lists that are related to the given one (changedSelect). This includes:
			- determining if the new value of the given select list is valid, and adding/removing alerts if the new value is invalid/valid.
			- updating the select option lists for all fields that are child fields of this one - fields where the value depends on the value of this field
		Note that this is made more complicated because it is theoretically possible that a given child field can have more than one parent field that affects its value - both parents must be taken into account when the child field is updated.
	Params:
		changedSelect: a select node, presumably one whose value just changed
	Returns:
		nothing
*/
function updateRelatedSelects(changedSelect)
{
	/*
	for every CHILD of THIS
		save CHILD list
		for every OPTION of CHILD list
			if OPTION value in ALL lists of values for ALL PARENTs of CHILD
				add OPTION to new list
		if the old value was not valid
			if there is only one valid item in the list
				select that item
				trigger an updateRelatedSelects for CHILD
			otherwise
				alert the user that the previous value was invalid
	*/
	/**/
	var childSelect;
	var list;
	for (var child in lookupPairs[idOf(changedSelect.name)])
	{
		childSelect = nodeForId(child);
		saveList(childSelect);

		var oldSelectedIndex = childSelect.selectedIndex;
		var selectedIndex = oldSelectedIndex;
		childSelect.options.length = 0;

		list = fieldLists[child];
		var parentFields = getParentsOf(childSelect);
		var option;
		var optionIndex = 0;
		for (var o = 0; o < lists[list].length; o++)
		{
			option = lists[list][o];
			var optionOk = true;
			for (var p = 0; p < parentFields.length; p++)
			{
				if (typeof lookupValues[parentFields[p][1]][parentFields[p][0].value][childNode.value] == "undefined")
					optionOk = false;
			}
			if (optionOk)
			{
				childSelect.options[optionIndex] = new Option(option.text, option.value);
				optionIndex++;
			}
		}
	}
	/**/
}
function updateThisSelect(select)
{
}
function getParentsOf(childSelect)
{
	var parents = new Array();
	var childId = childSelect.name;
	for (var parent in lookupPairs)
	{
		if (typeof lookupPairs[parent][childId] != "undefined")
			parents.push(new Array(nodeForId(parent), lookupPairs[parent][childId]));
	}
	return parents;
}

function openDebug()
{
	/*
	POPUP = window.open("dynamic/debug.html", 'debug', "width=600,height=400,scrollbars=1");
	POPUP.focus();
	*/
}

function debug(message)
{
	/*
	if (typeof POPUP == "undefined" || ! POPUP) openDebug();
	var messagePanel = POPUP.document.getElementById(POPUP_DIV);
	var newMessageNode = POPUP.document.createElement("div");
	newMessageNode.innerHTML = message;
	messagePanel.appendChild(newMessageNode);
	POPUP.focus();
	*/
}

/*
DEPRECATED
*/
function writeFieldDescription(selectNode)
{
	var parent = selectNode.parentNode;
	if ( ! parent || typeof parent == "undefined")
	{
		alert("no parent, can't add description");
		return;
	}
	var nextSibling = selectNode.nextSibling;
	var newNode = document.createElement("div");
	//newNode.setAttribute("class",ALERT_CLASS);

	var message = "";
	for (var parent in lookupPairs)
	{
		if (typeof lookupPairs[parent][idOf(selectNode.name)] != "undefined")
		{
			newNode.appendChild(document.createTextNode(nodeForId(parent).name));
		}
	}

	newNode.style.color = "green";
	newNode.style.fontSize = "75%";
	try {
		parent.appendChild(newNode);
	}
	catch(error)
	{
		alert("can't add child to parent: " + parent);
	}
}
