define("dojox/css3/transition", ["dojo/_base/lang",
		"dojo/_base/array",
		"dojo/Deferred",
		"dojo/when",
		"dojo/promise/all",
		"dojo/on",
		"dojo/sniff"],
		function(lang, array, Deferred, when, all, on, has){
	// module: 
	//		dojox/css3/transition
	
	//create cross platform animation/transition effects
	//TODO enable opera mobile when it is hardware accelerated
	// IE10 is using standard names so this is working without any modifications
	var transitionEndEventName = "transitionend";
	var transitionPrefix = "t"; //by default use "t" prefix and "ransition" to make word "transition"
	var translateMethodStart = "translate3d(";//Android 2.x does not support translateX in CSS Transition, we need to use translate3d in webkit browsers
	var translateMethodEnd = ",0,0)";
	if(has("webkit")){
		transitionPrefix = "WebkitT";
		transitionEndEventName = "webkitTransitionEnd";
	}else if(has("mozilla")){
		transitionPrefix = "MozT";
		translateMethodStart = "translateX(";
		translateMethodEnd = ")";
	}
	
	//TODO find a way to lock the animation and prevent animation conflict
	//Use the simple object inheritance
	var transition = function(/*Object?*/args){
		// summary:
		//		This module defines the transition utilities which can be used
		//		to perform transition effects based on the CSS Transition standard.
		// args:
		//		The arguments which will be mixed into this transition object.
		
		//default config should be in animation object itself instead of its prototype
		//otherwise, it might be easy for making mistake of modifying prototype
		var defaultConfig = {
				startState: {},
				endState: {},
				node: null,
				duration: 250,
				"in": true,
				direction: 1,
				autoClear: true
		};

		lang.mixin(this, defaultConfig);
		lang.mixin(this, args);

		//create the deferred object which will resolve after the animation is finished.
		//We can rely on "onAfterEnd" function to notify the end of a single animation,
		//but using a deferred object is easier to wait for multiple animations end.
		if(!this.deferred){
			this.deferred = new Deferred();
		}
	};
	
	lang.extend(transition, {
		
		play: function(){
			// summary:
			//		Plays the transition effect defined by this transition object.
			transition.groupedPlay([this]);
		},
		
		//method to apply the state of the transition
		_applyState: function(state){
			var style = this.node.style;
			for(var property in state){
				if(state.hasOwnProperty(property)){
					style[property] = state[property];
				}
			}
		},
		
		
		initState: function(){
			// summary:
			//		Method to initialize the state for a transition.
			
			//apply the immediate style change for initial state.
			this.node.style[transitionPrefix + "ransitionProperty"] = "none";
			this.node.style[transitionPrefix + "ransitionDuration"] = "0ms";
			this._applyState(this.startState);
			
		},
		
		_beforeStart: function(){
			if (this.node.style.display === "none"){
				this.node.style.display = "";
			}
			this.beforeStart();
		},
		
		_beforeClear: function(){
			this.node.style[transitionPrefix + "ransitionProperty"] = "";
			this.node.style[transitionPrefix + "ransitionDuration"] = "";
			if(this["in"] !== true){
				this.node.style.display = "none";
			}			 
			this.beforeClear();
		},
		
		_onAfterEnd: function(){
			this.deferred.resolve(this.node);
			if(this.node.id && transition.playing[this.node.id]===this.deferred){
				delete transition.playing[this.node.id];
			}
			this.onAfterEnd();
		},
		
		beforeStart: function(){
			// summary:
			//		The callback which will be called right before the start
			//		of the transition effect.
		},
		
		beforeClear: function(){
			// summary:
			//		The callback which will be called right after the end
			//		of the transition effect and before the final state is
			//		cleared.
		},
		
		onAfterEnd: function(){
			// summary:
			//		The callback which will be called right after the end
			//		of the transition effect and after the final state is
			//		cleared.
		},
		
		start: function(){
			// summary:
			//		Method to start the transition.
			this._beforeStart();
			this._startTime = new Date().getTime(); // set transition start timestamp
			this._cleared = false; // set clear flag to false

			var self = this;
			//change the transition duration
			self.node.style[transitionPrefix + "ransitionProperty"] = "all";
			self.node.style[transitionPrefix + "ransitionDuration"] = self.duration + "ms";
			
			//connect to clear the transition state after the transition end.
			//Since the transition is conducted asynchronously, we need to 
			//connect to transition end event to clear the state
			on.once(self.node, transitionEndEventName, function(){
				self.clear();
			});
			
			this._applyState(this.endState);
		},
		
		clear: function(){
			// summary:
			//		Method to clear the state after a transition.
			if(this._cleared) {
				return;
			}
			this._cleared = true; // set clear flag to true

			this._beforeClear();
			this._removeState(this.endState);
			// console.log(this.node.id + " clear.");
			this._onAfterEnd();
		},
		
		//create removeState method
		_removeState: function(state){
			var style = this.node.style;
			for(var property in state){
				if(state.hasOwnProperty(property)){
					style[property] = "";
				}
			}
		}
		
	});
	
	//TODO add the lock mechanism for all of the transition effects
	//	   consider using only one object for one type of transition.
	
	transition.slide = function(node, config){
		// summary:
		//		Method which is used to create the transition object of a slide effect.
		// node:
		//		The node that the slide transition effect will be applied on.
		// config:
		//		The cofig arguments which will be mixed into this transition object.

		//create the return object and set the startState, endState of the return
		var ret = new transition(config);
		ret.node = node;
		
		var startX = "0";
		var endX = "0";
		
		if(ret["in"]){
			if(ret.direction === 1){
				startX = "100%";
			}else{
				startX = "-100%";
			}
		}else{
			if(ret.direction === 1){
				endX = "-100%";
			}else{
				endX = "100%";
			}
		}
		
		ret.startState[transitionPrefix + "ransform"]=translateMethodStart+startX+translateMethodEnd;
		
		ret.endState[transitionPrefix + "ransform"]=translateMethodStart+endX+translateMethodEnd;
		
		return ret;
	};
		
	transition.fade = function(node, config){
		// summary:
		//		Method which is used to create the transition object of fade effect.
		// node:
		//		The node that the fade transition effect will be applied on.
		// config:
		//		The cofig arguments which will be mixed into this transition object.
		var ret = new transition(config);
		ret.node = node;
		
		var startOpacity = "0";
		var endOpacity = "0";
		
		if(ret["in"]){
			endOpacity = "1";
		}else{
			startOpacity = "1";
		}
		
		lang.mixin(ret, {
			startState:{
				"opacity": startOpacity
			},
			endState:{
				"opacity": endOpacity
			}
		});
		
		return ret;
	};
	
	transition.flip = function(node, config){
		// summary:
		//		Method which is used to create the transition object of flip effect.
		// node:
		//		The node that the flip transition effect will be applied on.
		// config:
		//		The cofig arguments which will be mixed into this transition object.
		
		var ret = new transition(config);
		ret.node = node;
	   
		if(ret["in"]){
			//Need to set opacity here because Android 2.2 has bug that
			//scale(...) in transform does not persist status
			lang.mixin(ret,{
				startState:{
					"opacity": "0"
				},
				endState:{
					"opacity": "1"
				}
			});
			ret.startState[transitionPrefix + "ransform"]="scale(0,0.8) skew(0,-30deg)";
			ret.endState[transitionPrefix + "ransform"]="scale(1,1) skew(0,0)";
		}else{
			lang.mixin(ret,{
				startState:{
					"opacity": "1"
				},
				endState:{
					"opacity": "0"
				}
			});			
			ret.startState[transitionPrefix + "ransform"]="scale(1,1) skew(0,0)";
			ret.endState[transitionPrefix + "ransform"]="scale(0,0.8) skew(0,30deg)";
		}
		
		return ret;
	};
	
	var getWaitingList = function(/*Array*/ nodes){
		var defs = [];
		array.forEach(nodes, function(node){
			//check whether the node is under other animation
			if(node.id && transition.playing[node.id]){
				//hook on deferred object in transition.playing
				defs.push(transition.playing[node.id]);
			}
			
		});
		return all(defs);
	};
	
	transition.getWaitingList = getWaitingList;
	
	transition.groupedPlay = function(/*Array*/args){
		// summary:
		//		The method which groups multiple transitions and plays 
		//		them together.
		// args: 
		//		The array of transition objects which will be played together.
		
		var animNodes = array.filter(args, function(item){
			return item.node;
		});
		
		var waitingList = getWaitingList(animNodes);

		//update registry with deferred objects in animations of args.
		array.forEach(args, function(item){
			if(item.node.id){
				transition.playing[item.node.id] = item.deferred;
			}
		});
		
		//wait for all deferred object in deferred list to resolve
		when(waitingList, function(){
			array.forEach(args, function(item){
				//set the start state
				item.initState();
			});
			
			//Assume the fps of the animation should be higher than 30 fps and
			//allow the browser to use one frame's time to redraw so that
			//the transition can be started
			setTimeout(function(){
				array.forEach(args, function(item){
					item.start();
				});

				// check and clear node if the node not cleared.
				// 1. on Android2.2/2.3, the "fade out" transitionEnd event will be lost if the soft keyboard popup, so we need to check nodes' clear status.
				// 2. The "fade in" transitionEnd event will before or after "fade out" transitionEnd event and it always occurs.
				//	  We can check fade out node status in the last "fade in" node transitionEnd event callback, if node transition timeout, we clear it.
				// NOTE: the last "fade in" transitionEnd event will always fired, so we bind on this event and check other nodes.
				on.once(args[args.length-1].node, transitionEndEventName, function(){
					var timeout;
					for(var i=0; i<args.length-1; i++){
						if(args[i].deferred.fired !== 0 && !args[i]._cleared){
							timeout = new Date().getTime() - args[i]._startTime;
							if(timeout >= args[i].duration){
								args[i].clear();
							}
						}
					}
				});
				setTimeout(function(){
					var timeout;
					for(var i=0; i<args.length; i++){
						if(args[i].deferred.fired !== 0 && !args[i]._cleared){
							timeout = new Date().getTime() - args[i]._startTime;
							if(timeout >= args[i].duration){
								args[i].clear();
							}
						}
					}
				}, args[0].duration+50);
			}, 33);
		});
	};
	
	transition.chainedPlay = function(/*Array*/args){
		// summary:
		//		The method which plays multiple transitions one by one.
		// args: 
		//		The array of transition objects which will be played in a chain.
		
		var animNodes = array.filter(args, function(item){
			return item.node;
		});
		
		var waitingList = getWaitingList(animNodes);

		//update registry with deferred objects in animations of args.
		array.forEach(args, function(item){
			if(item.node.id){
				transition.playing[item.node.id] = item.deferred;
			}
		});
		
		when(waitingList, function(){
			array.forEach(args, function(item){
				//set the start state
				item.initState();
			});
			
			//chain animations together
			for (var i=1, len=args.length; i < len; i++){
				args[i-1].deferred.then(lang.hitch(args[i], function(){
					this.start();
				}));
			}
			
			//Assume the fps of the animation should be higher than 30 fps and
			//allow the browser to use one frame's time to redraw so that
			//the transition can be started
			setTimeout(function(){
				args[0].start();
			}, 33);
		});		   
	};
	
	//TODO complete the registry mechanism for animation handling and prevent animation conflicts
	transition.playing = {};
	
	return transition;
});
