// date parsing and formatting

function parseDate(dt, style)
{
	// parse date or date/time, return Date object or null
	
	if (dt === null) return null;
	
	// expected date style: mdy, dmy, ymd
	style = (arguments.length > 1) ? style.toLowerCase() : 'mdy';

	// split value into date and time components
	// String.split() is too limited to do this correctly
	var a = dt.match(/^\s*(\S*)\s*(.*)$/);
	if (a.length < 2 || a.length > 3) return null;
	if (a.length == 1) a[1] = '';
	var ds = a[1];
	var ts = a[2];

	// take date apart into year, month, day
	a = ds.split(/[\.\-\/]/);
	if (a.length != 3) return null;

	// always accept iso-style yyyy-mm-dd dates
	if (a[0].length == 4) style = 'ymd';

	var m, d, y;

	switch (style)
	{
		case 'ymd':
			y = a[0]; m = a[1]; d = a[2];
			break;
		case 'dmy':
			y = a[2]; m = a[1]; d = a[0];
			break;
		case 'mdy':
		default:
			y = a[2]; m = a[0]; d = a[1];
			break;
	}

	if (y.length < 2 || m.length == 0 || d.length == 0) return null;
	y = parseInt(y, 10);
	m = parseInt(m, 10);
	d = parseInt(d, 10);
	if (m < 1 || m > 12) return null;
	if (d < 1 || d > 31) return null;
	
	// adjust two-digit year
	y = (y <= 50) ? 2000 + y : ((y < 100) ? y + 1900 : y);
	if (y < 1000 || y > 9999) return null;

	// parse into a new Date object
	ds = m + '/' + d + '/' + y;
	return new Date(ds + ' ' + ts);
}

function formatDate(dt, style)
{
	// format date in m/d/y, d/m/y, or y/m/d format according to style
	// returns empty string if date is not valid
	
	// expected date style: mdy, dmy, ymd
	style = (arguments.length > 1) ? style.toLowerCase() : 'mdy';

	var d;
	if (dt instanceof Date)
		d = dt;
	else
		d = parseDate(dt, style);

	if (d)
	{
		var yyyy = d.getFullYear();
		var mm = ('00' + (d.getMonth() + 1)).slice(-2);
		var dd = ('00' + d.getDate()).slice(-2);
		
		if (style == 'mdy')
			return mm + '/' + dd + '/' + yyyy;
		else if (style == 'dmy')
			return dd + '/' + mm + '/' + yyyy;
		else if (style == 'ymd')
			return yyyy + '-' + mm + '-' + dd;
	}
	
	return '';
}

function parseTime(t)
{
    // time validator
    // returns formatted time or null if t is not valid time
	
	if (t === null) return null;
	
    // regex matches any of: hhhh hh:mm hh:mm:ss
    // with optional trailing am/pm
    var a = t.match(/^(\d+):(?:(\d\d?)(?::(\d\d?))?)?\s*(a|am|a\.m\.|p|pm|p\.m\.)?$/i);
    if (a && a.length == 5)
    {
        var h = parseInt(a[1] || 0, 10);
        var m = parseInt(a[2] || 0, 10);
        var s = parseInt(a[3] || 0, 10);
        var ap = (a[4] || '').charAt(0).toLowerCase();

        // am/pm specified hour must be valid clock time
        if (ap && (h < 1 || h > 12)) return null;
        if (ap == 'a' && h == 12) h = 0;
        if (ap == 'p' && h < 12) h += 12;

        // return time as seconds
        return h * 3600 + m * 60 + s;
    }

    return null;
}

function formatTime(t)
{
    // format time as hh:mm:ss (24-hour clock)
    // time in t may be a string or a number

    var tt = parseTime(t);
    if (tt === null) return '';

    var h = Math.floor(tt / 3600);
    tt = tt % 3600;
    var m = Math.floor(tt / 60);
    tt = tt % 60;
    var s = Math.floor(tt);

    h = h < 9 ? ('0' + h) : h;
    m = m < 9 ? ('0' + m) : m;
    s = s < 9 ? ('0' + s) : s;

    return h + ':' + m + ':' + s;
}

