/*!
 * jQuery Data Link Plugin
 * http://github.com/jquery/jquery-datalink
 *
 * Copyright Software Freedom Conservancy, Inc.
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 */
(function($){

var oldcleandata = $.cleanData,
	links = [],
	fnSetters = {
		val: "val",
		html: "html",
		text: "text"
	};

function setValue(target, field, value) {
	if ( target.nodeType ) {
		var setter = fnSetters[ field ] || "attr";
		$(target)[setter](value);
	} else {
		$(target).data( field, value );
	}
}

function getLinks(obj) {
	var data = $.data( obj ),
		cache,
		fn = data._getLinks || (cache={s:[], t:[]}, data._getLinks = function() { return cache; });
	return fn();
}

function bind(obj, wrapped, handler) {
	wrapped.bind( obj.nodeType ? "change" : "changeData", handler );
}
function unbind(obj, wrapped, handler) {
	wrapped.unbind( obj.nodeType ? "change" : "changeData", handler );
}

$.extend({
	cleanData: function( elems ) {
		for ( var j, i = 0, elem; (elem = elems[i]) != null; i++ ) {
			// remove any links with this element as the source
			// or the target.
			var links = $.data( elem, "_getLinks" );
			if ( links ) {
				links = links();
				// links this element is the source of
				var self = $(elem);
				$.each(links.s, function() {
					unbind( elem, self, this.handler );
					if ( this.handlerRev ) {
						unbind( this.target, $(this.target), this.handlerRev );
					}
				});
				// links this element is the target of
				$.each(links.t, function() {
					unbind( this.source, $(this.source), this.handler );
					if ( this.handlerRev ) {
						unbind( elem, self, this.handlerRev );
					}
				});
				links.s = [];
				links.t = [];
			}
		}
		oldcleandata( elems );
	},
	convertFn: {
		"!": function(value) {
			return !value;
		}
	}
});

function getMapping(ev, changed, newvalue, map, source, target) {
	var target = ev.target,
		isSetData = ev.type === "changeData",
		mappedName,
		convert,
		name;
	if ( isSetData ) {
		name = changed;
		if ( ev.namespace ) {
			name += "." + ev.namespace;
		}
	} else {
		name = (target.name || target.id);
	}
	
	if ( !map ) {
		mappedName = name;
	} else {
		var m = map[ name ];
		if ( !m ) {
			return null;
		}
		mappedName = m.name;
		convert = m.convert;
		if ( typeof convert === "string" ) {
			convert = $.convertFn[ convert ];
		}
	}
	return {
		name: mappedName,
		convert: convert,
		value: isSetData ? newvalue : $(target).val()
	};
}

$.extend($.fn, {
	link: function(target, mapping) {
		var self = this;
		if ( !target ) {
			return self;
		}
		function matchByName(name) {
			var selector = "[name=" + name + "], [id=" + name +"]";
			// include elements in this set that match as well a child matches
			return self.filter(selector).add(self.find(selector));
		}
		if ( typeof target === "string" ) {
			target = $( target, this.context || null )[ 0 ];
		}
		var hasTwoWay = !mapping,
			map,
			mapRev,
			handler = function(ev, changed, newvalue) {
				// a dom element change event occurred, update the target
				var m = getMapping( ev, changed, newvalue, map );
				if ( m ) {
					var name = m.name,
						value = m.value,
						convert = m.convert;
					if ( convert ) {
						value = convert( value, ev.target, target );
					}
					if ( value !== undefined ) {
						setValue( target, name, value );
					}
				}
			},
			handlerRev = function(ev, changed, newvalue) {
				// a change or changeData event occurred on the target,
				// update the corresponding source elements
				var m = getMapping( ev, changed, newvalue, mapRev );
				if ( m ) {
					var name = m.name,
						value = m.value,
						convert = m.convert;
					// find elements within the original selector
					// that have the same name or id as the field that updated
					matchByName(name).each(function() {
						newvalue = value;
						if ( convert ) {
							newvalue = convert( newvalue, target, this );
						}
						if ( newvalue !== undefined ) {
							setValue( this, "val", newvalue );
						}
					});
				}
				
			};
		if ( mapping ) {
			$.each(mapping, function(n, v) {
				var name = v,
					convert,
					convertBack,
					twoWay;
				if ( $.isPlainObject( v ) ) {
					name = v.name || n;
					convert = v.convert;
					convertBack = v.convertBack;
					twoWay = v.twoWay !== false;
					hasTwoWay |= twoWay;
				} else {
					hasTwoWay = twoWay = true;
				}
				if ( twoWay ) {
					mapRev = mapRev || {};
					mapRev[ n ] = {
						name: name,
						convert: convertBack
					};
				}
				map = map || {};
				map[ name ] = { name: n, convert: convert, twoWay: twoWay };
			});
		}

		// associate the link with each source and target so it can be
		// removed automaticaly when _either_ side is removed.
		self.each(function() {
			bind( this, $(this), handler );
			var link = {
				handler: handler,
				handlerRev: hasTwoWay ? handlerRev : null,
				target: target,
				source: this
			};
			getLinks( this ).s.push( link );
			if ( target.nodeType ) {
				getLinks( target ).t.push( link );
			}
		});
		if ( hasTwoWay ) {
			bind( target, $(target), handlerRev );
		}
		return self;
	},
	unlink: function(target) {
		this.each(function() {
			var self = $(this),
				links = getLinks( this ).s;
			for (var i = links.length-1; i >= 0; i--) {                
				var link = links[ i ];
				if ( link.target === target ) {
					// unbind the handlers
					//wrapped.unbind( obj.nodeType ? "change" : "changeData", handler );
					unbind( this, self, link.handler );
					if ( link.handlerRev ) {
						unbind( link.target, $(link.target), link.handlerRev );
					}
					// remove from source links
					links.splice( i, 1 );
					// remove from target links
					var targetLinks = getLinks( link.target ).t,
						index = $.inArray( link, targetLinks );
					if ( index !== -1 ) {
						targetLinks.splice( index, 1 );
					}
				}
			}
		});
	}
});

})(jQuery);

