// create namespace container object
var XmlForm = {};

/**
 * Form class
 */
XmlForm.Form = function(xml) {
	var self = this;
	self._interpreter = null;
	self._definitions = null;
	self._listeners = null;
	self._modifyListeners = null;
	self._instanceElements = null;

	/**
	 * Constructor
	 * @param Document xml		XML form description
	 */
	self.init = function(xml) {
		self._xml = xml;
		self._definitions = [];
		self._listeners = [];
		self._modifyListeners = [];
		self._interpreter = new XmlForm.Interpreter(self);
		self._instanceElements = {};

		// build an array of HTML elements which have an ID containing '_inst'
		// group these by their base (the bit bofore the prefix)
		// We use this later to allow a Definition to retrieve all its HTML elements
		$('*[@id]').each(function(){
			var idParts = this.id.split('_inst');
			if (idParts.length > 1) {
				if (!self._instanceElements[idParts[0]]) {
					self._instanceElements[idParts[0]] = [];
				}
				self._instanceElements[idParts[0]][idParts[1]] = this;
			}
		});

		// Create definition objects for Form Element nodes
		$('> form > *', xml).each(function(){
			self._definitions.push(self.definitionFactory(this));
		});

		//console.log('init');
		self.notifyChange();

		$('#eventName').blur(function(){
			self.validateName();
		});
		$('.questionForm').submit(function(event){
			if (!self.validateAll()) {
				event.preventDefault();
				event.stopImmediatePropagation();
				
				alert($.i18n._('There are problems on the form. Please correct them before proceeding'));
				var target = $('div.error');
				if (target.length) {
					var targetOffset = target.offset().top - 25;
					$('html,body').animate({scrollTop: targetOffset}, 500);
				}
			}
		});
	}

	/**
	 * gets HTML elements for all instances of a particular Form Element ID
	 * @param 	String id	Form Element ID
	 * @return	Array		Instance objects for this Form Element
	 */
	self.getInstanceElements = function(id) {
		return this._instanceElements[id];
	}

	/**
	 * gets instance of correct definition class for a node
	 * @param 	Node node		Form Element Node from Form Description XML
	 * @return 	Definition		Definition object
	 */
	self.definitionFactory = function(node) {
		switch (node.tagName) {
			case 'tab':
				return new XmlForm.TabDefinition(node, this);
			case 'group':
				return new XmlForm.GroupDefinition(node, this);
			case 'question':
				return new XmlForm.QuestionDefinition(node, this);
			// Fields all share one definition class
			case 'input':
			case 'bool':
			case 'singleSelection':
			case 'multipleSelection':
				return new XmlForm.FieldDefinition(node, this);
		}
	}

	/**
	 * gets instance of correct instance class for a node
	 * @param 	Node node			Form Element Node from Form Description XML
	 * @param 	Element element		HTML Element for this instance
	 * @param 	Definition definition	Definition object for this Form Element
	 * @param	Int position			Specifies position in instances array if we're inserting rather than appending
	 * @param,	mixed primary		An optional reference to a Primary Instance to save us looking it up. If bool 'true' then this Instance is primary.
	 * @return 	Instance			Instacne object
	 */
	self.instanceFactory = function(node, element, definition, position, primary) {
		switch (node.tagName) {
			case 'tab':
				return new XmlForm.TabInstance(element, definition, position, primary);
			case 'group':
				return new XmlForm.GroupInstance(element, definition, position, primary);
			case 'question':
				return new XmlForm.QuestionInstance(element, definition, position, primary);
			case 'input':
				return new XmlForm.InputTextInstance(element, definition, position, primary);
			case 'bool':
				// different display types have their own Instance class - do another switch on this
				switch ($(node).attr('type')) {
					case 'checkbox' :
						return new XmlForm.BoolCheckboxInstance(element, definition, position, primary);
					case 'radios' :
						return new XmlForm.BoolRadiosInstance(element, definition, position, primary);
				}
			case 'singleSelection':
			 	switch ($(node).attr('type')) {
			 		case 'dropdown':
			 			return new XmlForm.SingleSelectionDropdownInstance(element, definition, position, primary);
			 		case 'radioList':
			 			return new XmlForm.SingleSelectionRadioListInstance(element, definition, position, primary);
			 	}
			case 'multipleSelection':
				switch ($(node).attr('type')) {
					case 'checkboxList' :
						return new XmlForm.MultipleSelectionCheckboxListInstance(element, definition, position, primary);
					case 'list' :
						return new XmlForm.MultipleSelectionListInstance(element, definition, position, primary);
				}
		}
	}

	/**
	 * gets a specified Instance object for a particular ID
	 * @param 	String id			Form Element ID
	 * @param 	Int instanceNumber	Number of instance to retrieve
	 * @return	Instance			Instance object
	 */
	self.getInstance = function(id, instanceNumber) {
		// defaults to first instance if not specified
		if (!instanceNumber) {
			instanceNumber = 0;
		}
		// assemble ID of HTML element
		var instanceId = id + '_inst' + instanceNumber;
		var element = document.getElementById(instanceId);
		if (!element) {
			//console.log(instanceId + ' not found');
		} else {
			// get Instance object reference from HTML element
			return element.instance;
		}
	}

	// get interpreter instance for this form
	self.getInterpreter = function(){
		return self._interpreter;
	}

	// get xml doc for this form
	self.getXml = function() {
		return self._xml;
	}

	// call isValid() on all field objects
	self.validateAll = function(){
		var valid = true;
		$(self._definitions).each(function(){
			$(this._instances).each(function(){
				if(!this.isValid()){
					valid = false;
				}
			});
		});
		if (!this.validateName()) {
			valid = false;
		}
		return valid;
	}

	self.validateName = function() {
		if (!$('#error_eventName').length) {
			return true;
		}
		if (!$('#eventName').val()) {
			$('#error_eventName').html($.i18n._('Please enter an event name'));
			$('#error_eventName').show();
			return false;
		} else {
			$('#error_eventName').html('');
			$('#error_eventName').hide();
			return true;
		}
	}

	/**
	 * Broadcast a 'notifyChange' event to all registered listeners to notify them that something on the form has changed
	 * Listeners are Instance objects which need to update display logic etc. based on the values of other fields
	 * This method must be called by fields when they change in order to update the rest of the form.
	 */
	self.notifyChange = function(){
		setTimeout(function(){
			$(self._listeners).each(function(){
				// call notify method on each listener
				if (this.notifyChange) {
					this.notifyChange();
				}
			});
		}, 100);
	}

	/**
	 * Broadcast a 'notifyModified' event to all registered listeners
	 */
	self.notifyModified = function(){
		setTimeout(function(){
			$(self._modifyListeners).each(function(){
				// call notify method on each listener
				if (this.notifyModified) {
					this.notifyModified();
				}
			});
		}, 150);
	}

	/**
	 * Register a listener object to receive 'notifyChange' when something on the form is changed
	 * @param Object listener	Listener object. Must have a 'notifyChange' method in order to recieve events
	 */
	self.addListener = function(listener){
		self._listeners.push(listener);
	}

	self.removeListener = function(listener){
		$(self._listeners).each(function(i){
			if (this == listener) {
				// remove instance from array
				self._listeners.splice(i, 1);
				return;
			}
		});
	}

	/**
	 * Register a listener object to receive 'notifyModified' when some html on the form has been modified.
	 * (eg repeated fields)
	 * @param Object listener	istener object. Must have a 'notifyModified' method in order to recieve events
	 */
	self.addModifyListener = function(listener){
		self._modifyListeners.push(listener);
	}

	self.removeModifyListener = function(listener){
		$(self._modifyListeners).each(function(i){
			if (this == listener) {
				// remove instance from array
				self._modifyListeners.splice(i, 1);
				return;
			}
		});
	}

	// call constructor
	self.init(xml);
}