function formatDateTime(s, style)
{
	// split value into date and time components
	// String.split() is too limited to do this correctly
	var a = s.match(/^\s*(\S*)\s*(.*)$/);
	if (a.length == 3)
	{
		var d = formatDate(a[1], style);
		var t = formatTime(a[2]);
		
		return [d, t].join(' ');
	}
	
	return '';
}

function formatTrueFalse(s)
{
	if (s === null || s.length < 1) return '';
	
	var c = s.charAt(0).toLowerCase();
	if (c == 't' || c == 'y' || s == '1')
		return 'True';
	else if (c == 'f' || c == 'n' || s == '0')
		return 'False';
	else
		return s;
}

// prompts for date styles

var dateprompts = {
	'ymd': 'yyyy-mm-dd',
	'mdy': 'mm/dd/yy or mm/dd/yyyy',
	'dmy': 'dd-mm-yy or dd-mm-yyyy'
}

// validation functions by field type

var validators = Object();
validators['text'] = function(s, minval, maxval, maxlen, datestyle) {
	return (maxlen && s.length > maxlen) ? ('up to ' + maxlen + ' characters allowed') : '';
}

validators['number'] = function(s, minval, maxval, maxlen, datestyle) {
	var n = Number(s.replace(/[, ]/g, ''));
	if (isNaN(n))
		return 'number expected';
	else if (minval != null && maxval != null && (n < minval || n > maxval))
		return 'number between ' + minval + ' and ' + maxval + ' expected';
	else if (minval != null && n < minval)
		return 'number greater than ' + minval + ' expected';
	else if (maxval != null && n > maxval)
		return 'number less than ' + maxval + ' expected';
	else
		return '';
}

validators['date'] = function(s, minval, maxval, maxlen, datestyle)
{
	var style = (arguments.length > 4) ? datestyle.toLowerCase() : 'mdy';
	
	var d = parseDate(s, style);
	if (!d)
		return 'date expected (' + dateprompts[style] + ')';
		
	var dmin = parseDate(minval, style);
	var dmax = parseDate(maxval, style);
	if (dmin && dmax && (d < dmin || d > dmax))
		return 'date between ' + formatDate(dmin, style) + ' and ' + formatDate(dmax, style) + ' expected';
	else if (dmin && d < dmin)
		return 'date before ' + formatDate(dmin, style) + ' expected';
	else if (dmax && d > dmax)
		return 'date after ' + formatDate(dmax, style) + ' expected';

	return '';
}

validators['time'] = function(s, minval, maxval, maxlen, datestyle)
{
	var t = parseTime(s);
	if (t === null)
		return 'time expected (hh:mm:ss am or hh:mm)';
		
	var tmin = parseTime(minval);
	var tmax = parseTime(maxval);
	if (tmin != null && tmax != null && (t < tmin || t > tmax))
		return 'time between ' + formatTime(minval) + ' and ' + formatTime(maxval) + ' expected';
	else if (tmin != null && t < tmin)
		return 'time greater than ' + formatTime(minval) + ' expected';
	else if (tmax != null && t > tmax)
		return 'time less than ' + formatTime(maxval) + ' expected';
		
	return '';
}

validators['datetime'] = function(s, minval, maxval, maxlen, datestyle)
{
	var style = (arguments.length > 4) ? datestyle.toLowerCase() : 'mdy';
	var prompt = 'date and time (' + dateprompts[style] + ') hh:mm:ss am or hh:mm:ss) expected';
	
	// split value into date and time components
	// String.split() is too limited to do this correctly
	var a = s.match(/^\s*(\S*)\s*(.*)$/);
	if (a.length != 3) return prompt;

	// do date and time portions validate?
	if (parseDate(a[1], style) == null)
		return prompt;
	else if (parseTime(a[2]) == null)
		return prompt;
		
	// check min/max
	var dt = parseDate(s);
	var dmin = parseDate(minval);
	var dmax = parseDate(maxval);
	
	if (dmin != null && dmax != null && (dt < dmin || dt > dmax))
		return 'date/time between ' + formatDateTime(minval, style) + ' and ' + formatDateTime(maxval, style) + ' expected';
	else if (dmin != null && dt < dmin)
		return 'date/time after ' + formatDateTime(minval, style) + ' expected';
	else if (dmax != null && dt > dmax)
		return 'date/time before ' + formatDateTime(maxval, style) + ' expected';
	
	return '';
}

