/*
 * UIGen-Pro standard JavaScript library.
 * Copyright (C) 2004, 2005 Ubicom Inc. <www.ubicom.com>.  All rights reserved.
 */

//**************************************************************************
// Global variables

iframe_timeout = 60;			// in 1/10ths of a second. Default is 6 seconds.
UNTRANSLATED = '(UNTRANSLATED)';	// Used in i18n mode when no translation present for a string.
XXX = '(XXX)';				// Used in i18n mode when only a dummy translation present for a string.

//***************************************************************************
// Utility functions

/*
 * getAttribute()
 *	Get the value of an attribute from a DOM node by name, or return
 *	null if the attribute doesn't exist. Thanks to the overly complex and
 *	generally sucky DOM standards, the proper way to do this is both
 *	browser dependent and node-type dependent. Try a bunch of different
 *	ways before giving up.
 */
function getAttribute (node,attribute_name)
{
	/*
	 * First try a fast way that works in newer versions of MSIE but not
	 * in Netscape/Mozilla.
	 */
	if (node[attribute_name]) {
		return node[attribute_name];
	}

	/*
	 * Hmm, that didn't work, so try a (hopefully) more portable way that
	 * should work with IE4+ and netscape. Of course it's quite possible
	 * that the attribute doesn't exist, but there's no harm in trying
	 * to access it a different way.
	 */
	if (node.getAttribute) {
		return node.getAttribute (attribute_name);
	}

	/*
	 * Well that didn't work either. The node doesn't have the getAttribute()
	 * function, so try another semi-portable XMLDOM-ish way.
	 */
	if (node.attributes) {
		var attr = node.attributes.getNamedItem (attribute_name);
		if (attr) return attr.nodeValue;
	}

	/*
	 * Give up.
	 */
	return null;
}


/*
 * getEventObject()
 *	This is called from event handling functions to find the object
 *	that invoked this event. This is horribly browser dependent,
 *	which is why we have a separate function to do it.
 *	The parameter 'e' is what is passed to the event handling function.
 */
function getEventObject (evt)
{
	evt = (evt) ? evt : ((window.event) ? window.event : "");
	if (evt) {
		var obj = (evt.srcElement != null) ? evt.srcElement : evt.target;
	} else {
		var obj = null;
	}
	return obj;
}


/*
 * cloneObject()
 *	Make a deep (i.e. recursive) cloned copy of the fields in the
 *	given Object. Note that a=cloneObject(b) is different from a=b,
 *	as the latter simply assigns a reference to the object to the
 *	variable, making both a and b refer to the same instance, whereas
 *	cloneObject() makes a separate instance.
 */
function cloneObject (obj)
{
	var newobj = new Object
	for (var i in obj) {
		if (typeof(obj[i]) == "object") {
			newobj[i] = cloneObject (obj[i]);
		}
		else {
			newobj[i] = obj[i];
		}
	}
	return newobj;
}


/*
 * copyObject()
 *	Make a deep (i.e. recursive) cloned copy of the fields in the
 *	source Object, copying them to the destination object.
 *	The destination is presumed to already have the necessary
 *	object members.
 */
function copyObject (dest,src,field_name_to_ignore)
{
	for (var i in src) {
		if (typeof(src[i]) == "object") {
			copyObject (dest[i],src[i]);
		}
		else {
			if (i != field_name_to_ignore) {
				dest[i] = src[i];
			}
		}
	}
}


/*
 * mergeObject()
 *	For all fields in the destination object, copy over the corresponding
 *	field from the source object if it exists. Don't change the structure
 *	of the destination object.
 */
function mergeObject (dest,src)
{
	for (var i in dest) {
		if (typeof(src[i]) != "undefined") {
			if (typeof(src[i]) == "object") {
				if (typeof(dest[i]) == "object") {
					// src and dest are objects
					mergeObject (dest[i],src[i]);
				}
				else {
					// src is an object but dest is not, so do nothing
					// as we do not want to change the structure of dest.
				}
			}
			else {
				// src is not an object
				if (typeof(dest[i]) == "object") {
					// dest is an object but src is not, so do nothing
					// as we do not want to change the structure of dest.
				}
				else {
					// src and dest are scalars
					dest[i] = src[i];
				}
			}
		}
	}
}


/*
 * compareObject()
 *	Make a deep (i.e. recursive) comparison of the fields in the
 *	A and B objects. return 1 if the same or 0 if not.
 *	B object is presumed to have the same object members as A,
 *	however if B has any members not present in A they will be
 *	ignored.
 */
function compareObject (A,B,field_name_to_ignore)
{
	for (var i in A) {
		if (typeof(B[i]) == "undefined") continue;
		if (typeof(A[i]) == "object") {
			if (compareObject (A[i],B[i])==0) return 0;
		}
		else {
			if (i != field_name_to_ignore && A[i] != B[i]) {
				return 0;
			}
		}
	}
	return 1;
}


/*
 * appendArray()
 *	Append array B to array A, but don't use concat() because that
 *	would create a separate array object, resulting in poor performance.
 */
function appendArray (A,B)
{
	var k = A.length;
	var n = B.length;
	for (var i=0; i<n; i++) {
		A[k] = B[i];
		k++;
	}
}


/*
 * stripWhite()
 *	Strip whitespace from the start and end of a string.
 */
function stripWhite (s)
{
	s = new String(s);
	var s2 = s.replace (/^\s*/,"");
	if (!s2) {
		return s;
	}
	s2 = s2.replace (/\s*$/,"");
	if (!s2) {
		return s;
	}
	return s2;
}


/*
 * addWhite()
 *	Add whitespace to ,'s and -'s from the start and end of a string.
 */
function addWhite (s)
{
	s = new String(s);


	var got = s.replace (/ /g, "");
	if (!got) {
		return "";
	}
	s = got;
	got = s.replace (/-/gi," - ");
	if (!got) {
		return "";
	}
	s = got;
	got = s.replace (/,/gi,", ");
	if (got) {
		return got;
	}
	return "";
}


/*
 * isBlank()
 *	Return 1 if the string consists entirely of whitespace, otherwise return 0.
 */
function isBlank (s)
{
	s = new String(s);
	return (s.match (/^\s*$/)) ? 1 : 0;
}


/*
 * convertNumber()
 *	Given an object, if it represents a number then return a number,
 *	otherwise return null.
 */
function convertNumber (s)
{
	s = new String(s);
	var got = s.match (/^\s*0*(\S*)\s*$/);
	if (got) {
		var q = got[1];
		var n = parseInt (q,10);
		if (!isNaN (n)) {
			var ns = n + "";	// convert number back to string
			if (ns == q) return n;	// compare with original stripped string
		}
	}
	return null;
}


/*
 * naturalize()
 *	Given a string, if it represents a number then return a number,
 *	otherwise return the original string.
 */
function naturalize (s)
{
	var n = convertNumber (s);
	return (n != null) ? n : s;
}


/*
 * normalizeHexString()
 *	Return a hex string in a normalized form so that if two normalized
 *	hex strings are the same then they represent the same byte array.
 */
function normalizeHexString (s)
{
	s = new String(s);
	s = s.replace (/[:-]/g,'');
	return s.toUpperCase();
}


/*
 * exists()
 */
function exists (x)
{
	return typeof(x) != "undefined";
}

//***************************************************************************
// Base-64 encoding
//
// base 64 encoding is used, except for the following changes:
//   * the symbols . and _ are used instead of + and /
//   * the binary byte string is assumed to be a multiple of 3 bytes, so that
//     the base-64 string is a multiple of 4 characters. if it is not, it is
//     padded with 0's. no '=' symbols are used to mark the end of the encoded
//     string.

/*
 * convertToBase64()
 *	Convert an array of 8-bit bytes into a base-64 encoded string.
 */
function convertToBase64 (a)
{
	/*
	 * First make 'a' a multiple of 3 for the convenience of the code that follows.
	 */
	while (a.length % 3) {
		a[a.length] = 0;
	}

	/*
	 * Create the encoded string in groups of 4 characters. Note that we don't
	 * use the += operator to conatenate the substrings together because that
	 * is *extremely* slow. The faster way is to build an array that contains
	 * the substrings and then use the array join() function.
	 */
	var b64 = ".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
	var barray = new Array();
	for (var i=0; i < a.length; i += 3) {
		var x = (a[i] << 16) | (a[i+1] << 8) | a[i+2];
		barray[barray.length] =
			b64.charAt((x >> 18) & 0x3f) +
			b64.charAt((x >> 12) & 0x3f) +
			b64.charAt((x >> 6) & 0x3f) +
			b64.charAt(x & 0x3f);
	}
	return barray.join ("");
}


/*
 * buildCharToSixBitsArray()
 *	Build a 256 entry array that convert a base-64 character
 *	to its corresponding 6-bit number. Invalid characters will
 *	be translated to 0.
 */
function buildCharToSixBitsArray()
{
	b64_char_to_6 = new Array;
	for (var i=0; i<256; i++) b64_char_to_6[i] = 0;
	for (var i=0; i<26; i++) b64_char_to_6[i+65] = i+1;
	for (var i=0; i<26; i++) b64_char_to_6[i+97] = i+27;
	for (var i=0; i<10; i++) b64_char_to_6[i+48] = i+53;
	b64_char_to_6[95] = 63;
}
buildCharToSixBitsArray();


/*
 * convertFromBase64()
 *	Read base-64 encoded binary data from the string `buf', and return
 *	the resulting array of 8-bit numbers. Invalid characters in the
 *	string will be interpreted as '.'.
 */
function convertFromBase64 (buf)
{
	while (buf.length & 3) buf += '.';	// make 'buf' a multiple of 4
	var dst = new Array;
	var j = 0;
	for (var i = 0; i < buf.length; i += 4) {
		var cc = (b64_char_to_6 [buf.charCodeAt(i)] << 18) |
			 (b64_char_to_6 [buf.charCodeAt(i+1)] << 12) |
			 (b64_char_to_6 [buf.charCodeAt(i+2)] << 6) |
			 (b64_char_to_6 [buf.charCodeAt(i+3)]);
		dst[j]   = (cc >> 16) & 0xff;
		dst[j+1] = (cc >> 8) & 0xff;
		dst[j+2] = (cc) & 0xff;
		j += 3;
	}
	return dst;
}

