/**
 * @license 
 * modernize 0.1 - Track versions of changes to forms and sync them with a server
 * Developed by Pete DeLaurentis
 * 
 * Depends on open source inflector.js for pluralization
 *
 * Copyright (C) ShapeTools 2010
 * 
 */


/**
 * Function : dump()
 * Arguments: The data - array,hash(associative array),object
 *    The level - OPTIONAL
 * Returns  : The textual representation of the array.
 * This function was inspired by the print_r function of PHP.
 * This will accept some data as the argument and return a
 * text that will be a more readable version of the
 * array/hash/object that is given.
 * Docs: http://www.openjs.com/scripts/others/dump_function_php_print_r.php
 */
function dump(arr,level) {
	var dumped_text = "";
	if(!level) level = 0;
	
	//The padding given at the beginning of the line.
	var level_padding = "";
	for(var j=0;j<level+1;j++) level_padding += "    ";
	
	if(typeof(arr) == 'object') { //Array/Hashes/Objects 
		for(var item in arr) {
			var value = arr[item];
			
			if(typeof(value) == 'object') { //If it is an array,
				dumped_text += level_padding + "'" + item + "' ...\n";
				dumped_text += dump(value,level+1);
			} else {
				dumped_text += level_padding + "'" + item + "' => \"" + value + "\"\n";
			}
		}
	} else { //Stings/Chars/Numbers etc.
		dumped_text = "===>"+arr+"<===("+typeof(arr)+")";
	}
	return dumped_text;
}

