/******************************************************************************
This work is licensed under the Creative Commons Attribution-Share 
Alike 3.0 Unported License. To view a copy of this license, visit 
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter 
to Creative Commons, 171 Second Street, Suite 300, 
San Francisco, California, 94105, USA.
*****************************************************************************/

/**
	Binding
	
	Binds the properties on objects together so that they are always in sync.
	
	Usage:
	
		Binding.bind(object, property, object2, property2);
		object.set('property', 'value');
		object2.get('property2'); // returns 'value';

*/
var Binding = function() {
	
	var BINDINGS = [],
		ID_COUNTER = 0,
		TRANSACTION_ID = 0;
	
	/**
		Bind
		@public
		@param	object	source
		@param	string	sourceProperty
		@param	object	target
		@param	string	targetProperty
		@param	boolean	bidirectional	optional	default true
	*/
	function bind(source, sourceProperty, target, targetProperty, bidirectional) {
		Binding.Casting.cast(source);
		Binding.Casting.cast(target);
		
		makeBindable(source).add(sourceProperty, target, targetProperty);
		
		if (bidirectional !== false) {
			makeBindable(target).add(targetProperty, source, sourceProperty);
		}
		
		trigger(source, sourceProperty);
	}
	
	/**
	 	makeBindable
		@private
		@param	object	object
	*/
	function makeBindable(object) {
		if (typeof object._BINDING_ID === "undefined" || ! BINDINGS[object._BINDING_ID]) {
			object._BINDING_ID = ++ID_COUNTER;
			BINDINGS[object._BINDING_ID] = new Binding.Object(object);
		}
		return BINDINGS[object._BINDING_ID];
	}
	
	/**
		trigger
		If this is the initial trigger called by a bound source object,
		a new transaction id is generated.
		@protected
		@param	object	source
		@param 	string	property
		@param	number	transactionId	optional
	*/
	function trigger(source, property, transactionId) {
		if (BINDINGS[source._BINDING_ID]) {
			if (typeof transactionId === "undefined") {
				transactionId = ++TRANSACTION_ID;
				source._LAST_TRANSACTION = transactionId;
			}
			BINDINGS[source._BINDING_ID].trigger(property, transactionId);
		}
	}
	
	return {
		bind: bind,
		trigger: trigger
	}
}();


/**
	Binding.Object
	
	Stores the bindings for an object in arrays based on the bound property
*/
Binding.Object = function(object) {
	this.object = object;
	this.bindings = {};
}
Binding.Object.prototype = {
	/**
		add
		@public
		@param	string	property
		@param	object	target
		@param	string	targetProperty
	*/
	add : function(property, target, targetProperty) {
		if (! this.bindings[property]) {
			this.bindings[property] = [];
		}
		this.bindings[property].push({ target: target, property: targetProperty });
	},
	
	/**
		trigger
		@public
		@param	string	property
		@param	number	transactionId
	*/
	trigger : function(property, transactionId) {
		if (this.bindings[property]) {
			var binding;
			for (var i = 0, j = this.bindings[property].length; i < j; i++) {
				binding = this.bindings[property][i];
				if (binding.target._LAST_TRANSACTION !== transactionId) {
					binding.target._LAST_TRANSACTION = transactionId;
					binding.target.set(binding.property, this.object.get(property), transactionId);
				}
			}
		}
	}
}

/**
	Binding.Casting
	
	Utility for making objects bindable. Adds `get` and `set` methods,
	and includes event listeners for basic HTML form UI elements.
	
	Usage:
		
		Binding.Casting.cast(object);
*/
Binding.Casting = function() {
	
	/**
	 	cast
		@public
		@param	object	object
	*/
	function cast(object) {
		if (object._BINDABLE) {
			return;
		}
		
		// adds a generic getter method
		object.get = function(property) {
			return get(this, property);
		};
		
		// adds a generic setter method
		object.set = function(property, value, transactionId) {
			return set(this, property, value, transactionId)
		};
		
		if (object.nodeName && Elements[object.nodeName]) {
			castElement(object);
		}
		object._BINDABLE = true;
	}
	
	/**
	 	castElement
		@private
		@param	object	object
	*/
	function castElement(object) {
		// look through the Element definitions based on nodeName
		if (typeof Elements[object.nodeName] === "function") {
			Elements[object.nodeName](object);
		} else {
			// look through the Element definitions based on the values of an attribute
			for (var attribute in Elements[object.nodeName]) {
				for (var subkey in Elements[object.nodeName][attribute]) {
					if (object[attribute] === subkey) {
						Elements[object.nodeName][attribute][subkey](object);
					}
				}
			}
		}
	}
	
	/**
	 	get
		@private
		@param	object	object
		@param	string	property
	*/
	function get(object, property) {
		return object[property];
	}
	
	/**
	 	set
		@private
		@param	object	object
		@param	string	property
	*/
	function set(object, property, value, transactionId) {
		object[property] = value;
		Binding.trigger(object, property, transactionId);
		return object;
	}
	
	var Elements = {
		INPUT : {
			type : {
				// adds event to track changes in value
				text : function(object) {
					object.addEventListener("keyup", function(event) {
						Binding.trigger(object, "value");
					}, false);
				},
				// adds mouse event to track changes in value
				checkbox : function(object) {
					object.addEventListener("click", function(event) {
						Binding.trigger(object, "checked");
					}, false);
				}
			}
		},
		SELECT : function(object) {
			// implements getter method for `value`
			object.get = function(property) {
				if (property === "value") {
					return this.options[object.selectedIndex].value;
				} else {
					return get(this, property);
				}
			};
			
			// implements setter method for `value`
			object.set = function(property, value, transactionId) {
				if (property === "value") {
					for (var i = 0, j = this.options.length; i < j; i++) {
						if (this.options[i].value === value) {
							this.selectedIndex = i;
						}
					}
				}
				return set(this, property, value, transactionId);
			}
			
			// adds event to track changes in value
			object.addEventListener("change", function(event) {
				object._value = object.options[object.selectedIndex].value;
				Binding.trigger(object, "value");
			}, false);
		},
		// adds keyboard event to track changes in value
		TEXTAREA : function(object) {
			object.addEventListener("keyup", function(event) {
				Binding.trigger(object, "value");
			}, false);
		}
	};
	
	return {
		cast: cast
	}
}();