/**
 * Base Definition class
 */
XmlForm.Definition = function(node, form){
	this.init(node, form);
};
XmlForm.Definition.prototype = {
	_instances: null,
	_children: null,
	_id: null,
	_node: null,
	_form: null,

	/**
	 * Constructor
	 * @param Node node		Form Element Node from Form Description XML
	 * @param Form			Reference to Form object
	 */
	init: function(node, form){

		this._node = node;
		this._form = form;
		this._instances = [];
		this._children = [];

		var self = this;

		// Get all HTML elements for this Form Element and instantiate Instance objects using factory method on Form
		// We don't store a reference to it - we leave it up to the child to register itself with its definition
		$(self._form.getInstanceElements(this.getId())).each(function(){
			var instance = self._form.instanceFactory(node, this, self);
		});

		// create child definitions
		this._addChildren();
	},

	// Instantiates Definition objects for children of this Form Element on Form Decription XML
	_addChildren: function() {
		var self = this;
		$(this._node.childNodes).each(function(){
			// Try to get instance for each child node using factory method on Form
			var child = self._form.definitionFactory(this);
			// If we have a child object, store a refernce to it in _children array
			if (child) {
			 	self._children.push(child);
			}
		});
	},

	// getters

	getId: function() {
		// get id of node
		if (!this._id) {
			this._id = $(this._node).attr('id');
		}
		return this._id;
	},

	getForm: function() {
		return this._form;
	},

	getNode: function() {
		return this._node;
	},

	// placeholder for callback
	updateDisplay : function() {
	},

	/**
	 * Register an Instance with its Definition (this object)
	 * @param Instance instance		Instance Object
	 * @param Int position			Specifies position in instances array if we're inserting rather than appending
	 *						Defaults to end of array if null
	 */
	addInstance: function(instance, position) {
		if (position != null) {
			// insert into array:
			// we rely on a space (null item)  having been created in advance using this.createInstanceSpace and overwrite it
			this._instances[position] = instance;
			// Tell Instance its position
			instance.setInstanceNumber(position, false);
		} else {
			// append to array
			this._instances.push(instance);
			// Tell Instance its position
			instance.setInstanceNumber(this._instances.length - 1, false);
		}
	},

	/**
	 * Unregister an Instance with its Definition (this object)
	 */
	removeInstance: function(instance) {
		var instances = this._instances;
		var found = false;
		$(instances).each(function(i){

			// First we need to find the Instance in our _instances array
			if (!found) {
				// Check if current item is the one we want to remove
				if (this == instance) {
					// remove instance from array
					instances.splice(i, 1);
					found = true;
				}

			// Once we've found and removed the instance, we need to decrement the InstanceNumbers of any instances that follow it
			// in order to keep them sequential
			} else {
				this.setInstanceNumber(i - 1);
			}
		});
		instance = null;
	},

	/**
	 * Create a Null item in our _instances array and increment the InstanceNumber of any Instances that follow it
	 * This must be done prior to creating a new Instance to insert in this order to avoid ID conflicts in our HTML
	 */
	createInstanceSpace: function(instance) {

		for (var i=instance; i < this._instances.length; i++) {
			if (this._instances[i]) {
				this._instances[i].setInstanceNumber(i + 1, true);
			}
		}
		this._instances.splice(instance, 0, null);
	},

	/**
	 * Get current number of instances for this Definition
	 */
	instanceCount: function() {
		return this._instances.length;
	}
}


/*********************************************************
  Definition implementatiions
*********************************************************/

// tab
XmlForm.TabDefinition = function(node, form){
	this.init(node, form);
};
$.extend(XmlForm.TabDefinition.prototype, XmlForm.Definition.prototype, {});

// group
XmlForm.GroupDefinition = function(node, form){
	this.init(node, form);
};
$.extend(XmlForm.GroupDefinition.prototype, XmlForm.Definition.prototype, {
	// IDs of groups are automatically generated and don't exist in Form Definition XML
	getId: function() {
		var node = this._node;
		var id;
		$('group', this._form.getXml()).each(function(i){
			if (this == node) {
				id = 'group' + (i + 1);
				return;
			}
		});
		return id;
	},

	// Children of Groups are defined in 'head' and 'body' nodes
	_addChildren: function() {
		var self = this;
		$('> head > *', this._node).each(function(){
			var child = self._form.definitionFactory(this);
			if (child) {
			 	self._children.push(child);
			}
		});
		$('> body > *', this._node).each(function(){
			var child = self._form.definitionFactory(this);
			if (child) {
			 	self._children.push(child);
			}
		});
	}
});

// question
XmlForm.QuestionDefinition = function(node, form){
	this.init(node, form);
};
$.extend(XmlForm.QuestionDefinition.prototype, XmlForm.Definition.prototype, {
	// IDs of questions are automatically generated and don't exist in Form Definition XML
	getId: function() {
		if (!this._id) {
			var node = this._node;
			var id;
			$('question', this._form.getXml()).each(function(i){
				if (this == node) {
					id = 'question' + (i + 1);
					return;
				}
			});
			this._id = id;
		}
		return this._id;
	}
});

// field
XmlForm.FieldDefinition = function(node, form){
	this.init(node, form);
};
$.extend(XmlForm.FieldDefinition.prototype, XmlForm.Definition.prototype, {
	// Fields cannot have children so we don't bother looking
	_addChildren: function() {
	}
});


/**
 * Instance base class
 */