//***************************************************************************
// Conversion functions: convert data between the string representation used
// by form elements and the binary array representation used to pass data to
// and from the server.

/*
 * intToByteArray()
 *	Convert an integer value to a byte array of size 'length'.
 */
function intToByteArray (value,length)
{
	var a = new Array;
	for (var i=0; i<length; i++) {
		a[i] = (value >>> ((length-1-i)*8)) & 0xff;
	}
	return a;
}


/*
 * byteArrayToInt()
 *	Convert the byte array indexes start_index to end_index-1 to an integer.
 *	The number of bytes in the range must be is 1 to 4.
 *	If 4 bytes are converted then the returned value will be signed,
 *	otherwise the returned value will be unsigned for 1-3 bytes converted.
 */
function byteArrayToInt (b,start_index,end_index)
{
	var n=0;
	for (var i=start_index; i<end_index; i++) n = (n<<8) + b[i];
	return n;
}


/*
 * stringToByteArray()
 *	Convert a string to a byte array of size length+1. The last byte is
 *	always zero. If the string is smaller than 'length' then it is padded
 *	with zeros. If the string is larger than 'length' then it is truncated.
 *	If the string contains wide characters then it is converted into UTF-8.
 *	Special care is needed to ensure that UTF-8 byte sequences are not
 *	partially truncated.
 */
function stringToByteArray (s,length)
{
	var a = new Array;
	var len = s.length;
	for (var i=0; i<length; i++) {
		a[i] = 0;
	}
	var j=0;		// Array index
	for (var i=0; i<len; i++) {
		// Read characters from the strings one by one and convert
		// them into UTF-8 byte sequences.
		var c = s.charCodeAt (i);
		if (c <= 0x7F) {
			if (j >= length) break;
			a[j] = c;
			j += 1;
		}
		else if (c <= 0x7FF) {
			if (j+1 >= length) break;
			a[j] = (c >> 6) | 0xc0;
			a[j+1] = (c & 0x3f) | 0x80;
			j += 2;
		}
		else if (c <= 0xFFFF) {
			if (j+2 >= length) break;
			a[j] = (c >> 12) | 0xe0;
			a[j+1] = ((c >> 6) & 0x3f) | 0x80;
			a[j+2] = (c & 0x3f) | 0x80;
			j += 3;
		}
		else if (c <= 0x10FFFF) {
			if (j+3 >= length) break;
			a[j+0] = (c >> 24) | 0xf0;
			a[j+1] = ((c >> 12) & 0x3f) | 0x80;
			a[j+2] = ((c >> 6) & 0x3f) | 0x80;
			a[j+3] = (c & 0x3f) | 0x80;
			j += 4;
		}
		else {
			// Default behavior when we don't know what else to do
			if (j >= length) break;
			a[j] = c;
			j += 1;
		}
	}
	a[length] = 0;
	return a;
}


/*
 * byteArrayToString()
 *	Convert a byte array to a string. Only array indexes
 *	between start_index and end_index-1 are examined.
 *	UTF-8 byte sequences are converted properly.
 */
function byteArrayToString (b,start_index,end_index)
{
	var s = "";
	for (var i=start_index; i<end_index; i++) {
		if (b[i] == 0) {
			break;
		}
		else if (b[i] <= 0x7f) {
			s += String.fromCharCode (b[i]);
		}
		else if ((b[i] & 0xe0) == 0xc0 && (i+1) < end_index) {
			s += String.fromCharCode (((b[i] & 0x1f) << 6) | (b[i+1] & 0x3f));
			i += 1;
		}
		else if ((b[i] & 0xf0) == 0xe0 && (i+2) < end_index) {
			s += String.fromCharCode (((b[i] & 0x0f) << 12) | ((b[i+1] & 0x3f) << 6) | (b[i+2] & 0x3f));
			i += 2;
		}
		else if ((b[i] & 0xf8) == 0xf0 && (i+3) < end_index) {
			s += String.fromCharCode (((b[i] & 0x07) << 18) | ((b[i+1] & 0x3f) << 12) | ((b[i+2] & 0x3f) << 6) | (b[i+3] & 0x3f));
			i += 3;
		}
	}
	return s;
}


/*
 * IPAddressToInteger()
 *	Convert an IP address formatted string into a 32 bit integer.
 *	Return null if the string is not formatted correctly.
 */
function IPAddressToInteger (s)
{
	s = new String(s);
	var got = s.match (/^\s*(\d+)\s*[.]\s*(\d+)\s*[.]\s*(\d+)\s*[.]\s*(\d+)\s*$/);
	if (got) {
		var x = 0;
		for (var i=1; i <= 4; i++) {
			var q = parseInt(got[i],10);
			if (q < 0 || q > 255) return null;
			x = x | (q << ((4-i)*8));
		}
		return x;
	}
	else return null;
}

/*
 * IPAddressToUnsignedInteger()
 *	Convert an IP address formatted string into an unsigned integer.
 *	Return null if the string is not formatted correctly.
 *	In JavaScript all numbers are floating-point numbers. The range of
 *	integer is from -2**53 to 2**53.
 *	Examples are
 *		IPAddressToUnsignedInteger("255.255.255.255") = 4294836225
 *  	IPAddressToInteger("255.255.255.255") = -1
 */
function IPAddressToUnsignedInteger (s)
{
	s = new String(s);
	var got = s.match (/^\s*(\d+)\s*[.]\s*(\d+)\s*[.]\s*(\d+)\s*[.]\s*(\d+)\s*$/);
	if (got) {
		var x = 0;
		for (var i=1; i <= 4; i++) {
			var q = parseInt(got[i],10);
			if (q < 0 || q > 255) return null;
			x = x * 256 + q;
		}
		return x;
	}
	else return null;
}

/*
 * IPAddressToByteArray()
 *	Convert an IP address formatted string into a 4 element byte array.
 *	Return 0 if the string is not formatted correctly.
 */
function IPAddressToByteArray (s)
{
	s = new String(s);
    var got = s.match (/^\s*(\d+)\s*[.]\s*(\d+)\s*[.]\s*(\d+)\s*[.]\s*(\d+)\s*$/);
	if (got) {
		var a = new Array;
		for (var i=1; i <= 4; i++) {
			var q = parseInt(got[i],10);
            if (q < 0 || q > 255) return 0;
			a[i-1] = q;
		}
		return a;
	}
	else return 0;
}


/*
 * byteArrayToIPAddress()
 *	Convert a byte array 'a' into an IP address formatted string.
 *	Elements in 'a' from start_index to start_index+3 are used.
 */
function byteArrayToIPAddress (a,start_index)
{
	var s = "";
	for (var i=0; i < 4; i++) {
		s += a[i+start_index].toString(10);
		if (i < 3) s += ".";
	}
	return s;
}


/*
 * hexStringToByteArray()
 *	Convert a hex string 's' into a byte array of 'length' bytes.
 *	If 's' is shorter than 'length' bytes, the result will be padded with zeros.
 *	Ignore leading and trailing whitespace. If there are any conversion errors
 *	or the string is invalid then return 0, else return a byte array.
 *	If the string is empty or all whitespace, return an array of all zeros.
 *	Ignore the commonly used separator characters (:,-)
 */
function hexStringToByteArray (s,length)
{
	// Ignore the commonly used separator characters
	s = new String(s);
	s = s.replace (/[:-]/g,'');

	// if the string is empty, return all zeros
	if (s.match (/^\s*$/)) {
		var a = new Array;
		for (var i=0; i<length; i++) a[i] = 0;
		return a;
	}

	// extract the hex digits of the string
	var got = s.match (/^\s*([0-9a-fA-F]+)\s*$/);
	if (got) {
		s = got[1];
		if (s.length > length*2) return 0;	// error: string to long
		if (s.length & 1) return 0;		// error: an even number of characters needed
		// pad string with zeros
		while (s.length < length*2) {
			s = s + '0';
		}
		var a = new Array;
		for (var i=0; i < length; i++) {
			a[i] = parseInt(s.substr(i*2,2),16);
		}
		return a;
	}
	else return 0;
}


/*
 * byteArrayToHexString()
 *	Convert a byte array 'a' into a hex string. Only array indexes
 *	between start_index and end_index-1 are examined.
 *	If the 'separator' argument is given it will be used to separate the bytes.
 */
function byteArrayToHexString (a,start_index,end_index,separator)
{
	var s = "";
	if (typeof(separator) != "string") separator = "";
	for (var i=start_index; i<end_index; i++) {
		var s2 = a[i].toString(16);
		if (s2.length < 2) s2 = '0' + s2;
		s += s2;
		if (i < end_index-1) s += separator;
	}
	return s.toUpperCase();
}


/*
 * byteArrayToHexString2()
 *	This is the same as byteArrayToHexString() but an all-zero array will
 *	be converted into a blank string.
 */
function byteArrayToHexString2 (a,start_index,end_index,separator)
{
	for (var i=start_index; i<end_index; i++) {
		if (a[i] != 0) return byteArrayToHexString (a,start_index,end_index,separator);
	}
	return "";
}

//***************************************************************************
// Set/get functions that convert data between the binary array representation
// and the representation used in the form elements.
// The s*() functions:
//	- Pull the data out of the global 'byte_array' at index 'i'.
//	- Increment the global variable 'i'.
//	- Return the data.
// The g*() functions:
//	- Accept data that is encoded into a byte array.
//	- Append that array to the global 'byte_array'.

/*
 * sS()
 *	C string of the given maximum length. The length does not include the
 *	terminating 0. This extracts length+1 bytes in the byte array, but note
 *	that only the first 'length' bytes are read, the last one is assumed to
 *	be a terminating 0.
 */
