/**
 * Form building utility class
 * 
 * Providers methods to form input elements and manage form dependencies
 * 
 * @author Ollie Maitland
 * @copyright Byng Systems LLP
 * 
 * @package forms
 */

/**
 * Form Builder constructor
 * 
 * @param String formName Form ID reference name
 * @param Boolean Use dependencies
 */
Byng.register('byng.forms.builder',
{
	
	/**
	 * Implement options on this class
	 * 
	 */
	Implements : Options,
	
	/**
	 * Options for this instance of FormBuilder
	 *
	 * @param Object 
	 */
	options : {
		chained  : [],
		formName : null,
		form     : $empty,
		fieldsets: $empty,
		cloneable: false
	},
	
	/**
	 * Holds the form dependencies
	 *
	 * @see FormBuilder
	 * 
	 * @type Array
	 */
	depends : [],	
	
	/**
	 * Holds a stack of instantiated plugins 
	 * 
	 * @type Array
	 */
	plugins : [],	
	
	/**
	 * Initialise a new instance of the formbuilder
	 * 
	 * @param String formName
	 * @return FormBuilder
	 */
	initialize : function (formName, options)
	{
		/**
		 * Holds the form name of the current form
		 * 
		 * @param String formName
		 */
		if (formName) {
			this.setFormName (formName, options);
		}
	},
	
	/**
	 * Clone this instance of FormBuilder
	 * 
	 * @param Mixed
	 * @return FormBuilder
	 */
	clone : function ()
	{
		return Byng.init('byng.forms.builder');
	},
	
	/**
	 * Attach the clone events
	 * 
	 * @param String fieldset
	 */
	attachCloneEvent : function ( fieldset )
	{
		$each(this.options.fieldsets, function(content,key){
			
			if (fieldset == key) {
				
				$each(content, function(field){
					
					if (field.hasClass('cloned') == false) {
						field.setProperty('fieldset', key);
						var str = field.getProperty('name').replace(key, key+'[0]');
						field.setAttribute('name', str);
						var str = field.getProperty('id').replace(key, key+'_0');
						field.setAttribute('id', str);
						field.setProperty('cloneIndex', 0);
					}
					
					field.addEvent('clone', function() {
						
						if (field.hasClass('cloned') == true) return;
						
						var parentIndex = parseInt(this.getProperty('cloneIndex'));
						var index = parentIndex+1;
						
						var str = field.getAttribute('name').replace(key+'['+parentIndex+']', key+'['+index+']');
						this.setAttribute('name', str);
						
						var str = field.getAttribute('id').replace(key+'_'+parentIndex, key+'_'+index);
						this.setAttribute('id', str);
						
						this.setProperty('cloneIndex', index);
					});
				});
				
				var el = Byng.ui.ge({'fieldset' : key}, 'fieldset')
					.addEvent('clone', function(){
						
						var dupe = el.clone().injectBefore(el);				
						$each(this.options.fieldsets[key], function(field){ 
								field.fireEvent('clone',field);
						});
						
						// render any plugins
						$each(this.plugins, function(plugin) {
							plugin.render(el);
							plugin.render(dupe);						
						});
						
				}.bind(this));
				
				// render any plugins
				$each(this.plugins, function(plugin) {
					plugin.render(el);
				});
			}			
		}.bind(this));
		
		
	},
	
	/**
	 * Clone a fielset by name
	 * 
	 * @param String fieldset
	 */
	cloneFieldset : function (fieldset)
	{
		Byng.ui.ge({'fieldset' : fieldset}, 'fieldset').fireEvent('clone');
	},
	
	/**
	 * Set the chained fields
	 * 
	 * @param Array chained
	 */
	setChained : function (chained)
	{
		this.options.chained = chained;
	},
	
	/**
	 * Prepare the form elements events
	 * 
	 */
	prepareForm : function ()
	{
		var form = $(this.options.formName);
		var elements = $H();
		if (!form) return;
		// load elements into a hash map with element name
		$each($A(form.elements), function(el){
			el = $(el);
			if ($A(['select','input','textarea']).contains(el.get('tag'))) {
				this.set(el.getProperty('name'),el);
			}
			// is this a required field
			var requiredFieldIcon = (el.getParent('div.field') || {getElement : $empty}).getElement('.field_required');
			if ( requiredFieldIcon ) {
				el.store('requiredField', true);
				if ( el.getProperty('disabled') == true ) {
					requiredFieldIcon.addClass('hide');
				}
				el.store('requiredFieldIcon', requiredFieldIcon);
			} else {
				el.store('requiredField', false);
			}
		}.bind(elements));
			
		// attach behaviours to elements
		elements.each(function(el) {
			
			// condition: element has a maxlength attribute set 
			if (el.get('maxlength') > 0) {
				// on key up skip to next field
				el.addEvent('keyup', function (event) {
					
					// condition: proceed with tab
					if ($A(['tab','shift','ctrl']).contains(event.key)) {
						return null;
					}
					// condition: control key to move from one field to another
					if ($A(['delete','backspace','left']).contains(event.key)) {
						if (event.target.value.length == 0) {
							return this.moveToElement(event.target,-1);
						} else {
							return null;
						}
					}
					
					if(event.target.value.length == event.target.getAttribute('maxlength')){
						return this.moveToElement(event.target,+1);
					}
					/*if(event.target.getAttribute('maxlength') &&
						event.target.value.length >= event.target.getAttribute('maxlength')) {
						return this.moveToElement(event.target,+1,event.key);
					}*/
				}.bind(this));
			}
		}.bind(this));
		
		// find any chained fields
		this.options.chained.each(function(chain) {
			
			var element = elements.each(function(source){
				// condition: the name contains the chain stub
				if (source.getProperty('name')&& source.getProperty('name').contains(chain[0])) {
					var cmp = source.getProperty('name').replace(chain[0],'');
					source.addEvent('change', function (source,target) {
						target.set('value', source.get('value'));
					}.pass([source,elements.get(chain[1] + cmp)]));
				}
			});
			
		}.bind(this));
		
		return elements;	
	},
	
	/**
	 * Attach the plugins to the fields
	 * 
	 */
	attachPlugins : function ( fieldsets )
	{
		if (!fieldsets) fieldsets = this.options.fieldsets;
		if ($type(this.options.plugins) != 'object') return;
		if (this.options.plugins.picker) {
			this.plugins.push(Byng.init('byng.forms.builder.picker', fieldsets, this.options.plugins.picker));
		}
	},
	
	/**
	 * Skip to the next element
	 * 
	 * @param DomElement element
	 */
	moveToElement : function (element, offset)
	{
		if (!offset) offset = 1;
		var indexOf = $A(element.form.elements).indexOf(element);
		if (indexOf < element.form.elements.length) {
			try { 
				// autocomplete fix for https://bugzilla.mozilla.org/show_bug.cgi?id=236791
				setTimeout(function (element) { 
								
								if( element.type != "hidden" && element.style.display != "none"  && !element.disabled) {  
									/*if ($type(key) == 'string') {
										if (element.get('value') == '') element.set('value', key);
										else element.set('value', key + element.get('value'));
									}*/			
										element.setProperty('autocomplete','off').focus();
									}

								}.pass(element.form.elements[indexOf+offset]), 10);
			} catch (e) {}
		}
	},
	
	/**
	 * Set the form name for this instance
	 *
	 * @param String name
	 */
	setFormName : function (formName, options)
	{	
		if (options) this.setOptions(options);	
		this.options.formName = formName;	
		
		// prepare the form when the Dom is ready
		window.addEvent('domready', function () {
				this.loadForm();
				this.prepareForm();
				this.attachPlugins();
		}.bind(this));
				
		return this;
	},
	
	/**
	 * Load the form into the builder
	 * 
	 */
	loadForm : function ()
	{
		this.options.form = $(this.options.formName);
		if (!this.options.form) return null;
		this.options.fieldsets = {};
		this.options.form.getElements('fieldset').each(function(fieldset) {
				this.loadFieldset(fieldset);
		}.bind(this));
	},
	
	/**
	 * Load a fieldset
	 * 
	 * @param String fieldset
	 */
	loadFieldset : function (fieldset)
	{
		var fieldsets = {}
		// loop over the other elements
		var meta = Byng.ui.fromIdTag(fieldset.getProperty('id'));
		if (!$chk(meta)) return;
		var fields = {};
		['select','textarea','input'].each(function(tagType){
			
			// Look around the fieldset for each input type
			fieldset.getElements(tagType).each(function(el) {
				if (el.getProperty('id')) {
					this[el.getProperty('id').replace(meta.fieldset,'').replace('_','')] = el;
				}
			}.bind(fields));
		}.bind(fields));
		fieldsets[meta.fieldset] = fields;
		this.options.fieldsets = $merge(this.options.fieldsets, fieldsets);
	},
	
	/**
	 * Form input manipluation
	 *
	 * @param String value
	 * @param DomElement select
	 * @param FormBuilder builder
	 */
	chooseOther : function (value, select) 
	{
		this.select = select;
		
		if (value != "") return;
		
		this.select.style.display = "none";
		
		this.input = this.elementFactory ("text", this.select.name);
		if (select.hasClass('s')) {
			this.input.addClass('s');
		}
		// Add the input block to the parent of the select block
		this.select.parentNode.appendChild (this.input);
		
		this.tag = this.getReturnTag ("Return");
		
		// Add the tag to get back
		this.select.parentNode.appendChild (this.tag);

		this.input.focus();
	},
	
	/**
	 * Return back to a select drop down
	 * 
	 */ 
	returnFromOther : function () 
	{
		this.select.parentNode.removeChild (this.input);
		this.select.parentNode.removeChild (this.tag);
		
		// Now select the first from the list
		this.select.value = this.select.options[0].value;
		
		this.select.style.display = "inline";
	},
	
	/**
	 * Form element factory
	 * 
	 * @param String type DomElement tag type
	 * @param String name DomElement name
	 * @param String value DomElement input value
	 * @return DomElement
	 */ 
	elementFactory : function (type, name, value) 
	{	
		if (!type) type = "text";
		if (!value) value = "";

		var input = document.createElement("input");
			input.setAttribute ('type', type);
			input.setAttribute ('name', name);
			input.setAttribute ('value', value);
		
		return input;
	},
	
	/**
	 * Create a new form in DOM
	 * 
	 * @param String uri
	 * @param String name
	 * @param String action
	 * @return DomElement
	 */
	formFactory : function (uri, name, action)
	{
		var form = document.createElement("form");
			form.name = name;
			form.action = uri;
			form.method = "POST";
		//	form.style.visibility = 'none';
		
		var input = this.elementFactory("hidden","action", action);
			form.appendChild (input);

		return form;
	},

	/**
	 * Get a tag to return to the select drop down
	 * 
	 * @param String s
	 * @param FormBuilder builder
	 * @return String
	 */ 
	getReturnTag : function (s)
	{
		var tag = document.createElement("span");
			tag.innerHTML = ' ( <a href="javascript:Byng.input.getBuilder().returnFromOther();">'+s+'</a> )';
		
		return tag;
	},
	
	/**
	 * Return the active input element
	 * 
	 * @return DomElement
	 */
	returnInput : function ()
	{
		return this.input;
	},
	
	/**
	 * Set a dependeny
	 * 
	 * @param String field DomElement name
	 */ 
	setDepend : function (field) 
	{
		// retrieve the form
		this.form = $(this.options.formName);
		
		// are there any dependencies for this field
		if ($chk(this.depends[field]) == false) return;	
		
		var depends = this.depends[field][0];
		var negate  = this.depends[field][1];

		// check for presence of children
		if ($chk(depends) == false) return;	
	
		// extend the field functionality
		f = this.form.elements[field];
		f = ( f.length > 0 ? $$(f) : $(f) )  

		$each(depends, function(element, i) {

			element = this.form.elements[element];
			element = ( element.length > 0 ? $$(element) : $(element) )  
			
			// Check whether the selected (parent) field is empty
			if ($type(f) == 'element') {
				switch (f.get('type')) {
					case "checkbox":
						active = f.getProperty('checked');
						break;
					case "text":
						if (f.get('value') != "") active = true;
						else active = false;
						break;
				}
			} else {
				for (k = 0; k < f.length; k++) {
					if (f[k].getProperty('checked') == true) {
						active = f[k].value;
					}
				}
			}

			// condition: dependency negated
			if (negate == true) {
				if (active == true) active = false;
				else active = true;
			}
			
			if (element.length) {

				if (element.type == "select-one") {
					// Select drop downs we just be disabled on the whole drop
					this.disableElement (element, active);
				} else {
					// Disable checkboxes and radios					
					for (k=0;k<element.length;k++) {
						this.disableElement (element[k], active);
					}
				}
			} else {
				
				var enabledBy = (element.retrieve('enabledBy') || []);
				if ( enabledBy.length > 0) {
					// condition: check that all fields that enabled this one are removed
					if (active == false && enabledBy.contains(field) == true) {
						enabledBy.erase( field );
					}
					// condition: no remaining parent fields keeping this one one
					if ( enabledBy.length == 0 ) {
						// so disable this element
						this.disableElement (element, active);
					}
				} else {
					this.disableElement (element, active);	
				}

				// push the current parent onto the list that enabled this field
				if ( active == true ) enabledBy.push( field );
				element.store('enabledBy', enabledBy);				
			}
			
		}.bind(this));
	},
	
	/**
	 * Disable an element
	 * 
	 * @param DomElement element
	 * @param Boolean active
	 */ 
	disableElement : function  (element, active)
	{
		element = $(element);
		// condition: active is true
		if (active == true) {
			element.setProperty('disabled', false).removeClass('disabled');
			// condition: a previous value is associated with this input
			if (element.retrieve('previousValue')) {
				element.set('value', element.retrieve('previousValue'));
			}
		} else {
			
			element.setProperty('disabled', true).addClass('disabled');
			// condition: disabled field is an input therefore should not have any input
			if (element.get('tag') == 'input') {
				element.store('previousValue', element.get('value'));
				element.set('value', '');
			}
		}
		
		// condition: the field we're disabling is a required field
		if (element.retrieve('requiredField')) {
			var icon = element.retrieve('requiredFieldIcon');
			if ( icon ) {
				// restore required icon for active fields
				( active == true ? icon.removeClass('hide') : icon.addClass('hide') );
			}
		}		
	},
	
	/**
	 * Add a dependency to the field
	 * 
	 * @param String f
	 * @param Array deps Dependencies for the DomElement f
	 * @param Boolean negate Negate the dependencies
	 */ 
	addDepend : function (f, deps, negate) 
	{	
		//this.depends[f] = new Array;
		this.depends[f] = [deps, negate];
	},
	
	/**
	 * Hydrate an XML request with a form
	 * 
	 * @param XmlPost postObj
	 * @param DomElement form Form to hydrate variables from
	 */
	hydrate : function (postObj, form) 
	{
		var throwStack = [];
	
		// Loop around the fieldset inputs
		form.getElements('input').each(function(el) {
			switch (el.getProperty('type')) {
				case "file":
					throwStack.push(el);
				break;
				case "checkbox":
				case "radio":
					if (el.getProperty('checked') == true) {	
						this.addParam (el.getProperty('name'), el.getProperty('value'));
					}
				break;
				default: 
					this.addParam (el.getProperty('name'), el.getProperty('value'));
			}
		}.bind(postObj));

		// loop over the other elements
		['select','textarea'].each(function(tagType){
			// Look around the fieldset textarea tags
			form.getElements(tagType).each(function(el) {
				this.addParam (el.getProperty('name'), el.getProperty('value'));
			}.bind(this));
		}.bind(postObj));
		
		if (throwStack.length > 0) throw throwStack[0];
		
		return postObj;
	},
	
	/**
	 * Set field values from an XML response
	 * 
	 * @param DomElement xml
	 */
	setFieldValues : function ( xml )
	{
		var resp = new ByngXml(xml);
		$each(resp.getParam('data'), function(data,fieldset) {
			fieldset = this.options.fieldsets[fieldset];
			$each(data, function (value, key) {
				if ($type(this[key]) != "element") {
					return; // skip this field 
				} else {
				 	this[key].value = value;
					this[key].fireEvent('onchange');
				}
			}.bind(fieldset));
		}.bind(this));
		return this;
	},

 	/**
	 * Run form event
	 * 
	 * Method provides abstraction to the JavaScript environment from FormBuilder
	 * 
	 * @param DomElement _element
	 * @param String fn Event to run
	 */
	runEvent : function (_element, fn) 
	{
		try {
 			eval (fn);
		} catch (e) {
			alert ("Invalid form event: " + e.toString());
		}
	}	
});

