define([
	"dojo/_base/array",
	"dojo/_base/lang",
	"dojo/_base/declare",
	"dojo/has",
	"./resolve",
	"./sync",
	"dojo/Stateful"
], function(array, lang, declare, has, resolve, sync, Stateful){
	if(has("mvc-bindings-log-api")){
		function getLogContent(/*dojo/Stateful*/ target, /*String*/ targetProp){
			return [target._setIdAttr || !target.declaredClass ? target : target.declaredClass, targetProp].join(":");
		}

		function logResolveFailure(target, targetProp){
			console.warn(targetProp + " could not be resolved" + (typeof target == "string" ? (" with " + target) : "") + ".");
		}
	}

	function getParent(/*dijit/_WidgetBase*/ w){
		// summary:
		//		Returns parent widget having data binding target for relative data binding.
		// w: dijit/_WidgetBase
		//		The widget.

		// Usage of dijit/registry module is optional. Return null if it's not already loaded.
		var registry;
		try{
			registry = require("dijit/registry");
		}catch(e){
			return;
		}
		var pn = w.domNode && w.domNode.parentNode, pw, pb;
		while(pn){
			pw = registry.getEnclosingWidget(pn);
			if(pw){
				var relTargetProp = pw._relTargetProp || "target", pt = lang.isFunction(pw.get) ? pw.get(relTargetProp) : pw[relTargetProp];
				if(pt || relTargetProp in pw.constructor.prototype){
					return pw; // dijit/_WidgetBase
				}
			}
			pn = pw && pw.domNode.parentNode;
		}
	}

	function bind(/*dojo/Stateful|String*/ source, /*String*/ sourceProp, /*dijit/_WidgetBase*/ target, /*String*/ targetProp, /*obno/mvc/sync.options*/ options){
		// summary:
		//		Resolves the data binding literal, and starts data binding.
		// source: dojo/Stateful|String
		//		Source data binding literal or dojo/Stateful to be synchronized.
		// sourceProp: String
		//		The property name in source to be synchronized.
		// target: dijit/_WidgetBase
		//		Target dojo/Stateful to be synchronized.
		// targetProp: String
		//		The property name in target to be synchronized.
		// options: obno/mvc/sync.options
		//		Data binding options.

		//if targetProp is a path then find real target and real targetProp
		if (sourceProp && source)
		{
			var segments = sourceProp.split(".");
			if (segments && segments.length > 1)
			{
				var tmp=source;
				for (var i=0; i<segments.length; i++){
					if (i==segments.length - 1){
						//last segment
						sourceProp = segments[i];
						source = tmp;
						if (!source.get(sourceProp))
						{
							//create sourceProp if not present
							source.set(sourceProp, null);
						}
					}else{
						var s = tmp.get(segments[i]);
						if (s && s.get)
						{
							tmp=s;
						}
						else
						{
							//if it does not exist create a new stateful
							var n = new Stateful();
							tmp.set(segments[i], n);
							tmp = n;
						}
					}
				}
			}
		}
		var _handles = {}, parent = getParent(target), relTargetProp = parent && parent._relTargetProp || "target";

		function resolveAndBind(){
			_handles["Two"] && _handles["Two"].unwatch();
			delete _handles["Two"];

			var relTarget = parent && (lang.isFunction(parent.get) ? parent.get(relTargetProp) : parent[relTargetProp]),
			 resolvedSource = resolve(source, relTarget),
			 resolvedTarget = resolve(target, relTarget);

			if(has("mvc-bindings-log-api") && (!resolvedSource || /^rel:/.test(source) && !parent)){ logResolveFailure(source, sourceProp); }
			if(has("mvc-bindings-log-api") && (!resolvedTarget || /^rel:/.test(target) && !parent)){ logResolveFailure(target, targetProp); }
			if(!resolvedSource || !resolvedTarget || (/^rel:/.test(source) || /^rel:/.test(target)) && !parent){ return; }
			if((!resolvedSource.set || !resolvedSource.watch) && sourceProp == "*"){
				if(has("mvc-bindings-log-api")){ logResolveFailure(source, sourceProp); }
				return;
			}

			if(sourceProp == null){
				// If source property is not specified, it means this handle is just for resolving data binding target.
				// (For obno/mvc/Group and obno/mvc/Repeat)
				// Do not perform data binding synchronization in such case.
				lang.isFunction(resolvedTarget.set) ? resolvedTarget.set(targetProp, resolvedSource) : (resolvedTarget[targetProp] = resolvedSource);
				if(has("mvc-bindings-log-api")){
					console.log("obno/mvc/_atBindingMixin set " + resolvedSource + " to: " + getLogContent(resolvedTarget, targetProp));
				}
			}else{
				// Start data binding
				_handles["Two"] = sync(resolvedSource, sourceProp, resolvedTarget, targetProp, options); // obno/mvc/sync.handle
			}
		}

		resolveAndBind();
		if(parent && /^rel:/.test(source) || /^rel:/.test(target) && lang.isFunction(parent.set) && lang.isFunction(parent.watch)){
			_handles["rel"] = parent.watch(relTargetProp, function(name, old, current){
				if(old !== current){
					if(has("mvc-bindings-log-api")){ console.log("Change in relative data binding target: " + parent); }
					resolveAndBind();
				}
			});
		}
		var h = {};
		h.unwatch = h.remove = function(){
			for(var s in _handles){
				_handles[s] && _handles[s].unwatch();
				delete _handles[s];
			}
		};
		return h;
	}

	// TODO: Like _DataBindingMixin, this should probably just be a plain Object rather than a Class
	var _atBindingMixin = declare("obno/mvc/_atBindingMixin", null, {
		// summary:
		//		The mixin for dijit/_WidgetBase to support data binding.

		// dataBindAttr: String
		//		The attribute name for data binding.
		dataBindAttr: "data-mvc-bindings",

		_dbpostscript: function(/*Object?*/ params, /*DomNode|String*/ srcNodeRef){
			// summary:
			//		See if any parameters for this widget are obno/mvc/at handles.
			//		If so, move them under this._refs to prevent widget implementations from referring them.

			var refs = this._refs = (params || {}).refs || {};
			for(var prop in params){
				if((params[prop] || {}).atsignature == "obno.mvc.at"){
					var h = params[prop];
					delete params[prop];
					refs[prop] = h;
				}
			}
		},

		_startAtWatchHandles: function(){
			// summary:
			//		Establish data bindings based on obno/mvc/at handles.

			var refs = this._refs;
			if(refs){
				var atWatchHandles = this._atWatchHandles = this._atWatchHandles || {};

				// Clear the cache of properties that data binding is established with
				this._excludes = null;

				// First, establish non-wildcard data bindings
				for(var prop in refs){
					if(!refs[prop] || prop == "*"){ continue; }
					atWatchHandles[prop] = bind(refs[prop].target, refs[prop].targetProp, this, prop, {bindDirection: refs[prop].bindDirection, converter: refs[prop].converter});
				}

				// Then establish wildcard data bindings
				if((refs["*"] || {}).atsignature == "obno.mvc.at"){
					atWatchHandles["*"] = bind(refs[prop].target, refs["*"].targetProp, this, "*", {bindDirection: refs["*"].bindDirection, converter: refs["*"].converter});
				}
			}
		},

		_stopAtWatchHandles: function(){
			// summary:
			//		Stops data binding synchronization handles as widget is destroyed.

			for(var s in this._atWatchHandles){
				this._atWatchHandles[s].unwatch();
				delete this._atWatchHandles[s];
			}
		},

		_setAtWatchHandle: function(/*String*/ name, /*Anything*/ value){
			// summary:
			//		Called if the value is a obno/mvc/at handle.
			//		If this widget has started, start data binding with the new obno/mvc/at handle.
			//		Otherwise, queue it up to this._refs so that _dbstartup() can pick it up.

			if(name == "ref"){
				throw new Error(this + ": 1.7 ref syntax used in conjuction with 1.8 obno/mvc/at syntax, which is not supported.");
			}

			// Claen up older data binding
			var atWatchHandles = this._atWatchHandles = this._atWatchHandles || {};
			if(atWatchHandles[name]){
				atWatchHandles[name].unwatch();
				delete atWatchHandles[name];
			}

			// Claar the value
			this[name] = null;

			// Clear the cache of properties that data binding is established with
			this._excludes = null;

			if(this._started){
				// If this widget has been started already, establish data binding immediately.
				atWatchHandles[name] = bind(value.target, value.targetProp, this, name, {bindDirection: value.bindDirection, converter: value.converter});
			}else{
				// Otherwise, queue it up to this._refs so that _dbstartup() can pick it up.
				this._refs[name] = value;
			}
		},

		_setBind: function(/*Object*/ value){
			// summary:
			//		Sets data binding described in data-mvc-bindings.

			var list = eval("({" + value + "})");
			for(var prop in list){
				var h = list[prop];
				if((h || {}).atsignature != "obno.mvc.at"){
					console.warn(prop + " in " + dataBindAttr + " is not a data binding handle.");
				}else{
					this._setAtWatchHandle(prop, h);
				}
			}
		},

		_getExcludesAttr: function(){
			// summary:
			//		Returns list of all properties that data binding is established with.

			if(this._excludes){ 
				return this._excludes;  // String[] 
			}
			var list = [];
			for(var s in this._atWatchHandles){
				if(s != "*"){ list.push(s); }
			}
			return list; // String[]
		},

		_getPropertiesAttr: function(){
			// summary:
			//		Returns list of all properties in this widget, except "id".
			// returns: String[]
			//		 The list of all properties in this widget, except "id"..

			if(this.constructor._attribs){
				return this.constructor._attribs; // String[]
			}
			var list = ["onClick"].concat(this.constructor._setterAttrs);
			array.forEach(["id", "excludes", "properties", "ref", "binding"], function(s){
				var index = array.indexOf(list, s);
				if(index >= 0){ list.splice(index, 1); }
			});
			return this.constructor._attribs = list; // String[]
		}
	});

	_atBindingMixin.prototype[_atBindingMixin.prototype.dataBindAttr] = ""; // Let parser treat the attribute as string
	return _atBindingMixin;
});