function sS (length)
{
	// note: array passed to byteArrayToString() does not include terminating 0
	var value = byteArrayToString (byte_array,i,i+length);
	i += length+1;
	return value;
}


/*
 * gS()
 *	C string of the given maximum length. The length does not include the
 *	terminating 0. The number of bytes appended is length+1.
 */
function gS (value,length)
{
	if (byte_array_has_error) return;
	if (value.length > length) {
		alert ('The string "' + value + '" is too long\n(maximum length is ' + length + ' characters).');
		byte_array_has_error = 1;
		return;
	}
	appendArray (byte_array,stringToByteArray (value,length));
}


/*
 * sC()
 *	Single byte character.
 */
function sC()
{
	var value = String.fromCharCode (byte_array[i]);
	i++;
	return value;
}


/*
 * gS()
 *	Single byte character.
 */
function gC (value)
{
	if (byte_array_has_error) return;
	byte_array[byte_array.length] = value.charCodeAt (0);
}


/*
 * sU()
 *	Unsigned integer of the given number of bytes (1 to 4).
 */
function sU (length)
{
	var value = byteArrayToInt (byte_array,i,i+length);
	i += length;
	if (length == 4 && value < 0) {
		// byteArrayToInt() returns a signed value if length==4, so compensate
		value = value + 4294967296;
	}
	return value;
}


/*
 * gIU_Helper()
 *	Helper function for gI() and gU()
 */
function gIU_Helper (value,length,signed)
{
	if (byte_array_has_error) return;
	var v;
	if (value==true || value=="true") {
		v = 1;
	}
	else if (value==false || value=="false") {
		v = 0;
	}
	else {
		v = convertNumber (value);
		if (v == null) {
			alert ('The number "' + value + '" is not valid.');
			byte_array_has_error = 1;
			return;
		}
	}
	if (signed==0 && v < 0) {
		alert ('The number "' + value + '" must be positive.');
		byte_array_has_error = 1;
		return;
	}

	// range checking
	var min,max;
	if (length <= 3) {
		if (signed==0) {
			max = (1<<(length*8))-1;
			min = 0;
		}
		else {
			max = (1<<(length*8-1))-1;
			min = -max-1;
		}
	}
	else {
		if (signed==0) {
			max = 4294967295;
			min = 0;
		}
		else {
			max = 2147483647;
			min = -max-1;
		}
	}
	if (v < min || v > max) {
		alert ('The number "' + value + '" should be in between '+ min + ' to ' + max + '.');
		byte_array_has_error = 1;
		return;
	}

	appendArray (byte_array,intToByteArray (v,length));
}


/*
 * gU()
 *	Unsigned integer of the given number of bytes (1 to 4).
 */
function gU (value,length)
{
	gIU_Helper (value,length,0);
}


/*
 * sI()
 *	Signed integer of the given number of bytes (1 to 4).
 */
function sI (length)
{
	var value = byteArrayToInt (byte_array,i,i+length);
	i += length;
	var topbit = 1 << (length*8-1);
	if (length < 4 && (value & topbit) != 0) {
		// do a sign conversion within the lower 'length' bytes.
		if (value == topbit) {
			value = -topbit;
		}
		else {
			value = -( ((~value)+1) & (topbit-1) );
		}
	}
	return value;
}


/*
 * gI()
 *	Signed integer of the given number of bytes (1 to 4).
 */
function gI (value,length)
{
	gIU_Helper (value,length,1);
}


/*
 * sH()
 *	Hex string of the given length (in bytes).
 */
function sH (length,separator)
{
	if (length==6 && separator==null) {
		separator = ':';		// hack to get MAC addresses to display with colons
	}
	var value = byteArrayToHexString (byte_array,i,i+length,separator);
	i += length;
	return value;
}


/*
 * gH()
 *	Hex string of the given length (in bytes).
 */
function gH (value,length)
{
	if (byte_array_has_error) return;
	var h = hexStringToByteArray (value,length);
	if (h==0) {
		alert ('The hex string "' + value + '" is not valid.')
		byte_array_has_error = 1;
		return;
	}
	appendArray (byte_array,h);
}


/*
 * sX()
 *	IP address.
 */
function sX()
{
	var value = byteArrayToIPAddress (byte_array,i);
	i += 4;
	return value;
}


/*
 * gX()
 *	IP address.
 */
function gX (value)
{
	if (byte_array_has_error) return;
	var ip = IPAddressToByteArray (value);
	if (ip==0) {
		alert ('The IP address "' + value + '" is not valid.')
		byte_array_has_error = 1;
		return;
	}
	appendArray (byte_array,ip);
}

//***************************************************************************
// Helper functions for various user interface elements

function doExpandCollapse (e)
{
	var obj = getEventObject (e);
	var s = getAttribute (obj,'_ALT_VALUE');
	obj.setAttribute ('_ALT_VALUE',obj.value);
	obj.value = s;
	var v = getAttribute (obj,'_TOGGLE_VARIABLE');
	eval (v + ' = !' + v);
	dataChanged();
}

//***************************************************************************
// Array editor class

/*
 * lookForUnusedEntry()
 *	Look for the first unused entry in an array given an arrayEditor object.
 *	Return -1 if all entries are used.
 */
function lookForUnusedEntry (ae)
{
	var i;
	if (ae.is_stack == 1) {
		for (i=ae.max_elements-1; i>=0; i--) {
			if (ae.thearray[i].used==0) return i;
		}
	}
	else
	{
		for (i=0; i<ae.max_elements; i++) {
			if (ae.thearray[i].used==0) return i;
		}
	}
	return -1;
}

/*
 * compactArray()
 *	Compact the array data after an entry is deleted or when an array gets
 *	initialized with the stack flag. This function can compact from the
 *	top down or the bottom up, depending on the arguments.
 */
function compactArray (ae, start_index, condition, increment_index)
{
	var dest=start_index;
	for (var src=start_index; src!=condition; src+=increment_index) {
		if (ae.thearray[src].used==1) {
			if (dest != src) {
				// move entry [src] to [dest]. do this by swapping object references.
				// note that it's not sufficient to just move the reference by 'array[dest]=array[src]',
				// since that would leave two array slots refering to the same object.
				var obj = ae.thearray[dest];
				ae.thearray[dest] = ae.thearray[src];
				ae.thearray[src] = obj;
			}
			dest += increment_index;
		}
	}
}

/*
 * arrayEditor object
 *	Constructor for the arrayEditor class.
 */
function arrayEditor (maximum_elements,thearray,clear_entry_function,primary_key,primary_key_name)
{
	/*
	 * Create object data
	 */
	this.max_elements = maximum_elements;			// total size of array to edit
	this.thearray = thearray;				// array to edit
	this.clear_entry_function = clear_entry_function;	// function to clear an array entry
	this.sel = -1;						// element currently selected for editing, -1 if none
	this.clear_entry_function_valid = (typeof(clear_entry_function)=="function");
	this.primary_key = primary_key;
	this.primary_key_name = primary_key_name;
	this.primary_key_hex_string = 0;
	this.is_stack = 0;					// if 1, array editor will be used as stack
	this.is_under_edit = 0;

	/*
	 * Make an array index -1 that is used to edit new and existing entries
	 */
	thearray[-1] = cloneObject (thearray[0]);
	if (this.clear_entry_function_valid) {
		clear_entry_function(-1);
	}

	/*
	 * Object methods
	 */
	this.saveEntry = arrayEditor_saveEntry;
	this.deleteEntry = arrayEditor_deleteEntry;
	this.editEntry = arrayEditor_editEntry;
	this.warnIfChanged = arrayEditor_warnIfChanged;
	this.checkIfEditing = arrayEditor_checkIfEditing;
	this.cancelEditingEntry = arrayEditor_cancelEditingEntry;
	this.makeStack = arrayEditor_makeStack;
	this.isFull = arrayEditor_isFull;

	/*
	 * Add to the list of array editors on this page
	 */
	if (typeof(page_array_editors) == 'undefined') {
		page_array_editors = new Array;
	}
	page_array_editors[page_array_editors.length] = this;
}

function arrayEditor_makeStack()
{
	/*
	 * Set behavior of the arrayEditor like stack
	 */
	this.is_stack = 1;
	compactArray (this, this.max_elements-1, -1, -1);
}

/*
 * arrayEditor:isFull()
 *	Check if array editior is full.
 */
function arrayEditor_isFull()
{
	if (this.is_under_edit == 1)
	{
		return 0;
	}
	/*
	 * Check that there is a place to save the entry to.
	 */
	var i = lookForUnusedEntry (this);
	if (i < 0) {
		return 1;
	}
	return 0;
}

/*
 * arrayEditor:saveEntry()
 *	Save/add the current entry.
 *	Return 1 on success or 0 if the data could not be saved.
 */
