/*jslint browser: true, eqeqeq: true, undef: true */
/*global $, jQuery */
/******************************************************************************
Lines above are for jslint, the JavaScript verifier.
http://www.jslint.com/
******************************************************************************/

//-----------------------------------------------------------------------------
// Functions and constants of general utility.
//-----------------------------------------------------------------------------

// A few special character used in various strings throughout the code.
var Unicode = {
	"reg":    "\u00ae",	// U+00AE REGISTERED SIGN
	"trade":  "\u2122",	// U+2122 TRADE MARK SIGN
	"hellip": "\u2026",     // U+2026 HORIZONTAL ELLIPSIS
	"eacute": "\u00e9"	// U+00E9 LATIN SMALL LETTER E WITH ACUTE
};

var RX = {
	// NOTE: capture groupings are used in RX_NORMALIZE, see below.
	EMAIL:             /^\s*(\w+(?:(?:-\w+)|(?:\.\w+)|(?:\+\w+))*\@[A-Za-z0-9]+(?:(?:\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9][A-Za-z0-9]+)\s*$/,
	MM_DD_YYYY:        /^\s*(\d{1,2})[\-\/](\d{1,2})[\-\/](\d{4,})\s*$/,
	YYYY_MM_DD:        /^\s*(\d{4,})[\-\/](\d{1,2})[\-\/](\d{1,2})\s*$/,
	US_ZIP_CODE:       /^\s*(\d{5})(?:\s*-?\s*(\d{4}))?\s*$/,
	NANP_PHONE_NUMBER: /^[^\d]*(?:1[^\d]*)?([2-9]\d{2})[^\d]*(\d{3})[^\d]*(\d{4})[^\d]*$/
};

var RX_NORMALIZE = {
	EMAIL: "$1",
	NANP_PHONE_NUMBER: "$1-$2-$3",
	US_ZIP_CODE: function (match, g1, g2) {
		return g1 + ((g2 !== undefined && g2 !== null && g2 !== "") ? 
			     ("-" + g2) : "");
	}
};

// A Date method returning a MM/DD/YYYY string with leading zeroes.
if (!Date.prototype.toDateString_mmddyyyy) {
	Date.prototype.toDateString_mmddyyyy = function () {
		return (("0" + (1 + this.getMonth())).slice(-2) + "/" +
			("0" + this.getDate()).slice(-2) + "/" +
			this.getFullYear());
	};
}

// A function to parse MM/DD/YYYY dates.
if (!Date.parse_mmddyyyy) {
	Date.parse_mmddyyyy = function (str) {
		var month, date, year, result;

		if (RX.MM_DD_YYYY.test(str)){
			month = RegExp.$1;
			date  = RegExp.$2;
			year  = RegExp.$3;
		}
		else if (RX.YYYY_MM_DD.test(str)){
			// also parse YYYY-MM-DD dates.
			year  = RegExp.$1;
			month = RegExp.$2;
			date  = RegExp.$3;
		}
		else {
			return null;
		}
		month = parseInt(month, 10);
		date  = parseInt(date,  10);
		year  = parseInt(year,  10);
		result = new Date(year, month - 1, date, 12, 0, 0, 0);
		if (result.getFullYear() !== year ||
		    result.getMonth() !== (month - 1) ||
		    result.getDate() !== date) {
			// exclude dates "out of range".
			// examples: 2010-12-32; 2010-02-31
			return null;
		}
		return result;
	};
}

// To compare the dates from two Date objects, we normalize their times
// to noon to factor out the time of day.
if (!Date.prototype.noon) {
	Date.prototype.noon = function () {
		// for comparison with Date.parse_mmddyyyy()'s return
		// value, we standardize the time at noon.
		return new Date(this.getFullYear(),
				this.getMonth(),
				this.getDate(),
				12, 0, 0, 0);
	};
}

// $foo.dom("foo") is equivalent to $foo.find("foo").get(0)
jQuery.fn.extend({
	"dom": function (/* arguments */) {
		return jQuery.fn.find.apply(this, arguments).get(0);
	}
});

// $.dom("foo") is equivalent to $("foo").get(0)
// $.dom("#foo") is mostly equivalent to document.getElementById("foo")
jQuery.extend({
	"dom": function (/* arguments */) {
		return jQuery.apply(this, arguments).get(0);
	}
});

String.prototype.normalizeModelNumber = function () {
	return this.replace(/[^A-Za-z0-9]+/g, "");
};

String.prototype.normalizeSerialNumber = function () {
	return this.replace(/[^A-Za-z0-9]+/g, "");
};

String.prototype.fixCharactersDeemedUnsafe = function () {
	return this.replace(/\#/g, "No."); 
	// "No." as in "Number", not as in the opposite of "Yes."
};

String.prototype.stripCharactersDeemedUnsafe = function () {
	return this.replace(/\#/g, ""); 
};

//-----------------------------------------------------------------------------
// Utility functions specific to this application
//-----------------------------------------------------------------------------

if (!String.prototype.isValidModelNumber) {
	String.prototype.isValidModelNumber = function () {
		var s = this.replace(/^\s+|\s+$/g, "");
		return (/^\s*[A-Za-z0-9]+\s*$/).test(s) && 
			(/[A-Za-z]/).test(s) && (/\d/).test(s);
	};
}

if (!String.prototype.isValidSerialNumber) {
	String.prototype.isValidSerialNumber = function (brand, appliance_type) {
		// the form values contain "number|descriptive text",
		// but we just want the "number" portion.
		appliance_type = appliance_type.replace(/\|.*$/, "");
		var s = this.replace(/^\s+|\s+$/g, "");
		if (brand && brand !== "UNK" &&
		    appliance_type &&
		    appliance_type !== "62" && // water heater - GeoSpring
		    appliance_type !== "49" && // water heater - electric
		    appliance_type !== "491") { // water heater - gas
			if (!/^[ADFGHLMRSTVZ][ADFGHLMRSTVZ]/i.test(s)) {
				return false;
			}
		}
		return (/^[A-Za-z0-9][A-Za-z0-9\-\s]*$/).test(s);
	};
}

var ApplianceRegistration = {

	//---------------------------------------------------------------------
	// APPLIANCE TYPE DATA SECTION
	//---------------------------------------------------------------------

	// Items in the "Brand:" dropdown will be in the order listed
	// here.
	applianceBrands: [
		{ code: 'GEC', description: 'GE' + Unicode.reg },
		{ code: 'MON', description: 'GE Monogram' + Unicode.reg },
		{ code: 'PFL', description: 'GE Profile' + Unicode.trade },
		{ code: 'GEJ', 
		  description: 'GE Caf' + Unicode.eacute + Unicode.trade },
		{ code: 'HOT', description: 'Hotpoint' + Unicode.reg },
		{ code: 'AMA', description: 'Americana' + Unicode.trade },
		{ code: 'UNK', description: 'Other' }
	],

	// Items in the "Appliance:" dropdown will be in the order
	// listed here.
	applianceCrossReferences: [
		{ code: 'V', description: 'Advantium Speed Cook Oven' },
		{ code: 'A', description: 'Air Conditioners/ Dehumidifiers' },
		{ code: 'C', description: 'Compactor/ Disposer' },
		{ code: 'E', description: 'Cooking: Electric' },
		{ code: 'G', description: 'Cooking: Gas' },
		{ code: 'L', description: 'Cooking: Dual-Fuel' },
		{ code: 'I', description: 'Dishwasher' },
		{ code: 'D', description: 'Dryer' },
		{ code: 'F', description: 'Freezer' },
		{ code: 'O', description: 'Grill (Outdoor)' },
		{ code: 'M', description: 'Microwave' },
		{ code: 'P', description: 'Portable Ceramic Heater' },
		{ code: 'R', description: 'Refrigerator' },
		{ code: 'W', description: 'Washer' },
		{ code: 'S', description: 'Water Systems' },
		{ code: 'H', description: 'Vent Hoods & Systems' }
	], 

	// Appliance types (a/k/a categories) to populate the "Type:"
	// dropdown.  Each call to addApplianceType accepts the
	// following three arguments:
	//
	//     crossReferences --- an array of one or more
	//     single-character appliance code strings, each of whose
	//     corresponding appliance (first dropdown) will be linked
	//     with the appliance type specified in the second and third
	//     arguments.
	//
	//     productLineCode
	//
	//     description
	// 
	// Once the appliance brand and appliance are selected, this
	// will determine what elements are used to populate the "Type:"
	// dropdown list.  The listing here determines the order.
	//
	// The applianceTypeException() function below determines which
	// appliance types are excluded from each brand and appliance.
	//
	// The productLineCodeException() function below is used to
	// provide different product line codes for certain GE Monogram
	// appliances.
	// 
	// EXAMPLE CALLS TO addApplianceType():
	//
	//     // Add this appliance type to Cooking: Electric ("E").
	//     this.addApplianceType(["E"], "123-3",
	//                           "Cooktop - modular and/or downdraft");
	//
	//     // Add this appliance type to both Cooking: Electric ("E")
	//     // and Microwave ("M").
	//     this.addApplianceType(["E", "M"], "27",
	//                           "Microwave - built-in");
	//
	// NOTE TO FUTURE CODERS: When adding or adjusting PL#'s, be
	// sure to check the exceptions for GE Monogram appliances in
	// the function productLineCodeException().

	addApplianceTypes: function () {
		this.addApplianceType(["R"], "116", "Compact");
		this.addApplianceType(["R"], "112", "Icemaker - freestanding, large capacity");
		this.addApplianceType(["R"], "119", "Freezer on bottom");
		this.addApplianceType(["R"], "113", "Freezer on top");
		this.addApplianceType(["R"], "110", "Side by side");
		this.addApplianceType(["R"], "114", "Wine chiller/ beverage center");
		this.addApplianceType(["R"], "110", "Built-in (no freezer)");
		this.addApplianceType(["F"], "13", "Chest");
		this.addApplianceType(["F"], "112", "Icemaker - freestanding, large capacity");
		this.addApplianceType(["F"], "117", "Professional (built-in all freezer)");
		this.addApplianceType(["F"], "13", "Upright");
		this.addApplianceType(["W"], "311", "Front loading");
		this.addApplianceType(["W"], "31", "Top loading");
		this.addApplianceType(["W"], "33", "Washer/dryer combination");
		this.addApplianceType(["D"], "32", "Electric");
		this.addApplianceType(["D"], "35", "Gas");
		this.addApplianceType(["D"], "33", "Washer/dryer combination");
		this.addApplianceType(["E"], "271", "Advantium 120 speed cook oven");
		this.addApplianceType(["E"], "271", "Advantium 240 speed cook oven");
		this.addApplianceType(["E"], "123", "Cooktop only (no oven)");
		this.addApplianceType(["E"], "123-3", "Cooktop - modular and/or downdraft");
		this.addApplianceType(["E", "M"], "27", "Microwave - built-in");
		this.addApplianceType(["E", "M"], "27", "Microwave - countertop");
		this.addApplianceType(["E", "M"], "27", "Microwave - over-the-range");
		this.addApplianceType(["E"], "25", "Range - professional");
		this.addApplianceType(["E"], "212", "Range (stove)");
		this.addApplianceType(["E"], "214", "Wall oven - built-in double");
		this.addApplianceType(["E"], "213", "Wall oven - built-in single ");
		this.addApplianceType(["E"], "216", "Warming drawer");
		this.addApplianceType(["G"], "123", "Cooktop only (no oven)");
		this.addApplianceType(["G"], "123-3", "Cooktop - modular and/or downdraft");
		this.addApplianceType(["G"], "28", "Range - professional");
		this.addApplianceType(["G"], "28", "Range (stove)");
		this.addApplianceType(["G"], "213", "Wall oven - built-in single");
		this.addApplianceType(["L"], "2142", "Free-standing Range");
		this.addApplianceType(["L"], "2142", "Slide-In Range" );
		this.addApplianceType(["H"], "121", "Down-draft hood");
		this.addApplianceType(["H"], "120", "Over-the-cooktop hood");
		this.addApplianceType(["I"], "41", "Built-in");
		this.addApplianceType(["I"], "41-3", "Built-in - digital controls");
		this.addApplianceType(["I"], "42", "Portable");
		this.addApplianceType(["S"], "621", "Filtration - drinking");
		this.addApplianceType(["S"], "622", "Filtration - household");
		this.addApplianceType(["S"], "611", "Water dispenser");
		this.addApplianceType(["S"], "62", "Water heater - GeoSpring");
		this.addApplianceType(["S"], "49", "Water heater - electric");
		this.addApplianceType(["S"], "491", "Water heater - gas");
		this.addApplianceType(["S"], "623", "Water softening system");
		this.addApplianceType(["A"], "51", "Air conditioner - built in");
		this.addApplianceType(["A"], "53", "Air conditioner - window");
		this.addApplianceType(["A"], "53", "Air conditioner - portable");
		this.addApplianceType(["A"], "92", "Dehumidifier");
		this.addApplianceType(["O"], "953", "Outdoor cooking center (stainless steel)");
		this.addApplianceType(["O"], "951", "Standard grill");
		this.addApplianceType(["V"], "271", "Advantium 120");
		this.addApplianceType(["V"], "271", "Advantium 240");
		this.addApplianceType(["P"], "190", "Ceramic");
		this.addApplianceType(["C"], "44", "Compactor");
		this.addApplianceType(["C"], "43", "Disposer");
	},

	// This function returns true if the specified
	// applianceTypeDescription is not to be listed when the
	// specified brandCode and applianceCode are selected.
	applianceTypeException: function (brandCode, applianceCode,
					  applianceTypeDescription) {
		if (brandCode === "MON") {
			if (applianceTypeDescription === "Range (stove)") {
				return true;
			}
			if (applianceCode === "I" &&
			    applianceTypeDescription === "Built-in") {
				return true;
			}
			if (applianceCode === "F" &&
			    (applianceTypeDescription === "Chest" ||
			     applianceTypeDescription === "Upright")) {
				return true;
			}
			if (applianceCode === "R" && 
			    (applianceTypeDescription === "Compact" ||
			     applianceTypeDescription === "Freezer on top")) {
				return true;
			}
		}
		else {
			if (applianceTypeDescription === "Range - professional") {
				return true;
			}
			if (applianceCode === "F" &&
			    (applianceTypeDescription === "Icemaker - freestanding, large capacity" || 
			     applianceTypeDescription === "Professional (built-in all freezer)")) {
				return true;
			}
			if (brandCode === "PFL") {
				if (applianceCode === "R" && 
				    (applianceTypeDescription === "Icemaker - freestanding, large capacity" ||
				     applianceTypeDescription === "Built-in (no freezer)")) {
					return true;
				}
			}
			else {
				if (applianceCode === "R" &&
				    (applianceTypeDescription === "Icemaker - freestanding, large capacity" ||
				     applianceTypeDescription === "Wine chiller/ beverage center" || 
				     applianceTypeDescription === "Built-in (no freezer)")) {
					return true;
				}
			}
		}
		return false;
	},
	
	// A few GE Monogram products have different PL #'s than those
	// of other brands in otherwise identical categories.
	//
	// Given its arguments, this argument returns either the
	// existing productLineCode, or a modified productLineCode for
	// aforementioned GE Monogram products.
	productLineCodeException: function (brandCode, applianceCode,
					    productLineCode, 
					    applianceTypeDescription) {
		if (brandCode === "MON") {
			// GE Monogram side by side refrigerator
			if (applianceCode === "R" &&
			    productLineCode === "110") {
				productLineCode = "117";
			}
		}
		return productLineCode;
	},
	
	//---------------------------------------------------------------------
	// INITIALIZATION SECTION
	//---------------------------------------------------------------------

	SITE_GEAPPLIANCES: "geappliances.com",
	SITE_MONOGRAM:     "monogram.com",
	SITE_HOTPOINT:     "hotpoint.com",
	SITEID: {
		"geappliances.com": "GEA",
		"monogram.com":     "MON",
		"hotpoint.com":     "HOT"
	},

	initializeSite: function () {
		if (/\bteamsite\b/.test(location.hostname)) {
			if (/\/www\.monogram\//.test(location.pathname)) {
				this.site = this.SITE_MONOGRAM;
			}
			else if (/\/www\.hotpoint\//.test(location.pathname)) {
				this.site = this.SITE_HOTPOINT;
			}
			else {
				this.site = this.SITE_GEAPPLIANCES;
			}
		}
		else if (/\bmonogram\.com$/.test(location.hostname)) {
			this.site = this.SITE_MONOGRAM;
		}
		else if (/\bhotpoint\.com$/.test(location.hostname)) {
			this.site = this.SITE_HOTPOINT;
		}
		else {
			this.site = this.SITE_GEAPPLIANCES;
		}
	},

	// This function is invoked at the end of this script file.
	initialize: function () {
		this.initializeSite();
		this.initializeIndexesByCode();
		this.addApplianceTypes();
	},

	// This function is invoked by addApplianceTypes() above.  See
	// comments for the addApplianceTypes() function for further
	// details.
	addApplianceType: function (crossReferences, productLineCode,
				    description) {
		crossReferences.forEach(function (code) {
			var appl = this.appliancesByCode[code];
			if (appl) {
				if (!("types" in appl)) {
					appl.types = [];
				}
				appl.types.push({
					code: code,
					productLineCode: productLineCode,
					description: description
				});
			}
		}, this);
	},

	// Creates an object to access the elements of the
	// applianceCrossReferences array by code, and an object to
	// access the elements of the applianceBrands array by its code.
	// Invoked as part of the initialization.
	initializeIndexesByCode: function () {
		this.brandsByCode = {};
		this.applianceBrands.forEach(function (brand) {
			this.brandsByCode[brand.code] = brand;
		}, this);
		
		this.appliancesByCode = {};
		this.applianceCrossReferences.forEach(function (xref) {
			this.appliancesByCode[xref.code] = xref;
		}, this);
	},

	//---------------------------------------------------------------------
	// FORM VALIDATION SECTION
	//---------------------------------------------------------------------

	// Generic form validation error function.  Displays the
	// provided message and moves input focus to the specified field
	// (and selects its contents if needed).  Returns false, so that
	// you can write the following code:
	//
	//         return validationError(field, message);
	//
	// instead of having to write two lines:
	//
	//         validationError(field, message);
	//         return false;
	
	validationError: function (field, message) {
		alert(message);
		field.focus();
		if (field.type === "text" || 
		    field.type === "password" || 
		    field.type === "textarea") {
			field.select();
		}
		return false;
	},

	// Generic form validation function.  The field argument
	// specifies a DOM reference to the field to be checked.  The
	// message argument specifies an error message to be displayed
	// if the validation fails.  The test argument can be one of the
	// following:
	//
	//   a RegExp object, in which case the field's value must match
	//   the regular expression in order to be considered valid;
	//
	//   a Function object with no formal arguments, in which case
	//   it is interpreted as a method on a String object (i.e., a
	//   property of String.prototype) and applied as such to the
	//   field's value, and its return value must be truthy for the
	//   value to be considered valid; or
	//
	//   a Function object with at least one formal argument, in
	//   which case it is called with the field's value as its only
	//   argument, and its return value must be truthy for the value
	//   to be considered valid.
	//
	// The normalize argument is optional, and only used when the
	// test is a RegExp object.  If the field passes form
	// validation, its contents are reformatted as follows:
	//
	//   value = value.replace(test, normalize);
	//
	// Examples:
	//
	//   return this.validate(form.name, "Please enter your name.", /\S/);
	//
	//   String.prototype.isValidEmail = function () { ... };
	//   // ...
	//   return this.validate(form.email, "Please enter an email address.",
	//                        String.prototype.isValidEmail);
	//
	//   function isValidEmail (s) { ... }
	//   // ...
	//   return this.validate(form.email, "Please enter an email address.",
	//                        isValidEmail);
	//
	//   return this.validate(field, message,
	//                        /^(\d{3})-?(\d{3})-?(\d{4})$/, "$1-$2-$3");

	validate: function (field, message, test, normalize) {
		if (test.constructor === RegExp) {
			if (!test.test(field.value)) {
				return this.validationError(field, message);
			}
			if (normalize) {
				field.value = field.value.replace(test,
								  normalize);
			}
		}
		else if (test.constructor === Function) {
			if (test.length === 0) {
				if (!test.apply(field.value)) {
					return this.validationError(field,
								    message);
				}
			}
			else {
				if (!test(field.value)) {
					return this.validationError(field,
								    message);
				}
			}
		}
		return true;
	},
	validateNotBlank: function (field, message) {
		return this.validate(field, message, /\S/);
	},
	validateEmail: function (field, message) {
		return this.validate(field, message,
				     RX.EMAIL,
				     RX_NORMALIZE.EMAIL);
	},
	validateZipCode: function (field, message) {
		return this.validate(field, message,
				     RX.US_ZIP_CODE,
				     RX_NORMALIZE.US_ZIP_CODE);
	},
	validatePhoneNumber: function (field, message) {
		return this.validate(field, message,
				     RX.NANP_PHONE_NUMBER,
				     RX_NORMALIZE.NANP_PHONE_NUMBER);
	},
	validateDate: function (field, message) {
		var that = this;
		return this.validate(field, message, function (s) {
			var date = Date.parse_mmddyyyy(s);
			if (date !== null) {
				field.value = date.toDateString_mmddyyyy();
				return true;
			}
			else {
				return false;
			}
		});
	},
	validateDropdownSelected: function (field, message) {
		return this.validate(field, message, function () {
			return (field.options[field.selectedIndex].value !==
				"");
		});
	},
	validateModelNumber: function (field, message) {
		return this.validate(field, message,
				     String.prototype.isValidModelNumber);
	},
	validateSerialNumber: function (field, brand, appliance_type, message) {
		return this.validate(field, message,
				     function () {
					     return this.isValidSerialNumber(brand, appliance_type);
				     });
	},

	validateOwnerInfoSection: function (form) { // because form follows function.
		form.first_name.value = form.first_name.value.stripCharactersDeemedUnsafe();
		form.last_name.value  = form.last_name.value.stripCharactersDeemedUnsafe();
		form.street.value     = form.street.value.fixCharactersDeemedUnsafe();
		form.street2.value    = form.street2.value.fixCharactersDeemedUnsafe();
		form.city.value       = form.city.value.stripCharactersDeemedUnsafe();
		// validation on email, zip, and phone fields already
		// won't allow characters deemed unsafe.

		if (!this.validateNotBlank(form.first_name, "Please enter your first name.") ||
		    !this.validateNotBlank(form.last_name, "Please enter your last name.") ||
		    !this.validateNotBlank(form.email, "Please enter your email address.") ||
		    !this.validateEmail(form.email, "Please enter a valid email address.") ||
		    !this.validateNotBlank(form.street, "Please enter your address.") ||
		    !this.validateNotBlank(form.city, "Please enter your city.") ||
		    !this.validateDropdownSelected(form.state, "Please select your state.") ||
		    !this.validateNotBlank(form.zip, "Please enter your zip code.") ||
		    !this.validateZipCode(form.zip, "Please enter a valid zip code.") ||
		    !this.validateNotBlank(form.phone, "Please enter your phone number.") ||
		    !this.validatePhoneNumber(form.phone, "Please enter a valid phone number.")) {
			return false;
		}
		return true;
	},

	validateApplianceInfoSection: function (form) {
		var brand, appliance_type;

		form.model.value  = form.model.value.normalizeModelNumber();
		form.serial.value = form.serial.value.normalizeSerialNumber();
		// validation on installation_date and expiration_date
		// fields already won't allow characters deemed unsafe.

		brand = form.brand[form.brand.selectedIndex].value;
		appliance_type = form.appliance_type[form.appliance_type.selectedIndex].value;

		if (!this.validateDropdownSelected(form.brand, "Please select a brand.") ||
		    !this.validateDropdownSelected(form.appliance, "Please select an appliance.") ||
		    !this.validateDropdownSelected(form.appliance_type, "Please select an appliance type.") ||
		    !this.validateNotBlank(form.model, "Please enter your appliance's model number.") ||
		    !this.validateModelNumber(form.model, "Please enter a valid appliance model number.") ||
		    !this.validateNotBlank(form.serial, "Please enter your appliance's serial number.") ||
		    !this.validateSerialNumber(form.serial, brand, appliance_type, "Please enter a valid appliance serial number.")) {
			return false;
		}

		if (!this.validateNotBlank(form.installation_date, "Please enter your installation date.\n") ||
		    !this.validateDate(form.installation_date, 
				       "Please enter a valid installation date.\n" +
				       "You must use the full four-digit year.")) {
			return false;
		}
		
		var todayAtNoon = new Date().noon().getTime();
		var installationDate = Date.parse_mmddyyyy(form.installation_date.value);
		var expirationDate = Date.parse_mmddyyyy(form.expiration_date.value);
		
		if (installationDate.getTime() > todayAtNoon) {
			return this.validationError(form.installation_date,
						    "That installation date appears to be " +
						    "in the future. Do you have the current " +
							"date set correctly on your computer? \n" +
							"Please check your computer's date setting, and try again.");
		}
		if (form.service[1].checked) {
			if (!this.validateNotBlank(form.expiration_date,
						   "Please enter your extended warranty expiration date.") ||
			    !this.validateDate(form.expiration_date,
					       "Please enter a valid expiration date.\n" +
					       "You must use the full four-digit year.")) {
				return false;
			}
//			if (expirationDate.getTime() < todayAtNoon) {
//				return this.validationError(form.expiration_date,
//							    "Sorry, the extended warranty expiration date " + 
//							    "you entered is in the past.\n" +
//							    "Please try again.");
//			}
		}

		return true;
	},

	validateInitialForm: function (form) {
		if (!this.validateOwnerInfoSection(form)) {
			return false;
		}
		if (!this.validateApplianceInfoSection(form)) {
			return false;
		}
		return true;
	},

	validateAnotherApplianceForm: function (form) {
		if (!this.validateApplianceInfoSection(form)) {
			return false;
		}
		return true;
	},

	validateEditOwnerInfoForm: function (form) {
		if (!this.validateOwnerInfoSection(form)) {
			return false;
		}
		return this.getOwnerInfoFromForm(form);
	},

	validateEditApplianceInfoForm: function (form, i) {
		if (!this.validateApplianceInfoSection(form)) {
			return false;
		}
		return this.getApplianceInfoFromForm(form);
	},

	//---------------------------------------------------------------------
	// THE GOOD STUFF
	//---------------------------------------------------------------------

	// Returns a data structure containing owner information,
	// obtained from either the initial form, or the "Edit Owner
	// Information" form.
	getOwnerInfoFromForm: function (form) {
		return {
			first_name: form.first_name.value,
			last_name:  form.last_name.value,
			email:      form.email.value,
			street:     form.street.value,
			street2:    form.street2.value,
			city:       form.city.value,
			state:      form.state.value,
			zip:        form.zip.value,
			phone:      form.phone.value
		};
	},

	// Returns a data structure containing information about one
	// appliance, obtained from either the initial form, the "Add
	// Another Appliance" form, or the "Edit Appliance Info" form.
	getApplianceInfoFromForm: function (form) {
		var brandIndex = form.brand.selectedIndex;
		var applianceIndex = form.appliance.selectedIndex;
		var applianceTypeIndex = form.appliance_type.selectedIndex;
		var applianceTypeValue = form.appliance_type.options[applianceTypeIndex].value;
		var applianceTypeCode;
		var applianceTypeDescription;
		if (/\|/.test(applianceTypeValue)) {
			applianceTypeCode = RegExp.leftContext;
			applianceTypeDescription = RegExp.rightContext;
		}
		var installationDate =
			Date.parse_mmddyyyy(form.installation_date.value);
		var expirationDate   =
			Date.parse_mmddyyyy(form.expiration_date.value);
		return {
			brandIndex:         brandIndex,
			applianceIndex:     applianceIndex,
			applianceTypeIndex: applianceTypeIndex,
			
			brand:              form.brand.options[brandIndex].value,
			appliance:          form.appliance.options[applianceIndex].value,
			applianceTypeCode:        applianceTypeCode,
			applianceTypeDescription: applianceTypeDescription,

			model:              form.model.value,
			serial:             form.serial.value,
			installationDate:   installationDate,
			service:            form.service[1].checked,
			expirationDate:     form.service[1].checked ? expirationDate : null
		};
	},

	hideAllScreens: function () {
		$("#initialScreen").hide();
		$("#confirmationScreen").hide();
		$("#editOwnerInfoScreen").hide();
		$("#editApplianceInfoScreen").hide();
		$("#anotherApplianceScreen").hide();
	},
	
	// Executed in <script> block below the form's HTML code.
	finalSetup: function () {
		this.finalSetupOfForm();
		if (this.supportsHistory()) {
			this.setupHistory();
		}
		else {
			this.hideAllScreens();
			$("#initialScreen").show();
		}
	},

	finalSetupOfForm: function () {
		var that = this;
		
		this.initialForm           = $.dom("#initialForm");
		this.confirmationForm      = $.dom("#confirmationForm");
		this.editOwnerInfoForm     = $.dom("#editOwnerInfoForm");
		this.editApplianceInfoForm = $.dom("#editApplianceInfoForm");
		this.anotherApplianceForm  = $.dom("#anotherApplianceForm");
		
		var s1 = $(this.initialForm).dom(".ownerInfoSection");
		var s2 = $(this.initialForm).dom(".applianceInfoSection");
		var s4;
		
		if (s1 && (s4 = ($(this.editOwnerInfoForm).
				 dom(".ownerInfoSection")))) {
			s4.innerHTML = s1.innerHTML;
		}
		if (s2 && (s4 = ($(this.editApplianceInfoForm).
				 dom(".applianceInfoSection")))) {
			s4.innerHTML = s2.innerHTML;
		}
		if (s2 && (s4 = ($(this.anotherApplianceForm).
				 dom(".applianceInfoSection")))) {
			s4.innerHTML = s2.innerHTML;
		}

		this.populateApplianceDropdowns(
			this.initialForm.brand,
			this.initialForm.appliance,
			this.initialForm.appliance_type);

		this.populateApplianceDropdowns(
			this.editApplianceInfoForm.brand,
			this.editApplianceInfoForm.appliance,
			this.editApplianceInfoForm.appliance_type);
		
		this.initialForm.onsubmit = function () {
			if (!that.validateInitialForm(this)) {
				return false;
			}

			var nextState = that.newState({
				description: "Confirmation Screen",
				ownerInfo: that.getOwnerInfoFromForm(this),
				applianceInfo: [that.getApplianceInfoFromForm(this)]
			});
			nextState.show = function () {
				that.switchToConfirmationScreen();
			};
			that.nextState(nextState);
			return false;
		};
	},

	// Populates the block containing information about the
	// appliances' owner and an edit button.  It is either on the
	// "Add Another Appliance" form and on the "Confirmation" form.
	populateOwnerConfirmation: function (j) {
		var that = this;
		
		var $info = $(j).find(".ownerInfo");
		var $edit = $(j).find(".editOwnerInfoButton");

		$info.empty();
		$info.append(
			$("<div></div>").text(this.ownerInfo.first_name +
					      " " + this.ownerInfo.last_name),
			$("<div></div>").text(this.ownerInfo.street),
			$("<div></div>").text(this.ownerInfo.street2),
			$("<div></div>").text(this.ownerInfo.city + ", " +
					      this.ownerInfo.state + " " +
					      this.ownerInfo.zip),
			$("<div></div>").text(this.ownerInfo.phone),
			$("<div></div>").text(this.ownerInfo.email)
		);
		$edit.unbind("click");
		$edit.click(function () {
			var nextState = that.newState({
				description: "'Edit Owner Information' Screen",
				show: function () {
					that.switchToEditOwnerInfoScreen();
				}
			});
			that.nextState(nextState);
			return false;
		});
	},
	
	// Populates the blocks containing information about the
	// appliances, each containing an edit button.  They are either
	// on the "Add Another Appliance" form and on the "Confirmation"
	// form.
	populateApplianceConfirmation: function (j) {
		var that = this;

		var $info = $(j).find(".applianceInfo");

		$info.empty();
		this.applianceInfo.forEach(function (applInfo, i) {
			if (i !== 0) {
				$info.append($("<hr />"));
			}
			var div = $("<div class='oneAppliance'></div>");
			$info.append(div);
			var floatDiv = $("<div style='float: right;'></div>");
			var buttonDiv = $("<div class='button'></div>");
			var a = $("<a href='javascript:void(0);'>Edit</a>");
			$(a).unbind("click");
			$(a).click(function (i) {
				return function () {
					var nextState = that.newState({
						description: "'Edit Appliance Information' Screen",
						show: function () {
							that.switchToEditApplianceInfoScreen(i);
						}
					});
					that.nextState(nextState);
					return false;
				};
			}(i));
			$(floatDiv).append(buttonDiv);
			$(buttonDiv).append(a);
			$(div).append(floatDiv);
			$(div).append($("<div></div>").text(that.brandsByCode[applInfo.brand].description));
			$(div).append($("<div></div>").text(that.appliancesByCode[applInfo.appliance].description));
			$(div).append($("<div></div>").text(applInfo.applianceTypeDescription));
			$(div).append($("<div></div>").text("Model: " + applInfo.model));
			$(div).append($("<div></div>").text("Serial: " + applInfo.serial));
			$(div).append($("<div></div>").text("Installation: " + applInfo.installationDate.toDateString_mmddyyyy()));
			if (applInfo.service) {
				$(div).append($("<div></div>").text("Extended Warranty"));
				$(div).append($("<div></div>").text("Expire: " + applInfo.expirationDate.toDateString_mmddyyyy()));
			}
			$(div).append($("<div style='clear: both;'></div>"));
		}, this);
	},

	switchToConfirmationScreen: function () {
		var that = this;
		
		this.hideAllScreens();

		this.populateOwnerConfirmation(
			$("#confirmationForm .ownerInfoBox")
		);
		this.populateApplianceConfirmation(
			$("#confirmationForm .applianceInfoBox")
		);
		$("#confirmationForm").unbind("submit");
		$("#confirmationForm").submit(function () {
			return that.populateConfirmationFormFields(this);
		});

		$("#confirmationForm a.addAnotherApplianceLink").unbind("click");
		$("#confirmationForm a.addAnotherApplianceLink").click(function () {
			var nextState = that.newState({
				description: "'Add Another Appliance' Screen",
				show: function () {
					that.switchToAnotherApplianceScreen();
				}
			});
			that.nextState(nextState);
			return false;
		});

		setTimeout(function () {
			// We defer this for a very short time to work
			// around a rendering bug involving repainting
			// that is only happening in Mac Safari.
			$("#confirmationScreen").show();
		}, 50);
	},

	populateConfirmationFormFields: function (form) {
		$(form).find("input[type='hidden']").remove();

		var add_hidden = function (name, value) {
			$(form).append($("<input type='hidden' />").
				       attr("name", name).
				       attr("value", value));
		};

		add_hidden("REQUEST", "REGISTER");

		// if (this.site && this.SITEID[this.site]) {
		//     add_hidden("SITEID", this.SITEID[this.site]);
		// }

		add_hidden("first_name", this.ownerInfo.first_name);
		add_hidden("last_name", this.ownerInfo.last_name);
		add_hidden("email", this.ownerInfo.email);
		add_hidden("street", this.ownerInfo.street);
		add_hidden("street2", this.ownerInfo.street2);
		add_hidden("city", this.ownerInfo.city);
		add_hidden("state", this.ownerInfo.state);
		add_hidden("zip", this.ownerInfo.zip);
		add_hidden("phone", this.ownerInfo.phone);

		add_hidden("number_of_appliances", this.applianceInfo.length);
		this.applianceInfo.forEach(function (applInfo, i) {
			add_hidden("brand_"             + i, applInfo.brand);
			add_hidden("appliance_"         + i, applInfo.appliance);
			add_hidden("appliance_type_"    + i, applInfo.applianceTypeCode + "|" + applInfo.applianceTypeDescription);
			add_hidden("model_"             + i, applInfo.model);
			add_hidden("serial_"            + i, applInfo.serial);
			add_hidden("installation_date_" + i, applInfo.installationDate.toDateString_mmddyyyy());
			add_hidden("service_"           + i, applInfo.service ? "1" : "0");
			if (applInfo.service) {
				add_hidden("expiration_date_" + i, applInfo.expirationDate.toDateString_mmddyyyy());
			}
		}, this);

		return true;
	},

	switchToEditOwnerInfoScreen: function () {
		var that = this;

		this.hideAllScreens();
		$("#editOwnerInfoScreen").show();
		
		var form = this.editOwnerInfoForm;
		var ownerInfo = this.ownerInfo;

		form.first_name.value = ownerInfo.first_name;
		form.last_name.value = ownerInfo.last_name;
		form.email.value = ownerInfo.email;
		form.street.value = ownerInfo.street;
		form.street2.value = ownerInfo.street2;
		form.city.value = ownerInfo.city;
		form.state.value = ownerInfo.state;
		form.zip.value = ownerInfo.zip;
		form.phone.value = ownerInfo.phone;

		form.onsubmit = function () {
			var ownerInfo = that.validateEditOwnerInfoForm(this);
			if (!ownerInfo) {
				return false;
			}
			var nextState = that.newState({
				description: "Confirmation Screen",
				ownerInfo: ownerInfo,
				show: function () {
					that.switchToConfirmationScreen();
				}
			});
			that.nextState(nextState);
			return false;
		};
	},

	switchToEditApplianceInfoScreen: function (i) {
		var that = this;

		this.hideAllScreens();
		$("#editApplianceInfoScreen").show();

		var form = this.editApplianceInfoForm;
		var applInfo = this.applianceInfo[i];

		form.brand.selectedIndex = applInfo.brandIndex;
		form.brand.onchange();
		form.appliance.selectedIndex = applInfo.applianceIndex;
		form.appliance.onchange();
		form.appliance_type.selectedIndex = applInfo.applianceTypeIndex;
		form.model.value = applInfo.model;
		form.serial.value = applInfo.serial;
		form.installation_date.value = applInfo.installationDate ? 
			applInfo.installationDate.toDateString_mmddyyyy() : "";
		form.expiration_date.value = applInfo.expirationDate ? 
			applInfo.expirationDate.toDateString_mmddyyyy() : "";
		form.service[applInfo.service ? 1 : 0].checked = true;
		
		form.onsubmit = function () {
			var applianceInfo = 
				that.validateEditApplianceInfoForm(this);
			if (!applianceInfo) {
				return false;
			}

			var nextState = that.newState({
				description: "Confirmation Screen",
				show: function () {
					that.switchToConfirmationScreen();
				}
			});
			nextState.applianceInfo[i] = applianceInfo;
			that.nextState(nextState);
			return false;
		};
	},

	switchToAnotherApplianceScreen: function (i) {
		var that = this;

		this.hideAllScreens();
		$("#anotherApplianceScreen").show();
		this.anotherApplianceForm.reset();

		this.populateApplianceDropdowns(
			this.anotherApplianceForm.brand,
			this.anotherApplianceForm.appliance,
			this.anotherApplianceForm.appliance_type);
		
		this.anotherApplianceForm.onsubmit = function () {
			if (!that.validateAnotherApplianceForm(this)) {
				return false;
			}

			var nextState = that.newState({
				description: "Confirmation Screen",
				show: function () {
					that.switchToConfirmationScreen();
				}
			});
			nextState.applianceInfo.push(that.getApplianceInfoFromForm(this));
			that.nextState(nextState);
			return false;
		};
	},

	populateApplianceDropdowns: function (brandDropdown,
					      applianceDropdown,
					      applianceTypeDropdown) {
		var that = this;
		
		$(brandDropdown).empty();
		$(applianceDropdown).empty();
		$(applianceTypeDropdown).empty();

		$(brandDropdown).append($("<option />").
					attr("selected", "selected").
					attr("value", "").
					text("Select one" + Unicode.hellip));
		this.applianceBrands.forEach(function (brand) {
			var code = brand.code;
			var description = brand.description;
			$(brandDropdown).append($("<option />").
						attr("value", code).
						text(description));
		}, this);

		$(applianceDropdown).append($("<option />").
					    attr("selected", "selected").
					    attr("value", "").
					    text("Select one" + Unicode.hellip));
		this.applianceCrossReferences.forEach(function (appl) {
			var code = appl.code;
			var description = appl.description;
			$(applianceDropdown).append($("<option />").
						    attr("value", code).
						    text(description));
		}, this);

		var onchange = function () {
			var brandCode = brandDropdown.options[brandDropdown.selectedIndex].value;
			var applianceCode = applianceDropdown.options[applianceDropdown.selectedIndex].value;
			that.changeAppliance(brandCode,
					     applianceCode,
					     applianceTypeDropdown);
		};
		
		brandDropdown.onchange = onchange;
		applianceDropdown.onchange = onchange;
	},

	changeAppliance: function (brandCode,
				   applianceCode,
				   applianceTypeDropdown) {
		if (brandCode === "" || applianceCode === "") {
			$(applianceTypeDropdown).empty();
			return;
		}

		var blankOption;
		blankOption = $("<option />");
		blankOption.attr("value", "");
		blankOption.text("Select one" + Unicode.hellip);

		$(applianceTypeDropdown).empty();
		$(applianceTypeDropdown).append(blankOption);

		var appl = this.appliancesByCode[applianceCode];
		var len = 0;
		var lastOption;
		appl.types.forEach(function (type) {
			var productLineCode = type.productLineCode;
			var description     = type.description;
			if (this.applianceTypeException(brandCode,
							applianceCode,
							description)) {
				return;
			}
			productLineCode = this.productLineCodeException(brandCode,
									applianceCode,
									productLineCode,
									description);
			lastOption = $("<option />");
			lastOption.attr("value", productLineCode + "|" + description);
			lastOption.text(description);
			$(applianceTypeDropdown).append(lastOption);
			len += 1;
		}, this);
		if (len === 1) {
			lastOption.attr("selected", "selected");
		}
		else {
			blankOption.attr("selected", "selected");
		}
	},

	//---------------------------------------------------------------------
	// History.
	// --------------------------------------------------------------------

	// Creates and returns a new state object based on the existing
	// state of the form.  The state can be modified, and you will
	// eventually pass it to the nextState() function below.
	// 
	// The info argument is optional.  If provided, it is an object
	// which may contain any of the following properties, all of
	// which are also optional:
	//
	//   ownerInfo --- if specified, an object containing modified
	//   (or new) owner information.  
	//
	//   Code calling this function should create a new object if
	//   it's going to pass one as ownerInfo, never modify the
	//   existing ownerInfo object.
	//
	//   applianceInfo --- if specified, an object containing a new
	//   array of appliance information objects.  If unspecified,
	//   existing appliance information is used, and a shallow copy
	//   of the array is created so the *array* can be modified.
	//
	//   Code calling this function should create a new array if
	//   it's going to pass one as applianceInfo.  If that new array
	//   is based on the existing applianceInfo array, you should
	//   not modify the individual objects within the array, but you
	//   can modify the array itself to insert, replace, or remove
	//   elements.
	//
	//   show --- a function called after the state is actually set.
	//   This is typically a function to change from one screen to
	//   another.
	//
	// Returns an object containing ownerInfo, applianceInfo, and
	// (optionally) show properties.  ownerInfo and applianceInfo
	// are always part of the object; if they're not specified as
	// properties of the argument to this function, they're based on
	// defaults.

	newState: function (info) {
		var state = {};
		if (info && ("ownerInfo" in info)) {
			state.ownerInfo = info.ownerInfo;
		}
		else {
			state.ownerInfo = this.ownerInfo;
		}
		if (info && ("applianceInfo" in info)) {
			state.applianceInfo = info.applianceInfo;
		}
		else {
			state.applianceInfo = this.applianceInfo.slice(0);
			// Shallow array copy.  We modify this array to
			// append elements to it and to replace its
			// existing elements, but never modify the
			// actual objects.
		}
		if (info && ("show" in info)) {
			state.show = info.show;
		}
		if (info && ("description" in info)) {
			state.description = info.description;
		}
		return state;
	},

	// Whether or not you're using the jQuery history plugin, use
	// this function to move on to a different state.  You pass a
	// state object created using newState(), and possibly
	// subsequently modified, as the argument to this function.
	
	nextState: function (state) {
		if (this.supportsHistory()) {
			var nextStateID = this.nextStateID();
			this.states[nextStateID] = state;
			this.eraseStatesAfter(nextStateID);
			$.history.load(nextStateID);
		}
		else {
			this.setState(state);
		}
	},

	// Returns true iff the jQuery history plugin is loaded.
	supportsHistory: function () {
		return ($ && $.history.init);
	},
	
	states: {},

	setStateByID: function (stateID) {
		this.currentStateID = stateID;
		if (stateID === undefined || stateID === null ||
		    stateID === "" || !(stateID in this.states)) {
			this.loadEmptyState();
		}
		else {
			var state = this.states[stateID];
			this.setState(state);
		}
	},
	
	setState: function (state) {
		this.ownerInfo     = state.ownerInfo;
		this.applianceInfo = state.applianceInfo;
		if (state.show) {
			state.show.apply(this);
		}
	},

	nextStateID: function () {
		if (!this.currentStateID) {
			return 1;
		}
		else {
			return parseInt(this.currentStateID, 10) + 1;
		}
	},
	
	loadEmptyState: function (state) {
		this.ownerInfo = {};
		this.applianceInfo = [];
		this.hideAllScreens();
		$("#initialScreen").show();
	},

	// invoked when $.history.load() or $.history.init() is called.
	onHistoryLoad: function (hash) {
		this.setStateByID(hash);
	},

	setupHistory: function () {
		var that = this;
		$.history.init(function (hash) {
			that.onHistoryLoad(hash);
		});
	},

	// This is called when you move to the next stage by clicking on
	// a form submission button, in which case all existing states
	// that came after are erased.  This is not called when you
	// click the forward button in your browser, in which case
	// keeping those states is necessary.
	eraseStatesAfter: function (afterStateID) {
		var stateIDs = [];
		for (var stateID in this.states) {
			if (this.states.hasOwnProperty(stateID)) {
				if (stateID > afterStateID) {
					stateIDs.push(stateID);
				}
			}
		}
		stateIDs.forEach(function (stateID) {
			delete this.states[stateID];
		}, this);
	},

	dummy: function () {}
	
};

ApplianceRegistration.initialize();


