// CRS_Utils Utility Object
// To use, instantiate one of these and call its functions...

Error.prototype.problemField = null; // We want our Errors to be able to indicate a problem field (usually to receive focus)

function CRS_Utils(sharedUrl, helpUrl)
{
	this.suppressAllAlerts = false;
	this.sharedUrl  = sharedUrl;
	this.helpUrl    = helpUrl;
	this.selectBoxDefaultText = "- Choose -";
	this.plusArrow  = null; // must be initialized by caller
	this.minusArrow = null; // must be initialized by caller
	this.invalidFields = new Array();
	
	this.confirmIfDirty = false;
	
	// WORKAROUND for weird IE/popCalendar interaction, where onbeforeunload gets called multiple times
	this.alreadyConfirming = false;
	this.trapMultipleConfirms = false;
}

// Set your HTML body's onload attribute to this function! It performs essential CRS_Utils initialization
// and then calls your own doOnLoad function (if defined) where you may perform any additional
// initialization.
CRS_Utils.prototype.doOnLoad = function(child)
{
	dtCh = '/'; // date validation
	//	imgDir = "/mycrs-shared/images/popcalendar/";	// TODO: do this here when popcalendar.js isn't so lame!!
	init(); // popupcalendar

	if ((child != null) && (child.doOnLoad != null))
	{
		child.doOnLoad();
	}
	else
	{
		doOnLoad();
	}
}

// TO USE:
// 1) add to your code: window.onbeforeunload = crs.confirmExit;
// 2) override crs.isDirty
// 3) NOTE: This assumes the presence of a CRS_Utils object called 'crs'! For some reason,
// setting the function pointer above does not seem to preserve the this-pointer (Anyone know why?!)

CRS_Utils.prototype.confirmExit = function(evt)
{
	var calendarShowing = (crossobj != null) && (crossobj.visibility != "hidden"); // stupid global popcal variable

	if (crs.trapMultipleConfirms && crs.alreadyConfirming)
	{
		crs.alreadyConfirming = false;
	}
	else if (crs.confirmIfDirty && crs.isDirty() && !calendarShowing)
	{
		if (!evt)
		{
			evt = window.event; // for IE (note Safari does not support either way)
			crs.alreadyConfirming = true;
		}
		
		evt.returnValue = "You have unsaved changes.";
	}
}

// client must override
CRS_Utils.prototype.isDirty = function()
{
	return false;
}

// Utility wrapper for getElementById. Throws exception if element not found.
// Also, by default, displays alert unless suppressAlert parameter is set or
// global gSuppressAllAlerts is set.
CRS_Utils.prototype.getRequiredElement = function(id, suppressAlert)
{
	var element = document.getElementById(id);
	
	if (element == null)
	{
		var msg = "Required element missing! id=[" + id + "]";
		
		if (!this.suppressAllAlerts && ((suppressAlert == null) || !suppressAlert))
		{
			alert(msg);
		}

		throw new Error(msg);
	}
	
	return element;
}

// Turns a series of checkboxes on or off.
// Items must have id's of the form [idStem]1, [idStem]2, etc.
CRS_Utils.prototype.multiCheckboxToggle = function(idStem, numItems, checkedVal)
{
    for (var i = 1; i <= numItems; i++)
    {
	    var element = CRS_Utils.prototype.getRequiredElement(idStem + i);
	    
	    element.checked = checkedVal; 
    }
}

// Wraps call to popupcalendar.js
CRS_Utils.prototype.doCalendar = function(calWidget, elementId, dateFormat)
{
	dateFormat = (dateFormat != null) ? dateFormat : 'mm/dd/yyyy';
	
	popUpCalendar(calWidget, this.getRequiredElement(elementId), dateFormat);
}

CRS_Utils.prototype.setElementVisibility = function(elementID,on)
{
	var element = crs.getRequiredElement(elementID);	
	
	element.style.display = on ? "block" : "none";
}

CRS_Utils.prototype.toggleElementVisibilty = function(elementID,forceVisible)
{
	var element = crs.getRequiredElement(elementID);	
	var hidden = (forceVisible == null) ? (element.style.display == "none") : forceVisible;
	
	element.style.display = hidden ? "block" : "none";
	
	var image = document.getElementById(elementID + "-img");
	
	if (image != null)
	{
		image.src = hidden ? this.minusArrow : this.plusArrow;
	}
}

