/*
	Class: Aurora.Form
*/
Aurora.Form = new Class({
	
	Implements: Options,
	
	options: {
		
		target: null,
		
		fields: [],
		
		submitMethod: null,
		
		submitBtn: null,
		feedbackEl: null,
		activityEl: null,
		messageEl: null,
		
		sections: false,
		
		fadeFeedback: false,
		
		displayFeedback: true,
		
		clearFeedback: true,
		
		submitForm: true,
		
		ignoreFields: false,
		
		detectFields: false,
		
		scrollToError: true,
		
		scrollToFeedback: false,
		
		refreshPage: null,
		
		keyboardShortcuts: false,
		
		validationMode: 'submit',
		
		hideHints: false,
		hideValidHints: false,
		
		resetFields: false,
		
		focusField: null,
		
		hideMenu: false,
		closePopup: false,
		
		splitMemberNameFrom: null,
		memberNameCheckPopup: null,
		
		invalidMessage: '',
		
		validFieldMessage: '',
		invalidFieldMessage: '',
		validatingFieldMessage: '',
		
		onSuccess: null,
		onFail: null,
		onPreSubmit: null,
		onValidation: null,
		onSubmit: null,
		onTransmission: null
		
	},
	
	initialize: function( target, options )
	{
		var $el = this.$el = $( target );
		
		if ( !options ) {
			$log( 'A form was detected without any valid options.', $el );
			return;
		}
		
		this.setOptions( options );
		
		if ( !this.options.target )
			$log( 'Warning: A form was detected without a target.', $el );
		
		this._status = 'waiting';
		this._preSubmitQueue = [];
		
		// Submit Button
		var $submit = this.$submit = [];
		
		if ( this.options.submitBtnQuery )
		{
			var $submit = this.$submit = $el.getElements( this.options.submitBtnQuery );
		}
		else if ( this.options.submitBtn )
		{
			var $submit = this.$submit = [ $( this.options.submitBtn ) ];
			
			if ( !$submit && this.options.submitBtn )
				alert( 'Submit button specified in form options is invalid, unable to find: [#' + this.options.submitBtn + ']' );
		}
		else
		{
			var detectedSubmitBtn = target.getElement( 'input[type=submit]' );
			
			if ( detectedSubmitBtn )
				$submit = this.$submit = this.options.submitBtn = detectedSubmitBtn;
		}
		
		
		// Feedback Element (optional)
		var $feedback = this.$feedback = this.options.feedbackEl = $( this.options.feedbackEl );
		
		if ( this.options.displayFeedback && !this.options.feedbackEl )
			$feedback = this.$feedback = this.options.feedbackEl = $el;
		
		
		// Activity (optional, tries to find it based on a class)
		var $activity = this.$activity = $el.getElements( '.aurora-form-activity' );
		
		if ( this.options.activityEl )
			$activity = this.$activity = [ $( this.options.activityEl ) ];
		
		
		// Message (optional, tries to find it based on a class)
		var $message = this.$message = $el.getElement( '.aurora-form-message' );
		
		if ( this.options.messageEl )
			$message = this.$message = $( this.options.messageEl );
		
		
		// Define field map
		var fieldMap = {};
		
		
		// Set validation errors
		this._validationErrors = [];
		
		
		// Gather
		var gather = (function() {
		
			// Add field (called from the loops below)
			var addField = (function(f) {
			
				var $el = null,
					options = {},
					key = null;
				
				switch( $type(f) )
				{
					case 'object':
					
						$el = this.$el.getElement( 'input[name=' + f.key + ']' );
						
						options = f;
						
						$extend( options, { fieldOnly: true } );
						
						key = f.key;
						
					break;
					
					case 'element':
					
						$el = f;
						
						// Simple field
						var isFieldTag = /^(input|select|textarea)$/i.test( $el.get( 'tag' ) );
						
						if ( isFieldTag )
						{
							$extend( options, { fieldOnly: true } );
							key = f.getProperty( 'name' );
						}
						
						// Complex field
						else
						{
							$el = f;
							
							options = $el.getDataFromComment() || {};
							
							var $fields = $el.getElements( 'textarea,select,input' );
							
							options.fields = $fields;
							
							if ( options.key )
							{
								key = options.key;
							}
							else
							{
								switch( $type( $fields ) )
								{
									case 'array':
										var firstField = $fields[0],
											key = firstField.getProperty( 'name' ); // For now this is fine
									break;
									
									default: // Single field
										key = $fields.getProperty( 'name' );
								}
							}
						}
						
					break;
				}
				
				// Create the field (only if it doesn't exist)
				if ( key && !fieldMap[ key ] )
					fieldMap[ key ] = new Aurora.Form.Field( $el, this, options );
			
			}.bind( this ));
		
			/* Method 1: Array (fields array supplied to form) */
			var suppliedFields = this.options.fields;
				suppliedFields.each( function(f) { addField(f) });
			
			/* Method 2: Class (gather values from fields with class .aurora-field) */
			var classFields = this.$el.getElements( '.aurora-field' );
				classFields.each( function(f) { addField(f) });
			
			/* Method 3: Loop all other fields (look for inputs that haven't been detected yet, only if detectFields is true) */
			if ( this.options.detectFields )
			{
				var otherFields = this.$el.getElements( 'input,textarea,select' );
					otherFields.each( function(f) { addField(f) });
			}
			
			// Log fields
			// $log( fieldMap );
			
			// Store fields
			var fields = this.fields = $H( fieldMap );
			
			// Check to see if we have any file fields, and change the submit method if need be
			fields.each( (function(f) {
			
				if ( f.type == 'file' )
					this.options.submitMethod = 'iframe';
			
			}.bind( this )));
		
		}.bind( this ));
		
		
		// Gather fields together
		gather();
		
		
		// Look for form sections
		this.initSections();
		
		
		// Add event to submit button
		if ( $submit.length )
		{
			$submit.each( (function( $s ) {
			
				$s.addEvent( 'click', (function(e) {
				
					e.stop();
					this.validateForm();
				
				}.bind( this )));
			
			}.bind( this )));
		}
		
		
		// Focus a field
		if ( this.options.focusField )
		{
			var focusField = this.fields[ this.options.focusField ];
			
			if ( focusField )
				focusField.$field.focus();
		}
		
		
		// Detect pages
		if ( this.options.pages )
		{
			this.initPages();
		}
	
	},
	
	initSections: function()
	{
		var $el = this.$el;
		
		var sections = $el.getElements( '.aurora-form-section' );
		
		this.sections = {};
		this.conditions = [];
		
		var fields = this.fields;
		
		var processSection = (function( $s ) {
		
			// Retrieve options
			var options = $s.getDataFromComment() || {};
			
			if ( !options )
			{
				$log( 'No section options found, they are required for element:', $s );
				return;
			}
			
			// Debug options
			// if ( this.options.debug && !options.debugThis )
				// return;
			
			// Add element to sections object
			var section = this.sections[ options.id ] = {
				$el: $s
			};
			
			// Build up conditions
			var condition = {
				values: options.showIf, // for now we assume its just showIf
				_last_: {}, // add last fields eventually (should map the fields in values struct)
				onChange: function( b, values ) {
				
					$s[ b ? 'show' : 'hide' ]();
				
				}
			};
			
			// Convert to an array if its not
			var wasSingleValue = false;
			
			if ( $type( condition.values ) != 'array' )
			{
				wasSingleValue = true;
				
				condition.values = [ condition.values ];
			}
			
			// Checks the conditions values against the last form values to determine if the condition is valid or not
			var checkCondition = function() {
			
				var checkValues = condition.values;
				
				// If either statements are true
				var oneSetOfConditionsIsValid = false;
				
				checkValues.each( (function( values ) {
					
					var lastValues = condition._last_;
					
					// If all statements are true
					var conditionsAreValid = true;
					
					$H( values ).each( function( requiredValue, key ) {
						
						// Abort if we already have a condition that is invalid
						if ( !conditionsAreValid )
							return;
						
						// Check the last value against the required value
						var lastValue = lastValues[ key ];
						
						if ( lastValue == requiredValue )
						{
							conditionsAreValid = true;
						}
						else
						{
							conditionsAreValid = false;
						}
					
					});
					
					// If at least one set of conditions is valid, we can show it
					if ( conditionsAreValid )
						oneSetOfConditionsIsValid = true;
				
				}));
				
				if ( condition.onChange )
				{
					condition.onChange( oneSetOfConditionsIsValid );
				}
			
			};
			
			// Add section to the fields inside the section, TODO: expand to support non aurora fields
			var auroraFields = $s.getElements( '.aurora-field' );
			
			auroraFields.each( function(f) {
			
				var theField = f.retrieve( 'field' );
				
				if ( !theField )
				{
					$log( 'No field class found for element, could because of duplicated [name] property somewhere else on the form.', f );
					return;
				}
				
				theField.section = options.id;
			
			});
			
			// Add inital values condition and events to the fields
			var values = $H( condition.values );
			
			condition.values.each( (function( values ) {
			
				$H( values ).each( function( v, k ) {
				
					var field = fields[ k ];
					
					if ( !field )
					{
						$log( 'Field inside section not be found!', k );
						return;
					}
					
					// Add events to field elements
					var $fields = field.$fields,
						type = field.type;
						
					if ( !$fields.length )
						$fields = [ field.$field ];
					
					// Don't add the event twice
					if ( $H( condition._last_ ).has( k ) )
						return;
					
					// Store inital value
					condition._last_[ k ] = field.get( 'value' );
					
					$fields.each( function( $f ) {
						
						switch( type )
						{
							case 'checkbox':
							case 'radio':
							
								$f.addEvents({
								
									'click': (function(e) {
									
										// Get the field value
										var fieldValue = field.get( 'value' );
										
										// Set the last value
										condition._last_[ k ] = fieldValue;
										
										// Check all the conditions in case its now true/false
										checkCondition();
									
									}.bind( this ))
								
								});
								
							break;
							
							case 'select-one':
								
								$f.addEvents({
								
									'change': (function(e) {
									
										// Get the field value
										var fieldValue = field.get( 'value' );
										
										// Set the last value
										condition._last_[ k ] = fieldValue;
										
										// Check all the conditions in case its now true/false
										checkCondition();
									
									}.bind( this ))
								
								});
							
							break;
						}
					
					});
				
				}.bind( this ));
			
			}.bind( this )));
			
			// Add conition to form conditions
			this.conditions.push( condition );
			
			// $log( condition );
			// $log( section );
		
		}.bind( this ));
		
		// Loop over sections
		sections.each( (function( $s ) {
			
			processSection( $s );
			
		}));
	
	},
	
	initPages: function()
	{
		var $el = this.$el;
		
		var pages = $el.getElements( '.aurora-form-page' ); // Add ability to customise
		
		if ( !pages.length )
		{
			$log( 'No form pages detected.' );
			return;
		}
		
		
		// Set the first page
		this._page = pages[0];
		
		
		// Scrolling function
		var scrollToPage = (function() {
		
			var pageStart = this._page.getPosition();
			
			window.document.body.scrollTo( 0, pageStart.y );
		
		}.bind( this ));
		
		
		// Check to see that the current page is still visible (corrects itself if not, allows us to view other pages with specific buttons)
		var checkCurrentPage = (function() {
		
			if ( !this._page.isVisible() )
			{
				pages.each( function(p) {
				
					if ( p.isVisible() )
						this._page = p;
				
				}.bind( this ));
			}
		
		}.bind( this ));
		
		
		// Fire any custom events
		var fireEvents = (function( options ) {
		
			if ( options && options.onDisplay )
				Aurora.doCallback( options.onDisplay );
		
		}.bind( this ));
		
		
		// Get the next page
		var setNextPage = (function( backwards, index ) {
		
			checkCurrentPage();
			
			this._page.hide();
			
			( backwards ? index-- : index++ );
			
			if ( pages[ index ] )
				this._page = pages[ index ];
			else
				$log( 'No next/prev page found.' );
			
			// Only show the page if we meet certain conditions, otherwise skip it
			var pageOptions = this._page.getDataFromComment() || {};
			
			if ( pageOptions.skipIf )
			{
				var formData = this.gatherData(),
					values = pageOptions.skipIf,
					skipToNextPage = false;
				
				$H( values ).each( function( v, k ) {
					
					var lv = formData[ k ];
					
					skipToNextPage = ( lv == v );
				
				});
				
				// Increment index by one (skip the page)
				if ( skipToNextPage )
				{
					$log( 'Next page is not applicable due to conditions based on form data, skipping...' );
					setNextPage( backwards, index );
					return;
				}
			}
			
			this._page.show();
			
			scrollToPage();
			
			fireEvents( pageOptions );
		
		}.bind( this ));
		
		
		// Next page
		var nextPage = (function( options, index ) {
		
			var pageIsValid = this.validatePage( this._page );
			
			if ( pageIsValid )
				setNextPage( false, index );
		
		}.bind( this ));
		
		
		// Previous page
		var previousPage = (function( options, index ) {
		
			setNextPage( true, index );
		
		}.bind( this ));
		
		
		// Init the buttons (if any)
		pages.each( function( p, i ) {
		
			var o = p.getDataFromComment() || {};
			
			var $next = p.getElement( '.aurora-form-next' ),
				$previous = p.getElement( '.aurora-form-previous' );
			
			// Handle next button
			if ( $next )
			{
				$next.addEvent( 'click', function(e) {
					e.stop();
					nextPage( o, i );
				}.bind( this ));
			}
			
			// Handle previous button
			if ( $previous )
			{
				$previous.addEvent( 'click', function(e) {
					e.stop();
					previousPage( o, i );
				}.bind( this ));
			}
		
		}.bind( this ));
	},
	
	validatePage: function( page )
	{
		var form = this.form;
		
		var fields = [];
		
		var options = page.getDataFromComment() || {};
		
		var auroraFields = page.getElements( '.aurora-field' );
		
		// TODO: Merge this into the main page validation array so we can support remote validation calls etc
		
		// TODO: Add support for detecting non aurora-fields when validating a page, see normal detection code for more details;
		
		auroraFields.each( function(f) {
		
			if ( f.retrieve( 'field' ) )
				fields.push( f.retrieve( 'field' ) );
		
		});
		
		// $log( 'Fields on this page:', fields );
		
		if ( !fields.length )
			return true;
		
		this._validationErrors = [];
		
		fields.each( (function(f) {
			
			// We always need to check if a field is valid even if we have validated it before, otherwise the user could have changed the value.
			if ( f.options.validate || f.options.required )
				f.validate();
			
		}.bind( this )));
		
		// Determine whether the page is valid or not
		if ( this._validationErrors.length )
		{
			if ( options.onValidation )
				Aurora.doCallback( options.onValidation, this._validationErrors );
			
			$log( this._validationErrors );
			
			return false;
		}
		else
		{
			if ( options.onValidation )
				Aurora.doCallback( options.onValidation, false );
			
			return true;
		}
	
	},
	
	validateForm: function()
	{
		// If using pages, validate the final page first
		if ( this.options.pages )
		{
			var validPage = this.validatePage( this._page );
		
			if ( !validPage )
				return;
		}
		
		if ( this._status == 'validating' || this._status == 'submitting' )
		{
			$log( 'The form is already in a validating or submitting state, please wait...' );
			return;
		}
		
		this._status = 'validating';
		
		this._validationErrors = [];
		
		this.fields.each( (function(f) {
			
			// We always need to check if a field is valid even if we have validated it before, otherwise the user could have changed the value.
			if ( f.options.validate || f.options.required )
				f.validate();
			
		}.bind( this )));
		
		this.preSubmitForm();
	
	},
	
	preSubmitForm: function()
	{
		// $log( 'Pre Submit Form...' );
		
		// Call custom function and pass errors (if it exists)
		if ( this.options.onPreSubmit )
			Aurora.doCallback( this.options.onPreSubmit );
		
		this.toggleForm();
		
		// Check if theres any actions queued first
		if ( this._preSubmitQueue.length )
		{
			// Show activity
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.show();
				});
			}
			
			$log( 'There are actions still queued, waiting for completion...' );
			
			return;
		}
		
		// Otherwise check if we have any validation errors
		else if ( this._validationErrors.length )
		{
			$log( 'There are validation errors:', this._validationErrors );
			
			// Scroll to first error
			if ( this.options.scrollToError )
			{
				var firstError = this._validationErrors[0],
					$errorEl = firstError.$el;
				
				if ( $errorEl )
					new Fx.Scroll( document.body, {
						offset: { x: 0, y: -80 }
					}).toElement( $errorEl ).chain( function() { firstError.$field.focus() } );
			}
			
			// Show message
			if ( this.$message )
				this.$message.show().set( 'html', ( this.options.invalidMessage ? this.options.invalidMessage : 'Please ensure all fields are completed.' ) );
			
			// Call custom function and pass errors (if it exists)
			if ( this.options.onValidation )
				Aurora.doCallback( this.options.onValidation, this._validationErrors );
			
			// Hide activity
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.hide();
				});
			}
			
			this.toggleForm( true );
			
			this._status = 'waiting';
			
			// Call custom function (if it exists)
			if ( this.options.onSubmit )
				Aurora.doCallback( this.options.onSubmit, false );
		}
		else
		{
			// Call custom function (if it exists)
			if ( this.options.onSubmit )
				Aurora.doCallback( this.options.onSubmit, true );
			
			// Call custom function and pass no errors (if it exists)
			if ( this.options.onValidation )
				Aurora.doCallback( this.options.onValidation, false );
			
			// Submit form
			if ( this.options.submitForm )
				this.submitForm();
		}
	},
	
	checkSubmitQueue: function( remKey )
	{
		$log( 'Removing [' + remKey + '] from submit queue.' );
		
		if ( remKey )
			this._preSubmitQueue.erase( remKey );
		
		// If the submit queue is empty, pre submit the form again
		if ( !this._preSubmitQueue.length )
		{
			if ( this._status == 'validating' )
			{
				$log( 'Submit Queue is empty, attempting to submit again...' );
				this.preSubmitForm();
			}
			else
			{
				$log( 'Submit Queue is empty with no form submit detected.' );
			}
		}
	
	},
	
	toggleForm: function( revert )
	{
		if ( revert )
		{
			this.$el.removeClass( 'aurora-form-processing' );
			
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.hide();
				});
			}
			
			this.$submit.each( (function( $s ) {
			
				if ( $s.retrieve( 'button' ) )
					$s.retrieve( 'button' ).enable();
			
			}.bind( this )));
		}
		else
		{
			this.$el.addClass( 'aurora-form-processing' );
			
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.show();
				});
			}
			
			this.$submit.each( (function( $s ) {
			
				if ( $s.retrieve( 'button' ) )
					$s.retrieve( 'button' ).disable();
			
			}.bind( this )));
		}
	},
	
	submitForm: function()
	{
		var $el = this.$el,
			$activity = this.$activity,
			$message = this.$message,
			$submit = this.$submit,
			$feedback = this.options.feedbackEl;
		
		// $log( 'Submitting form...' );
		
		// Check to see if we have already submitted the form
		if ( this._status == 'submitting' )
		{
			$log( 'The form is already in a submitting state, please wait...' );
			return;
		}
		
		this._status = 'submitting';
		
		// Call custom function (if it exists)
		if ( this.options.onTransmission )
			Aurora.doCallback( this.options.onTransmission );
		
		if ( $message )
			$message.hide();
		
		// Gather data
		this.gatherData();
		
		// Debug
		if ( this.options.debug )
		{
			$log( 'Data:', this.data );
		}
		
		// If theres no target, don't call any actions
		if ( !this.options.target )
		{
			this.formCompleted();
			return;
		}
		
		// Add some custom duplicates (for now its just file handling)
		this.fields.each( (function(f) {
		
			var type = f.options.type;
			
			switch( type )
			{
				case 'file':
				
					var name = f.name,
						$field = f.$field;
					
					var fieldData = this.data[ name ];
					
					if ( !fieldData )
						return;
					
					this.data[ name + '_fileName' ] = fieldData;
					
					if ( this.options.submitMethod == 'iframe' )
						new Element( 'input', { type: 'hidden', name: name + '_fileName', value: fieldData } ).inject( $el, 'top' );
				
				break;
			}
		
		}.bind( this )));
		
		// Call action
		switch( this.options.submitMethod )
		{
			case 'iframe':
				
				var key = $time();
				
				// Add a store for hidden fields
				this.hidden = new Hash();
				
				// Process some fields so the iframe gets the proccesed data
				this.fields.each( (function(f) {
				
					var type = f.options.type;
					
					switch( type )
					{
						case 'textarea':
						case 'date':
						
							var name = f.name,
								$field = f.$field;
							
							if ( type == 'textarea' && !$field.hasClass( 'aurora-wysiwyg' ) )
								return;
							
							var fieldData = this.data[ name ];
							
							if ( !fieldData )
								return;
							
							$field.setProperty( 'name', '_' + name + '_' );
							
							this.hidden[ name ] = {
								clone: new Element( ( type == 'textarea' ? 'textarea' : 'input' ), { type: 'hidden', name: name, value: fieldData } ).inject( $el, 'top' ),
								original: $field
							}
							
						break;
						
						default:
						
						
						break;
					}
				
				}.bind( this )));
				
				// Create temporary iframe
				var $iframe = new Element( 'iframe', {
					id: key,
					name: key,
					src: 'about:blank',
					styles: 'display: none;'
				}).inject( $el, 'top' );
				
				// Debug
				if ( this.options.debug )
					$iframe.setStyles( 'width: 100%; height: 400px; border: 1px solid #B5B5B5; margin-bottom: 20px; display: block;' );
				
				$el.setProperties({
					action: callActionURL + '?__iframeCallbackId__=' + key + '&action=sparkle.parse&path=' + this.options.target,
					enctype: 'multipart/form-data',
					method: 'post',
					target: key
				});
				
				$log( 'Processed form data:', this.data );
				
				$el.submit();
			
			break;
			
			default:
			
				// $log( 'Submit form with:', data );
				
				switch( this.options.target.toLowerCase() )
				{
					case 'signin':
					
						// The action doesn't return html, so don't show clear anything
						this.options.clearFeedback = false;
						
						Aurora.callAction({
							action: 'member.signin',
							data: this.data,
							onComplete: (function( rtn ) {
								
								this.processResponse( rtn );
								
							}.bind( this ))
						});
					
					break;
					
					default:
					
						Aurora.loadSparkle({
							path: this.options.target,
							data: this.data
						}).then( (function( rtn ) {
						
							this.processResponse( rtn );
						
						}.bind( this )));
						
					break;
				}
				
			break;
		}
	},
	
	gatherData: function()
	{
		// Gather values
		var data = this.data = {};
		
		// Ignore fields
		var ignoreFields = [];
		
		if ( this.options.ignoreFields )
			ignoreFields = this.options.ignoreFields.split( ',' );
		
		this.fields.each( (function( f, k ) {
		
			if ( ignoreFields.contains( f.name ) )
				return;
			
			var value = f.get( 'value' );
			
			switch( $type( value ) )
			{
				case 'string':
				case 'boolean':
				
					if ( ( this.options.splitMemberNameFrom && this.options.splitMemberNameFrom == f.name ) || f.options.splitAsMemberName )
					{
						if ( !value )
							return;
						
						var memberName = this.splitMemberName( value );
						
						$H( memberName ).each( function( string, key ) {
						
							if ( f.options.splitAsMemberName )
								data[ f.options.splitAsMemberName + '_' + key ] = string;
							else
								data[ key ] = string;
							
						});
					}
					else
						data[ k ] = value;
				
				break;
				
				case 'array':
					data[ k ] = value.toString();
				break;
				
				case 'object': // e.g group of checkboxes or date
				
					switch( f.options.type )
					{
						case 'dateSelects':
							return data[ k ] = value.cf;
						break;
					}
					
					switch( f.options.validate )
					{
						case 'date':
						case 'dob':
							return data[ k ] = value.cf;
						break;
						
						default:
							// $log( 'Detected object, adding the following group of data:', value );
							$extend( data, value );
					}
				
				break;
				
				default:
					$log( 'Warning: Unsupported data type from field detected: [' + $type( value ) + '], your form may be not be submitting correctly.' );
					return;
			}
		
		}.bind( this )));
		
		return data;
	
	},
	
	processResponse: function( rtn )
	{
		var $el = this.$el,
			$activity = this.$activity,
			$message = this.$message,
			$submit = this.$submit,
			$feedback = this.options.feedbackEl;
		
		if ( this._status != 'submitting' )
		{
			$log( 'Cannot process form, currently in [' + this._status + '] stage.' );
			return;
		}
		
		this._status = 'submitted';
		
		if ( rtn.success )
		{
			// $log( 'Form submission successful.', rtn );
			
			// Display feedback
			if ( $feedback )
			{
				if ( this.options.fadeFeedback )
				{
					$el.get( 'tween' )
						.setOptions({
							duration: 500,
							transition: 'quad:in:out'
						})
						.start( 'opacity', 0 )
						.chain( (function() {
							
							var formSize = $el.setStyle( 'overflow', 'hidden' ).getDimensions();
							
							$el.hide().setOpacity(1);
							
							if ( this.options.clearFeedback )
							{
								$feedback.empty();
								$feedback.set( 'html', rtn.html );
							}
							
							var fbSize = $feedback.setStyle( 'overflow', 'hidden' ).getDimensions();
							
							$feedback.setStyles({ height: formSize.y });
							
							$feedback.show().setOpacity(0);
							
							$feedback.get( 'tween' )
								.setOptions({
									duration: 500,
									transition: 'quad:in:out'
								})
								.start( 'height', fbSize.y )
								.chain( (function() {
									
									$feedback.get( 'tween' )
										.start( 'opacity', 1 );
									
									this.formCompleted();
									
								}.bind( this )));
						
						}.bind( this )));
				}
				else
				{
					$el.hide();
					
					if ( this.options.clearFeedback )
					{
						$feedback.empty();
						$feedback.set( 'html', rtn.html );
					}
					
					$feedback.show();
					
					this.formCompleted();
				}
			}
			else
				this.formCompleted();
			
			// Clean up any hidden fields (if we submitted with an iframe)
			if ( this.options.submitMethod == 'iframe' )
			{
				if ( this.hidden && this.hidden.getLength() )
				{
					this.hidden.each((function( f, k ) {
					
						f.clone.destroy();
						
						f.original.setProperty( 'name', k );
					
					}.bind( this )));
				}
			}
			
			// Refresh page
			if ( this.options.refreshPage )
				top.location.reload();
			
			// On Success function
			if ( this.options.onSuccess )
				Aurora.doCallback( this.options.onSuccess, { html: rtn, data: this.data } );
		}
		else
		{
			switch( this.options.target.toLowerCase() )
			{
				case 'signin':
				
					// Usually always going to be the same (for now, maybe unverified later)
					alert( 'Invalid username or password, please try again.' );
					
					this.resetForm();
				
				break;
				
				default:
				
					// $log( 'Form submission failed.' );
					
					if ( $message )
						$message.show().set( 'html', 'The form could not be submitted. ' + rtn.message );
					
					if ( this.options.onFail )
						Aurora.doCallback( this.options.onFail, rtn );
					
				break;
			}
			
		}
	},
	
	formCompleted: function()
	{
		$feedback = this.options.feedbackEl;
	
		this.resetForm();
		
		// Init Aurora Classes
		Aurora.initClasses( $feedback );
		
		// Scroll to feedback
		if ( this.options.scrollToFeedback )
		{
			if ( $feedback )
				new Fx.Scroll( document.body ).toElement( $feedback );
		}
		
		if ( this.options.hideMenu )
			this.hideMenu();
			
		if ( this.options.hidePopclosePopupup )
			this.closePopup();
	
	},
	
	resetForm: function()
	{
		var $el = this.$el,
			$activity = this.$activity,
			$message = this.$message,
			$submit = this.$submit,
			$feedback = this.options.feedbackEl;
		
		// Enable submit button (if its a button)
		this.$submit.each( (function( $s ) {
		
			if ( $s.retrieve( 'button' ) )
				$s.retrieve( 'button' ).enable();
		
		}.bind( this )));
		
		// Remove class from form
		$el.removeClass( 'aurora-form-processing' );
		
		// Hide activity
		if ( this.$activity )
		{
			this.$activity.each( function( $a ) {
				$a.hide();
			});
		}
		
		// Reset fields
		if ( this.options.resetFields )
			this.resetFields();
	},
	
	resetFields: function()
	{
		this.fields.each( function(f) {
			f.resetField();
		});
	},
	
	hideMenu: function()
	{
		var $curEl = this.$el,
			button = $( this.options.hideMenu ).retrieve( 'button' );
		
		(function() {
			
			var $menu = button.$target;
			
			$menu.get( 'tween' )
				.setOptions({
					duration: 150,
					transition: 'linear'
				})
				.start( 'opacity', 0 )
				.chain( (function() {
				
					$menu.setOpacity(1).hide();
				
				}.bind( this )));
			
			button._menuIsOpen = false;
		
		}).delay( 1500 );
		
	},
	
	closePopup: function()
	{
		(function() {
			
			var parents = this.$el.getParents();
			
			parents.each( function(p) {
			
				if ( p.hasClass( 'aurora-popup' ) )
					p.retrieve( 'popup' ).close();
			
			});
		
		}.bind( this )).delay( 2500 );
		
	},
	
	splitMemberName: function( name )
	{
		var rtn = { firstName: '', lastName: '', salutation: '' };
		
		var a = name.split(' ');
		
		for ( var i = 0; i < a.length; i++ )
		{
			if ( !/[a-z]/.test( a[i] ) ) a[i] = a[i].toLowerCase();
			a[i] = a[i].capitalize( true );
		}
		
		if ( a.length == 1 )
		{
			rtn.firstName = a[0];
		}
		else
		{
			var sal = a[0].toLowerCase();
			
			if ( /^(mr|mrs|miss|ms|sir|madam|dr)$/.test( sal ) )
				rtn.salutation = a.shift();
			
			rtn.lastName = a.pop();
			rtn.firstName = a.join(' ');
		}
		
		return rtn;
	},
	
	toElement: function()
	{
		return this.$el;
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			default:
				this.store[ what ] = value;
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			case 'fields':
				return this.fields;
			
			default:
				return this.store[ what ];
		}
	}
	
});