function arrayEditor_saveEntry()
{
	/*
	 * Check that there is a place to save the entry to.
	 */
	var i = this.sel >= 0 ? this.sel : lookForUnusedEntry (this);
	if (i < 0) {
		alert ('There is no room for any more entries.');
		return 0;
	}

	/*
	 * Check for blank or duplicate primary keys
	 */
	if (typeof(this.primary_key)=="string") {
		this.thearray[-1][this.primary_key] = trimString(this.thearray[-1][this.primary_key]);
		if (this.thearray[-1][this.primary_key] == "") {
			alert ("The '"+this.primary_key_name+"' field can not be blank");
			return 0;
		}
		for (var j=0; j<this.max_elements; j++) {
			if (this.thearray[j].used==1 && j != this.sel) {
				var comp1 = this.thearray[-1][this.primary_key];
				var comp2 = this.thearray[j][this.primary_key];
				if (this.primary_key_hex_string == 1) {
					comp1 = normalizeHexString (comp1);
					comp2 = normalizeHexString (comp2);
				}
				if (comp1 == comp2) {
					alert (this.primary_key_name+" '"+ this.thearray[-1][this.primary_key] +"' is already used");
					return 0;
				}
			}
		}
	}

	/*
	 * Verify the entry we are about to save.
	 */
	// @@@ Currently we copy the edited entry to all of the array entries before
	//     calling pageVerify(), so that we can ensure that only this entry is
	//     checked. This is kind of inefficient. Note that we must ensure that
	//     no other entries are checked, as the user might have toggled the
	//     'on' checkbox for those entries, invalidating them.
	var saved_array = new Array;				// save all array elements here
	for (var k=0; k<this.max_elements; k++) {
		saved_array[k] = this.thearray[k];
		this.thearray[k] = this.thearray[-1];		// put edited entry as thearray[i]. NOTE, making a reference, not a copy!
	}
	this.thearray[-1].used = 1;

	byte_array_has_error = 0;
	createBinaryArrayFromDataObject();
	if (byte_array_has_error) {
		for (var k=0; k<this.max_elements; k++) {		// restore thearray to its original state
			this.thearray[k] = saved_array[k];
		}
		return 0;
	}

	if (typeof(pageVerify) == "function") {
		if (!pageVerify()) {
			for (var k=0; k<this.max_elements; k++) {	// restore thearray to its original state
				this.thearray[k] = saved_array[k];
			}
			return 0;
		}
	}

	for (var k=0; k<this.max_elements; k++) {		// restore thearray to its original state
		this.thearray[k] = saved_array[k];
	}

	/*
	 * Save the entry, but not the 'enabled' field which is usually edited elsewhere.
	 */
	copyObject (this.thearray[i],this.thearray[-1]);
	this.thearray[i].used = 1;
	this.cancelEditingEntry();
	return 1;
}


function arrayEditor_deleteEntry (i)
{
	/*
	 * Sanity checking
	 */
	if (i < 0 || this.thearray[i].used==0) return;
	if (!confirm ("Are you sure you want to delete this entry?")) return;

	/*
	 * Make sure user is not in the middle of editing something else.
	 */
	if (this.warnIfChanged()==0) return;

	/*
	 * Mark the entry unused.
	 */
	this.thearray[i].used = 0;
	this.thearray[i].enabled = 0;		// this will be okay even if there's no 'enabled' field

	/*
	 * Pack the used elements in the array, shuffling undeleted entries down.
	 */
	if (this.is_stack == 1) {
		compactArray (this, this.max_elements-1, -1, -1);
	}
	else {
		compactArray (this, 0, this.max_elements, 1);
	}

	this.cancelEditingEntry()
}


function arrayEditor_editEntry (i)
{
	/*
	 * Make sure user is not in the middle of editing something else.
	 */
	if (this.warnIfChanged()==0) return;

	/*
	 * Edit entry 'i'.
	 */
	this.is_under_edit = 1;
	copyObject (this.thearray[-1],this.thearray[i]);
	this.sel = i;
	dataChanged();
}

/*
 * arrayEditor:warnIfChanged()
 *	Return 0 if there is any changed-but-unsaved data, and the user doesn't want to abandon it.
 *	Otherwise return 1.
 */
function arrayEditor_warnIfChanged()
{
	/*
	 * Note that fields called 'enabled' are generally edited in a different
	 * way, as checkboxes next to entries in the array list, so don't warn
	 * about changes in that field.
	 */
	if (this.checkIfEditing('enabled') > 0) {
		if (!confirm ("You have unsaved changes in the entry you are editing.\n"+
			      "Press 'Ok' to abandon these changes and perform the requested action.\n"+
			      "Otherwise press 'Cancel'.")) {
			return 0;
		}
	}
	return 1;
}

/*
 * arrayEditor:checkIfEditing()
 *	Return 1 if there is changed-but-unsaved data in the array itself.
 *	Return 2 if there are new edits not yet saved in the array.
 *	Otherwise return 0.
 */
function arrayEditor_checkIfEditing (field_to_ignore)
{
	if (this.sel >= 0) {
		if (compareObject (this.thearray[-1],this.thearray[this.sel],field_to_ignore) == 0) {
			return 1;
		}
	}
	else {
		/*
		 * Prepare element [-2] to be a cleaned clone of a proper element.
		 */
		this.thearray[-2] = cloneObject (this.thearray[0]);
		if (this.clear_entry_function_valid) {
			this.clear_entry_function(-2);
		}

		/*
		 * Compare the clean element against editing element (-1) to see if the user has
		 * made changes.
		 */
		if (compareObject (this.thearray[-1],this.thearray[-2]) == 0) {
			return 2;
		}
	}
	return 0;
}


function arrayEditor_cancelEditingEntry()
{
	if (this.clear_entry_function_valid) {
		this.clear_entry_function (-1);
	}
	this.sel = -1;
	this.is_under_edit = 0;
	dataChanged();
}


function generateArray (ae, has_enabled_column, has_edit_delete_column, elements)
{
	for (var i=0; i<ae.max_elements; i++) {
		if (ae.thearray[i].used==0) {
			continue;
		}
		var html = '<tr _BGCOLOR="(ae.sel=='+i+') ? \'#c0c0c0\' : \'\'">';
		if (has_enabled_column) {
			html += '<td><input type="checkbox" _VALUE="ae.thearray['+i+'].enabled"></td>';
		}
		for (var j=0; j<elements.length; j++) {
			html += '<td>' + eval (elements[j].replace (/\@index\@/g,i)) + '</td>';
		}
		if (has_edit_delete_column) {
			html += '<td><a href="javascript:ae.editEntry ('+i+')"><img src="img_edit.gif" alt="Edit" border=0></a></td>';
			html += '<td><a href="javascript:ae.deleteEntry ('+i+')"><img src="img_delete.gif" alt="Delete" border=0></a></td>';
		}
		html += '</tr>';
		document.write (html);
	}
}

/*
 * anyPageArrayEditorsChanged()
 *	Return 1 if any of the non- information-only array editors on the page
 *	contain changed data and the user does not want to abandon it.
 *	Otherwise make sure all the information is saved and return 0 if it
 *	was saved successfully or 1 if not.
 */
function anyPageArrayEditorsChanged()
{
	if (typeof(page_array_editors) != 'undefined') {
		for (var i=0; i<page_array_editors.length; i++) {
			if (page_array_editors[i].clear_entry_function_valid) {
				if (page_array_editors[i].warnIfChanged()==0) {
					return 1;
				}
			}
		}
	}
	return 0;
}

//***************************************************************************
// Tree walkers

/*
 * initializeFormElements()
 *	Do some once-only setup for the form elements.
 *	Provide onclick and onchange event handlers for form elements that don't have them.
 */
function initializeFormElements()
{
	for (var i=0; i<document.forms.length; i++) {
		for (var j=0; j<document.forms[i].length; j++) {
			var element = document.forms[i].elements[j];
			var name = getAttribute (element,'_VALUE');
			if (name) {
				switch (element.type) {
					case 'text':
					case 'password':
					case 'select-one':
						if (element.onchange == null) {
							element.onchange = formChanged;
						}
						break;
					case 'checkbox':
					case 'radio':
						if (element.onclick == null) {
							element.onclick = formChanged;
						}
						break;
				}
			}
		}
	}
}


/*
 * initializeDoDupActions()
 *	Walk the DOM tree starting from 'node' and run all the _DUP_ACTION attributes.
 */
function initializeDoDupActions (node,index)
{
	var action = getAttribute (node,'_DUP_ACTION');
	if (action) {
		obj = node;
		eval (action.replace (/\@index\@/g,index));
	}
	for (var n=node.firstChild; n != null; n=n.nextSibling) {
		initializeDoDupActions (n,index);
	}
}


/*
 * initializeDuplicateTableRows()
 *	Walk the DOM tree starting from 'node' and do the once-only duplication of table rows
 */
function initializeDuplicateTableRows (node,parent)
{
	if (node.tagName == 'TR') {
		var array_editor = getAttribute (node,'_DUPLICATE');
		if (array_editor) {
			var lae = eval(array_editor);		// NOTE: don't call this 'ae' as it conflicts with a common global 'ae'
			var num = lae.max_elements;
			var bnode = node.nextSibling;
			for (var i=0; i<num; i++) {
				var n = node.cloneNode(true);

				// @@@ this needs to be generalized?
				if (typeof(lae.thearray) != "undefined" && typeof(lae.thearray[i]) != "undefined" &&
				    typeof(lae.thearray[i].used) != "undefined") {
					n.style.display = 'none';	// cloned rows are initially invisible
					n.setAttribute ('_DISPLAYED',array_editor+".thearray["+i+"].used");
					n.setAttribute ('_BGCOLOR',"("+array_editor+".sel=="+i+") ? '#c0c0c0' : ''");
				}

				initializeDoDupActions (n,i);
				if (bnode) {
					parent.insertBefore (n,bnode);
				}
				else {
					parent.appendChild (n);
				}
			}
			parent.removeChild (node);
			return;
		}
	}
	var childcount=0;
	for (var n = node.firstChild; n != null; n=n.nextSibling) childcount++;
	for (var n = node.firstChild, i=0; n && i<childcount; i++) {
		var next = n.nextSibling;
		initializeDuplicateTableRows (n,node);
		n = next;
	}
}


/*
 * buildActiveNodeList()
 *	Walk the DOM tree starting from 'node' searching for active tags,
 *	and putting all active nodes in the global arrays _active_XXX.
 */
function buildActiveNodeListHelper (node)
{
	if (node.getAttribute) {
		if (node.getAttribute ('_INNERHTML')) _active_INNERHTML[_active_INNERHTML.length] = node;
		if (node.getAttribute ('_DISABLED')) _active_DISABLED[_active_DISABLED.length] = node;
		if (node.getAttribute ('_DISPLAYED')) _active_DISPLAYED[_active_DISPLAYED.length] = node;
		if (node.getAttribute ('_BGCOLOR')) _active_BGCOLOR[_active_BGCOLOR.length] = node;
	}

	for (var n=node.firstChild; n != null; n=n.nextSibling) {
		buildActiveNodeListHelper (n);
	}
}
function buildActiveNodeList (node)
{
	_active_INNERHTML = new Array;
	_active_DISABLED = new Array;
	_active_DISPLAYED = new Array;
	_active_BGCOLOR = new Array;
	buildActiveNodeListHelper (node);
}