// Open Help window.
//   - if editable=true user will be able to edit help text, and even create if entryId does not exist
//   - categoryId is optional, and is used simply to set suggested category when entryId does not exist
//   - editable flag is obviously very hackable. TODO: Fix?
CRS_Utils.prototype.showHelp = function(entryId, editable, categoryId)
{
	if (categoryId == "") categoryId = -1;
	
	var destURL = this.helpUrl + "/help/edit?helpId=" + entryId + "&editable=" + editable + "&categoryId=" + categoryId;
	window.open(destURL,"","height=600,width=640,scrollbars=yes,resizable=yes");
}

	
CRS_Utils.prototype.showHelpCategory = function(categoryId,editable)
{
	var destURL = this.helpUrl + "/help/editCategory?categoryId=" + categoryId + "&editable=" + editable;
	window.open(destURL,"","height=400,width=440,scrollbars=yes,resizable=yes");
}


/*
	Dynamic Lists (e.g. Countries, States, etc.)
	
	- Take data supplied in page "model" and populate Javascript data structures to support dynamic browser-side behavior:
		-	Populate select boxes that can be "chained" (e.g. selecting a country changes the list of states)
		-	Handle display of existing data by selecting items appropriately
		-	Play nice with validation scheme
		-	Allow for the same data to be used in multiple places on the page (e.g. country and state lists
			that appear in both a business and shipping address).
		-	Allow for the hooking up of these lists to also be somewhat dynamic. That is, technique should
			not be overly dependent on knowing the entire form layout in advance (e.g. support DataFlexer schema
			forms).
		
	TBD:
		-	What if existing data no longer corresponds to available choices?
		-	Can we support input fields where data can be entered directly, but that use our (possibly chained)
			select boxes as "helpers"?
		
	IMPLEMENTATION NOTES:
		-	Since select boxes can be dependent on other choices, their population must orchestrated after the
			page loads via Javascript. The role of the FreeMarker code is to set up the HTML elements and generate
			Javascript that populates whatever data structures may be needed.
		-	The needed data structures are:
			-	Arrays for each of the lists, containing the code and name used to populate select boxes. If an
				array is dependent, it must be "addressable" by the code of the array it depends on.
			-	Something to capture the meta-information about the lists, e.g. their dependencies and which
				form fields are tied to them.
*/
CRS_Utils.prototype.populateSelect = function(selectBox,optList,value)
{
	selectBox.options.length = 0;
	var companion = document.getElementById(selectBox.id + "_buddy");
	var companionVisible = (companion != null) && (companion.style.display != "none");
	 
	if (optList == null)
	{
		if ((companion != null) && !companionVisible)
		{
			companion.style.display = "block";
			selectBox.style.display = "none";
			companion.name = selectBox.id;
			selectBox.name = selectBox.id + "_buddy";
		}
	}
	else if (optList.length > 0)
	{
		if (companion != null)
		{
			companion.value = "";

			if (companionVisible)
			{
				companion.style.display = "none";
				selectBox.style.display = "block";
				companion.name = selectBox.id + "_buddy";
				selectBox.name = selectBox.id;
			}
		}
		
		selectBox.options[0] = new Option(this.selectBoxDefaultText,"");
		
		for (var i = 0; i < optList.length; i++)
		{
			var item = optList[i];
			
			selectBox.options[i + 1] = new Option(item.name,item.code);
		}
	}
	
	if (value != null)
	{
		selectBox.value = value;
		
		if (selectBox.value != value)
		{
			alert('CRS_Utils.populateSelect: Couldn\'t set field: ' + selectBox.name + ' to: ' + value);
		}
		else
		{
			selectBox.onchange();
		}
	}
}


CRS_Utils.prototype.dynamicSelect = function(selectBox,page)
{
	if ((page != null) && !page.validationInProgress)
	{
		var dynamicList = page.dynamicLists[selectBox.id];

		if ((dynamicList != null) && (dynamicList.target != null))
		{
			var targetList = page.dynamicLists[dynamicList.target];
			var target = this.getRequiredElement(targetList.fieldId);
			var targetOpts = (selectBox.value.length > 0) ? targetList.list[selectBox.value] : null;

			if ((targetOpts != null) && (targetOpts.length == 0))
			{
				targetOpts = null;	// TODO: FIX: distinguish between null and empty?
			}
			
			this.populateSelect(target,targetOpts,null);
		}
	}
	
	return this.alwaysValid(selectBox);
}

