dojo.provide("dojox.lang.aspect");

(function(){
	var d = dojo, aop = dojox.lang.aspect, ap = Array.prototype,
		contextStack = [], context;
		
	// this class implements a topic-based double-linked list
	var Advice = function(){
		this.next_before = this.prev_before =
		this.next_around = this.prev_around =
		this.next_afterReturning = this.prev_afterReturning =
		this.next_afterThrowing = this.prev_afterThrowing =
			this;
		this.counter = 0;
	};
	d.extend(Advice, {
		add: function(advice){
			var dyn = d.isFunction(advice),
				node = {advice: advice, dynamic: dyn};
			this._add(node, "before", "", dyn, advice);
			this._add(node, "around", "", dyn, advice);
			this._add(node, "after", "Returning", dyn, advice);
			this._add(node, "after", "Throwing", dyn, advice);
			++this.counter;
			return node;
		},
		_add: function(node, topic, subtopic, dyn, advice){
			var full = topic + subtopic;
			if(dyn || advice[topic] || (subtopic && advice[full])){
				var next = "next_" + full, prev = "prev_" + full;
				(node[prev] = this[prev])[next] = node;
				(node[next] = this)[prev] = node;
			}
		},
		remove: function(node){
			this._remove(node, "before");
			this._remove(node, "around");
			this._remove(node, "afterReturning");
			this._remove(node, "afterThrowing");
			--this.counter;
		},
		_remove: function(node, topic){
			var next = "next_" + topic, prev = "prev_" + topic;
			if(node[next]){
				node[next][prev] = node[prev];
				node[prev][next] = node[next];
			}
		},
		isEmpty: function(){
			return !this.counter;
		}
	});

	var getDispatcher = function(){
	
		return function(){
			
			var self = arguments.callee,	// the join point
				advices = self.advices,		// list of advices for this joinpoint
				ret, i, a, e, t;

			// push context
			if(context){ contextStack.push(context); }
			context = {
				instance: this,					// object instance
				joinPoint: self,				// join point
				depth: contextStack.length,		// current level of depth starting from 0
				around: advices.prev_around,	// pointer to the current around advice
				dynAdvices: [],					// array of dynamic advices if any
				dynIndex: 0						// index of a dynamic advice
			};

			try{
				// process before events
				for(i = advices.prev_before; i != advices; i = i.prev_before){
					if(i.dynamic){
						// instantiate a dynamic advice
						context.dynAdvices.push(a = new i.advice(context));
						if(t = a.before){ // intentional assignment
							t.apply(a, arguments);
						}
					}else{
						t = i.advice;
						t.before.apply(t, arguments);
					}
				}

				// process around and after events
				try{
					// call the around advice or the original method
					ret = (advices.prev_around == advices ? self.target : aop.proceed).apply(this, arguments);
				}catch(e){
					// process after throwing and after events
					context.dynIndex = context.dynAdvices.length;
					for(i = advices.next_afterThrowing; i != advices; i = i.next_afterThrowing){
						a = i.dynamic ? context.dynAdvices[--context.dynIndex] : i.advice;
						if(t = a.afterThrowing){ // intentional assignment
							t.call(a, e);
						}
						if(t = a.after){ // intentional assignment
							t.call(a);
						}
					}
					// continue the exception processing
					throw e;
				}
				// process after returning and after events
				context.dynIndex = context.dynAdvices.length;
				for(i = advices.next_afterReturning; i != advices; i = i.next_afterReturning){
					a = i.dynamic ? context.dynAdvices[--context.dynIndex] : i.advice;
					if(t = a.afterReturning){ // intentional assignment
						t.call(a, ret);
					}
					if(t = a.after){ // intentional assignment
						t.call(a);
					}
				}
				// process dojo.connect() listeners
				var ls = self._listeners;
				for(i in ls){
					if(!(i in ap)){
						ls[i].apply(this, arguments);
					}
				}
			}finally{
				// destroy dynamic advices
				for(i = 0; i < context.dynAdvices.length; ++i){
					a = context.dynAdvices[i];
					if(a.destroy){
						a.destroy();
					}
				}
				// pop context
				context = contextStack.length ? contextStack.pop() : null;
			}
			
			return ret;
		};
	};

	aop.advise = function(/*Object*/ obj,
						/*String|RegExp|Array*/ method,
						/*Object|Function|Array*/ advice
						){
		// summary:
		//		Attach AOP-style advices to a method.
		//
		// description:
		//		Attaches AOP-style advices to a method. Can attach several
		//		advices at once and operate on several methods of an object.
		//		The latter is achieved when a RegExp is specified as
		//		a method name, or an array of strings and regular expressions
		//		is used. In this case all functional methods that
		//		satisfy the RegExp condition are processed. This function
		//		returns a handle, which can be used to unadvise, or null,
		//		if advising has failed.
		//		
		//		This function is a convenience wrapper for
		//		dojox.lang.aspect.adviseRaw().
		//
		// obj:
		//		A source object for the advised function. Cannot be a DOM node.
		//		If this object is a constructor, its prototype is advised.
		//
		// method:
		//		A string name of the function in obj. In case of RegExp all
		//		methods of obj matching the regular expression are advised.
		//
		// advice:
		//		An object, which defines advises, or a function, which
		//		returns such object, or an array of previous items.
		//		The advice object can define following member functions:
		//		before, around, afterReturning, afterThrowing, after.
		//		If the function is supplied, it is called with a context
		//		object once per call to create a temporary advice object, which
		//		is destroyed after the processing. The temporary advice object
		//		can implement a destroy() method, if it wants to be called when
		//		not needed.
		
		if(typeof obj != "object"){
			obj = obj.prototype;
		}

		var methods = [];
		if(!(method instanceof Array)){
			method = [method];
		}
		
		// identify advised methods
		for(var j = 0; j < method.length; ++j){
			var t = method[j];
			if(t instanceof RegExp){
				for(var i in obj){
					if(d.isFunction(obj[i]) && t.test(i)){
						methods.push(i);
					}
				}
			}else{
				if(d.isFunction(obj[t])){
					methods.push(t);
				}
			}
		}

		if(!d.isArray(advice)){ advice = [advice]; }

		return aop.adviseRaw(obj, methods, advice);	// Object
	};
	
	aop.adviseRaw = function(/*Object*/ obj,
						/*Array*/ methods,
						/*Array*/ advices
						){
		// summary:
		//		Attach AOP-style advices to methods.
		//
		// description:
		//		Attaches AOP-style advices to object's methods. Can attach several
		//		advices at once and operate on several methods of the object.
		//		The latter is achieved when a RegExp is specified as
		//		a method name. In this case all functional methods that
		//		satisfy the RegExp condition are processed. This function
		//		returns a handle, which can be used to unadvise, or null,
		//		if advising has failed.
		//
		// obj:
		//		A source object for the advised function.
		//		Cannot be a DOM node.
		//
		// methods:
		//		An array of method names (strings) to be advised.
		//
		// advices:
		//		An array of advices represented by objects or functions that
		//		return such objects on demand during the event processing.
		//		The advice object can define following member functions:
		//		before, around, afterReturning, afterThrowing, after.
		//		If the function is supplied, it is called with a context
		//		object once per call to create a temporary advice object, which
		//		is destroyed after the processing. The temporary advice object
		//		can implement a destroy() method, if it wants to be called when
		//		not needed.

		if(!methods.length || !advices.length){ return null; }
		
		// attach advices
		var m = {}, al = advices.length;
		for(var i = methods.length - 1; i >= 0; --i){
			var name = methods[i], o = obj[name], ao = new Array(al), t = o.advices;
			// create a stub, if needed
			if(!t){
				var x = obj[name] = getDispatcher();
				x.target = o.target || o;
				x.targetName = name;
				x._listeners = o._listeners || [];
				x.advices = new Advice;
				t = x.advices;
			}
			// attach advices
			for(var j = 0; j < al; ++j){
				ao[j] = t.add(advices[j]);
			}
			m[name] = ao;
		}
		
		return [obj, m];	// Object
	};

	aop.unadvise = function(/*Object*/ handle){
		// summary:
		//		Detach previously attached AOP-style advices.
		//
		// handle:
		//		The object returned by dojox.lang.aspect.advise().
		
		if(!handle){ return; }
		var obj = handle[0], methods = handle[1];
		for(var name in methods){
			var o = obj[name], t = o.advices, ao = methods[name];
			for(var i = ao.length - 1; i >= 0; --i){
				t.remove(ao[i]);
			}
			if(t.isEmpty()){
				// check if we can remove all stubs
				var empty = true, ls = o._listeners;
				if(ls.length){
					for(i in ls){
						if(!(i in ap)){
							empty = false;
							break;
						}
					}
				}
				if(empty){
					// revert to the original method
					obj[name] = o.target;
				}else{
					// replace with the dojo.connect() stub
					var x = obj[name] = d._listener.getDispatcher();
					x.target = o.target;
					x._listeners = ls;
				}
			}
		}
	};
	
	aop.getContext = function(){
		// summary:
		//		Returns the context information for the advice in effect.
		
		return context;	// Object
	};
	
	aop.getContextStack = function(){
		// summary:
		//		Returns the context stack, which reflects executing advices
		//		up to this point. The array is ordered from oldest to newest.
		//		In order to get the active context use dojox.lang.aspect.getContext().
		
		return contextStack;	// Array
	};
	
	aop.proceed = function(){
		// summary:
		//		Call the original function (or the next level around advice) in an around advice code.
		//
		// description:
		//		Calls the original function (or the next level around advice).
		//		Accepts and passes on any number of arguments, and returns a value.
		//		This function is valid only in the content of around calls.
		
		var joinPoint = context.joinPoint, advices = joinPoint.advices;
		for(var c = context.around; c != advices; c = context.around){
			context.around = c.prev_around;	// advance the pointer
			if(c.dynamic){
				var a = context.dynAdvices[context.dynIndex++], t = a.around;
				if(t){
					return t.apply(a, arguments);
				}
			}else{
				return c.advice.around.apply(c.advice, arguments);
			}
		}
		return joinPoint.target.apply(context.instance, arguments);
	};
})();

/*
Aspect = {
	before: function(arguments){...},
	around: function(arguments){...returns value...},
	afterReturning: function(ret){...},
	afterThrowing: function(excp){...},
	after: function(){...}
};

Context = {
	instance:  ..., // the instance we operate on
	joinPoint: ...,	// Object (see below)
	depth:     ...	// current depth of the context stack
};

JoinPoint = {
	target:     ...,	// the original function being wrapped
	targetName: ...		// name of the method
};
*/