// Put ourselves into a jQuery function
(function($) {
	
	// This is our version database 
	$.VersionRepository = function(){

		// These are defaults sent when we create the object
		this.fieldDefaults = {};

		// These are active edits to fields
		this.fieldEdits = {};
		
		// This lists the fields that have changed 
		this.fieldChanges = {};
		
		// This lists the versions of each field
		this.fieldVersions = {};
		
		// This is our list of changes we sent with request IDs
		// so that we don't skip new changes made to fields while
		// we were sending the old changes
		this.fieldSubmissions = {};
		
		// These are fields that are streaming
		this.fieldStreams = {};
		
		// These fields are calculated on the server
		this.fieldsCalculatedOnServer = {};
		
		// These fields are locked as read-only
		this.fieldLocks = {};
		
		// This is our timer for streaming fields
		this.streamingTimer = null;
		
		// This is whether we are currently doing a POST.
		// Only one allowed to be queued at a time.
		this.posting = false;
		
		// This is the callback for whenever a change is made to the object being edited
		this.onFormChanged = null;
		
		// This is called when the object is first created on the server
		this.onCreated = null;
		
		// Called before and after we patch our local data from a remote server
		this.beforePatched = null;
		this.onPatched = null;
		
		// Called if we can't find the given object
		this.onNotFound = null;
		
		// Called if they aren't authorized to access the data
		this.onNotAuthorized = null;

		// Called when an object is destroyed
		this.onDestroyed = null;
		
		// This is the key-field for the form (defaults to id)
		this.keyField = "id";
		this.objectName = null;
		this.url = null;

		// This is our ROM.  The data here comes from the server
		// and can't be changed locally.
		this.rom = {};
		this.keyValue = null;
		
		// These hooks are callback functions for specific fields
		this.hooks = {};
		
		// This is our sync timer
		this.syncTimer = null;
		
		// Start out with our version at 0
		this.currentVersion = 0;
		
		// These are links between fields that we should auto-update
		this.links = [];
	};

	// This version will start tracking changes to a form
	$.fn.trackVersions = function(options) {

		$.debug("<font color='#FF0000'><b>Track Versions</b></font>");
		
		// Store the new this pointer
		var $t = this;
		if ($t) {

			// Fill in the version database
			var formID = $t.attr("id");
			
			// See if this form exists in the database
			var repo = $.modernize[formID];
			if ( repo == null ) {

				// Create a new repository now
				repo = $.modernize[formID] = new $.VersionRepository();
			}
			
			// Now, take the repository and store our key field 
			if ( options ) {
				
				// Set the name of our object form the name of the form
				repo.objectName = $t.attr("name");
				
				// Setup our key-field (override the default)
				if ( options["key"] ) {
					repo.keyField = options["key"];
				}
				
				// See if the key-field was provided
				// and if so, put it into our ROM.  
				// This is data we can't change / edit.
				if ( options[repo.keyField] ) {
					repo.rom[repo.keyField] = options[repo.keyField];
				}

				// This is the callback for whenever a change is made to the object being edited
				repo.onFormChanged = options.onFormChanged;
			}

			// Called when a radio button is clicked
			var radioMethod = function() {
				
				// Get the input
				var $input = $($(this).find("input").get(0));
				
				// See if the radio button is readonly
				if ( $input.attr("readonly") ) {
					return false;
				}
				
				// See if there are any hooks for this field
				$input.applyHook('before', 'click');

				// Snip off the value part of the field name
				var fieldValueName = $input.attr("name");
				var fieldName = fieldValueName.replace("[value]", "");

				// Turn off all labels
				$("label input[name='" + fieldValueName + "']").each( function() {
					$(this).parent().removeClass("checked");
					$(this).attr("checked", false);
				});
				
				// Set the value now to be checked
				$input.attr("checked", true);

				// See if the radio button has a label, and if so, add the checked style
				var $parent = $input.parents("label");
				if ( $parent ) {
					$parent.addClass("checked");
				}

				// The value changed from what it used to be, so we want to mark it in our list
				repo.fieldChanges[fieldName] = true;

				// Do a callback to whoever is listening
				if ( repo.onFormChanged ) {
					repo.onFormChanged.apply($t);
				}
				
				// See if there are any hooks for this field
				$input.applyHook('after', 'click');
				return false;
			};

			// Setup a method for when the checkbox is clicked
			var checkboxMethod = function() {
					
				// Find if this is an element
				$.debug("Just clicked checkbox: " + this.tagName);
				if ( this.tagName.toUpperCase() == "LABEL" ) {
					$.debug("Just clicked checkbox");

					// Get the input
					var $input = $($(this).find("input").get(0));
					
					// See if the radio button is readonly
					if ( $input.attr("readonly") ) {
						return false;
					}

					// Make sure the class is not local.  If it is, we'll not sync with the server.
					if ( $(this).hasClass("local")) {
						$input.applyHook('before', 'click');
						$input.applyHook('on', 'click');
						$input.applyHook('after', 'click');
						return false;
					}
				
					// Flip the value of the checkbox
					$input.attr("checked", !$input.attr("checked"));
				
					// See if there are any hooks for this field
					$input.applyHook('before', 'click');

					// Snip off the value part of the field name
					var fieldValueName = $input.attr("name");
					var fieldName = fieldValueName.replace("[value]", "");

					// See if the radio button has a label, and if so, add the checked style
					var $parent = $input.parents("label");
					if ( $parent ) {
						if ( $input.attr("checked") ) {
							$parent.addClass("checked");
						}
						else {
							$parent.removeClass("checked");
						}
						$.debug("Mark as checked? " + $input.attr("checked") + " checkbox has class checked? " + $parent.hasClass("checked"));
					}

					// The value changed from what it used to be, so we want to mark it in our list
					repo.fieldChanges[fieldName] = true;

					// See if there are any links referencing this condition, and if so apply it
					$input.applyLinksForCondition(true);
					
					// Do a callback to whoever is listening
					if ( repo.onFormChanged ) {
						repo.onFormChanged.apply($t);
					}
				
					// See if there are any hooks for this field
					$input.applyHook('after', 'click');
					return false;
				}
			};

			// Handle checkbox clicks
			//$("input[type='checkbox']", $t).live('click', checkboxMethod);
			$("label.checkbox", $t).live('click', checkboxMethod);
			$("label.radio", $t).live('click', radioMethod);
			
			

			// Query if device is running iPhone OS
			// And if so, hack it so we can still do checkboxes and radio buttons
/*			var apple = {}
			apple.UA = navigator.userAgent;  
			apple.device = false;  
			apple.types = ["iPhone", "iPod", "iPad"];  
			for (var d = 0; d < apple.types.length; d++) {  
			    var t = apple.types[d];  
			    apple[t] = !!apple.UA.match(new RegExp(t, "i"));  
			    apple.device = apple.device || apple[t];  
			}
			if ( apple.device ) {
				$("label.checkbox", $t).live('click', checkboxMethod);
				$("label.radio", $t).live('click', radioMethod);
			}*/

			// Setup keyup handlers
			$("input,textarea", $t).live('keyup', function() {
			
				// See if there are any hooks for this field
				$(this).applyHook('before', 'keyup');
			
				// Sync any linked fields now, but don't send them to the server
				$(this).applyLinksFromField();

				// Snip off the value part of the field name
				var fieldValueName = $(this).attr("name");
				var fieldName = fieldValueName.replace("[value]", "");
				
				// See if the field is live streaming to the server
				var streamingInterval = repo.fieldStreams[fieldName];	
				if ( streamingInterval && repo.streamingTimer == null ) {
					
					// Setup our callback for streaming - this throttles it
					var streamingField = this;
					var streamingMethod = function() { 
						$(streamingField).touch(); 
						repo.streamingTimer = null; 
					};
					
					// Kick off our streaming timer
					repo.streamingTimer = setTimeout(streamingMethod, streamingInterval);
				}
				
				// See if there are any hooks for this field
				$(this).applyHook('after', 'keyup');
			});

			// Setup the blur handler  
			$("input,textarea", $t).live('focusout', function() {

				// Clear the streaming update timer when we leave focus on a field
				if ( repo.streamingTimer ) {
					clearInterval(repo.streamingTimer);
					repo.streamingTimer = null;
				}
				
				// See if there are any hooks for this field
				$(this).applyHook('before', 'focusout');

				// Snip off the value part of the field name
				var fieldValueName = $(this).attr("name");
				var fieldName = fieldValueName.replace("[value]", "");

				// Sync any linked fields now (they're already marked as edited by our focusin method)
				$(this).applyLinksFromField();

				// Look-up all edits we've made to fields being edited, and send them
				// (this includes one made in the call above)
				
				// This commits all of our field edits
				var dirty = false;
				for ( var prop in repo.fieldEdits ) {
					
					// Get the next edit
					var fieldEdit = repo.fieldEdits[prop];
					if ( fieldEdit ) {

						// If the field, changed, send it
						var currentValue = $("input[name='" + fieldEdit.name + "[value]'],textarea[name='" + fieldEdit.name + "[value]']").attr("value");
						if ( fieldEdit.previousValue != currentValue ) {

							// The value changed from what it used to be, so we want to mark it in our list
							repo.fieldChanges[fieldEdit.name] = true;

							// Mark that something changed
							dirty = true;
						}
						else if ( fieldEdit.possibleValueSubmitted ) {
							$(this).attr("value", (fieldEdit.possibleValue != null) ? fieldEdit.possibleValue : "");
						}
						else {
						}
						
					}
				}
				
				// Clear all of our field edits now
				repo.fieldEdits = {};

				// If changes were made, write them
				if ( dirty == true ) {

					// Do a callback to whoever is listening
					if ( repo.onFormChanged ) {
						repo.onFormChanged.apply($t);
					}
				}
				
				// See if there are any hooks for this field
				$(this).applyHook('after', 'focusout');
			});

			// Setup the focus handler
			$("input,textarea", $t).live('focusin', function() {
				
				// See if there are any hooks for this field
				$(this).applyHook('before', 'focusin');

				// Note that we're editing this field
				$(this).editing();
				
				// Mark that we're editing any links too
				$(this).editLinksFromField();
				
				// See if there are any hooks for this field
				$(this).applyHook('after', 'focusin');
			});
			
			// Return our repository
			return repo;
		}
		
		// Don't return anything
		return null;
	};

	// Reads content from a JavaScript / JSON object and populates the form
	$.fn.deserializeWithVersions = function(data, incomingVersion, name, options) {
		
		// If our data is blank, bail
		if ( data == null ) return;
		
		// Figure out our base name if one isn't provided
		if ( name == null ) { name = ""; }

		// Just work with the first node matching the given pattern
		var $t = this;
		if ( $t ) {
			
			// Fill in the version database
			var formID = $t.attr("id");
			var repo = $.modernize[formID];
			if ( repo ) {
			
				// Prepare our flags
				var overwriteChanges = false;
				if ( options && options.overwriteChanges ) {
					overwriteChanges = true;
				}
					
				// Recursively apply changes
				if ( data.isArray ) {
					for ( var i=0; i<data.length; i++ ) {
					
						// We want to always make the numeric index the value of our ref parameter, if we have one
						var current = data[i];
						if (( typeof(current) == "object" ) && ( current["ref"] != null )) {

							// If they have a ref parameter, we'll do specific numbers
							$t.deserializeWithVersions(data[i], incomingVersion, name + "[" + current["ref"] + "]", options);
						}
						else {
						
							// Right now, we only support arrays of ref's.
							// We can add simple arrays in the future
						
							// Otherwise for simple arrays, we'll do standard []'s syntax
							// $t.deserializeWithVersions(data[i], name + "[]");
						}
					}
				}
				else if ( typeof(data) == "object" ) {
				
					// See if the hash contains "value", and if so we'll treat it differently
					if ( data.value != null ) {
					
						// Find the input with the matching name
						var selector = "input[name='" + name + "[value]'],textarea[name='" + name + "[value]']";
						$(selector, $t).each(function () {
							
							// Get the element
							var element = this;
							if ( element ) {
						
								// See if we're getting something that changed
								if ( data["value"] != $(element).attr("value") ) {
									$.debug("Received a change for " + name + ", to value '" + data["value"] + "' from '" + $(element).attr("value") + "'");
								}
						
								// See if we have any changes that haven't been received by our server yet
								// If fieldChanges is true, it means we have data that's not even on the queue yet
								// If field submissions is there, it means we have data on the queue, that's not
								// been received by the server yet, and we shouldn't go in any further
								if ( repo.fieldChanges[name] != true && repo.fieldSubmissions[name] == null ) {
							
									// Now, see if we either have no versions tracked, or if the remote value is newer
									// NOTE: Switch < to <= to allow the same version # to overwrite individual fields
									// This was because we could have multiple stages of update from the server
									// for a given version #.  
									 if ( repo.fieldVersions[name] == null || 
										  (data["version"] == null && repo.fieldVersions[name] <= incomingVersion) ||
										  (data["version"] != null && repo.fieldVersions[name] <= parseInt(data["version"], 10) )) {
						
										// If we see the order state here, call out
										if ( name == "order[state]" ) {
											$.debug("Order state changed from " + $(element).attr("value") + " to " + data["value"]);
										}
						
										// Set the version to the incoming version if we're doing a GET
										// Problem is: syncing two elements.  Version # locally is going to get
										// matched to what we just read from the server.  And it's not the correct version #.
										// If we're in a commit, assigning the incoming version is NOT a good idea.

										// Some fields are calculated on the server side.  When orders get backed up
										// we can hit a condition where the previous values of these fields (from the 
										// server-side queue) get displayed to the client.  Setting the field
										// version # for these fields during the PUT will prevent later
										// updates from overwriting them
										if (( options && options.official == true ) || 
											( repo.fieldsCalculatedOnServer[name] == true )) {

											// Set the version # of the field.
											repo.fieldVersions[name] = incomingVersion;
										}
									
										// See if this field is the focus field
										if ( repo.fieldEdits[name] ) {
										
											// Note that it was in field edits
											$.debug("Rejected field " + name + " because it in the field edits.");
										
											// In this case, push a new possible value but don't update it
											repo.fieldEdits[name].possibleValue = (data["value"] != null) ? data["value"] : "";
											repo.fieldEdits[name].possibleValueSubmitted = true;
										}
										else {
									
											// See what the element is
											if ( $(element).attr("type") == "radio" ) {

												// Uncheck all the others
												$(selector, $t).each( function() { 
												
													// See if it matches the data value
													if ( data["value"] == $(this).attr("value") ) {

														// Mark off the match
														$(this).attr("checked", true);
														$(this).parent().addClass("checked"); 
													}
													else {
														// Remove the checked class
														$(this).parent().removeClass("checked"); 
													}
												});
											}
											else if ( $(element).attr("type") == "checkbox" ) {

												// See if the checkbox is marked as local
												if ( !$(element).parent().hasClass("local") ) {

													// See if we should check it
													var flagIsSet = (data["value"] != null && data["value"] != "" && data["value"] != "false" && data["value"] != false && data["value"] != 0); //(data["value"] == "true" || data["value"] == true || data["value"] == "on" || data["value"] == 1);
													var flagWasSet = $(element).attr("checked"); //($(element).attr("value") == "true" || $(element).attr("value") == true || $(element).attr("value") == "on");

													// Flag was set:
													$.debug("Flag " + $(element).attr("name") +  " is set: " + flagIsSet + "->"+ data["value"] + ", Flag WAS set: " + flagWasSet + "->" + $(element).attr("value"));

													// See if we need to change anything
													if ( flagIsSet != flagWasSet ) {

														// Note that it changed
														$.debug("Flag " + $(element).attr("name") + " is being changed to " + flagIsSet);

														// Update based on whether the flag is set
														if ( flagIsSet ) {
															$(element).attr("checked", true);
															$(element).parent().addClass("checked");
														}
														else {
															$(element).attr("checked", false);
															$(element).parent().removeClass("checked");
														}
											
														// See if there are any live mappings depending on the checkbox
														$(element).applyLinksForCondition(false);
													}
												}
											}
											else {
										
												// Otherwise just set the new value in
												if ( data["value"] != null ) {
													if ( data["value"] != $(element).attr("value")) {
														$.debug("Property " + $(element).attr("name") + " set to " + data["value"]);
													}
													$(element).attr("value", data["value"]);
												}
												else {
													$(element).attr("value", "");
												}
											}
										}
						
									}
								}
								else {
									$.debug("Rejected field " + name + " because it was changed or in submissions.");
								}
							}
						});
					}
					else {

						// Go through all properties and run this function recursively
						for (var prop in data) {
							
							// Take the property and convert to snake case
							var snakeProp = prop.replace(new RegExp("([A-Z])", "g"), "_$1").toLowerCase();
							
							// If someone gave us the top level, dig in
							if ( name == "" ) {
								$t.deserializeWithVersions(data[prop], incomingVersion, snakeProp, options);
							}
							else {
								
								// See what the type of the data at the property is
								if (( typeof(data[prop]) == "object" ) ||
									( data[prop].isArray )) {

									// If it's an object or array, go recursive
									$t.deserializeWithVersions(data[prop], incomingVersion, name + "[" + snakeProp + "]", options);
								}
								else if ( typeof(data[prop]) != "function" ) {
									
									// If we're at the root level of our current object, 
									// store any not hash / arrays into the ROM
									if ( name == repo.objectName ) {
										repo.rom[snakeProp] = data[prop];
									}
								}
							}
						}
					}
				}
			}
		}
		
		// Preserve the chain
		return this;
	};
	
	// Creates content we could post in a form
	$.fn.serializeWithVersions = function(requestId) {
		
		$.debug("Serializing form for request " + requestId);
		
		// Traverse all nodes matching the pattern
		var $t = this;
		if ( $t ) {
		
			// Do the default serialize method
			var postPieces = [];
			
			// See if this form exists in the database
			var repo = $.modernize[$t.attr("id")];
			if ( repo ) {
				
				// For each element, add to our pieces
				$("input[type='text'], input[type='hidden'], input[type='password'], input[type='radio']:checked, textarea", $t).each(function() {
					
					// Get the name and value
					var fieldValue = $(this).attr("value");
					var fieldValueName = $(this).attr("name");
					var fieldName = fieldValueName.replace("[value]", "");

					// If we have a field that's not streaming, don't send it if it's being edited
					if ( repo.fieldEdits[fieldName] && !repo.fieldStreams[fieldName] ) {
						postPieces.push(fieldValueName + "=" + encodeURIComponent(repo.fieldEdits[fieldName].previousValue));
					}
					else {
						postPieces.push(fieldValueName + "=" + encodeURIComponent(fieldValue));
					}
					
				});

				// Checkbox has some browser differences with value, so we'll use the checked attribute to ensure consistent results
				$("input[type='checkbox']", $t).each(function() {
					
					// See if the checkbox is local, and if so, don't sync it
					if ( !$(this).parent().hasClass("local") ) {
					
						// Get the name and value
						var fieldValueName = $(this).attr("name");
						var fieldName = fieldValueName.replace("[value]", "");

						// If we have a field that's not streaming, don't send it if it's being edited
						//$.debug("*** CHECKBOX serializes with checked attr: " + $(this).attr("checked") + " and value: " + $(this).attr("value"));
						if ( $(this).attr("checked") ) {
							postPieces.push(fieldValueName + "=true");
						}
						else {
							postPieces.push(fieldValueName + "=false");
						}
					}
				});
				
				// Push our key to the thing we're serializing
				if ( repo.keyValue ) {
					postPieces.push(repo.objectName + "[" + repo.keyField + "]=" + encodeURIComponent(repo.keyValue));
				}
			
				// Mark all of our changes
				
				
				// Multiple sets of changes:
				// 1. Changed but not on queue  (set gets zeroed out each time)
				// Field submissions get set at this same time
				// 2. On queue, but not in server
				// 3. Being sent to server
				// 4. Was sent to server
				for (var fieldName in repo.fieldSubmissions) {
				
					// Push what has changed for this request
					if ( repo.fieldSubmissions[fieldName] <= requestId ) {   // was >=
						$.debug("Serializing submission for " + fieldName + ", request " + requestId + ", value " + $t.valueForField(fieldName));
						postPieces.push(fieldName + "[changed]" + "=true");
					}
				}

				// See if this is the first time we're creating the object
				// In this case, the keyfield is always blank
				if ( !repo.rom[repo.keyField] ) {
					
					// If so, send all the defaults over
					for ( var fieldName in repo.fieldDefaults ) {

						// Only proceed it if it's not already in field submissions (no duplicates please)
						if ( !repo.fieldSubmissions[fieldName] || 
							 repo.fieldSubmissions[fieldName] < requestId ) {
							
							// Push this field onto there now
							postPieces.push(fieldName + "[changed]=true");
						}
						
					}
				}
				
				// Add our versions set
				for (var fieldName in repo.fieldVersions) {
				
					// Push what has changed
					var fieldVersion = repo.fieldVersions[fieldName];
					if ( fieldVersion != null ) {
						postPieces.push(fieldName + "[version]" + "=" + encodeURIComponent(fieldVersion));
					}
				}
			}
			
			// Sort them now
			postPieces.sort();
			
			// Here's the rub.  Our changes and versions come in out of order with the HTTP POST form.
			// So, our changes might map to the wrong array.  Options for dealing with this include.  
			// Creating extra input fields... but if they appear in the wrong place, we're in trouble.
			// Idea: group these fields in our JSON response so they go together.  Need the ref's in here
			// so we can track them to the right location.
		
			$.debug("Serialized form for request " + requestId);
			
			// Return our value now, joining together our different arrays
			return postPieces.join("&");
		}
		else {
			return null;
		}
	};
	
	// Setup syncing of this form with the server
	$.fn.sync = function(options) {

		// See if the form exists
		$t = this;
		if ( $t ) {

			// Add the on form changed function to the options
			options.onFormChanged = function() { $t.commit(); };

			// Start tracking versions for this form, and whenever the form changes, 
			// commit those changes to the remote server
			var repo = $t.trackVersions(options);
			
			// Store what happens when we need to create the object
			repo.onCreated = options.onCreated;
			repo.beforePatched = options.beforePatched;
			repo.onPatched = options.onPatched;
			repo.onNotFound = options.onNotFound;
			repo.onNotAuthorized = options.onNotAuthorized;
			repo.onDestroyed = options.onDestroyed;

			// Use the key field to fill out our URL now
			repo.url = "/api/" + $.pluralize(repo.objectName);

			// Before we pull any data, call the setup method
			if ( options.onSetup ) {
				options.onSetup.apply($t);
			}
			
			// See if a key value is available, if so, do a pull
			if ( repo.rom[repo.keyField] ) {
				$t.pull();
			}

			// Figure out how often to pull from the server
			var pullInterval = 1000;
			if ( jQuery.browser.msie && parseFloat(jQuery.browser.version) < 8 ) {
				pullInterval = 5000;
			}
			
			// Setup a regular pull
			repo.syncTimer = setInterval(function() { $t.pull({ idle: true }); }, pullInterval);
		}	
		
	};
	
	// This stops syncing anything we're currently synching
	$.fn.unsync = function() {
	
		// See if the form exists
		$t = this;
		if ( $t ) {

			// See if this form exists in the database
			var repo = $.modernize[$t.attr("id")];
			if ( repo ) {
				
				// Stop background refresh
				if ( repo.syncTimer ) {
					clearInterval(repo.syncTimer);
				}
				
				// Clear everything out
				//$("input", $t).attr("value", "");
				
				// Clear this entry from the repo
				$.modernize[$t.attr("id")] = null;
			}
		}
	};
	
	// This will patch our local data with a remote object
	$.fn.patch = function(data, options) {
		
		// See if the form exists
		$t = this;
		if ( $t ) {

			// See if this form exists in the database
			var repo = $.modernize[$t.attr("id")];
			if ( repo ) {
			
				// Our data should contain
				if ( data && data[repo.objectName] ) {
				
					// Before we deserialize our data, we'll call our optional callback
					if ( repo.beforePatched ) {
						repo.beforePatched.apply($t, [data]);
					}
					
					// Get our object data
					var objectData = data[repo.objectName];
					
					// TODO
					// We can prevent the update from happening if the version # of the local object is greater
					if ( repo.currentVersion == null || (objectData.version != null && repo.currentVersion < objectData.version) ) {
					
						// Deserialize the any values in the returned data
						// and get any fields that should go into our ROM
						$.debug("Deserializing version " + objectData.version);
						$t.deserializeWithVersions(objectData, objectData.version, repo.objectName, options);
					
						// Save the version # of the object coming in now - only if it's a GET
						// this is because POST and PUT don't always return the latest object state
						// and until we get push notifications, it would get missed
						if ( options && options.official == true ) {
							repo.currentVersion = objectData.version;
						}
					}
					
					// Call this after we've done a patch
					if ( repo.onPatched ) {
						repo.onPatched.apply($t, [data]);
					}
				}
			}
		}
	};
	
	// This will pull data from the remote server
	$.fn.pull = function(options) {

		// See if the form exists
		$t = this;
		if ( $t ) {

			// See if this form has a repository
			var repo = $.modernize[$t.attr("id")];
			if ( repo ) {
			
				// Look at our key field and use it to get our URL to lookup
				if ( repo.rom[repo.keyField] ) {
					
					// The idle field says that we only send the request if it's idle
					var idle = false;
					if ( options && options.idle ) {
						idle = true;
					}

					// jQuery
					$.iQ({ type: "GET",
					       url: repo.url + "/" + repo.rom[repo.keyField],
					       idle: idle,
		        		   success: function(request, data, textStatus)
						   {
								// Deserialize the any values in the returned data
								// and get any fields that should go into our ROM
								
								// NOTE: GET represents the official database version
								// It's not in RAM, and the version #'s are correct for all the fields
								// If official flag is set, all version #'s will match the received object
								$t.patch(data, { official: true });
						   },
						   error: function(request, xhr, textStatus) {
                           
						      // This means the object couldn't be found
						      if (xhr.status == "404" && request.id == 1) {
						   
						      	// Clear out our key-field
						      	repo.rom[repo.keyField] = null;
						
								// Do the failed to load callback
								if ( repo.onNotFound ) {
									repo.onNotFound.apply($t, [request]);
								}
						      }

						      // This means they couldn't access the order
						      if ( xhr.status == "403" || xhr.status == "401" ) {
						   
						      	// Clear out our key-field
						      	repo.rom[repo.keyField] = null;
						
								// Do the failed to load callback
								if ( repo.onNotAuthorized ) {
									repo.onNotAuthorized.apply($t, [request]);
								}
						      }

						   }
					});
					
				}
			}
		}
	};

	// This will commit any changes we've made to the server
	// It will do a POST if we don't have the keyValue yet,
	// and will do a PUT if we do
	$.fn.commit = function() {
		
		// ONE ALTERNATE IDEA: if we have a PUT out, then one idea is to not do another one till it gets back
		// when it gets back, create another with any new fields that have come up
		// Filtering duplicates may be silly.
		
		// See if the form exists
		$t = this;
		if ( $t ) {

			// See if this form exists in the database
			var repo = $.modernize[$t.attr("id")];
			if ( repo ) {
			
				// This method will 
				var applyServerFeedback = function(request, data) {

					// Take the data coming back
					var remoteSettings = data[repo.objectName];
					if ( remoteSettings ) {
					
						// Apply the version # from the modified object to anything that has changed
						// Look for any fields changed, and then stamp them with the version 
						if ( remoteSettings.version ) {
							
							// Go through all the changed fields and apply them
							for ( var prop in repo.fieldSubmissions ) {
							
								// For every change, store the version now
								if ( repo.fieldSubmissions[prop] > 0 && repo.fieldSubmissions[prop] <= request.id ) {
									repo.fieldVersions[prop] = remoteSettings.version;
									delete repo.fieldSubmissions[prop];
									$.debug("Cleared submission for field " + prop + ", request = " + request.id + ", version stamped = " + remoteSettings.version);
								}
							}
						}
						
						// Deserialize the any versioned values in the returned data
						// and get any fields that should go into our ROM
						$t.patch(data);
					}
					
					// See if we were posting
					if ( repo.posting == true ) {
						
						// Clear it out and allow POSTS again
						// (in case we failed)
						repo.posting = false;
						
						// Now we need to catch up on any PUTs we missed
						var changesCount = 0;
						for ( var prop in repo.fieldChanges ) {
							changesCount += 1;
						}
						
						// If there were any changes, then commit again
						if ( changesCount > 0 ) {
							$t.commit();
						}
					}
				};

				// Once the request has been queued, we'll add the changes to the submissions hash
				// with the ID of the request.  Then we'll clear out our fieldChanges hash.
				var queuedMethod = function(request) {

					// Now, apply that request ID to our fields that have changed
					for ( var prop in repo.fieldChanges ) {
						$.debug("Submission added for " + prop + ", request = " + request.id);
						repo.fieldSubmissions[prop] = request.id;
					}

					// Clear our field changes now
					repo.fieldChanges = {};
				};

				// How to transmit our key value?
				// We can use the form itself... or maybe just append the value in
				if ( repo.rom[repo.keyField] ) {
		
					// Queue up a request
					$.iQ({ type: "PUT",
						   url: repo.url + "/" + repo.rom[repo.keyField],
						   retries: 10,
						   queued: queuedMethod,
						   provideData: function(request) { return $t.serializeWithVersions(request.id); },
			        	   success: function(request, data, textStatus)
						   {
						     // Call our server feedback function
						     applyServerFeedback.apply($t, [request, data]);
						   }
					});
					
				}
				else if ( repo.posting != true ) {
					
					// Set that we're posting the object.  This prevents further POSTs
					// One we're done posting, we'll go through and see if there are any field changes left
					// If so, we'll queue up another PUT call to take care of them
					repo.posting = true;
					
					// Queue up a request
					$.iQ({ type: "POST",
					       url: repo.url,
						   retries: 10,
						   queued: queuedMethod,
						   provideData: function(request) { return $t.serializeWithVersions(request.id); },
		        		   success: function(request, data, textStatus)
						   {
						      // Call our server feedback function
						      applyServerFeedback.apply($t, [request, data]);
                                               
						      // Callback that the object has been created
						      if ( repo.onCreated ) {
						      	repo.onCreated.apply($t, [data]);
						      }
						   }
					});			
				}
			}
		}
	};
	
	// This is called on a field in the form
	$.fn.touch = function(options) {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
					
					// Snip off the value part of the field name
					var fieldName = $field.attr("name").replace("[value]", "");

					// Now, add this field to the changed list
					repo.fieldChanges[fieldName] = true;
					
					// Now, commit our repository
					if ( options == undefined || options.commit == true ) {
						$t.commit();
					}
				}
			}
		}
	};
	
	// This sets up a function handler that we call after one of our internal calls (like click or focusout)
	$.fn.hook = function(when, evt, method) {
		
		// Go through all of the fields matching the given expression
		$(this).each ( function() {

			// See if the field exists
			var $field = $(this);
			if ( $field ) {

				// If so, lookup the form
				var $t = $field.parents("form"); 
				if ( $t ) {

					// Snip off the value part of the field name
					var fieldName = $field.attr("name").replace("[value]", "");

					// See if this form exists in the database
					var repo = $.modernize[$t.attr("id")];
					if ( repo ) {
						
						// Add a hook for this method
						repo.hooks[when + "." + evt + "." + fieldName] = method;
					}
				}
			}
		});
	};

	// This will run the given hook
	$.fn.applyHook = function(when, evt) {

		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
				
					// Snip off the value part of the field name
					var fieldName = $field.attr("name").replace("[value]", "");

					// Add a hook for this method
					method = repo.hooks[when + "." + evt + "." + fieldName];
					if ( method ) {
						method.apply($field);
					}
				}
			}
		}
	};
	
	// This will destroy certain objects on the server
	// NOTE: This doesn't work for most objects
	$.fn.destroy = function() {
		
		// See if the form exists
		$t = this;
		if ( $t ) {

			// See if this form exists in the database
			var repo = $.modernize[$t.attr("id")];
			if ( repo ) {

				// Use the key field to delete the object
				if ( repo.rom[repo.keyField] ) {
		
					// Queue up a request
					$.iQ({ type: "DELETE",
						   url: repo.url + "/" + repo.rom[repo.keyField],
						   retries: 10,
			        	   success: function(request, data, textStatus)
						   {
								// Clear out our key-field
								repo.rom[repo.keyField] = null;

								// Do the on destroy load callback
								if ( repo.onDestroyed ) {
									repo.onDestroyed.apply($t, [request]);
								}
						   }
					});
					
				}
			}
		}
	};
	
	
	// Sets up a link between fields
	$.fn.link = function(options) {
		
		// See if the form exists
		$t = this;
		if ( $t ) {
			
			// Fill in the version database
			var formID = $t.attr("id");
			
			// See if this form exists in the database
			var repo = $.modernize[formID];
			if ( repo != null ) {

				// Push the link onto our list
				repo.links.push(options);

				// Clear the value, then sync with server
				var $to = $("input[name='" + options.to + "[value]']");

				// Lock this field based on the condition
				if ( $("input[name='" + options.condition + "[value]']").attr("checked") ) {
					$to.lock("link");
				}
				else {
					$to.unlock("link");
				}
				
				// Apply the link now
				// $t.applyLink(options);
			}
		}
	};
	
	// Applies all links for a given condition
	$.fn.applyLinksForCondition = function(touch) {
			
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
				
					// Snip off the value part of the field name
					var fieldName = $field.attr("name").replace("[value]", "");

					// Lookup all the links that match
					for ( var index=0; index<repo.links.length; index++ ) {
						
						// Get the link and see if it matches
						var link = repo.links[index];
						if ( link.condition == fieldName ) {

							// Clear the value, then sync with server
							var $to = $("input[name='" + link.to + "[value]']");
							
							// Mark that we changed it if the touched flag is set
							if ( touch ) { $to.editing(true); }
							
							// Apply the link
							$t.applyLink(link);
							
							// See if the condition is blank, then we'll remove those values
							if ( !$field.attr("checked") ) {
								
								// Clear out the value now (stores the previous value)
								$to.attr("value", "");
								$to.unlock("link");
							}
							
						}
					}
					
					// Look for all field edits now that might get 
					// turned on by applyLink and commit them now
					if ( touch ) {
						
						// Mark off anything that's edited in applyLink as changed
						for ( var prop in repo.fieldEdits ) {
							
							$.debug("When applying links for condition, marked field " + prop + " as changed.");
							
							var fieldEdit = repo.fieldEdits[prop];
							repo.fieldChanges[fieldEdit.name] = true;
						}
						
						// Clear out our edits now
						repo.fieldEdits = {};
					}
				}
			}
		}
	};

	// Applies all links for a given field
	$.fn.applyLinksFromField = function() {
	
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
				
					// Snip off the value part of the field name
					var fieldName = $field.attr("name").replace("[value]", "");

					// Lookup all the links that match
					for ( var index=0; index<repo.links.length; index++ ) {
						
						// Get the link and see if it matches
						var link = repo.links[index];
						if ( link.from == fieldName ) {
							$t.applyLink(link);
						}
					}
				}
			}
		}
	};
	
	// This edits all links to a given field 
	$.fn.editLinksFromField = function() {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
				
					// Snip off the value part of the field name
					var fieldName = $field.attr("name").replace("[value]", "");

					// Lookup all the links that match
					for ( var index=0; index<repo.links.length; index++ ) {
						
						// Get the link and see if it matches
						var link = repo.links[index];
						if ( link.from == fieldName ) {
							$t.editLink(link);
						}
					}
				}
			}
		}
	};
	
	// Mark links as edited
	$.fn.editLink = function(link) {

		// Note that we're applying our link now
		$.debug("Marking link as edited from " + link.from + " to " + link.to);
		
		// See if the form exists
		$t = this;
		if ( $t ) {
			
			// Fill in the version database
			var formID = $t.attr("id");
			
			// See if this form exists in the database
			var repo = $.modernize[formID];
			if ( repo != null ) {

				// Lookup the from field
				var $from = $("input[name='" + link.from + "[value]']");
				var $to = $("input[name='" + link.to + "[value]']");
				
				// If they both exist, proceed
				if ( $from && $to ) {

					// Lookup our condition
					var $condition = link.condition ? $("input[name='" + link.condition + "[value]']") : null;
					if ( $condition == null || $condition.attr("checked") ) {

						// Mark that we're editing this link
						$to.editing(true);
					}
				}
			}
		}
	};
	
	// Applies a link between fields
	$.fn.applyLink = function(link) {
		
		// Note that we're applying our link now
		$.debug("Applying link from " + link.from + " to " + link.to);
		
		// See if the form exists
		$t = this;
		if ( $t ) {
			
			// Fill in the version database
			var formID = $t.attr("id");
			
			// See if this form exists in the database
			var repo = $.modernize[formID];
			if ( repo != null ) {

				// Lookup the from field
				var $from = $("input[name='" + link.from + "[value]']");
				var $to = $("input[name='" + link.to + "[value]']");
				
				// If they both exist, proceed
				if ( $from && $to ) {

					// Lookup our condition
					var $condition = link.condition ? $("input[name='" + link.condition + "[value]']") : null;
					if ( $condition == null || $condition.attr("checked") ) {

						// If the value changed, update it
						if ( $to.attr("value") != $from.attr("value") ) {
							
							// Update the value
							var fromValue = $from.attr("value");
							$to.attr("value", fromValue ? fromValue : "");
						}
						
						// Turn on read-only
						$to.lock("link");

						//if ( $to.attr("readonly") != "readonly") {
						//	$to.attr("readonly", "readonly");
						//}
					}
					else {

						// Unlock the field, at a level 1
						// When we unlock the field, all locks must be removed
						//$to.unlock("link");
						
						// Turn off read-only
						//if ( $to.attr("readonly") ) {
						//	$to.attr("readonly", "");
						///}
					}
				}
			}
		}
	};
	
	// Sets up this field as streaming
	$.fn.stream = function(interval) {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
					
					// Snip off the value part of the field name
					var fieldValueName = $field.attr("name");
					var fieldName = fieldValueName.replace("[value]", "");
					
					// Note that this field is streaming
					repo.fieldStreams[fieldName] = interval;
				}
			}
		}
	};
	
	// Sets up this field as calculated on the server
	$.fn.calculateOnServer = function() {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {
					
					// Snip off the value part of the field name
					var fieldValueName = $field.attr("name");
					var fieldName = fieldValueName.replace("[value]", "");
					
					// Note that this field is calculated on the server
					repo.fieldsCalculatedOnServer[fieldName] = true;
				}
			}
		}
	};
	
	// Sets up this field as being edited
	$.fn.editing = function(override) {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {

					// See if it's already being edited
					var fieldName = $(this).attr("name").replace("[value]", "");
					var fieldEdit = repo.fieldEdits[fieldName];
					if ( fieldEdit == null ) { //}|| override ) {

						// Create a new field edit object
						fieldEdit = {};
						fieldEdit.name = fieldName;
						fieldEdit.previousValue = $(this).attr("value");
						fieldEdit.possibleValueSubmitted = false;
						
						// Push it onto our hash
						repo.fieldEdits[fieldName] = fieldEdit;
					}
				}
			}
		}
	};	
	
	// Returns whether it's focused
	$.fn.focused = function() {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {

					// See if it's already being edited
					var fieldName = $(this).attr("name").replace("[value]", "");

					// See if it's in the list of field edits
					return ( repo.fieldEdits[fieldName] != null );
				}
			}
		}
		
		// Assume not
		return false;
	};
	
	// Locks the field, making it read-only (no matter what)
	$.fn.lock = function(lockName, isVisual) {
		
		// Don't lock links for now
		if ( lockName == "link" ) { return; }

		// See if the field exists
		var $fields = $(this);
		if ( $fields ) {
			$fields.each(function() {

				// Capture the field
				var $field = $(this);

				// If so, lookup the form
				var $t = $field.parents("form"); 
				if ( $t ) {

					// See if this form exists in the database
					var repo = $.modernize[$t.attr("id")];
					if ( repo ) {

						// Lookup the field lock
						var fieldName = $field.attr("name");
						var fieldLock = repo.fieldLocks[fieldName] || {};
					
						// Add it in
						fieldLock[lockName] = true;
						repo.fieldLocks[fieldName] = fieldLock;

						// Mark the field as read-only
						$field.attr("readonly", "readonly");
						if ( isVisual == true ) {
							$field.parent().addClass("readonly");
						}
					}
				}
			});
		}
	};
	
	// Unlocks the field, making it editable - IF all locks have been removed
	$.fn.unlock = function(lockName) {
		
		// Don't lock links for now
		if ( lockName == "link" ) { return; }

		// See if the field exists
		var $fields = $(this);
		if ( $fields ) {
			$fields.each(function() {

				// Capture the field
				var $field = $(this);

				// If so, lookup the form
				var $t = $field.parents("form"); 
				if ( $t ) {

					// See if this form exists in the database
					var repo = $.modernize[$t.attr("id")];
					if ( repo ) {

						// Lookup the field lock
						var fieldName = $field.attr("name");
						var fieldLock = repo.fieldLocks[fieldName] || {};
					
						// Remove the field lock
						fieldLock[lockName] = false;
						delete fieldLock[lockName];
					
						// Count how many locks are left
						var lockCount = 0;
						for ( var keys in fieldLock ) {
							if ( fieldLock[keys] == true ) {
								lockCount += 1;
							}
						}

						// Free the field
						if ( lockCount == 0 ) {

							// Mark the field as read-only
							$field.attr("readonly", "");
							$field.removeAttr("readonly");
							$field.parent().removeClass("readonly");
						}
					}
				}
			});
		}
	};
	
	
	// Sets the default value for a given field (sent when we first POST the object)
	$.fn.defaultValue = function(defaultValue) {
		
		// See if the field exists
		var $field = $(this);
		if ( $field ) {

			// If so, lookup the form
			var $t = $field.parents("form"); 
			if ( $t ) {

				// See if this form exists in the database
				var repo = $.modernize[$t.attr("id")];
				if ( repo ) {

					// See if it's already being edited
					var fieldName = $(this).attr("name").replace("[value]", "");

					// Set the default value
					repo.fieldDefaults[fieldName] = defaultValue;
					$(this).attr("value", defaultValue);
				}
			}
		}
	};
	
	// Return the value of the given field
	$.fn.valueForField = function(fieldName) {
		
		// Lookup this field now
		var field = this.field(fieldName);
		return field ? field.attr("value") : null;
	};

	// Return the value of the given field
	$.fn.field = function(fieldName) {
		
		// Look for the field in this form
		return $("input[name='" + fieldName + "[value]']", this);
	};
	
	// Add a debug function
	$.debug = function(text) {
		//$(".debug").prepend(text + "<br/>");
	};
	
	// Setup our repository
	$.modernize = {};
	
})(jQuery);