/*
 * executeActiveTags()
 *	Execute all active tags. This assumes that buildActiveNodeList() was
 *	called prior to calling this function.
 */
function executeActiveTags()
{
	for (var i=0; i<_active_INNERHTML.length; i++) {
		var node = _active_INNERHTML[i];
		var ihtml = getAttribute (node,'_INNERHTML');
		var v = new String (eval (ihtml));
		v = v.replace(/&(?!nbsp;)/g,"&amp;");
		if (node.innerHTML != v) {
			node.innerHTML = v;
		}
	}
	for (var i=0; i<_active_DISABLED.length; i++) {
		var node = _active_DISABLED[i];
		var disabled = getAttribute (node,'_DISABLED');
		var v = eval(disabled);
		v = !!v;
		if (node.disabled != v) {
			node.disabled = v;
		}
	}
	for (var i=0; i<_active_DISPLAYED.length; i++) {
		var node = _active_DISPLAYED[i];
		var displayed = getAttribute (node,'_DISPLAYED');
		var v = eval(displayed) ? '' : 'none';
		if (node.style.display != v) {
			node.style.display = v;
		}
	}
	for (var i=0; i<_active_BGCOLOR.length; i++) {
		var node = _active_BGCOLOR[i];
		var bgcolor = getAttribute (node,'_BGCOLOR');
		var v = eval(bgcolor);
		if (node.bgColor != v) {
			node.bgColor = v;
		}
	}
}


/*
 * dataChanged()
 *	User functions call this when they have changed data that needs to
 *	be reflected back to the page.
 */
function dataChanged (dont_call_on_data_changed)
{
	if (dont_call_on_data_changed == null) {
		if (typeof(onDataChanged) == "function") {
			onDataChanged();
		}
	}
	// try to work around bugs in explorer 5.x
	var agent = navigator.userAgent.toLowerCase();
	if (agent.match (/msie 5[.]/)) {
		window.setTimeout ('executeActiveTags();',200);
	}
	else {
		executeActiveTags();
	}
	transferFormElementData (true);
	if (typeof(onDataEval) == "function") {
		onDataEval();
	}
}


/*
 * formChanged()
 *	Event handler that is called when form controls change.
 */
function formChanged()
{
	transferFormElementData (false);
	dataChanged();
}


/*
 * transferFormElementData()
 *	Walk all form elements, transferring data between the form elements and
 *	the local data object.
 */
function transferFormElementData (data_to_form)
{
	for (var i=0; i<document.forms.length; i++) {
		for (var j=0; j<document.forms[i].length; j++) {
			var element = document.forms[i].elements[j];
			if (name = element.getAttribute ('_VALUE')) {
				var toform='',fromform=null;
				switch (element.type) {
					case 'text':
					case 'password':
						toform = 'element.value = ' + name;
						fromform = element.value;
						break;
					case 'checkbox':
						var bitnum = name.match (/:(\d+)$/);
						if (bitnum) {
							var bitnum = bitnum[1];
							name = name.replace (/:(\d+)$/,'');
							toform = 'element.checked = ((' + name + ' & (1<<bitnum)) != 0);';
							if (!data_to_form && element.getAttribute ('_READONLY')==null) {
								if (element.checked) {
									eval (name + '=' + name + '| (1<<bitnum)');
								}
								else {
									eval (name + '=' + name + '& (~(1<<bitnum))');
								}
							}
						}
						else {
							toform = 'element.checked = (' + name + '!= 0);';
							fromform = element.checked ? 1 : 0;
						}
						break;
					case 'radio':
						if (data_to_form) {
							if (eval(name) == element.value) {
								element.checked = true;
							}
						}
						else {
							if (element.checked) fromform = element.value;
						}
						break;
					case 'select-one':
						if (data_to_form) {
							element.selectedIndex = 0;		// the default
							var val = eval(name);
							for (var k=0; k<element.options.length; k++) {
								if (val == element.options[k].value) {
									element.selectedIndex = k;
									break;
								}
							}
						}
						else {
							if (element.selectedIndex >= 0) {
								fromform = element.options [element.selectedIndex].value;
							}
						}
						break;
				}
				if (data_to_form) {
					if (toform != '') eval (toform);
				}
				else {
					if (fromform != null && getAttribute (element,'_READONLY')==null) {
						eval (name + '=fromform');
					}
				}
			}
		}
	}

	if (!data_to_form) {
		if (typeof(naturalizeDataObject) == "function") {
			naturalizeDataObject();
		}
	}
}

//***************************************************************************
// Form submission

function doSave()
{
	if (anyPageArrayEditorsChanged()) {
		return 0;
	}
	formChanged (null);
	if (typeof(pageVerify) == "function") {
		if (!pageVerify()) return;
	}

	/*
	 * See if anything has changed. If not, issue a warning about redundant saving
	 * so the user knows that the save button will have no effect.
	 */
	if (dataObjectChanged()==0) {
		if (!confirm ("Nothing has changed on this page. Do you want to save it anyway?\n")) {
			return;
		}
	}

	byte_array_has_error = 0;
	createBinaryArrayFromDataObject();
	if (byte_array_has_error) return;

	/*
	 * Invoke onSavePage() page function
	 */
	if (typeof(onSavePage) == "function") {
		onSavePage();
	}

	/*
	 * Post the new configuration to the server
	 */
	document.postform.data.value = convertToBase64 (byte_array);
	document.postform.submit();
}


function doCancel()
{
	if (confirm ("Do you want to abandon all changes you made to this page?")) {
		if (typeof(onCancelPage) == "function") {
		    onCancelPage();
		}
		uigenInitAtStart (false);
		if (typeof(onCancelComplete) == "function") {
		    onCancelComplete();
		}
		dataChanged();
	}
}


function restoreError (message)
{
	alert ("The restoration of settings failed ("+message+")\nPress OK to continue");
	restore_error = 1;
	history.go(-1);
	return 0;
}


function showRestoreMessage (i)
{
	eval ('restore_phase_done_' + i + ' = 1');
	executeActiveTags();
}


function doRestore (phase)
{
	if (phase==1) {
		restore_error = 0;
	}
	else {
		if (restore_error) return;
	}
	if (phase==1) {
		/*
		 * Convert the data string ('data' comes from the server)
		 */
		restore_error = 0;
		byte_array = convertFromBase64 (data);
		showRestoreMessage (1);
	}
	else if (phase==2) {
		/*
		 * Create the local data object (unpackAll() comes from the server).
		 * We set all data to the existing values so that (1) values not specified in the
		 * saved file will not be changed, and (2) there will be no undefined values
		 * when we repack.
		 */
		data = null;
		unpackAll();
		showRestoreMessage (2);

		/*
		 * Reset variables that will be loaded from the saved settings file.
		 */
		current_data = data;
		data = null;
		unpackAll = null;
	}
	else if (phase==3) {
		/*
		 * Convert the data string ('data' comes from the saved settings file)
		 */
		if (typeof(data) != "string") {
			return restoreError ("bad settings file");
		}
		byte_array = convertFromBase64 (data);
		showRestoreMessage (3);
	}
	else if (phase==4) {
		/*
		 * Unpack the saved settings into new separate data object (unpackAll() comes from the settings file)
		 */
		if (typeof(unpackAll) != "function") {
			return restoreError ("Bad settings file");
		}
		data = null;
		unpackAll();

		/*
		 * Fixup old settings if necessary
		 */
		if (typeof(fixup_old_settings) == "function") {
			restored_data = data;
			data = null;
			fixup_old_settings();
			data = restored_data;
		}

		/*
		 * Copy restored settings over current settings
		 */
		mergeObject (current_data,data)
		data = current_data;
		showRestoreMessage (4);
	}
	else if (phase==5) {
		/*
		 * Repack the data object into byte_array (packAll() comes from the server)
		 */
		byte_array_has_error = 0;
		packAll();
		if (byte_array_has_error) {
			return restoreError ("Restored data not acceptable");
		}
		showRestoreMessage (5);
	}
	else if (phase==6) {
		/*
		 * Post the new configuration to the server
		 */
		document.postform.data.value = convertToBase64 (byte_array);
		showRestoreMessage (6);
		document.postform.submit();
	}
}

//***************************************************************************
// Send data to the server and get returned status information without needing
// a page load. This is done be loading an IFRAME block.
// Data may be encoded in the URL thusly: "foo.cgi?data=...."

var iframeCallback;
var iframeDoneMessage;
var iframeNumber = 0;
var iframeCheckCount = 0;


function iframeStandardCallback (iframe)
{
	alert (iframeDoneMessage);
}


function checkIframeLoad()
{
	iframeCheckCount++;
	if (iframeCheckCount > iframe_timeout) {
		alert ("The action can not complete because the network connection seems to be down");
		location.reload (true);
		return;
	}
	if (typeof(iframeCallback) == "function") {
		try {
			var doc = eval('window.frames.myframe_'+iframeNumber+'.window.document');
			var data = getDataFromXML (doc);
			if (typeof(data)=="string" && data.length > 0) {
				iframeCallback (doc);
				return;
			}
		}
		catch (err) {
		}
		window.setTimeout ('checkIframeLoad();',100);
	}
}


/*
 * sendDataToServer()
 *	Load an arbitrary URL from the server. Call the callback
 *	function when the returned data is available (or if it is a string, display that
 *	message when done).
 */
function sendDataToServer (url,callback_or_string)
{
	if (typeof(callback_or_string) != "function") {
		if (typeof(callback_or_string) == "string") iframeDoneMessage = callback_or_string; else iframeDoneMessage = "Done";
		callback_or_string = iframeStandardCallback;
	}
	iframeCallback = callback_or_string;

	var ifr = document.createElement ('DIV');
	ifr.style.visibility = 'hidden';
	ifr.style.position = 'absolute';
	ifr.style.top = '0px';
	ifr.style.left = '0px';

	iframeNumber++;
	ifr.innerHTML = '<iframe src="'+url+'" name="myframe_'+iframeNumber+'" height="0" width="0"><\/iframe>';

	document.body.appendChild (ifr);
	iframeCheckCount = 0;
	window.setTimeout ('checkIframeLoad();',100);
}