XmlForm.Instance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
XmlForm.Instance.prototype = {
	_node : null,
	_element: null,
	_definition: null,
	_form: null,
	_children: null,
	_parent: null,
	_instanceNumber: 0,
	_isPrimary: true,
	_primary: null,
	_repeatedInstances: null,
	_repeat: null,
	_repeatNumber: 0,
	_hidden: null,

	/**
	 * @param 	Element element		HTML Element for this instance
	 * @param 	Definition definition	Definition object for this Form Element
	 * @param	Int position			Specifies position in instances array if we're inserting rather than appending
	 * @param,	mixed primary		An optional reference to a Primary Instance to save us looking it up. If bool 'true' then this Instance is primary.
	 */
	init: function(element, definition, position, primary){

		// get refernce to Form Element Node from Definition
		this._node = definition.getNode()
		this._element = element;
		this._definition = definition;
		this._form = definition.getForm();
		this._children = [];
		this._repeatedInstances = [];

		// set reference on element
		element.instance = this;

		// associate with parent
		var ancestor = element;
		var i = 0;
		while (ancestor.parentNode) {
			i++;
			ancestor = ancestor.parentNode;
			if (ancestor.instance) {
				ancestor.instance.addChild(this);
				break;
			}
		}

		// register this Instance with its Definition
		definition.addInstance(this, position);

		/*
		We need to differentiate between repeated Instances (clones) and those which are either not repeated, or or the first item in a repeated group
		(the item which is cloned). We call these Primary Instances. They are responsible for keeping track of any repeated instances that belong to them.

		Note that this is not the same thing as being InstanceNumber 0 of our Definition, as repeating parent items could result in multiple repeated sets
		for one definition. Each of these has a Primary Instances.

		We need to check if this is a Primary Instance and if it's not we need to register with its Primary Instance. In order to find this out we look back
		through the previous siblings of this instance's HTML Element until we find either an Instance of the same Definition which IS primary (in which
		case we register with it) or we find an Instance of a different Definition innwhich case we assume this is the Primary Instance.
		*/

		// taken this out as it doesn't seem to work !
		var sibling = element;
		while (1) {
			// Get previous sibling of HTML element
			var sibling = $(sibling).prev()[0];
			// check if sibling is an Instance of the same Definition
			if (sibling && sibling.instance && sibling.instance.getBaseId() == this.getBaseId()) {
				this._isPrimary = false;
				// sibling is primary - register this instance with it
				if (sibling.instance.isPrimary()) {
					sibling.instance.addRepeatedInstance(this);
					break;
				}
			// sibling is not of same type - this must be primary
			} else {
				break;
			}
		}

		this._doInit();
		this._attachEvents();
		this._form.addListener(this);

	},

	// callback for attatching DOM Events to our HTML Element
	_attachEvents: function() {
	},

	// If this is a Primary Instance, recalculate our repeat count and update
	_updateRepeat: function() {
		if (this.isPrimary()) {
			// check for cached value
			if (this._repeat == null) {
				// look up repeat node if it exists
				var repeatNode = $('> repeat', this._node);
				if (repeatNode.length > 0) {
				
					var expression = repeatNode[0].firstChild;

					try {
						repeat = this._form.getInterpreter().evaluate(expression, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()]);
					} catch (e) {
						return;
					}

					// cache static expressions
					if (!this._form.getInterpreter().isDynamic(expression)) {
						this._repeat = repeat;
					}

					// update count
					this.setRepeatCount(repeat);
					
				} else {
					this._repeat = 1;
				}
			}
		}
	},

	// callback for constructor type stuff
	_doInit: function() {
	},

	/**
	 * Validate this Instance and its children and return status
	 */
	isValid: function() {
		if (!this._doValidation()) {
			return false;
		} else {
			var valid = true;
			$(this._children).each(function(){
				if (!this.isValid()) {
					valid = false;
					return;
				}
			});
			return valid;
		}
	},

	// callback for validation logic
	_doValidation: function() {
		return true;
	},

	// Listener callback to receive events from Form
	notifyChange : function(){

	},

	/**
	 * Register a child Instance
	 */
	addChild: function(child) {
		this._children.push(child);
		child.setParent(this);
	},

	/**
	 * Set a reference to the Instance's parent Instance
	 */
	setParent: function(parent) {
		this._parent = parent;
	},

	/**
	 * Unregister a child Instance
	 */
	removeChild: function(child) {
		var children = this._children;
		$(this._children).each(function(i){
			if (this == child) {
				children.splice(i, 1);
				return;
			}
		});
	},

	/**
	 * Get an array of child instances
	 * @param Boolean deep	Recursively include all children, grandchildren etc. - not just one level deep
	 * @return Array		Child Instance objects
	 */
	getChildren: function(deep) {
		if (deep) {
			var children = [];
			// get children of children using deep mode
			$(this._children).each(function() {
				children.push(this);
				$(this.getChildren(true)).each(function() {
					children.push(this);
				});
			});
			return children;
		} else {
			// Not deep copy - just get children of this Instance
			return this._children;
		}
	},

	/**
	 * Make a copy of this Instance
	 * @return Instance			Cloned instance
	 */
	clone: function() {

		// get Form Element ID and InstanceNumber of source Instance (this)
		var base = this.getDefinition().getId();
		var currentInstance = this._instanceNumber;

		// Work out what the instance number of our target will be
		var nextInstanceMain = currentInstance + 1;

		/*
		Tell our Definition to make space at the target position.
		This will increment the instance numbers of any Instances with a position >= this and avoid any ID conficts in the HTML we're about to generate.
		*/
		this.getDefinition().createInstanceSpace(nextInstanceMain);

		// clone our HTML Element and set new ID based on our target InstanceNumber
		var clone = $(this._element).clone(true).attr('id', base + '_inst' + nextInstanceMain);
		// Insert the cloned HTML after our existing HTML Element
		$(this._element).after(clone);

		// Update instance suffixes in cloned HTML to refer to target InstanceNumber
		var html = clone.html();
		html = html.replace(new RegExp(base + '_inst' + currentInstance, "g"), base + '_inst' + nextInstanceMain);
		html = html.replace(base + '[' + currentInstance + ']', base + '[' + nextInstanceMain + ']');

		// increment ids and attach objects to children
		var children = [];

		var self = this;

		var repeatedChildren = {};

		// Loop through all children (deep) of source Instance
		$(this.getChildren(true)).each(function(){

			// get child Instance's definition
			var definition = this.getDefinition();

			// get Form Element ID and InstanceNumber of source Child Instance
			var base = definition.getId();
			var current = this.getInstanceNumber();

			// Work out what the instance number of our target Child Instance will be
			if (repeatedChildren[base]) {
				repeatedChildren[base]++;
				nextInstance = repeatedChildren[base];
			} else {
				var nextInstance = current + 1;
				while ($('#' + base + '_inst' + nextInstance, self._element).length) {
					nextInstance++;
					repeatedChildren[base] = nextInstance;
				}
			}

			// Tell child's Definition to make space at the target position.
			definition.createInstanceSpace(nextInstance);

			// increase child instance suffix in html
			html = html.replace(new RegExp(base + '_inst' + current, "g"), base + '_inst' + nextInstance);
			html = html.replace(base + '[' + current + ']', base + '[' + nextInstance + ']');

			// store references to cloned children so we can give them Instance objects later
			children.push([this.getNode(), base + '_inst' + nextInstance, definition, nextInstance]);
		});

		// update cloned html now
		clone.html(html);

		var self = this;

		// Instantiate Instance Object for clone
		var instance = this._form.instanceFactory(this._node, clone[0], this._definition, nextInstanceMain, self.getPrimary());
		// Call post-clone logic on Instance object
		instance.postClone();

		// Instantiate Instance Objects for clone's children using the array we created earlier
		var lastId = null;
		var lastPrimary = null;

		$(children).each(function(){
			var element = document.getElementById(this[1]);
			if (element) {
				var child = self._form.instanceFactory(this[0], element, this[2], this[3]);
				// Call post-clone logic on Instance object
				child.postClone();
			}
		});

		return instance;
	},

	// Callback for post-clone logic
	postClone: function() {
	},

	/**
	 * Set repeat count for a Primary Instance
	 */
	setRepeatCount: function(count) {
		/*
		Hide first element if count is zero
		We don't want to really remove it in case we need to put it back so we set count
		to 1 and cheat :-)
		*/
		if (count == 0) {
			$(this._element).hide(0);
			count = 1;
		} else {
			$(this._element).show(0);
		}

		// Is new length < current?
		if (this._repeatedInstances.length + 1 > count) {
			// remove Instances after new last position
			for (var i=count - 1; i < this._repeatedInstances.length; i++) {
				if (this._repeatedInstances[i]) {
					this._repeatedInstances[i].remove();
				}
			}
			// remove references to what we've deleted in _repeatedInstances array
			this._repeatedInstances = this._repeatedInstances.slice(0, count - 1);

		// Is new length < current?
		} else if (this._repeatedInstances.length + 1 < count) {
			// create Instances to reach target repeat count
			var newCount = count - this._repeatedInstances.length - 1;
			for (var i=0; i < newCount; i++) {
				// get reference to last item in repeated set to clone
				if (this._repeatedInstances.length) {
					var last = this._repeatedInstances[this._repeatedInstances.length - 1];
				} else {
					var last = this;
				}
				// clone item
				last.clone();
			}
			// update form
			this._form.notifyModified();
		}
	},

	remove: function() {
		$(this._children).each(function(){
			this.remove();
		});
		if (this._parent) {
			this._parent.removeChild(this);
		}
		$(this._element).remove();
		//console.log('remove: ' + this._element.id);
		this._definition.removeInstance(this);
		this._form.removeListener(this);
	},

	// Getters

	getDefinition: function() {
		return this._definition;
	},

	getBaseId: function() {
		return this._definition.getId();
	},

	getInstanceNumber: function() {
		return this._instanceNumber;
	},

	getRepeatNumber: function() {
		return this._repeatNumber;
	},

	/**
	 * Set instance number
	 * @param Int number			New instance number
	 * @param Boolean updateHtml	Update instance suffixes in HTML to reflect change
	 */
	setInstanceNumber: function (number, updateHtml) {
		// check if number has changed if if we're updating HTML
		if (updateHtml && this._instanceNumber && this._instanceNumber != number) {
			// get Form Element ID and current InstanceNumber
			var base = this.getBaseId();
			var current = this._instanceNumber;

			// set ID on root HTML Element
			$(this._element).attr('id', base + '_inst' + number);

			// replace instance suffixes on input fields
			var input = document.getElementById('input_' + base + '_inst' + current);
			if (input) {
				input.id = 'input_' + base + '_inst' + number;
			}
			var singleInputs = document.getElementsByName(base + '[' + current + ']');
			$(singleInputs).each(function(){
				this.name = base + '[' + number + ']';
			});
			var multipleInputs = document.getElementsByName(base + '[' + current + '][]');
			$(singleInputs).each(function(){
				this.name = base + '[' + number + '][]';
			});
			// replace instance suffixes on labels
			$('label[@for="input_' + base + '_inst' + current + '"]').each(function(){
				$(this).attr('for', 'input_' + base + '_inst' + number);
			});
			// replace instance suffixes on error placeholder
			var error = document.getElementById('error_' + base + '_inst' + current);
			if (error) {
				error.id = 'error_' + base + '_inst' + number;
			}
		}
		// update value of _instanceNumber
		this._instanceNumber = number;
	},

	setRepeatNumber: function(number) {
		this._repeatNumber = number;
	},

	// Getters

	getNode: function() {
		return this._node;
	},

	getElement: function() {
		return this._element;
	},

	isPrimary: function() {
		return this._isPrimary;
	},

	getPrimary: function() {
		if (this.isPrimary()) {
			return this;
		} else {
			return this._primary;
		}
	},

	setPrimary: function(instance) {
		this._primary = instance;
	},

	/**
	 * Register a repeated instance with it's Primary Instance (this)
	 * @param Instance instance		Repeated Instance
	 */
	addRepeatedInstance: function(instance) {
		this._repeatedInstances.push(instance);
		instance.setPrimary(this);
		instance.setRepeatNumber(this._repeatedInstances.length);
	},

	/**
	 * Recalculate and update hidden state
	 */
	_updateHidden : function() {
		// check for cached value
		if (this._hidden === null) {
			// look up repeat node if it exists
			var hiddenNode = $('> display > hidden', this._node);
			if (hiddenNode.length > 0) {
				var expression = hiddenNode[0].firstChild;

				hidden = this._form.getInterpreter().evaluate(expression, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()]);
				// cache static expressions
				if (!this._form.getInterpreter().isDynamic(expression)) {
					this._hidden = hidden;
				}
				if (hidden) {
					$(this._element).hide();
				} else {
					$(this._element).show();
				}

			} else {
				this._hidden = false;
			}
		}
	}
}


