/*!
* 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);