/*
 * sendByteToServer()
 *	Send the byte 'n' as base64 encoded data to the given URL.
 */
function sendByteToServer (n,url,callback_or_string)
{
	var a = new Array;
	a[0] = n;
	url = url + '?data=' + convertToBase64(a);
	sendDataToServer (url,callback_or_string);
}


/*
 * getDataFromXML()
 *	Return the contents of the iframe.
 *	Return null on error.
 */
function getDataFromXML (iframe)
{
	if (!iframe || !iframe.body) return null;
	var n = iframe.body;
	while (n.firstChild) n = n.firstChild;
	return n.nodeValue;
}

//***************************************************************************
// Rebooting and resetting to factory defaults

function whenDoneRebooting(xml)
{
	var x = getDataFromXML (xml);
	if (x != "X") {
		alert (x);
	}
	else {
		location.replace ('Reboot.html');
	}
}

function doReboot()
{
	if (!confirm ("Are you sure you want to reboot the device?\nRebooting will disconnect any currently active sessions.")) {
		return;
	}
	global_msg = "Rebooting.";
	sendByteToServer(1,'cmd.cgi',whenDoneRebooting);
}

function doRestoreToFactoryDefaults()
{
	if (!confirm ("Are you sure you want to reset the device to its factory default settings?\nThis will cause all current settings to be lost.")) {
		return;
	}
	global_msg = "Resetting to factory defaults and rebooting.";
	sendByteToServer(2,'cmd.cgi',whenDoneRebooting);
}

//***************************************************************************
// Miscellaneous things

/*
 * convertHexString()
 *	convert a hex string of 'len' bytes into a byte array of 'len2' bytes.
 *	ignore leading and trailing whitespace. if there are any conversion errors
 *	or the string is the wrong length then return 0, else return the
 *	byte array. if the string is empty or all whitespace, return an array of
 *	all zeros.
 */
function convertHexString (s,len,len2)
{
	s = new String(s);
	if (s.match (/^\s*$/)) {
		var a = new Array;
		for (var i=0; i<len2; i++) a[i] = 0;
		return a;
	}
	var got = s.match (/^\s*([0-9a-fA-F]+)\s*$/);
	if (got) {
		if (got[1].length != len*2) return 0;
		var a = new Array;
		var i;
		for (i=0; i < len; i++) {
			a[i] = parseInt(got[1].substr(i*2,2),16);
		}
		for (; i < len2; i++) {
			a[i] = 0;
		}
		return a;
	}
	else return 0;
}

/*
 * verifySubnet()
 *	Given a subnet (IP address and mask), test if another IP address is
 *	within the same subnet. Return 1 if so, 0 if not. If the addresses
 *	are badly formatted, return 2.
 */
function verifySubnet (subnet_ip, subnet_mask, ip_to_test)
{
	var si = IPAddressToInteger (subnet_ip);
	var sm = IPAddressToInteger (subnet_mask);
	var ip = IPAddressToInteger (ip_to_test);
	if (si==null || sm==null || ip==null) return 2;
	if ((si & sm) == (ip & sm)) return 1;
	return 0;
}

/*
 * validateIPRange()
 *	Validate ip address range
 */
function validateIPRange(start_ip_address, end_ip_address)
{
	var ipStart = IPAddressToByteArray(start_ip_address);
	var ipEnd = IPAddressToByteArray(end_ip_address);
	for (var j = 0; j < 4; j++) {
		/*
		 * first check whether specified end ip address byte is grater than start ip
		 * address byte, if YES then no need to check remaining bytes. It is a valid range.
		 * Check whether specified end ip address byte is less than start ip address byte.
		 * if YES return 0, it is invalid range.
		 */
		if (ipEnd[j] > ipStart[j]) {
			return 1;
		}
		if (ipEnd[j] < ipStart[j]) {
			return 0;
		}
	}
	// if all bytes are equal return 1, it is valid range
	return 1;
}

/*
 * isValidHostIPAddress()
 *	Validate ip address
 */
function isValidHostIPAddress(ip_address, subnet_mask)
{
	if (ip_address == '0.0.0.0') {
		return 0;
	}
	var ipStart = IPAddressToByteArray(ip_address);
    if (ipStart == 0) {
        return 0;
    }
    if (ipStart[0] == 0 && ipStart[1] == 0 && ipStart[3] == 0 && ipStart[3] == 0) {
        return 0;
    }
	if ((ipStart[0] < 0) || (ipStart[0] > 223)){
		return 0;
	}
	return 1;
}

/*
 * isValidSubnet()
 *	validate the subnetmask.
 */
function isValidSubnet (subnet_mask)
{
	var sm = IPAddressToInteger (subnet_mask);
	if ((((~sm) + 1) & (~sm)) != 0) {
		return 0;
	}
	return 1;
}

/*
 * GetSubnet()
 * 	return subnet from given subnet ip and subnet mask
 */
function GetSubnet (subnet_ip, subnet_mask)
{
	var si = IPAddressToInteger (subnet_ip);
	var sm = IPAddressToInteger (subnet_mask);
	var subnet1 = intToByteArray(si & sm, 4);
	var subnet = byteArrayToIPAddress (subnet1, 0);
	return subnet;
}

/*
 * validatePortRangeString()
 *	Perform validation of a port range string and allow empty string.
 */
function validatePortRangeString (input_string,name)
{
	input_string = new String (input_string);
	var s = input_string;
	if (s.match (/^\s*$/)) {
		// Empty port range strings are okay
		return 1;
	}

	// check the syntax of the whole string
	if (!s.match (/,\s*$/)) s += ",";
	if (!s.match (/^(\s*\d+(\s*-\s*\d+)?\s*,\s*)+$/)) {
		alert ("The "+name+" port range string '"+input_string+"' is invalid.");
		return 0;
	}

	// check that individual port numbers are in range
	var got = s.match (/\d+/g);
	if (got) {
		for (var i=0; i<got.length; i++) {
			var n = parseInt(got[i],10);
			if ((n > 65535) || (n < 1)) {
				alert ("The "+name+" port '"+n+"' in the port range string '"+input_string+"' should be in between 1 to 65535.");
				return 0;
			}
		}
	}

	// check that port ranges go from low ports to high ports
	got = s.match (/\d+\s*-\s*\d+/g);
	if (got) {
		for (var i=0; i<got.length; i++) {
			var got2 = got[i].match (/(\d+)\s*-\s*(\d+)/);
			if (got2) {
				var start = parseInt(got2[1]);
				var end = parseInt(got2[2]);
				if (start > end) {
					alert ("The "+name+" port range'"+got2[0]+"' in the port range string '"+input_string+"' should go from low port to high port.");
					return 0;
				}
			}
		}
	}
	return 1;
}

/*
 * validatePortRangeStringNoEmpty()
 *	Perform validation of a port range string and not allow empty string
 */
function validatePortRangeStringNoEmpty (input_string,name)
{
	input_string = new String (input_string);
	var s = input_string;
	if (s.match (/^\s*$/)) {
		// Empty port range strings are invalid
		alert ("The "+name+" port range string can't be empty.");
		return 0;
	}

	// check the syntax of the whole string
	if (!s.match (/,\s*$/)) s += ",";
	if (!s.match (/^(\s*\d+(\s*-\s*\d+)?\s*,\s*)+$/)) {
		alert ("The "+name+" port range string '"+input_string+"' is invalid.");
		return 0;
	}

	// check that individual port numbers are in range
	var got = s.match (/\d+/g);
	if (got) {
		for (var i=0; i<got.length; i++) {
			var n = parseInt(got[i],10);
			if ((n > 65535) || (n < 1)) {
				alert ("The "+name+" port '"+n+"' in the port range string '"+input_string+"' should be in between 1 to 65535.");
				return 0;
			}
		}
	}

	// check that port ranges go from low ports to high ports
	got = s.match (/\d+\s*-\s*\d+/g);
	if (got) {
		for (var i=0; i<got.length; i++) {
			var got2 = got[i].match (/(\d+)\s*-\s*(\d+)/);
			if (got2) {
				var start = parseInt(got2[1]);
				var end = parseInt(got2[2]);
				if (start > end) {
					alert ("The "+name+" port range'"+got2[0]+"' in the port range string '"+input_string+"' should go from low port to high port.");
					return 0;
				}
			}
		}
	}
	return 1;
}


/*
 * validateMACAddress()
 *	Return 1 if the string is a valid MAC address, else return 0 and give an
 *	error message.
 */
function validateMACAddress (input_string)
{
	input_string = new String (input_string);
	var s = input_string;
	s = s.replace (/[:-]/g,'');		// Ignore the commonly used separator characters
	if (s.match (/^\s*$/)) {
		alert ("A MAC address field can not be empty.");
		return 0;
	}
	if (s.match (/^\s*[0][0][0][0][0][0][0][0][0][0][0][0]\s*$/)) {
		alert (input_string+" is not a valid MAC address");
		return 0;
	}
	if (!s.match (/^\s*[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]\s*$/)) {
		alert (input_string+" is not a valid MAC address");
		return 0;
	}
	return 1;
}


/*
 * trimString(s)
 *  Remove leading and trailing blank spaces from the string and returns the same string
 */
function trimString(s)
{
	while (s.substring(0, 1) == ' ') {
		s = s.substring(1, s.length);
	}
	while (s.substring(s.length-1, s.length) == ' ') {
		s = s.substring(0, s.length-1);
	}
	return s;
}


/*
 * submitChanges()
 *	If the user clicks the apply button on array editor pages that don't have separate
 *	array save/cancel buttons, then this function should be called to save any changed
 *	array data before saving the page.
 */