/*********************************************************
  Instance implementatiions
*********************************************************/

XmlForm.TabInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.TabInstance.prototype, XmlForm.Instance.prototype, {

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateRepeat();
	}
});

XmlForm.GroupInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.GroupInstance.prototype, XmlForm.Instance.prototype, {

	_collapsed : null,

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateRepeat();
	},

	_doInit : function(){
		var self = this;
		if ($('> display > collapsed', this._node).length){
			// create a link in legend if it doesn't exist (via cloning)
			if (!$('> legend > a', self._element).length) {
				$('> legend', self._element).html('<a href="javascript:;">' + $('> legend', self._element).html() + '</a>');
				Cufon.replace('legend', {fontFamily:'swiss-thin'});
			}
			// add toggle tio link
			$('> legend > a', self._element).click(function(){
				self._toggleCollapsed();
				
			});
			this._updateCollapsed();
		}
	},

	_updateCollapsed: function() {
		var collapseElement = $('> div', this._element);
		collapseElement[0].style.height = null;
		//collapseElement[0].style.overflow = null;
		this._collapsed = this._form.getInterpreter().evaluate($('> display > collapsed', this._node)[0].firstChild, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()]);

		if (this._collapsed == true) {
			collapseElement.slideUp(0);
			$('> legend > a', this._element).removeClass('open');
		} else {
			collapseElement.slideDown(0);
			$('> legend > a', this._element).addClass('open');
		}
	},

	_toggleCollapsed : function(){
		var self = this;
		if (self._collapsed){
			self._collapsed = false;
			$('> div', self._element).slideDown('slow');
			$('> legend > a', self._element).addClass('open');
		}
		else{
			self._collapsed = true;
			$('> div', self._element).slideUp('slow');
			$('> legend > a', self._element).removeClass('open');
		}
	},

	postClone: function() {
		var legend = $('> legend > a', this._element);

		if (!legend.length) {
			legend = $('> legend', this._element);
		}

		var legendTextNode = $('> header > title', this._node);
		var expression = legendTextNode[0].firstChild;
		var legendText = this._form.getInterpreter().evaluate(expression, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()]);

		legend.html(legendText);

		this._updateRepeat();
		Cufon.replace('legend', {fontFamily:'swiss-thin'});
	}
});