validators['true/false'] = function(s, minval, maxval, maxlen, datestyle)
{
	var pat = /^(t|tr|tru|true|y|ye|yes|f|fa|fal|fals|false|n|no|1|0)$/i
	return pat.test(s) ? '' : 'True/False or Yes/No expected';
}

function validatechoice(s, choices)
{
	if (!choices) return '';
	
	// return error message if s not in choices, empty string if it is
	s = s.toLowerCase();
	for (var i = 0; i < choices.length; i++)
	{
		if (choices[i].toLowerCase() == s)
			return '';
	}
	
	return '"' + s + '" is not a valid choice';
}

// input cleaning functions by field type
var cleaners = Object();
cleaners['text'] = trim;
cleaners['number'] = function(s) {s = s.replace(/[^0-9.e+-]/g, ''); var n = parseFloat(s); return isNaN(n) ? '' : n;};
cleaners['date'] = formatDate;
cleaners['time'] = formatTime;
cleaners['datetime'] = formatDateTime;
cleaners['true/false'] = formatTrueFalse;


// utilities
function trim(s)
{
	// trim leading and trailing whitespace from string
	return s.replace(/^\s+/, '').replace(/\s+$/, '');
}

function nextsiblingbytag(e, tag)
{
	// return next sibling of element e that has
	// the specified tag, or null if none found
	tag = tag.toLowerCase();
	while (e && e.nextSibling)
	{
		e = e.nextSibling;
		if (e.tagName && e.tagName.toLowerCase() == tag)
			return e;
	}

	return null;
}


// validation functions
function validatefield(fld)
{
	// field validations
	// returns true if the field validated, false if not

	// get the validations for this field
	// for subtable fields change the record number to 1,
	// because that's the only entry in the global
	// validations object
	if (!fld.name) return true;
	var fldname = fld.name;
	var vname = fldname.replace(/_r[0-9]+_/g, '_r1_');
	if (!validations[vname]) return true;

	var v = validations[vname];
	var fldtype = v['type'] || 'text';
	var required = (v['required'] == "1");
	var fldmin = v['min'];
	var fldmax = v['max'];
	var fldlen = v['maxlength'];
	var datestyle = v['datestyle'];
	var choices = v['choices'];
	var validatefn = validators[fldtype];
	if (!validatefn) validatefn = function(s) {return ''};
	
	// interpret 'null' in a string as a null value
	if (fldmin == 'null') fldmin = null;
	if (fldmax == 'null') fldmax = null;
	if (fldlen == 'null') fldlen = null;
	if (choices == 'null') choices = null;
	
	if (choices && choices.length)
		choices = choices.split('\n');
	else
		choices = null;

	var val = fld.value;
	if (fld.type == 'textarea')
	{
		// normalize end-of-lines for textarea fields,
		// split value into individual lines
		// HTML spec says lines are separated by \r\n
		val = fld.value.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\s*,\s*/g, '\n');
	}

	var lines = (val == '') ? [] : val.split('\n');

	// count number of non-empty values and start with empty error message
	var values = 0;
	var msg = '';
	for (var i=0; i < lines.length; i++)
	{
		var s = String(trim(lines[i]));
		if (s)
		{
			values++;
			if (choices)
				msg = validatechoice(s, choices);
			else
				msg = validatefn(s, fldmin, fldmax, fldlen, datestyle);
				
			if (msg) break;
		}
	}

	// check required
	if (required && !values)
		msg = 'required';

	// get the message area to display errors/prompts
	// if none make a dummy object and continue validating
	var m = nextsiblingbytag(fld, 'span');
	if (m)
	{
		if (msg == '')
			m.innerHTML = '&nbsp;';
		else
			m.innerHTML = msg;
	}

	return (msg == '');
}

function validate(evt)
{
	// field validation as an event handler
	// find the target (field) for the event,
	var e = evt || window.event;
	var fld = e.target || e.srcElement;
	var c = e.charCode || e.keyCode;
	
	validatefield(fld);
	return true;
}