/*
	Populate dynamic lists, including chained select boxes. 
	NOTE: dependent lists must come after their dependecies, because this
	"triggers" each point in the chain.
*/
CRS_Utils.prototype.initDynamicUILists = function(page)
{
	for (var i = 0; i < page.dynamicLists.length; i++)
	{
		var item = page.dynamicLists[i];
		
		if (item.fieldId != null)
		{
			page.dynamicLists[item.fieldId] = item;
			
			var field = this.getRequiredElement(item.fieldId);

			if (!item.useSelector)
			{
				this.populateSelect(field,item.list,item.value);
			}
			else
			{
				field.value = item.value;
			}
		}
	}
}

/*
	Caller should populate a data structure identifying the form fields to be validated,
	as follows:
		fieldAttributes = [ { id:"blahblah", required:true, valid:false }, ... ];
		
	Fields with validation rules beyond simple required/optional should have an
	onchange function. In addition to performing immediate validation, this function
	will be called during batch validation below. It should call CRS_Utils.hiliteField(valid,message)
	for the field (or fields) in question.
	
	A number of generic onchange functions are provided below.
*/

/*
	link up field attributes w/dynamic lists
*/
CRS_Utils.prototype.linkFieldAttributes = function(page)
{
	for (var i = 0; i < page.fieldAttributes.length; i++)
	{
		var item = page.fieldAttributes[i];
		
		if (item.dynamicList != null)
		{
			page.dynamicLists[item.dynamicList].fieldId = item.id;
			page.dynamicLists[item.dynamicList].value = item.listValue;
		}
	}
}

CRS_Utils.prototype.clearForm = function(page)
{
	for (var i = 0; i < page.fieldAttributes.length; i++)
	{
		var attrs = page.fieldAttributes[i];
		var field = this.getRequiredElement(attrs.id);
		
		field.value = "";
	}
}

CRS_Utils.prototype.validateForm = function(page)
{
	page.validationInProgress = true;

	var valid = true;
		
	try
	{
		var fieldAttributes = page.fieldAttributes;
		
		for (var i = 0; i < fieldAttributes.length; i++)
		{
			var attrs = fieldAttributes[i];
			var field = this.getRequiredElement(attrs.id);
			
	//		alert('field: ' + field.name);
			if (attrs.required && (field.value.length == 0) && (field.style.display != "none"))
			{
				attrs.valid = false;
			}
			else
			{
				attrs.valid = (field.onchange != null) ? field.onchange() : true;
			}
	
			valid = valid && attrs.valid;
	//		alert('field: ' + field.name + ' valid: ' + attrs.valid + ' overall: ' + valid);
		}
		
		valid = valid && page.doExtraValidation();
		
		if (!valid)
		{
			this.hiliteInvalidFields(fieldAttributes);
			alert("Please correct form errors...");
		}
	}
	finally
	{
		page.validationInProgress = false;
	}
	
	return valid;
}

CRS_Utils.prototype.hiliteInvalidFields = function(fieldAttributes)
{
	for (var i = 0; i < fieldAttributes.length; i++)
	{
		var attrs = fieldAttributes[i];
		
		this.hiliteField(this.getRequiredElement(attrs.id),attrs.valid,null);
	}
}

/* 	TODO: handle the various types of input fields differently/explicitly.
	For example, the backgroundColor works for select boxes on Firefox and IE, but
	not Safari.
*/ 
CRS_Utils.prototype.hiliteField = function(field,valid,message)
{
	field.style.backgroundColor = (valid ? "white" : "red");
	
	if (message != null) // caller wants title to refer to field's validity
	{
		field.title = valid ? null : message;
	}
	
	if (!valid && (this.invalidFields != null))
	{
		this.invalidFields[this.invalidFields.length] = field;
	}
}

CRS_Utils.prototype.prepareToValidate = function()
{
	for (var i = 0; i < crs.invalidFields.length; i++)
	{
		this.hiliteField(this.invalidFields[i],true);
	}
	
	this.invalidFields = new Array();
}

CRS_Utils.prototype.validationFailed = function(e)
{
	if ((e.message == null) || (e.message.length == 0))
	{
		e.message = "Validation failed. Form not submitted.";
	}
	
	var firstProblemField = e.problemField;
	
	if ((firstProblemField == null) && (this.invalidFields.length > 0))
	{
		firstProblemField = this.invalidFields[0];
	}
	
	alert(e.message);
	
	if (firstProblemField != null)
	{
		setTimeout("focusElement('" + firstProblemField.id + "')",0);
	}
}

CRS_Utils.prototype.alwaysValid = function(field)
{
	this.hiliteField(field,true);
	
	return true;
}