XmlForm.QuestionInstance = function(element, definition, position) {
	this.init(element, definition, position);
}
$.extend(XmlForm.QuestionInstance.prototype, XmlForm.Instance.prototype, {

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateRepeat();
	},

	postClone: function() {
		var textNode = $('> text', this._node);
		if (textNode) {
			var expression = textNode[0].firstChild;
			var questionText = this._form.getInterpreter().evaluate(expression, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()]);

			var textElement = $('> label,> th', this._element)[0];
			if (textElement) {
				$(textElement).html(questionText);
			}
		}
	}
});

XmlForm.FieldInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.FieldInstance.prototype, XmlForm.Instance.prototype, {

	_errorText: null,
	_defaultValue: null,

	_doInit: function() {
		this._errorText = $('#error_' + $(this._element).attr('id'));

		var defaultNode = $('values > default', this._node);
		if (defaultNode.length > 0 && defaultNode[0].firstChild) {
			// todo: interpret this
			this._defaultValue = defaultNode[0].firstChild.nodeValue;
		}
	},

	getValue: function() {
 		return $('#input_' + $(this._element).attr('id')).val();
	},

	setValue: function(value, suppressChangeEvent) {
 		this._doSetValue(value);
		if (!suppressChangeEvent) {
			this._form.notifyChange();
		}
	},

	_doSetValue: function(value) {
		$('#input_' + $(this._element).attr('id'), this._element).val(value);
	},

	resetValue: function(suppressChangeEvent) {
		var self = this;
		setTimeout(function() {
			self.setValue(this._defaultValue, suppressChangeEvent);
		}, 100);
	},

	_clearError : function() {
		$(this._element).removeClass('error');
		this._errorText.text('');
	},

	_setError: function(message) {
		this._errorText.text(message);
		$(this._element).addClass('error');
	},

	postClone: function() {
		this.resetValue(true);
	},

	_updateDisabled : function(){
		var self = this;
		var disabledNode = $('> display > disabled', self._node);
		if (disabledNode.length > 0){
			if (self._form.getInterpreter().evaluate(disabledNode[0].firstChild, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()])){
				$('#input_' + $(self._element).attr('id')).attr('disabled','disabled');
			}
			else{
				$('#input_' + $(self._element).attr('id')).removeAttr('disabled');
			}
		}
	}
});


// InputText & InputTextarea field
XmlForm.InputTextInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.InputTextInstance.prototype, XmlForm.FieldInstance.prototype, {

	_doInit: function() {
		this._errorText = $('#error_' + $(this._element).attr('id'));

		var defaultNode = $('values > default', this._node);
		if (defaultNode.length > 0 && defaultNode[0].firstChild) {
			// todo: interpret this
			this._defaultValue = defaultNode[0].firstChild.nodeValue;
		}

		if ($(this._node).attr('type') == 'date') {
			$('#input_' + $(this._element).attr('id')).datepicker({
				showOn: "both",
	   			buttonImage: "/images/icons/calendar.gif",
				buttonImageOnly: true,
				minDate: new Date(),
				maxDate: '+3y',
				dateFormat: 'dd/mm/yy'
			});
			$('#input_' + $(this._element).attr('id')).addClass('datePicker');
		}
	},

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}

			switch (this.tagName) {
				case 'maxLength':
					var length = parseInt($(this).find('length').text());
					valid = (self.getValue().length <= length);
					break;
				case 'required':
					valid = (self.getValue() != '');
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
				case 'match':
					var pattern_regex = $(this).find('expression').text(); // RegEx pattern (without slashes and attributes)
					if (pattern_regex == '') {
						var match_type = $(this).attr('type'); // common types of RegEx matches
						switch(match_type){
							// NB in pattern_regex strings, must be \\ instead of \
							case 'email':
								pattern_regex='^[A-Za-z0-9](([_\\.\\-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([\\.\\-]?[a-zA-Z0-9]+)*)\\.([A-Za-z]{2,})$';
								break;
							case 'emails':
								pattern_regex='^(([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5}){1,25})+([;.](([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5}){1,25})+)*$';
								break;
							case 'ukPostCode':
								pattern_regex='^[A-Za-z]{1,2}[\\d]{1,2}([A-Za-z])?\\s?[\\d][A-Za-z]{2}$';
								break;
							case 'ukPhone':
								pattern_regex='^(((\\+44\\s?\\d{4}|\\(?0\\d{4}\\)?)\\s?\\d{3}\\s?\\d{3})|((\\+44\\s?\\d{3}|\\(?0\\d{3}\\)?)\\s?\\d{3}\\s?\\d{4})|((\\+44\\s?\\d{2}|\\(?0\\d{2}\\)?)\\s?\\d{4}\\s?\\d{4}))(\\s?\\#(\\d{4}|\\d{3}))?$';
								break;
						}
					}
					var re=new RegExp(pattern_regex,'g');
					valid = re.test(self.getValue());
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_changeCase : function(value, type){
		switch (type) {
			case 'lower':
				return value.toLowerCase();
				break;
			case 'upper':
				return  value.toUpperCase();
				break;
			case 'caps':
				return ucwords(value);
				break;
			case 'capsFirst':
				return ucfirst(value);
				break;
		}
	},

	_changeCharacters : function(value, type){
		switch (type) {
			case 'alpha':
				var re=new RegExp('[^a-zA-Z\\s]','gi');
				return (value+'').replace(re,''); // remove non-ABC
				break;
			case 'numeric':
				var re=new RegExp('[^0-9\\s]','gi');
				return (value+'').replace(re,''); // remove non-numeric
				break;
			case 'alphaNumeric':
				var re=new RegExp('[^a-zA-Z0-9\\s]','gi');
				return (value+'').replace(re,''); // remove non-alphanumeric
				break;
		}
	},

	filter : function() {
		var self = this;
		$('filter > *', self._node).each(function() {
			switch (this.tagName) {
				case 'trim':
					if (this.firstChild.nodeValue == 'true') {
						self.setValue($.trim(self.getValue()));
					}
					break;
				case 'case':
					self.setValue(self._changeCase(self.getValue(),this.firstChild.nodeValue));
					break;
				case 'characters':
					self.setValue(self._changeCharacters(self.getValue(),this.firstChild.nodeValue));
					break;
				case 'max':
					if (parseInt(self.getValue()) > parseInt(this.firstChild.nodeValue)) {
						self.setValue(this.firstChild.nodeValue);
					}
					break;
			}
		});
	},

	_attachEvents : function() {
		var self = this;
		if ($(this._node).attr('type') == 'date') {
			$('#input_' + $(this._element).attr('id')).bind('change',function() {
				self.filter();
				self._doValidation();
				//console.log('change');
				self._form.notifyChange();

			});
		} else {
			$('#input_' + $(this._element).attr('id')).bind('blur',function() {
				self.filter();
				self._doValidation();
				//console.log('blur');
				self._form.notifyChange();

			});
		}
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	}
});