function submitChanges (ae1,ae2)
{
	/*
	 * If the user is editing an entry then we need to save it back into the array.
	 */
	if (typeof(ae1) != 'undefined') {
		if (ae1.checkIfEditing() > 0) {
			if (ae1.saveEntry() == 0) {
				return;
			}
		}
	}
	if (typeof(ae2) != 'undefined') {
		if (ae2.checkIfEditing() > 0) {
			if (ae2.saveEntry() == 0) {
				return;
			}
		}
	}

	/*
	 * We now need to submit the configuration back to the server.
	 */
	doSave();
}


/*
 * checkEmailAddress()
 *	Given an email address string, this returns true if it looks valid, or
 *	false otherwise. Only weak validation is done, so this will accept some
 *	email addresses that are not valid, but this is really just to sanity
 *	check what the user has entered.
 */
function checkEmailAddress (e)
{
	if (typeof(e)=="string") return e.match (/^[^@ ]+@[^.]+[.].+/);
	return false;
}

//***************************************************************************
// Support for various pages

function add (name,value,usename)
{
	if (typeof(page_channel_count)!='number') {
		page_channel_count=0;
	}
	if (usename) {
		document.mainform.channel.options[page_channel_count++] = new Option (name,value);
	} else {
		document.mainform.channel.options[page_channel_count++] = new Option (value,value);
	}
}

function computeChannelsFromRegion(usename,extra)
{
	var region = data.wireless.region_id;
	if (region==2) {
		add ("2.432 GHz - CH 5",5,usename);
		add ("2.437 GHz - CH 6",6,usename);
		add ("2.442 GHz - CH 7",7,usename);
		add ("2.447 GHz - CH 8",8,usename);
		add ("2.452 GHz - CH 9",9,usename);
		add ("2.457 GHz - CH 10",10,usename);
		add ("2.462 GHz - CH 11",11,usename);
		add ("2.467 GHz - CH 12",12,usename);
		add ("2.472 GHz - CH 13",13,usename);
	}
	if (region==10) {
		add ("2.457 GHz - CH 10",10,usename);
		add ("2.462 GHz - CH 11",11,usename);
		add ("2.467 GHz - CH 12",12,usename);
		add ("2.472 GHz - CH 13",13,usename);
	}

	if (region!=2 && region!=10) {
		add ("2.412 GHz - CH 1",1,usename);
		add ("2.417 GHz - CH 2",2,usename);
		add ("2.422 GHz - CH 3",3,usename);
		add ("2.427 GHz - CH 4",4,usename);
		add ("2.432 GHz - CH 5",5,usename);
		add ("2.437 GHz - CH 6",6,usename);
		add ("2.442 GHz - CH 7",7,usename);
		add ("2.447 GHz - CH 8",8,usename);
		add ("2.452 GHz - CH 9",9,usename);
		add ("2.457 GHz - CH 10",10,usename);
		add ("2.462 GHz - CH 11",11,usename);
	}
	if (region!=2 && region!=10 && region!=4 && region!=6 && region!=30 && region!=31) {
		add ("2.467 GHz - CH 12",12,usename);
		add ("2.472 GHz - CH 13",13,usename);
	}
	if (region==16 || region==17 || region==19) {
		add ("2.484 GHz - CH 14 (802.11b ONLY)",14,usename);
	}
	if (region==26 || region==27 || region==28) {
		add ("2.484 GHz - CH 14",14,usename);
	}
	if (extra) {
		add ("2.492 GHz - CH 17",17,usename);
		add ("2.512 GHz - CH 21",21,usename);
		add ("2.532 GHz - CH 25",25,usename);
		add ("2.382 GHz - CH -6",250,usename);
		add ("2.362 GHz - CH -10",246,usename);
		add ("2.342 GHz - CH -14",242,usename);
	}

	// Now add the 802.11a channels.
	// The radio_supports_xxx variables may be defined (or not) externally.

	if (typeof(radio_supports_802_dot_11A) == "undefined") {
		return;
	}

	if (region==4 || region==5 || region==6 || region==7 || region==8) {
		add ("5.180 GHz - CH 36",36,usename);
		add ("5.200 GHz - CH 40",40,usename);
		add ("5.220 GHz - CH 44",44,usename);
		add ("5.240 GHz - CH 48",48,usename);
		add ("5.260 GHz - CH 52",52,usename);
		add ("5.280 GHz - CH 56",56,usename);
		add ("5.300 GHz - CH 60",60,usename);
		add ("5.320 GHz - CH 64",64,usename);
		add ("5.745 GHz - CH 149",149,usename);
		add ("5.765 GHz - CH 153",153,usename);
		add ("5.785 GHz - CH 157",157,usename);
		add ("5.805 GHz - CH 161",161,usename);
		add ("5.825 GHz - CH 165",165,usename);

		if (region==4 && typeof(radio_supports_802_dot_11A_turbo) != "undefined") {
			add ("5.210 GHz - CH 42 Turbo",42,usename);
			add ("5.250 GHz - CH 50 Turbo",50,usename);
			add ("5.290 GHz - CH 58 Turbo",58,usename);
			add ("5.760 GHz - CH 152 Turbo",152,usename);
			add ("5.800 GHz - CH 160 Turbo",160,usename);
		}
	}
	if (region==9 || region==10 || region==11 || region==12 || region==13 || region==14 || region==15) {
		add ("5.180 GHz - CH 36",36,usename);
		add ("5.200 GHz - CH 40",40,usename);
		add ("5.220 GHz - CH 44",44,usename);
		add ("5.240 GHz - CH 48",48,usename);
		if (region != 15) {
			add ("5.260 GHz - CH 52",52,usename);
			add ("5.280 GHz - CH 56",56,usename);
		}
		if (region != 15 && region != 11) {
			add ("5.300 GHz - CH 60",60,usename);
			add ("5.320 GHz - CH 64",64,usename);
		}
		if (region==11 || region==14) {
			add ("5.500 GHz - CH 100",100,usename);
			add ("5.520 GHz - CH 104",104,usename);
			add ("5.540 GHz - CH 108",108,usename);
			add ("5.560 GHz - CH 112",112,usename);
			add ("5.580 GHz - CH 116",116,usename);
			add ("5.600 GHz - CH 120",120,usename);
			add ("5.620 GHz - CH 124",124,usename);
			add ("5.640 GHz - CH 128",128,usename);
			add ("5.660 GHz - CH 132",132,usename);
			add ("5.680 GHz - CH 136",136,usename);
			add ("5.700 GHz - CH 140",140,usename);
		}
	}
	if (region==16 || region==17 || region==19) {
		add ("5.170 GHz - CH 34",34,usename);
		add ("5.190 GHz - CH 38",38,usename);
		add ("5.210 GHz - CH 42",42,usename);
		add ("5.230 GHz - CH 46",46,usename);
		if (region==19) {
			add ("4.920 GHz - CH 184",184,usename);
			add ("4.940 GHz - CH 188",188,usename);
			add ("4.960 GHz - CH 192",192,usename);
			add ("4.980 GHz - CH 196",196,usename);
		}
	}
	if (region==18 || region==20 || region==21 || region == 22 || region ==23 || region==24 || region==25) {
		add ("5.745 GHz - CH 149",149,usename);
		add ("5.765 GHz - CH 153",153,usename);
		add ("5.785 GHz - CH 157",157,usename);
		add ("5.805 GHz - CH 161",161,usename);
		if (region==18 || region==21 || region==23 || region==24) {
			add ("5.825 GHz - CH 165",165,usename);
		}
		if (region==18) {
			add ("5.180 GHz - CH 36",36,usename);
			add ("5.200 GHz - CH 40",40,usename);
			add ("5.220 GHz - CH 44",44,usename);
			add ("5.240 GHz - CH 48",48,usename);
		}
		if (region==21) {
			add ("5.260 GHz - CH 52",52,usename);
			add ("5.280 GHz - CH 56",56,usename);
			add ("5.300 GHz - CH 60",60,usename);
			add ("5.320 GHz - CH 64",64,usename);
		}
	}
	if (region>=26 && region<=31) {
		add ("5.180 GHz - CH 36",36,usename);
		add ("5.200 GHz - CH 40",40,usename);
		add ("5.220 GHz - CH 44",44,usename);
		add ("5.240 GHz - CH 48",48,usename);
		add ("5.260 GHz - CH 52",52,usename);
		add ("5.280 GHz - CH 56",56,usename);
		add ("5.300 GHz - CH 60",60,usename);
		add ("5.320 GHz - CH 64",64,usename);
		if (region>=26 && region <=28) {
			add ("5.500 GHz - CH 100",100,usename);
			add ("5.520 GHz - CH 104",104,usename);
			add ("5.540 GHz - CH 108",108,usename);
			add ("5.560 GHz - CH 112",112,usename);
			add ("5.580 GHz - CH 116",116,usename);
			add ("5.600 GHz - CH 120",120,usename);
			add ("5.620 GHz - CH 124",124,usename);
			add ("5.640 GHz - CH 128",128,usename);
			add ("5.660 GHz - CH 132",132,usename);
			add ("5.680 GHz - CH 136",136,usename);
			add ("5.700 GHz - CH 140",140,usename);
		}
		add ("5.745 GHz - CH 149",149,usename);
		add ("5.765 GHz - CH 153",153,usename);
		add ("5.785 GHz - CH 157",157,usename);
		add ("5.805 GHz - CH 161",161,usename);
		add ("5.825 GHz - CH 165",165,usename);
	}
}