CRS_Utils.prototype.isAlphaNumeric = function(field)
{
	var valid = true;
	var str = field.value.toString();
	var numberMatch = /^\w*$/;	

	valid = str.match(numberMatch) != null;

	this.hiliteField(field,valid,"Must be alpha-numeric!");
	
	return valid;
}


CRS_Utils.prototype.isNumber = function(field)
{
	var valid = true;
	var str = field.value.toString();
	var numberMatch = /^[-]?\d*\.?\d*$/;	

	valid = (str.match(numberMatch) != null);

	this.hiliteField(field,valid,"Must be numeric!");
	
	return valid;
}

CRS_Utils.prototype.isIntegerRange = function(field1,field2,min,max)
{
	var msg;
	
	if ((min == null) && (max == null))
	{
		msg = "Pair must define a range!";
	}
	else if ((min == 1) && (max == null))
	{
		msg = "Pair must define a positive range!";
	}
	else if (min == null)
	{
		msg = "Pair must define a range with a maximum of " + max + "!";
	}
	else if (max == null)
	{
		msg = "Pair must define a range with a minimum of " + min + "!";
	}
	else
	{
		msg = "Pair must define a range between " + min + " and " + max + "!";
	}

	var valid = this.isInteger(field1,min,max) && this.isInteger(field2,min,max);
		
	if (valid && (field1.value.length > 0) && (field2.value.length > 0))
	{
		var value1 = parseInt(field1.value);
		var value2 = parseInt(field2.value);
		
		valid = (value1 <= value2);
	}

	this.hiliteField(field1,valid,msg);
	this.hiliteField(field2,valid,msg);
	
	return valid;
}

CRS_Utils.prototype.getMinMaxMessage = function(min,max,isInteger)
{
	var msg;
	var numLabel = isInteger ? "integer" : "number";
	var prefix   = isInteger ? "an "     : "a ";
	
	if ((min == null) && (max == null))
	{
		msg = "Must be " + prefix + numLabel + "!";
	}
	else if ((min == 1) && (max == null))
	{
		msg = "Must be a positive " + numLabel + "!";
	}
	else if ((max == -1) && (min == null))
	{
		msg = "Must be a negative " + numLabel + "!";
	}
	else if (min == null)
	{
		msg = "Must be " + prefix + numLabel + " <= " + max + "!";
	}
	else if (max == null)
	{
		msg = "Must be " + prefix + numLabel + " >= " + min + "!";
	}
	else
	{
		msg = "Must be " + prefix + numLabel + " between " + min + " and " + max + "!";
	}
	
	return msg;
}

CRS_Utils.prototype.isInteger = function(field,min,max)
{
	return this.validateNumber(field,min,max,true);
}

CRS_Utils.prototype.isFloat = function(field,min,max)
{
	return this.validateNumber(field,min,max,false);
}

CRS_Utils.prototype.validateNumber = function(field,min,max,isInteger)
{
	var msg = this.getMinMaxMessage(min,max,isInteger);
	var valid = false;

	if (field.value.length == 0)
	{
		valid = true;
	}
	else
	{
		var numValue = isInteger ? parseInt(field.value) : parseFloat(field.value);
	
		if (!isNaN(numValue))
		{
			var str = field.value.toString();
			var numberMatch = isInteger ? /^[-]?\d*$/ : /^[-]?\d*\.?\d*$/;	// parseFloat/parseInt too lenient, so double-check
		
			if (str.match(numberMatch) != null)
			{
				if (((min == null) || (min <= numValue)) && ((max == null) || (numValue <= max)))
				{
					valid = true;
				}
			}
		}
	}
	
	this.hiliteField(field,valid,msg);
	
	return valid;
}

// Tests an array of fields to see if they're all empty
CRS_Utils.prototype.allEmpty = function(fieldArray)
{
	var allEmpty = true;
	
	for (var i = 0; i < fieldArray.length; i++)
	{
		if (fieldArray[i].value.length > 0)
		{
			allEmpty = false;
			break;
		}
	}
	
	return allEmpty;
}

// Emptys an array of fields
CRS_Utils.prototype.clearAll = function(fieldArray)
{
	for (var i = 0; i < fieldArray.length; i++)
	{
		fieldArray[i].value = "";
	}
}

CRS_Utils.prototype.isPresent = function(field)
{
	var valid = (field.value.length > 0);
		
	crs.hiliteField(field,valid,"Required!");
	
	return valid;
}