XmlForm.BoolCheckboxInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.BoolCheckboxInstance.prototype, XmlForm.FieldInstance.prototype, {

	getValue: function() {
 		return $('#input_' + $(this._element).attr('id')).attr('checked');
	},

	_doSetValue: function(value) {
		if (value){
			$('#input_' + $(this._element).attr('id')).attr('checked', 'checked');
		}
		else{
			$('#input_' + $(this._element).attr('id')).attr('checked', '');
		}
	},

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}
			switch (this.tagName) {
				case 'required':
					valid = self.getValue() ;
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_attachEvents : function() {
		var self = this;
		$('#input_' + $(this._element).attr('id')).bind('click',function() {
			self._doValidation();
			self._form.notifyChange();
		});
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	}

});

XmlForm.BoolRadiosInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.BoolRadiosInstance.prototype, XmlForm.FieldInstance.prototype, {

	_fields : [],

	_doInit: function() {
		// split id into array of id, instance_number
		var idParts = $(this._element).attr('id').split('_inst');
		var elementName = idParts[0] + '[' + idParts[1] + ']';
		this._errorText = $('#error_' + $(this._element).attr('id'));
		this._fields = $('input[@name="' + elementName + '"]');
	},

	// get primitive value (bool)
	getValue: function() {
		var checkedValue = '0';
		this._fields.each(function(){
			if (this.checked){
				checkedValue = ($(this).val()); // ='1'
				return; // breaks out of each(...) loop
			}
		});
		if (checkedValue == '1'){
			return true;
		} else {
			return false; // what is not true, becomes false explicitly
		}

	},

	// set primitive value (bool)
	_doSetValue: function(value) {
		this._fields.each(function(){
				this.checked = false;
		});
		if (value !== true)
			value = false; // what is not true, becomes false explicitly
		this._fields.each(function(){
			if ( ( (value === true) && ($(this).val() == '1') )
					||
					( (value === false) && ($(this).val() == '0') ) ){
				this.checked = true;
			}
		});
	},

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}
			switch (this.tagName) {
				case 'required':
					valid = self.getValue() ;
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_attachEvents : function() {
		var self = this;
		self._fields.bind('click',function() {
			self._doValidation();
			self._form.notifyChange();
		});
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	},

	_updateDisabled : function(){
		var self = this;
		var disabledNode = $('> display > disabled', self._node);
		if (disabledNode.length > 0){
			if (self._form.getInterpreter().evaluate(disabledNode[0].firstChild, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()])){
				this._fields.each(function(){
					$(this).attr('disabled','disabled');
				});
			}
			else{
				this._fields.each(function(){
					$(this).removeAttr('disabled');
				});

			}
		}
	}

});


XmlForm.MultipleSelectionCheckboxListInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.MultipleSelectionCheckboxListInstance.prototype, XmlForm.FieldInstance.prototype, {

	_fields : [],

	_doInit: function() {
		// split id into array of id, instance_number
		var idParts = $(this._element).attr('id').split('_inst');
		var elementName = idParts[0] + '[' + idParts[1] + ']' + '[]';
		this._errorText = $('#error_' + $(this._element).attr('id'));
		this._fields = $('input[@name="' + elementName + '"]');
	},

	// get primitive value( array of strings)
	getValue: function() {
		var checkedFields = [];
		this._fields.each(function(){
			if (this.checked){
				checkedFields.push($(this).val())
			}
		});
		return checkedFields;
	},

	// set primitive value (array of strings)
	_doSetValue: function(checkedFields) {
		// loop thru all fields, if field.val is in value array, set checked =true, else checked =false
		this._fields.each(function(){
			if (jQuery.inArray($(this).val(), checkedFields) > -1){
				this.checked = true;
			}
			else{
				this.checked = false;
			}
		});
	},

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}
			switch (this.tagName) {
				case 'minSelected':
					var minSelectedSize = parseInt($(this).find('size').text());
					var actualSelected = self.getValue();
					var actualSelectedSize = 0;
					if (actualSelected != null){
						actualSelectedSize = actualSelected.length;
					}
					valid = (actualSelectedSize >= minSelectedSize);
					break;
				case 'maxSelected':
					var maxSelectedSize = parseInt($(this).find('size').text());
					var actualSelected = self.getValue();
					var actualSelectedSize = 0;
					if (actualSelected != null){
						actualSelectedSize = actualSelected.length;
					}
					valid = (actualSelectedSize <= maxSelectedSize);
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_attachEvents : function() {
		var self = this;

		self._fields.bind('change',function() {
			self._form.notifyChange();
		});
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	},


	_updateDisabled : function(){
		var self = this;
		var disabledNode = $('> display > disabled', self._node);
		if (disabledNode.length > 0){
			if (self._form.getInterpreter().evaluate(disabledNode[0].firstChild, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()])){
				this._fields.each(function(){
					$(this).attr('disabled','disabled');
				});
			}
			else{
				this._fields.each(function(){
					$(this).removeAttr('disabled');
				});

			}
		}
	}

});