function validateall()
{
	// validate all fields, return number of invalid entries
	errors = 0;
	var fields = getElementsByTagNames(document.theform, 'input textarea');
	for (var i=0; i < fields.length; i++)
	{
		var f = fields[i];
		if ((f.type == 'textarea' || f.type == 'text') && !f.disabled)
		{
			if (!validatefield(f))
				errors++;
		}
	}
	
	return errors;
}

// input cleaning functions

function cleanfield(fld)
{
	// get the validations for this field
	// for subtable fields change the record number to 1,
	// because that's the only entry in the global
	// validations object
	if (!fld.name) return true;
	var fldname = fld.name;
	var vname = fldname.replace(/_r[0-9]+_/g, '_r1_');
	if (!validations[vname]) return true;

	var v = validations[vname];
	var fldtype = v['type'];
	var datestyle = v['datestyle'];
	var cleanfn = cleaners[fldtype];
	if (!cleanfn) cleanfn = function(s) {return s};

	var val = trim(fld.value);
	if (fld.type == 'textarea')
	{
		// normalize end-of-lines for textarea fields,
		// split value into individual lines
		// HTML spec says lines are separated by \r\n
		val = trim(fld.value.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\s*,\s*/g, '\n'));
	}
	
	var lines = (val == '') ? [] : val.split('\n');

	for (var i=0; i < lines.length; i++)
	{
		var s = String(trim(lines[i]));
		if (s)
			lines[i] = cleanfn(s, datestyle);
	}

	// make lines back into a single string, removing
	// empty lines, then change the field's value
	val = lines.join('\r\n').replace(/(\r\n)+/g, '\r\n');
	fld.value = val;

	return true;
}

function clean(evt)
{
	// field cleaning as an event handler
	// find the target (field) for the event,
	var e = evt || window.event;
	var fld = e.target || e.srcElement;

	return cleanfield(fld);
}

function characterfilter(evt)
{
	// filter multibyte unicode characters from text fields
	var e = evt || window.event;
	var c = e.charCode || e.keyCode;
	return (c < 256);
}

function focusFirstField(elem)
{
	// set input focus to first eligible input element
	// within elem

	var fields = getElementsByTagNames(elem, 'input textarea');
	for (var i=0; i < fields.length; i++)
	{
		var f = fields[i];
		if ((f.type == 'textarea' || f.type == 'text') && !f.disabled)
		{
			f.focus();
			return;
		}
	}
}

function addchoice(fieldname, value)
{
	// add value to multiline text field named fld
	
	fld = document.theform[fieldname];
	if (fld)
	{
		var val = fld.value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
		var lines = (val == '') ? [] : val.split('\n');
		lines.push(value);
		fld.value = lines.join('\r\n');
		validatefield(fld);
	}
}
function openprivacypage()
{
	var left = (window.screenX || window.screenLeft) + 125;
	var top = (window.screenY || window.screenTop) + 125;
	window.open('/privacypolicy.html', 'privacy', 'left=' + left + ',top=' + top + ',width=500,height=480');
}

// functions for handling subtable records

function getElementsByTagNames(elem, tags)
{
	// find all children of elem with tag namein tags
	// tags is a space-delimited list of tag names,
	// e.g. "input textarea"

	tags = tags.split(' ');
	var elems = [];
	for (var i=0; i < tags.length; i++)
	{
		nodes = elem.getElementsByTagName(tags[i]);
		// nodes is a nodeList, not an array,
		// so Array.concat() won't work here
		if (nodes)
		{
			for (var j=0; j < nodes.length; j++)
				elems.push(nodes[j])
		}
	}

	return elems;
}

function getChildrenByTagNames(elem, tags)
{
	// find immediate children of elem with tag name in tags
	// tags is a space-delimited list of tag names,
	// e.g. "input textarea"
	
	tags = tags + ' '
	var elems = [];
	var children = elem.childNodes;
	for (var i = 0; i < children.length; i++)
	{
		var e = children[i];
		if (tags.indexOf(e.nodeName.toLowerCase()+' ') != -1)
			elems.push(e);
	}
	
	return elems;
}