function writeTimeZoneSelector(disable_row, disable_input)
{
	document.write('<select name="timezone" onchange="data.tz_timezone_index=this.selectedIndex;data.tz_timezone=this.value;dataChanged()">');
	document.write('<option value="-43200">(GMT-12:00) Eniwetok, Kwajalein</option>');
	document.write('<option value="-39600">(GMT-11:00) Midway Island, Samoa</option>');
	document.write('<option value="-36000">(GMT-10:00) Hawaii</option>');
	document.write('<option value="-32400">(GMT-09:00) Alaska</option>');
	document.write('<option value="-28800">(GMT-08:00) Pacific Time (US/Canada), Tijuana</option>');
	document.write('<option value="-25200">(GMT-07:00) Arizona</option>');
	document.write('<option value="-25200">(GMT-07:00) Mountain Time (US/Canada)</option>');
	document.write('<option value="-21600">(GMT-06:00) Central America</option>');
	document.write('<option value="-21600">(GMT-06:00) Central Time (US/Canada)</option>');
	document.write('<option value="-21600">(GMT-06:00) Mexico City</option>');
	document.write('<option value="-21600">(GMT-06:00) Saskatchewan</option>');
	document.write('<option value="-18000">(GMT-05:00) Bogota, Lima, Quito</option>');
	document.write('<option value="-18000">(GMT-05:00) Eastern Time (US/Canada)</option>');
	document.write('<option value="-18000">(GMT-05:00) Indiana (East)</option>');
	document.write('<option value="-14400">(GMT-04:00) Atlantic Time (Canada)</option>');
	document.write('<option value="-14400">(GMT-04:00) Caracas, La Paz</option>');
	document.write('<option value="-14400">(GMT-04:00) Santiago</option>');
	document.write('<option value="-10800">(GMT-03:00) Newfoundland</option>');
	document.write('<option value="-10800">(GMT-03:00) Brazilia</option>');
	document.write('<option value="-10800">(GMT-03:00) Buenos Aires, Georgetown</option>');
	document.write('<option value="-10800">(GMT-03:00) Greenland</option>');
	document.write('<option value="-7200">(GMT-02:00) Mid-Atlantic</option>');
	document.write('<option value="-3600">(GMT-01:00) Azores</option>');
	document.write('<option value="-3600">(GMT-01:00) Cape Verde Is.</option>');
	document.write('<option value="0">(GMT) Casablanca, Monrovia</option>');
	document.write('<option value="0">(GMT) Greenwich Time: Dublin, Edinburgh, Lisbon, London</option>');
	document.write('<option value="3600">(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna</option>');
	document.write('<option value="3600">(GMT+01:00) Belgrade, Brastislava, Budapest, Ljubljana, Prague</option>');
	document.write('<option value="3600">(GMT+01:00) Brussels, Copenhagen, Madrid, Paris</option>');
	document.write('<option value="3600">(GMT+01:00) Sarajevo, Skopje, Sofija, Vilnus, Warsaw, Zagreb</option>');
	document.write('<option value="3600">(GMT+01:00) West Central Africa</option>');
	document.write('<option value="7200">(GMT+02:00) Athens, Istanbul, Minsk</option>');
	document.write('<option value="7200">(GMT+02:00) Bucharest</option>');
	document.write('<option value="7200">(GMT+02:00) Cairo</option>');
	document.write('<option value="7200">(GMT+02:00) Harare, Pretoria</option>');
	document.write('<option value="7200">(GMT+02:00) Helsinki, Riga, Tallinn</option>');
	document.write('<option value="7200">(GMT+02:00) Jerusalem</option>');
	document.write('<option value="10800">(GMT+03:00) Baghdad</option>');
	document.write('<option value="10800">(GMT+03:00) Kuwait, Riyadh</option>');
	document.write('<option value="10800">(GMT+03:00) Moscow, St. Petersburg, Volgograd</option>');
	document.write('<option value="10800">(GMT+03:00) Nairobi</option>');
	document.write('<option value="10800">(GMT+03:00) Tehran</option>');
	document.write('<option value="14400">(GMT+04:00) Abu Dhabi, Muscat</option>');
	document.write('<option value="14400">(GMT+04:00) Baku, Tbilisi, Yerevan</option>');
	document.write('<option value="16200">(GMT+04:30) Kabul</option>');
	document.write('<option value="18000">(GMT+05:00) Ekaterinburg</option>');
	document.write('<option value="18000">(GMT+05:00) Islamabad, Karachi, Tashkent</option>');
	document.write('<option value="19800">(GMT+05:30) Calcutta, Chennai, Mumbai, New Delhi</option>');
	document.write('<option value="20700">(GMT+05:45) Kathmandu</option>');
	document.write('<option value="21600">(GMT+06:00) Almaty, Novosibirsk</option>');
	document.write('<option value="21600">(GMT+06:00) Astana, Dhaka</option>');
	document.write('<option value="21600">(GMT+06:00) Sri Jayawardenepura</option>');
	document.write('<option value="23400">(GMT+06:30) Rangoon</option>');
	document.write('<option value="25200">(GMT+07:00) Bangkok, Hanoi, Jakarta</option>');
	document.write('<option value="25200">(GMT+07:00) Krasnoyarsk</option>');
	document.write('<option value="28800">(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi</option>');
	document.write('<option value="28800">(GMT+08:00) Irkutsk, Ulaan Bataar</option>');
	document.write('<option value="28800">(GMT+08:00) Kuala Lumpur, Singapore</option>');
	document.write('<option value="28800">(GMT+08:00) Perth</option>');
	document.write('<option value="28800">(GMT+08:00) Taipei</option>');
	document.write('<option value="32400">(GMT+09:00) Osaka, Sapporo, Tokyo</option>');
	document.write('<option value="32400">(GMT+09:00) Seoul</option>');
	document.write('<option value="32400">(GMT+09:00) Yakutsk</option>');
	document.write('<option value="34200">(GMT+09:30) Adelaide</option>');
	document.write('<option value="34200">(GMT+09:30) Darwin</option>');
	document.write('<option value="36000">(GMT+10:00) Brisbane</option>');
	document.write('<option value="36000">(GMT+10:00) Canberra, Melbourne, Sydney</option>');
	document.write('<option value="36000">(GMT+10:00) Guam, Port Moresby</option>');
	document.write('<option value="36000">(GMT+10:00) Hobart</option>');
	document.write('<option value="36000">(GMT+10:00) Vladivostok</option>');
	document.write('<option value="39600">(GMT+11:00) Magadan, Solomon Is., New Caledonia</option>');
	document.write('<option value="43200">(GMT+12:00) Auckland, Wellington</option>');
	document.write('<option value="43200">(GMT+12:00) Fiji, Kamchatka, Marshall Is.</option>');
	document.write('<option value="46800">(GMT+13:00) Nuku\'alofa, Tonga</option>');
	document.write('</select>');
	var oldEvt = window.onload;
	window.onload = function() {
		if (typeof(oldEvt) == 'function') {
			oldEvt();
		}
		document.mainform.timezone.selectedIndex = data.tz_timezone_index;
	};
}

//***************************************************************************
// I18n support

function i18n_language_changed (xml)
{
	var x = getDataFromXML (xml);
	if (x != "X") {
		alert (x);
	} else {
		location.reload(true);
	}
}

function i18n_change_language (element)
{
	var lang = element.options [element.selectedIndex].value;
	sendDataToServer ("i18n_change_lang.cgi?lang="+lang,i18n_language_changed);
}

//***************************************************************************
// Initialization, error handling and some global navigation stuff

function uigenHandleError (msg,url,line)
{
	var code = confirm (
		"An error occurred on this page. This might be because you are\n"+
		"not properly logged in, for example just after a reboot.\n"+
		"Press OK to go to the login page, or Cancel if you want to see\n"+
		"the error message.");
	if (code) {
		location.replace ("/");
	} else {
		alert ("The error on line "+line+" of "+url+" is:\n\""+msg+"\".");
	}
	return true;
}


function rememberCurrentDataForExitComparison()
{
	initial_data = cloneObject (data);
}


function uigenInitAtStart (initial)
{
	/*
	 * The server CGI function has already been invoked to supply the data="..." line.
	 */
	if (typeof(initial) == "undefined") initial = true;
	if (initial) original_data_string = data;
	byte_array = convertFromBase64 (original_data_string);
	if (typeof(createDataObjectFromBinaryArray) == "function") {
		createDataObjectFromBinaryArray (initial);
	}
	if (initial) {
		rememberCurrentDataForExitComparison();
	}
}


function uigenInitAtEnd()
{
	initializeDuplicateTableRows (document.body);
	initializeFormElements();
	transferFormElementData (true);
	buildActiveNodeList (document.body);

	// try to work around bugs in explorer 5.x
	var agent = navigator.userAgent.toLowerCase();
	if (agent.match (/msie 5[.]/)) {
		window.setTimeout ('executeActiveTags();',200);
	}
	else {
		executeActiveTags();
	}
}


/*
 * dataObjectChanged()
 *	Return 1 if the data object has changed, 0 otherwise.
 */
function dataObjectChanged()
{
	if (typeof(initial_data)=="object") {
		if (!compareObject (data,initial_data)) {
			return 1;
		}
	}
	return 0;
}


/*
 * jumpTo()
 *	The user wants to navigate to the given URL. Warn if the page data
 *	has been changed without being saved.
 */
function jumpTo (url)
{
	if (dataObjectChanged()) {
		if (!confirm ("There is unsaved data on this page. Do you want to abandon it?\n"+
			      "If not, press Cancel and then click Save Settings.\n"+
			      "If so, press Ok.")) {
			return;
		}
	}
	if (anyPageArrayEditorsChanged()) {
		return;
	}
	location.href = url;
}
/*
 * jumpIf()
 *	The user wants to navigate to the URL in the href of an HTML hyperlink. If the page data
 *	has been changed without being saved, warn and return false to prevent jump.
 *    Usage: <a href="Advanced_Miscellanea.html" onclick="return jumpIf();">Miscellanea</a>
 */
function jumpIf ()
{
	if (dataObjectChanged()) {
		if (!confirm ("There is unsaved data on this page. Do you want to abandon it?\n"+
			      "If not, press Cancel and then click Save Settings.\n"+
			      "If so, press Ok.")) {
			return false;
		}
	}
	if (anyPageArrayEditorsChanged()) {
		return false;
	}
	return true;
}


/*
 * Set up error handling.
 */
window.onerror = uigenHandleError;

function uigenpro_js_loaded() { return true; }