XmlForm.MultipleSelectionListInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.MultipleSelectionListInstance.prototype, XmlForm.FieldInstance.prototype, {

	// get primitive value (array of strings)
	// overrideen from parent class to always return array
	getValue: function() {
		var value = $('#input_' + $(this._element).attr('id')).val();
		if (value == null){
			return Array();
		}
		else{
			return value;
		}
	},

	// setValue derived from parent class

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}
			switch (this.tagName) {
				case 'minSelected':
					var minSelectedSize = parseInt($(this).find('size').text());
					var actualSelected = self.getValue();
					var actualSelectedSize = 0;
					if (actualSelected != null){
						actualSelectedSize = actualSelected.length;
					}
					valid = (actualSelectedSize >= minSelectedSize);
					break;
				case 'maxSelected':
					var maxSelectedSize = parseInt($(this).find('size').text());
					var actualSelected = self.getValue();
					var actualSelectedSize = 0;
					if (actualSelected != null){
						actualSelectedSize = actualSelected.length;
					}
					valid = (actualSelectedSize <= maxSelectedSize);
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_attachEvents : function() {
		var self = this;
		$('#input_' + $(this._element).attr('id')).bind('change',function() {
			self._doValidation();
			self._form.notifyChange();
		});
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	}

});

XmlForm.SingleSelectionDropdownInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.SingleSelectionDropdownInstance.prototype, XmlForm.FieldInstance.prototype, {

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}
			switch (this.tagName) {
				case 'required':
					valid = (self.getValue() != '') ;
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_attachEvents : function() {
		var self = this;
		$('#input_' + $(this._element).attr('id')).bind('change',function() {
			self._doValidation();
			self._form.notifyChange();
		});
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	}

});

XmlForm.SingleSelectionRadioListInstance = function(element, definition, position, primary) {
	this.init(element, definition, position, primary);
}
$.extend(XmlForm.SingleSelectionRadioListInstance.prototype, XmlForm.FieldInstance.prototype, {

	_fields : [],

	_doInit: function() {
		// split id into array of id, instance_number
		var idParts = $(this._element).attr('id').split('_inst');
		var elementName = idParts[0] + '[' + idParts[1] + ']';
		this._errorText = $('#error_' + $(this._element).attr('id'));
		this._fields = $('input[@name="' + elementName + '"]');
	},

	getValue: function(){
   	var checkedValue = '';
   	this._fields.each(function(){
   		if (this.checked) {
   			checkedValue = ($(this).val()); // ='1'
						return; // breaks out of each(...) loop
					}
				});
				return checkedValue;
	},
	// set primitive value (string)
	_doSetValue: function(value){
		var self = this;
	   	this._fields.each(function(){
			// uncheck all for null value
	   		if (value != null && $(this).val() == value) {
	   			this.checked = true;
	   		} else {
	   			this.checked = false;
	   		}
	   	});
   },

	// test validation, and populate error text on failure
	_doValidation: function() {
		this._clearError();
		var valid = true;
		var self = this;
		$('validation > *', this._node).each(function() {
			if (!valid){
				return; // prevent further checks if we have already encountered 1 error, and have already set up error message
			}
			switch (this.tagName) {
				case 'required':
					valid = self.getValue() ;
					break;
				case 'custom':
					$(this).find('conditions').each(function(){
					     valid = self._form.getInterpreter().evaluate(this, [self._instanceNumber, self._repeatNumber]); // <conditions>
					});
					break;
			}
			if (!valid) {
				var message = $(this).find('message').text();
				self._setError(message);
			}
		});
		return valid;
	},

	_attachEvents : function() {
		var self = this;
		this._fields.bind('click',function() {
			self._doValidation();
			self._form.notifyChange();
		});
	},

	// hook called by form to broadcast change to all listeners (instances)
	notifyChange : function(){
		this._updateHidden();
		this._updateDisabled();
		this._updateRepeat();
	},


	_updateDisabled : function(){
		var self = this;
		var disabledNode = $('> display > disabled', self._node);
		if (disabledNode.length > 0){
			if (self._form.getInterpreter().evaluate(disabledNode[0].firstChild, [this._instanceNumber, this._repeatNumber, this._parent.getInstanceNumber(), this._parent.getRepeatNumber()])){
				this._fields.each(function(){
					$(this).attr('disabled','disabled');
				});
			}
			else{
				this._fields.each(function(){
					$(this).removeAttr('disabled');
				});

			}
		}
	}

});

/**
 * Interpretor for our XML form query language
 * @todo implement config element
 */