function renumber(subtableid)
{
	// renumber records inside a subtable fieldset
	// done after adding or removing a record
	var subtable = document.getElementById(subtableid);
	var records = getChildrenByTagNames(subtable, 'div')
	for (var i = 0; i < records.length; i++)
	{
		var rec = i+1;

		// change the legend to show the correct (1-based) record number
		var legend = getChildrenByTagNames(records[i], 'h5')[0];
		legend.innerHTML = '(' + rec + ')';
		
		// change the record id
		records[i].id = subtableid + '_r' + rec;

		// rename the input fields and subtables (and their fields, etc.)
		// to reflect the correct record number
		// subtable fields are named like: _t2_r2_f9
		// where the _r component indicates the record number
		// note that this will rename nested subtable records as well
		var elems = getElementsByTagNames(records[i], 'div input textarea select img');

		// regex matches the subtable element name up to the record number
		var regex = new RegExp('^' + subtableid + '_r\\d+');

		for (var j=0; j < elems.length; j++)
		{
			if (elems[j].name)
				elems[j].name = elems[j].name.replace(regex, subtableid + '_r' + rec);
			if (elems[j].id)
				elems[j].id = elems[j].id.replace(regex, subtableid + '_r' + rec);

			// attach validation/cleaning event handlers, validate contents
			if (elems[j].type == 'text' || elems[j].type == 'textarea')
			{
				elems[j].onkeyup = validate;
				elems[j].onkeypress = characterfilter;
				elems[j].onblur = clean;
				validatefield(elems[j]);
			}
		}
	}
	
	// return number of records in subtable
	return records.length;
}

function addrecord(subtableid)
{
	// add a record fieldset to the subtable (div)
	// identified by subtableid
	// new records are cloned from the global subtables
	// object, which has an empty record element for
	// each subtable, indexed by id

	var subtable = document.getElementById(subtableid);
	subtable.appendChild(subtables[subtableid].cloneNode(true));
	var r = renumber(subtableid);
	
	// set focus to first field of new record
	var record = document.getElementById(subtableid + '_r' + r);
	if (record)
		focusFirstField(record);
}

function removerecord(evt)
{
	var e = evt || window.event;
	var target = e.target || e.srcElement;

	// find enclosing record and subtable div
	var record = target;
	while (record && record.tagName.toLowerCase() != 'div')
	{
		record = record.parentNode;
	}

	if (record)
	{
		var subtable = record.parentNode;
		record = subtable.removeChild(record);
		renumber(subtable.id);
	}
}

// image preloads/rollover functions

function loadimage(url)
{
	// do image preloads
	var img = new Image();
	img.src = url;
	return img;
}

function swapimage(evt, src)
{
	// rollover handler
	// called from onmouseover, onmousedown, onmouseout
	var e = evt || window.event;
	var elem = e.target || e.srcElement;
	if (elem && elem.src)
		elem.src = '/images/' + src + '.gif';
}


// handle cancel
function docancel()
{
	location.href = location.protocol + '//' + location.host + '/cancelled.html';
	return true;
}
// handle form submit
function dosubmit()
{
	if (validateall())
	{
		alert('Some questions have missing or incorrect answers. Please correct the problems indicated on the survey.');
		return false;
	}
	
	return true;
}

// subtables holds an empty record for each subtable,
// indexed by subtable id
// used to add subtable records
var subtables = Object();

function pageload()
{
	// onload handler
	
	// set initial field validation messages
	var fields = getElementsByTagNames(document.theform, 'input textarea');
	for (var i=0; i < fields.length; i++)
	{
		var f = fields[i];
		if ((f.type == 'textarea' || f.type == 'text') && !f.disabled)
		{
			f.onkeyup = validate;
			f.onkeypress = characterfilter;
			f.onblur = clean;
			validatefield(f);
		}
	}
	
	// find all subtable records and clone a copy into
	// the global subtables object
	var divs = document.getElementsByTagName('div');
	for (var i=0; i < divs.length; i++)
	{
		var d = divs[i];
		if (d.className == 'subtablerecord')
		{
			// found a subtable record
			// the subtable id is the parent node's id
			var id = d.parentNode.id ;
			subtables[id] = d.cloneNode(true);
		}
	}
	
	// put focus in first eligible form field
	var fields = document.theform.elements;
	for (var i=0; i < fields.length; i++)
	{
		var f = fields[i];
		if (f.disabled) continue;
		if (f.tagName == 'SELECT' || f.tagName == 'TEXTAREA' || f.type == 'text')
		{
			f.focus();
			break;
		}
	}
}