Aurora.Form.Field = new Class({
	
	Implements: [ Events, Options ],
	
	options: {
	
		form: null,
		
		type: null, // file, dob
		
		key: null,
		
		required: false,
		
		validate: false,
		
		fieldOnly: false, // internal use only
		
		showHintOnFocus: false,
		showHint: false,
		
		hideHint: false,
		
		submitForm: false,
		
		section: null,
		
		matchField: null,
		
		minLength: 1,
		
		hint: '',
		
		fileType: null,
		fileTypes: null,
		
		minDate: false,
		maxDate: false,
		
		validMessage: '',
		invalidMessage: ''
		
	},
	
	initialize: function( field, form, options )
	{
		this.setOptions( options );
		
		this.$el = field;
		
		field.store( 'field', this );
		
		// Define field(s)
		this.$field = null;
		this.$fields = [];
		
		if ( this.options.fieldOnly )
		{
			this.$field = this.$el;
		}
		else
		{
			if ( !options.fields )
				return;
			
			// If the field has multiple elements, it means we have more than one field
			if ( options.fields.length > 1 )
				this.$fields = options.fields;
				
			// Set the field to the first element in the array (so if the array was only equal to 1, handle it like a single field, otherwise
			// if there are several, we use the first field to detect the type of input and other information
			this.$field = options.fields[0];
		}
		
		
		// Hint
		this.$hint = this.$el.getElement( '.aurora-field-feedback' );
		
		
		// Form
		this.form =	form;
		this.$form = this.form.toElement();
		
		
		// Tag
		this.tag = this.$field.get( 'tag' );
		this.type = this.$field.getProperty( 'type' );
		
		
		// Name
		this.name = this.$field.getProperty( 'name' );
		
		if ( !this.name )
			$log( 'WARNING! Name not detected for field, form will fail.', this.field );
		
		
		// Type
		if ( !this.options.type && this.type == 'file' )
			this.options.type = 'file';
			
		switch( this.options.type )
		{
			case 'file':
			
				this.$selection = this.$el.getElement( '.aurora-file-selection' );
				
				if ( this.$selection )
					this.$selection.addClass( 'aurora-file-selection-none' );
				
				this.$browse = this.$el.getElement( '.aurora-file-browse' );
			
			break;
			
			case 'date':
				
				new Aurora.Form.Field.Date( this );
				
				// Enforce date validation
				this.options.validate = 'date';
				
			break;
			
			case 'dateSelects':
				
				var hasSelects = this.$el.getElement( 'select' );
				
				if ( !hasSelects )
				{
					var $input = this.$el.getElement( 'input' );
					
					if ( !$input ) {
						$log( 'Date selects require a standard input field.' );
						return;
					}
					
					$input.hide();
					
					var $selects = new Element( 'div', { 'class': 'aurora-date-selects' } );
					
					// Day
					var $day = this.$day = new Element( 'select', { 'class': 'aurora-date-day' } ).inject( $selects );
					
					new Element( 'option' ).inject( $day );
					
					for ( var d = 1; d <= 31; d++ )
						new Element( 'option', { html: d, value: d } ).inject( $day );
					
					// Month
					var $month = this.$month = new Element( 'select', { 'class': 'aurora-date-month' } ).inject( $selects );
					
					var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
					
					new Element( 'option' ).inject( $month );
					
					months.each( function( m, i ) {
						new Element( 'option', { html: m, value: i } ).inject( $month );
					});
					
					// Year
					var $year = this.$year = new Element( 'select', { 'class': 'aurora-date-year' } ).inject( $selects );
					
					new Element( 'option' ).inject( $year );
					
					for ( var y = 1900; y <= 2020; y++ )
						new Element( 'option', { html: y, value: y } ).inject( $year );
						
					$selects.inject( $input, 'after' );
				}
				else
				{
					var $day = this.$day = this.$el.getElement( '.aurora-date-day' );
					var $month = this.$month = this.$el.getElement( '.aurora-date-month' );
					var $year = this.$year = this.$el.getElement( '.aurora-date-year' );
				}
				
				this.$fields.push( $day, $month, $year );
				
				// Enforce dateSelects validation
				this.options.validate = 'dateSelects';
				
				// Overide the type in select fields case as we replace hide the original input field
				this.type = 'select-one';
			
			break;
		}
		
		
		// Min/Max Dates
		if ( this.options.minDate )
			this.minDate = new Date().parse( this.options.minDate );
			
		if ( this.options.minDate )
			this.maxDate = new Date().parse( this.options.maxDate );
		
		
		// Misc
		if ( ( this.$hint && this.form.options.hideHints && !this.options.showHint ) || this.options.hideHint )
			this.$hint.hide();
		
		
		if ( this.$hint && this.options.hint )
			this.$hint.set( 'html', this.options.hint );
		
		if ( this.options.required && !this.options.validate )
			this.options.validate = 'length';
		
		if ( !this.options.required && !this.options.validate )
			this.valid = 'yes';
		else
			this.valid = 'no';
		
		
		// Add flag if its an action
		this.customAction = false;
		
		if ( !/^(length|password|match|numeric|date|dob|email|dateSelects)$/.test( this.options.validate ) )
			this.customAction = true;
		
		
		// Init Events
		this.initEvents();
	},
	
	initEvents: function()
	{
		var form = this.form;
	
		// Generic events
		this.$field.addEvents({
		
			'keypress': (function( event ) {
				
				if ( this.tag != 'textarea' && event.key == 'enter' )
				{
					event.stop();
					
					if ( form.options.keyboardShortcuts )
					{
						(function() {
							form.validateForm();
						}).delay( 100 );
					}
				}
			
			}.bind( this ))
		
		});
		
		// Validation Mode
		var mode = form.options.validationMode;
		
		
		// Force length checking for file fields
		if ( this.options.type == 'file' )
			this.options.validate = 'length';
		
		
		// Add validation
		if ( this.options.validate )
		{
			switch( mode )
			{
				case 'instant':
				
					// Execute validation
					var doValidation = (function( event, blur ) {
					
						var delay = ( blur ? 0 : 500 );
						
						if ( /^(length|password|match|numeric|email|dob|phone)$/i.test( this.options.validate ) )
							delay = 250;
						
						if ( this.tag == 'select' )
							delay = 0;
							
						if ( /^(radio|checkbox|file)$/i.test( this.type ) )
							delay = 0;
						
						// If its not required and we don't have a value, we don't need a delay
						if ( !this.options.required && !this.get( 'value' ) )
							delay = 0;
						
						if ( !blur && event.key == 'tab' )
							return;
						
						$clear( this.executeValidation );
						
						this.executeValidation = (function() {
						
							this.validate();
							
						}.bind( this )).delay( delay );
					
					}.bind( this ));
					
					
					// Add events to execute validation
					var addEvents = (function( $field ) {
					
						$field.addEvents({
						
							'keyup': (function(e) { doValidation(e) }),
							
							'blur': (function(e) {
							
								if ( this.valid != 'processing' ) // don't do anything if 'processing'
									doValidation( e, true );
								
								// TW: Future enhancement - if ( this.valid == 'no' ) // don't do anything if its 'yes' or 'processing'
							
							}.bind( this ))
						
						});
						
						switch( this.type )
						{
							case 'file': // if ( this.options.type == 'file' )
							
								$field.addEvents({
								
									'change': (function(e) { doValidation(e) }.bind( this )),
									
									'click': (function(e) { doValidation(e) }.bind( this ))
								
								});
							
							break;
							
							case 'checkbox':
							case 'radio':
							case 'select-one': // $field.get( 'tag' ) == 'select';
								
								$field.addEvents({
								
									'click': (function(e) {
									
										// Special case: If its a select field and we haven't registered a click yet, don't do anything (otherwise
										// when the user clicks it the first time, it will display the valid message, as the selectfield might have
										// a predefined value set...)
										if ( this.type == 'select-one' && !this._clicked )
										{
											this._clicked = true;
											return;
										}
										
										doValidation(e);
									
									}.bind( this ))
								
								});
								
							break;
						}
					}.bind( this ));
					
					
					// Add events to each field inside the group or add it to the single field
					if ( this.$fields.length )
					{
						this.$fields.each( function(f) {
							addEvents(f);
						});
					}
					else
					{
						addEvents( this.$field );
					}
					
				break;
			}
		}
		
		// Show Hint on Focus
		if ( this.options.showHintOnFocus )
		{
			this.$field.addEvents({
			
				'focus': (function() {
					
					if ( this.$hint )
						this.$hint.show();
				
				}.bind( this ))
			
			});
		}
	},
	
	clearStatus: function()
	{
		// Remove current status
		this.valid = 'no';
		
		this.$field
			.removeClass( 'aurora-field-valid' )
			.removeClass( 'aurora-field-invalid' );
			
		this.$el
			.removeClass( 'aurora-field-container-valid' )
			.removeClass( 'aurora-field-container-invalid' );
		
		if ( this.$hint )
			this.$hint
				.removeClass( 'aurora-field-feedback-validating' )
				.removeClass( 'aurora-field-feedback-valid' )
				.removeClass( 'aurora-field-feedback-invalid' );
	
	},
	
	isValid: function( msg, hide )
	{
		// $log( '--> Field [' + this.name + '] is valid.' );
		
		var form = this.form;
		
		// Valid (ability to provide a custom msg (from an action) as well as hide the
		// hint if need be (field isn't required yet field is valid if nothing is entered))
		
		this.clearStatus();
		
		this.$field.addClass( 'aurora-field-valid' ); // Should we do this if field is not required yet field is valid? (already hiding hint)
		this.$el.addClass( 'aurora-field-container-valid' );
		
		// Determine message
		var validMessage = this.validMessage = msg;
		
		// Manage hint
		if ( this.$hint )
		{
			switch( this.options.type )
			{
				case 'date':
				
					if ( this.options.dateType != 'tooltip' )
						this.$hint.show().addClass( 'aurora-field-feedback-valid' );
				
				break;
				
				default:
				
					if ( hide || form.options.hideValidHints )
					{
						this.$hint.hide();
					}
					else
					{
						if ( !validMessage )
							validMessage = this.validMessage = this.options.validMessage || form.options.validFieldMessage;
						
						this.$hint.show().addClass( 'aurora-field-feedback-valid' ).empty();
						this.$hint.set( 'html', validMessage );
					}
			}
		}
		
		// Custom procesing
		switch( this.options.type )
		{
			case 'file':
			
				if ( this.options.validBrowseText )
					if ( this.$browse )
						this.$browse.set( 'text', this.options.validBrowseText );
				
				if ( this.$selection )
				{
					var value = this.get( 'value' );
					
					if ( !value )
						return;
					
					this.$selection
						.set( 'html', value )
						.removeClass( 'aurora-file-selection-none' )
						.addClass( 'aurora-file-selection-selected' )
						.show();
				}
			
			break;
		}
		
		this.valid = 'yes';
		
		// Submit form (for file fields)
		if ( this.options.submitForm )
		{
			// Don't submit twice (the change event is sometimes fired twice)
			if ( this._file == this.get( 'value' ) )
			{
				$log( 'Change event fired but the same file was selected.' );
				return;
			}
			
			this._file = this.get( 'value' );
			
			form.submitForm();
		}
		
		return 'yes';
	},
	
	isInvalid: function( msg )
	{
		// $log( '--> Field [' + this.name + '] is invalid: ' + msg );
		
		var form = this.form;
		
		this.clearStatus();
		
		this.$field.addClass( 'aurora-field-invalid' );
		this.$el.addClass( 'aurora-field-container-invalid' );
		
		// Determine message
		var invalidMessage = this.invalidMessage = msg;
		
		// Manage hint
		if ( this.$hint )
		{
			switch( this.options.type )
			{
				case 'date':
				
					if ( this.options.dateType != 'tooltip' )
						this.$hint.show().addClass( 'aurora-field-feedback-invalid' );
				
				break;
				
				default:
					
					if ( !invalidMessage )
						invalidMessage = this.invalidMessage = this.options.invalidMessage || form.options.invalidFieldMessage;
					
					this.$hint.show().addClass( 'aurora-field-feedback-invalid' ).empty();
					this.$hint.set( 'html', invalidMessage );
			}
		}
		
		// Custom procesing
		switch( this.type )
		{
			case 'file':
			
				if ( this.$selection )
					this.$selection
						.set( 'html', '(none selected)' )
						.removeClass( 'aurora-file-selection-selected' )
						.addClass( 'aurora-file-selection-none' );
			
			break;
		}
		
		this.valid = 'no';
		
		// Add to validation errors array
		form._validationErrors.push( this );
		
		return 'no';
	},
	
	validate: function()
	{
		var form = this.form,
			name = this.name,
			value = this.get( 'value' );
		
		var fields = this.$form.retrieve( 'form' ).get( 'fields' ); // TW: Unable to get directly from form...
		
		// Perform specific validation
		var check = (function( type ) {
		
			// If a field is not required and has no value, its theoretically valid
			if ( !this.options.required && !value.length )
			{
				this.isValid( false, true );
				return;
			}
			
			// If the field is in a section, and the section isn't visible, the field is theoretically valid
			if ( this.section )
			{
				var section = form.sections[ this.section ];
				
				if ( !section )
				{
					$log( 'A section was marked inside a field but could not be found.' )
					return;
				}
				
				if ( !section.$el.isVisible() )
				{
					// $log( 'A field inside a section may have been required but it was on a section that was not visible, skipping validation.' );
					this.isValid( false, true );
					return;
				}
			}
			
			switch( type )
			{
				// Length (plus support for min length)
				case 'length':
				
					switch( this.type )
					{
						case 'file':
							
							if ( !value.length || value.length < this.options.minLength )
								this.isInvalid();
							
							var fileTypes = [];
							
							if ( this.options.fileType )
							{
								switch( this.options.fileType )
								{
									case 'image': fileTypes = [ 'jpg', 'jpeg', 'gif', 'png' ]; break;
									case 'document': fileTypes = [ 'doc', 'docx', 'pdf', 'pages' ]; break;
									case 'audio': fileTypes = [ 'wav', 'mp3', 'aac' ]; break;
								}
							}
							else if ( this.options.fileTypes )
							{
								fileTypes = this.options.fileTypes.split(',');
							}
							
							if ( !fileTypes.length )
							{
								this.isValid();
								return;
							}
							
							var ext = value.split('.').getLast().toLowerCase();
							
							if ( fileTypes.contains( ext ) )
								this.isValid();
							else
							{
								// alert( 'This file is not supported. Please choose a valid file.' )
								this.isInvalid();
							}
						
						break;
						
						case 'checkbox':
							
							if ( value )
								this.isValid();
							else
								this.isInvalid();
						
						break;
						
						case 'checkboxes':
							
							// Multiple fields (handle the object returned)
							if ( !$H( value ).getLength() )
								this.isInvalid();
							else
								this.isValid();
						
						break;
						
						default:
						
							// Single field
							if ( value.length < this.options.minLength )
								this.isInvalid();
							else
								this.isValid();
					}
					
				break;
				
				// Password (same as length, keeping it seperate for now...)
				case 'password':
				
					if ( value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
					
				break;
				
				// Match
				case 'match':
				
					var matchField = fields[ this.options.matchField ];
					
					if ( !matchField ) {
						$log( 'No matching field could be found, check that you have specified the correct field first.' );
						return;
					}
					
					var matchValue = matchField.get( 'value' );
					
					if ( !value || value != matchValue )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Numeric (plus support for min length)
				case 'numeric':
				
					value = value.toString().replace( / /g, '' );
					
					if ( !/^ *[0-9]+ *$/.test( value ) || value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Phone (numeric plus support for (, ), -, + and min length)
				case 'phone':
				
					value = value.toString().replace( / /g, '' ).replace( /\(/g, '' ).replace( /\)/g, '' ).replace( /\+/g, '' ).replace( /\-/g, '' );
					
					if ( !/^ *[0-9]+ *$/.test( value ) || value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Date
				case 'date':
				
					var valueTime = value.parsed.get( 'time' );
					
					// Check min date
					if ( this.options.minDate )
					{
						minDateTime = this.minDate.get( 'time' );
					
						if ( valueTime < minDateTime )
						{
							this.isInvalid();
							return;
						}
					}
					
					// Check max date
					if ( this.options.maxDate )
					{
						maxDateTime = this.maxDate.get( 'time' );
					
						if ( valueTime > maxDateTime )
						{
							this.isInvalid();
							return;
						}
					}
					
					if ( value.cf )
						this.isValid();
					else
						this.isInvalid();
				
				break;
				
				case 'dateSelects':
				
					// Validate only if we have all 3 values or are now validating the form before submit
					if ( this.form._status != 'validating' )
						if ( !value.day || !value.month || !value.year )
							return;
					
					if ( value.cf )
						this.isValid();
					else
						this.isInvalid();
				
				break;
				
				// DOB (backwards compatibility, no longer used)
				case 'dob':
				
					// Caters for slash, hyphen and period, must be valid date, takes leap years into account)
					if ( !/^((((0?[1-9]|[12]\d|3[01])[\.\-\/](0?[13578]|1[02])[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|((0?[1-9]|[12]\d|30)[\.\-\/](0?[13456789]|1[012])[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|((0?[1-9]|1\d|2[0-8])[\.\-\/]0?2[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|(29[\.\-\/]0?2[\.\-\/]((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00)|00)))|(((0[1-9]|[12]\d|3[01])(0[13578]|1[02])((1[6-9]|[2-9]\d)?\d{2}))|((0[1-9]|[12]\d|30)(0[13456789]|1[012])((1[6-9]|[2-9]\d)?\d{2}))|((0[1-9]|1\d|2[0-8])02((1[6-9]|[2-9]\d)?\d{2}))|(2902((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00)|00))))$/.test( value.stored ) )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Email
				case 'email':
				
					if ( !/^[\w\-]+(\.[\w\-]+)*@[\w\-]+\.([\w\-]+\.)*[a-z]{2,}$/i.test( value ) )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Test for Sparkle action (plus support for min length)
				default:
					
					// $log( 'Calling sparkle action...' );
					
					if ( !value.length || value.length < this.options.minLength )
					{
						this.isInvalid();
						return;
					}
					
					form._preSubmitQueue.push( this.name );
					
					this.valid = 'processing';
					
					this.clearStatus();
					
					if ( this.$hint )
					{
						this.$hint.show().addClass( 'aurora-field-feedback-validating' );
						this.$hint.set( 'html', this.options.validatingMessage || form.options.validatingFieldMessage );
					}
					
					var process = (function( rtn ) {
					
						if ( !rtn.success )
							$log( 'Sparkle validation failed:', rtn );
							
						var msg = rtn.html.clean();
							fail = ( msg.slice( 0, 4 ) == 'fail' ),
							success = ( msg.slice( 0, 7 ) == 'success' );
						
						if ( fail )
						{
							msg = msg.slice( 6 );
							
							this.isInvalid( msg );
						}
						else if ( success )
						{
							msg = msg.slice( 9 );
							
							this.isValid( msg );
						}
						else
						{
							$log( 'Sparkle validation return could not be parsed for success/fail.', rtn );
						}
						
						// Remove action from queue and check if we have any other actions
						form.checkSubmitQueue( name );
						
					}.bind( this ));
					
					// Determine path
					var validatePath = this.options.validate,
						validateData = {};
					
					if ( $type( this.options.validate ) == 'object' )
					{
						validatePath = this.options.validate.path;
						validateData = this.options.validate.data;
					}
					
					// Call action
					var send = {
						path: validatePath,
						data: validateData
					};
					
					var name = this.get( 'name' );
					
					send.data[ name ] = value;
					
					Aurora.loadSparkle( send )
						.then( function( rtn ) {
						
							// Illusion... (the question is, should there be one?)
							(function() {
								process( rtn )
							}).delay( 500 );
						
						}.bind( this ));
				
			}
		
		}.bind( this ));
		
		
		// Check for a basic value if the field is required, has no value and is a simple field type
		if ( this.options.required && !value && /^(length|password|match|numeric|date|dob|email)$/.test( this.options.type ) )
		{
			this.isInvalid();
		}
		else
		{
			// Validation types
			var types = [];
			
			switch( $type( this.options.validate ) )
			{
				case 'string':
					types = $A( this.options.validate.split(':') );
				break;
				
				case 'object':
					types = [ this.options.validate ];
				break;
			}
			
			// validated = false;
			types.each( function( type ) {
			
				check( type );
			
				// if ( validated && !check( type ) )
					// validated = false;
			
			});
		}
	},
	
	toElement: function()
	{
		return this.$field;
	},
	
	getValue: function()
	{
		// Get field value
		var $form = this.$form,
			key = this.get( 'name' ),
			field = $form[ key ];
		
		// If its a collection of fields (like a radio, just use the first one, we don't need references to the others)
		if ( $type( field ) == 'collection' || $type( field ) == 'object' )
			field = field[0];
		
		// Gather some information
		if ( field )
		{
			var value = field.value,
				tag = field.get( 'tag' ),
				type = field.getProperty( 'type' );
		}
		
		// If we have a type, bypass usual field detection, as we know what field to expect
		if ( this.options.type )
		{
			switch ( this.options.type )
			{
				case 'date':
				
					if ( !value )
						return false;
					
					// Try inserting / instead of - so it recognises it as a standard date
					value = value.toString().replace( /-/g, '/' );
					
					var date = new Date.parse( value );
					
					if ( !date.isValid() )
					{
						$log( 'Date could not be parsed.' );
						return false;
					}
					
					var dateObj = {
						stored: value,
						parsed: date,
						cf: date.format( '%Y-%m-%d' )
					}
					
					return dateObj;
				
				break;
				
				case 'dateSelects':
				
					var day = this.$day.value,
						month = this.$month.value,
						year = this.$year.value;
					
					if ( !day || !month || !year )
						return false;
					
					var value = day + '/' + month + '/' + year,
						date = new Date.parse( value );
					
					if ( !date.isValid() )
						return false;
					
					var dateObj = {
						day: day,
						month: month,
						year: year,
						stored: value,
						parsed: date,
						cf: date.format( '%Y-%m-%d' )
					}
					
					return dateObj;
					
				break;
			}
		}
		
		switch( tag )
		{
			case 'input':
			
				switch( type )
				{
					case 'text':
					case 'password':
					case 'file':
					
						switch( this.options.validate )
						{
							case 'date':
							case 'dob':
							
								if ( !value )
									return false;
								
								// Try inserting / instead of - so it recognises it as a standard date
								value = value.toString().replace( /-/g, '/' );
								
								var date = new Date.parse( value );
								
								if ( !date.isValid() )
								{
									$log( 'Date could not be parsed.' );
									return false;
								}
								
								var dateObj = {
									stored: value,
									parsed: date,
									cf: date.format( '%Y-%m-%d' )
								};
								
								return dateObj;
							
							break;
							
							default:
								return value;
						}
					
					break;
					
					case 'checkbox':
					
						// Single
						if ( !this.$fields.length )
						{
							return field.get( 'checked' );
						}
						
						// Multiple
						var values = {};
						
						// If we have a key, we need to send it all together as an array
						if ( this.options.key )
							values = [];
						
						for ( var i = 0; i <= this.$fields.length - 1; i++ ) {
						
							var $field = this.$fields[ i ];
							
							var checked = $field.checked;
							
							var name = this.options.key || $field.getProperty( 'name' );
							
							// If the checkbox has been checked, send it
							if ( checked )
							{
								switch( $type( values ) )
								{
									case 'object':
										values[ name ] = true;
									break;
									
									case 'array':
									
										var fieldValue = $field.get( 'value' ).toInt();
										
										if ( $type( fieldValue ) != 'number' || fieldValue == 'NaN' )
											$log( 'A checkbox was checked but the value was not a number, therefore it was not sent, the value was: ' + fieldValue );
										else
											values.push( fieldValue );
									
									break;
								}
								
							}
						
						};
						
						return values;
					
					break;
					
					case 'radio':
						
						var value = false;
						
						for ( var i = 0; i <= $form[ key ].length - 1; i++ ) {
							
							var checked = $form[ key ][ i ].checked;
							
							if ( checked )
								value = $form[ key ][ i ].value;
							
							// Check to see if its a stringed boolean
							if ( $type( value ) == 'string' )
							{
								if ( value == 'true' || value == 'on' )
									value = true;
								else if ( value == 'false' || value == 'off' )
									value = false;
							}
						};
						
						return value;
						
					break;
					
					default:
					
						return value;
				}
			
			case 'textarea':
			
				// TW: Build in proper WYSIWYG support in the future
				if ( this.$field.hasClass( 'aurora-wysiwyg' ) )
				{
					var id = this.get( 'id' );
					
					var wysiwygEditor = tinyMCE.get( id );
					
					if ( wysiwygEditor )
						return wysiwygEditor.getContent();
					else
						return false;
				}
				else
					return value;
			
			break;
			
			case 'select':
			
				return value;
			
			break;
		}
	
	},
	
	resetField: function()
	{
		var $form = this.$form;
		
		var $field = this.$field,
			$fields = this.$fields;
		
		var name = this.get( 'name' );
		
		var tag = this.tag,
			type = this.type;
		
		// Don't reset hidden fields
		if ( type == 'hidden' )
			return;
		
		var reset = (function( field ) {
			
			switch( tag )
			{
				case 'input':
				
					switch( type )
					{
						case 'text':
						case 'password':
							field.value = '';
						break;
						
						case 'checkbox':
							field.checked = false;
						break;
						
						case 'radio':
								
							var key = this.get( 'name' );
								
							for ( var i = 0; i <= $form[ name ].length - 1; i++ ) {
								$form[ name ][ i ].checked = false;
							};
							
						break;
						
						default:
							field.value = '';
					}
				
				case 'textarea':
					field.value = '';
				break;
				
				case 'select':
					field.selectedIndex = 0;
				break;
			}
		
		}.bind( this ));
		
		// Reset values (still need to handle selects and radios)
		if ( $fields.length )
		{
			$fields.each( function(f) {
				reset(f);
			});
		}
		else
		{
			reset( $field );
		}
		
		// Remove classes
		this.clearStatus();
		
		// Empty and hide the hint element
		if ( this.$hint )
			this.$hint.empty().hide();
			
		// Remove status
		this._file = null;
		this._clicked = null;
	
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			case 'value':
			
				if ( this.value == value )
					return;
					
				this.value = value;
				
			break;
			
			default:
				this.store[ what ] = value;
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			case 'value':
				return this.getValue();
			
			case 'id':
				return this.$field.getProperty( 'id' ) || false;
			
			case 'name':
				return this.$field.getProperty( 'name' );
			
			default:
				return this.store[ what ];
		}
	}
	
});

Aurora.Form.Field.Date = new Class({
	
	Extends: Aurora.Form.Field,
	
	options: {
	
		dateHint: 'eg. today, last monday',
		
		fillField: true,
		
		tooltip: false
		
	},
	
	initialize: function( field, options )
	{
		this.setOptions( options );
		
		this.field = field;
		this.$field = field.$field;
		
		this.form = field.form;
		
		this.$el = field.$hint;
		
		if ( field.options.dateType == 'tooltip' )
		{
			this.options.tooltip = true;
			this.$el = field.$el.getElement( '.aurora-date-hint' ).hide();
		}
		
		this.$el.set( 'html', this.options.dateHint );
		
		this.$field.addEvents({
		
			'keyup': (function() {
			
				var showDateHint = (function() {
				
					$clear( this._hintUpdate );
					
					this._hintUpdate = function() { this.parse() }.delay( 200, this );
				
				}.bind( this ));
				
				showDateHint();
				
			}.bind( this )),
			
			'focus': (function() {
			
				if ( this.options.tooltip )
				{
					this.$el.show().set( 'opacity', 0 );
					
					this.$el.get( 'tween' )
						.setOptions({
							duration: 150,
							transition: 'linear'
						})
						.start( 'opacity', 1 );
						
					if ( this.options.tooltip )
					{
						this.$el.position({ relativeTo: field.$field, position: 'bottomLeft', align: 'topLeft', offset: { x: 0, y: 0 } });
					}
				}
			
			}.bind( this )),
			
			'blur': (function() {
			
				if ( this.options.tooltip )
				{
					this.$el.get( 'tween' )
						.setOptions({
							duration: 150,
							transition: 'linear'
						})
						.start( 'opacity', 0 )
						.chain( (function() {
						
							if ( this.options.fillField && this.parsedDate )
								this.field.$field.value = this.parsedDate.format( '%d/%m/%Y' );
							
							this.$el.hide();
						
						}.bind( this )));
				}
			
			}.bind( this ))
		
		});
		
	},
	
	parse: function()
	{
		var value = this.field.get( 'value' ).stored;
		
		if ( !value )
		{
			this.$el.set( 'html', this.options.dateHint );
			return;
		}
			
		var parsedDate = this.parsedDate = new Date.parse( value );
		
		if ( parsedDate.isValid() )
			this.$el.set( 'html', parsedDate.format( '%A %d%o %B %Y' ) );
		else
			this.$el.set( 'html', this.options.dateHint );
	
	}
	
});