XmlForm.Interpreter = function(form) {
	var self = this;
	self._form = null;

	self.init = function(form) {
		self._form = form;
	}

	self.isDynamic = function(xml) {
		return ($('field', xml.parentNode).length > 0);
	}

	self.evaluate = function(xml, params) {
		return self._evaluateNode(xml, params);
	}

	self._evaluateNode = function(node, params) {

		var items = node.childNodes;

		switch (node.tagName) {

			// logical grouping
			case 'conditions':
			case 'all':
				for (var i=0; i < items.length; i++) {
					if (this._evaluateNode(items[i], params) == false) {
						return false;
					}
		        }
				return true;
			case 'any':
				for (var i=0; i < items.length; i++) {
					if (this._evaluateNode(items[i], params) == true) {
						return true;
					}
		        }
				return false;
			case 'none':
				for (var i=0; i < items.length; i++) {
					if (this._evaluateNode(items[i], params) == true) {
						return false;
					}
		        }
				return true;

			// logical operators
			case 'equal':
				var previous = null;
				for (var i=0; i < items.length; i++) {
					value = this._evaluateNode(items[i], params);
					if (previous !== null && previous != value) {
						return false;
					}
					previous = value;
		        }
				return true;
			case 'notEqual':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1 != value2);
				break;
			case 'greater':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1 > value2);
			case 'greaterOrEqual':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1 >= value2);
			case 'less':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1 < value2);
			case 'lessOrEqual':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1 <= value2);

			// arithmetic operators
			case 'add':
				result = parseFloat(this._evaluateNode(items[0], params));
				for (i=1; i < items.length; i++) {
					result += parseFloat(this._evaluateNode(items[i], params));
		        }
				return result;
			case 'subtract':
				result = this._evaluateNode(items[0], params);
				for (i=1; i < items.length; i++) {
					result -= this._evaluateNode(items[i], params);
		        }
				return result;
			case 'multiply':
				result = this._evaluateNode(items[0], params);
				for (i=1; i < items.length; i++) {
					result *= this._evaluateNode(items[i], params);
		        }
				return result;
			case 'divide':
				result = this._evaluateNode(items[0], params);
				for (i=1; i < items.length; i++) {
					result /= this._evaluateNode(items[i], params);
		        }
				return result;
			case 'mod':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1 % value2);

			// string operators
			case 'concat':
				result = this._evaluateNode(items[0], params);
				for (i=1; i < items.length; i++) {
					result += this._evaluateNode(items[i], params);
		        }
				return result;
			case 'contains':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return (value1.indexOf(value2) !== -1);
			case 'length':
				return (this._evaluateNode(items[0], params).length);
			case 'replace':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				value3 = this._evaluateNode(items[2], params);
				return value1.replace(value2, value3);
			case 'split':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return value1.split(value2);
			case 'subString':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				value3 = this._evaluateNode(items[2], params);
				return value1.substr(value2, value3);
			case 'trim':
				return $.trim(this._evaluateNode(items[0], params));
			case 'lowerCase':
				return (this._evaluateNode(items[0], params).toLowerCase());
			case 'upperCase':
				return (this._evaluateNode(items[0], params).toUpperCase());
			case 'caps':
				return ucwords(this._evaluateNode(items[0], params));
			case 'capsFirst':
				return ucfirst(this._evaluateNode(items[0], params));
			case 'match':
				var subject = this._evaluateNode(items[0], params);
				var regexStr = (this._evaluateNode(items[1], params));
				var lastSlash = regexStr.lastIndexOf('../index.html');
				var regexExp = regexStr.substr(1, lastSlash - 1);
				var regexMod = regexStr.substr(lastSlash + 1, lastSlash + 2);
				var expression = new RegExp(regexExp, regexMod);
				var result = (subject.match(expression));
				return (result == null) ? 0 : result.length;

			// array operators
			case 'count':
				return (this._evaluateNode(items[0], params).length);
			case 'empty':
				result = this._evaluateNode(items[0], params);
				return empty(result);
			case 'containsValue':
				value1 = this._evaluateNode(items[0], params); // array
				value2 = this._evaluateNode(items[1], params);
				for (x in value1)
				{
					if (value1[x] == value2){
						return true;
					}
				}
				return false;
			case 'join':
				value1 = this._evaluateNode(items[0], params);
				if (items.length == 2){
					glue = this._evaluateNode(items[1], params);
				} else {
					glue = ''; //default
				}
				return value1.join(glue);
			case 'slice':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				value3 = this._evaluateNode(items[2], params);
				// js slice uses start /end, not start / length
				return (value1.slice(value2, value2 + value3));
			case 'item':
				value1 = this._evaluateNode(items[0], params);
				value2 = this._evaluateNode(items[1], params);
				return value1[value2];
			case 'first':
				value1 = this._evaluateNode(items[0], params);
				return value1[0];
			case 'last':
				value1 = this._evaluateNode(items[0], params);
				return value1[value1.length -1];

			// values
			case 'field':
				var name = this._evaluateNode($('> name', node)[0].firstChild, params);
				var instance = 0;
				var instNodes = $('> instance', node);
				if (instNodes.length) {
					var instance = this._evaluateNode(instNodes[0].firstChild, params);
				}
				var element = this._form.getInstance(name, instance);
				if (element) {
					return this._form.getInstance(name, instance).getValue();
				} else {
					return null;
				}
			case 'list':
				list = [];
				for (var i=0; i < items.length; i++) {
					list[i] = this._evaluateNode(items[i], params);
		        }
				return list;
			case 'value':
				if (items.length) {
					return items[0].nodeValue;
				} else {
					return '';
				}
			case 'param':
				return params[items[0].nodeValue];

			// syntax error
			default:
				if (items && items.length) {
					return this._evaluateNode(items[0], params);
				} else {
					return node.nodeValue;
				}
		}
	}
	self.init(form);
}

/**
 * Manager for tabs in form
 * Uses JQuery History plugin to support back button
 */
XmlForm.TabManager = function(ul, form) {

	var self = this;
	self._tabIds = [];
	self._currentTab = null;
	self._form = form;

	self.init = function(ul) {

		if (!ul.length) {
			return;
		}

		var prev;
		$(ul).find('a').each(function() {
			this.tabId = this.href.split('#').pop();
			this.id = this.tabId + '_tab'
			self._tabIds.push(this.tabId);
			if (prev) {
				$('#' + this.tabId).append('<a class="buttonBottomLeft tabNav floatLeft" href="#' + prev.tabId + '">&laquo; ' + $(prev).children().html() + '</a>');
				$('#' + prev.tabId).append('<a class="buttonBottomRight tabNav floatRight" href="#' + this.tabId + '">' + $(this).children().html() + ' &raquo;</a>');
			}
			prev = this;
			this.href += '_';
		}).click(function(event){
			if (!self._currentTab || self.isValid()) {
				self.showTab(this.tabId);
			} else {
				event.preventDefault();
			}
		});

		$('.tabNav').each(function(){
			this.tabId = this.href.split('#').pop();
		}).click(function(event){
			if (!self._currentTab || self.isValid()) {
				self.showTab(this.tabId);
			} else {
				event.preventDefault();
			}
		});
		
		
		if(navigator.appVersion.indexOf('WebKit') == -1) {
			$.historyInit(function(hash) {
				if (hash.substr(hash.length - 1, 1) == '_') {
					hash = hash.substr(0, hash.length - 1);
				}
				if (!hash) {
					hash = self._tabIds[0];
				}
				if (hash != self._currentTab) {
					self.showTab(hash);
				}
			});
		} else {
			// just init the first tab on first page load if no hash is part of url, ie. direct link to page
			self.showTab(self._tabIds[0]);
		}
	}

	self.isValid = function() {
		if (self._currentTab) {
			if (!self._form.getInstance(self._currentTab.split('_')[0]).isValid()) {
				alert($.i18n._('There are problems on this section of the form. Please correct them before proceeding'));
				var target = $('div.error');
				var targetOffset = target.offset().top - 25;
				$('html,body').animate({scrollTop: targetOffset}, 500);
				return false;
			}
		}
		return true;
	}

	self.showTab = function(id) {
		$(self._tabIds).each(function(){$('#' + this).addClass('ui-tabs-hide')});
		$('#' + id).removeClass('ui-tabs-hide');

		$('.ui-tabs-active').removeClass('ui-tabs-active');
		$('#' + id + '_tab').parent().addClass('ui-tabs-active');
		self._currentTab = id;
		
		if(navigator.appVersion.indexOf('AppleWebKit') == -1) {
			$.historyLoad(id + '_');
		}
	}

	self.init(ul);
}

