define([
	"dojo/aspect",
	"dojo/_base/declare",
	"dojo/_base/Deferred",
	"dojo/_base/lang",
	"dojo/_base/array",
	"dojo/_base/fx",
	"dojo/dom",
	"dojo/dom-attr",
	"dojo/dom-construct",
	"dojo/dom-geometry",
	"dojo/dom-style",
	"dojo/topic",
	"dojo/on",
	"dojo/parser",
	"dojo/query",
	"dojo/fx/easing",
	"dojo/NodeList-dom"
], function(aspect, declare, Deferred, lang, array, fx, dom, domAttr, domConstruct, domGeometry, domStyle, topic,
			on, parser, query){

	// build friendly strings
	var _defaultTransition = "dojox.widget.rotator.swap", // please do NOT change
		_defaultTransitionDuration = 500,
		_displayStr = "display",
		_noneStr = "none",
		_zIndex = "zIndex";

	var Rotator = declare("dojox.widget.Rotator", null, {
		// summary:
		//		A widget for rotating through child nodes using transitions.
		//
		// description:
		//		A small, fast, extensible, awesome rotator that cycles, with transitions,
		//		through panes (child nodes) displaying only one at a time and ties into
		//		controllers used to change state.
		//
		//		The Rotator does not rely on dijit.  It is designed to be as lightweight
		//		as possible.  Controllers and transitions have been externalized
		//		so builds can be as optimized with only the components you want to use.
		//
		//		For best results, each rotator pane should be the same height and width as
		//		the Rotator container node and consider setting overflow to hidden.
		//		While the Rotator will accept any DOM node for a rotator pane, a block
		//		element or element with display:block is recommended.
		//
		//		Note: When the Rotator begins, it does not transition the first pane.
		//
		//		subscribed topics:
		//
		//			[id]/rotator/control - Controls the Rotator
		//			Parameters:
		//
		//			- /*string*/ action        - The name of a method of the Rotator to run
		//			- /*anything?*/ args       - One or more arguments to pass to the action
		//
		//		published topics:
		//
		//		[id]/rotator/update - Notifies controllers that a pane or state has changed.
		//		Parameters:
		//
		//		- /*string*/ type          - the type of notification
		//		- /*dojox.widget.Rotator*/ rotator - the rotator instance
		//		- /*object?*/ params		 - params
		//
		//		declarative dojo/method events (per pane):
		//
		//		- onBeforeIn  - Fired before the transition in starts.
		//		- onAfterIn   - Fired after the transition in ends.
		//		- onBeforeOut - Fired before the transition out starts.
		//		- onAfterOut  - Fired after the transition out ends.
		//
		// example:
		//	|	<div dojoType="dojox.widget.Rotator">
		//	|		<div>Pane 1!</div>
		//	|		<div>Pane 2!</div>
		//	|	</div>
		//
		// example:
		//	|	<script type="text/javascript">
		//	|		require("dojo/parser", "dojo/domReady!", "dojox/widget/Rotator", "dojox/widget/rotator/Fade"],
		//	|           function(parser) { parser.parse(); });
		//	|	</script>
		//	|	<div dojoType="dojox/widget/Rotator" transition="dojox/widget/rotator/crossFade">
		//	|		<div>Pane 1!</div>
		//	|		<div>Pane 2!</div>
		//	|	</div>

		// transition: string
		//		The name of a function that is passed two panes nodes and a duration,
		//		then returns a dojo.Animation object. The default value is
		//		"dojox.widget.rotator.swap".
		transition: _defaultTransition,

		// transitionParams: string
		//		Parameters for the transition. The string is read in and eval'd as an
		//		object.  If the duration is absent, the default value will be used.
		transitionParams: "duration:" + _defaultTransitionDuration,

		// panes: array
		//		Array of panes to be created in the Rotator. Each array element
		//		will be passed as attributes to a html.create() call.
		panes: null,

		constructor: function(/*Object*/params, /*DomNode|string*/node){
			// summary:
			//		Initializes the panes and events.
			lang.mixin(this, params);

			var _t = this,
				t = _t.transition,
				tt = _t._transitions = {},
				idm = _t._idMap = {},
				tp = _t.transitionParams = eval("({ " + _t.transitionParams + " })"),
				node = _t._domNode = dom.byId(node),
				cb = _t._domNodeContentBox = domGeometry.getContentBox(node); // we are going to assume the rotator will not be changing size

			// if we don't have an id, then generate one
			_t.id = node.id || (new Date()).getTime();

			// force the rotator DOM node to a relative position and attach the container node to it
			if(domStyle.get(node, "position") == "static"){
				domStyle.set(node, "position", "relative");
			}

			// create our object for caching transition objects
			tt[t] = lang.getObject(t);
			if(!tt[t]){
				this._transitionWarn(t, _defaultTransition);
				tt[_t.transition = _defaultTransition] = lang.getObject(_defaultTransition);
			}

			// clean up the transition params
			if(!tp.duration){
				tp.duration = _defaultTransitionDuration;
			}

			// if there are any panes being passed in, add them to this node
			array.forEach(_t.panes, function(p){
				domConstruct.create("div", p, node);
			});

			// zero out our panes array to store the real pane instance
			_t.panes = [];

			// find and initialize the panes
			query("> *", node).forEach(function(n, i){
				_t._initializePane(n, i);
			});

			_t._controlSub = topic.subscribe(_t.id + "/rotator/control", lang.hitch(_t, this.control));
		},

		insert: function(/*DomNode*/node, /*number?*/index){
			// summary:
			// 		Inserts a new pane into the rotator at a given index. If no index is
			// 		given, the new pane is inserted at the end of the pane list.
			var pane,
				panes = this.panes,
				paneNode;

			if (index == null) {
				index = panes.length;
			}

			if (index < panes.length) {
				pane = panes[index];
				domConstruct.place(node, pane.node, 'before');
			}
			else {
				domConstruct.place(node, this._domNode, 'last');
			}

			this._initializePane(node, index);
		},

		remove: function(/*DomNode|number*/nodeOrIndex){
			// summary:
			// 		Removes a pane from the rotator.
			function removeFromPanes(idx) {
				var removed = panes.splice(idx, 1)[0];
				if (removed) {
					if (removed.id) {
						_t._idMap[removed.id] = undefined;
					}
					_t._domNode.removeChild(removed.node);
				}
				if (_t.idx > idx) {
					_t.idx--;
				}
			}

			var index,
				_t = this,
				panes = this.panes;

			if (typeof nodeOrIndex === "number") {
				index = nodeOrIndex;
			}
			else {
				for (var i = 0; i < panes.length; i++) {
					if (panes[i].node === nodeOrIndex) {
						index = i;
						break;
					}
				}

				if (index == null) {
					return;
				}
			}

			if (index === this.idx) {
				var def = this.go(this.idx - 1);
				if (def) {
					return def.then(function () {
						removeFromPanes(index);
					});
				}
				else {
					removeFromPanes(index);
				}
			}
			else {
				removeFromPanes(index);
			}
		},

		destroy: function(){
			// summary:
			//		Destroys the Rotator and its DOM node.
			array.forEach([this._controlSub, this.wfe], function(wfe) { wfe && wfe.remove() });
			domConstruct.destroy(this._domNode);
			this.panes = [];
		},

		next: function(){
			// summary:
			//		Transitions the Rotator to the next pane.
			return this.go(this.idx + 1);
		},

		prev: function(){
			// summary:
			//		Transitions the Rotator to the previous pane.
			return this.go(this.idx - 1);
		},

		go: function(/*int|string?*/p){
			// summary:
			//		Transitions the Rotator to the specified pane index.
			var _t = this,
				i = _t.idx,
				pp = _t.panes,
				len = pp.length,
				idm = _t._idMap[p];

			// we gotta move on, so if the current pane is waiting for an event, just
			// ignore it and clean up
			_t._resetWaitForEvent();

			// determine the next index and set it to idx for the next go to
			p = idm != null ? idm : (p || 0);
			p = p < len ? (p < 0 ? len-1 : p) : 0;

			// if we're already on the requested pane or still transitioning, then return
			if(p == i || _t.anim){
				return null;
			}

			// get the current and next panes
			var current = pp[i],
				next = pp[p];

			// adjust the zIndexes so our animations look good... this must be done before
			// the animation is created so the animation could override it if necessary
			domStyle.set(current.node, _zIndex, 2);
			domStyle.set(next.node, _zIndex, 1);

			// info object passed to animations and onIn/Out events
			var info = {
					current: current,
					next: next,
					rotator: _t
				},

				// get the transition
				anim = _t.anim = _t._transitions[next.trans](lang.mixin({
					rotatorBox: _t._domNodeContentBox
				}, info, next.params));

			if(anim){
				// create the deferred that we'll be returning
				var def = new Deferred(),
					ev = next.waitForEvent,

				h = aspect.after(anim, "onEnd", function(){
					// reset the node styles
					domStyle.set(current.node, {
						display: _noneStr,
						left: 0,
						opacity: 1,
						top: 0,
						zIndex: 0
					});

					h.remove();
					_t.anim = null;
					_t.idx = p;

					// fire end events
					if(current.onAfterOut){ current.onAfterOut(info); }
					if(next.onAfterIn){ next.onAfterIn(info); }

					_t.onUpdate("onAfterTransition");

					if(!ev){
						// if there is a previous waitForEvent, then we need to make
						// sure it gets unsubscribed
						_t._resetWaitForEvent();

						// animation is all done, fire the deferred callback.
						def.callback();
					}
				}, true);

				// if we're waiting for an event, subscribe to it so we know when to continue
				_t.wfe = ev ? topic.subscribe(ev, function(){
					_t._resetWaitForEvent();
					def.callback(true);
				}) : null;

				_t.onUpdate("onBeforeTransition");

				// fire start events
				if(current.onBeforeOut){ current.onBeforeOut(info); }
				if(next.onBeforeIn){ next.onBeforeIn(info); }

				// play the animation
				anim.play();

				// return the deferred
				return def; /*Deferred*/
			}
		},

		onUpdate: function(/*string*/type, /*object?*/params){
			// summary:
			//		Send a notification to all controllers with the state of the rotator.
			topic.publish(this.id + "/rotator/update", type, this, params || {});
		},

		_initializePane: function(/*node*/node, /*number*/index) {
			// summary:
			// 		Initializes a new pane node.
			var tp = this.transitionParams,
				q = { node: node, idx: index, params: lang.mixin({}, tp, eval("({ " + (domAttr.get(node, "transitionParams") || "") + " })")) },
				r = q.trans = domAttr.get(node, "transition") || this.transition,
				tt = this._transitions,
				panes = this.panes,
				// default styles to apply to all the container node and rotator's panes
				p = {
					left: 0,
					top: 0,
					position: "absolute",
					display: _noneStr
				};

			// cache each pane's title, duration, and waitForEvent attributes
			array.forEach(["id", "title", "duration", "waitForEvent"], function(a){
				q[a] = domAttr.get(node, a);
			});

			if(q.id){
				this._idMap[q.id] = index;
			}

			// cache the transition function
			if(!tt[r] && !(tt[r] = lang.getObject(r))){
				this._transitionWarn(r, q.trans = this.transition);
			}

			// find the selected pane and initialize styles
			if(this.idx == null || domAttr.get(node, "selected")){
				if(this.idx != null){
					domStyle.set(panes[this.idx].node, _displayStr, _noneStr);
				}
				this.idx = index;
				p.display = "";
			}
			domStyle.set(node, p);

			// check for any declarative script blocks
			// TODO: The way declarative scripts are handled here needs to be updated to no conflict with dojo/parser
			query("> script[type^='dojo/method']", node).orphan().forEach(function(s){
				var e = domAttr.get(s, "event");
				if(e){
					q[e] = parser._functionFromScript(s);
				}
			});

			// add this pane to the array of panes
			panes.splice(index, 0, q);
		},

		_resetWaitForEvent: function(){
			// summary:
			//		If there is a waitForEvent pending, kill it.
			if(this.wfe){
				this.wfe.remove();
				delete this.wfe;
			}
		},

		control: function(/*string*/action){
			// summary:
			//		Dispatches an action, first to this engine, then to the Rotator.
			var args = lang._toArray(arguments),
				_t = this;
			args.shift();

			_t._resetWaitForEvent();

			if(_t[action]){
				// action exists, so call it and fire deferred if applicable
				var def = _t[action].apply(_t, args);
				if(def){
					def.addCallback(function(){
						_t.onUpdate(action);
					});
				}

				// since this action was triggered by a controller, we assume this was a
				// manual action, so check if we should pause
				_t.onManualChange(action);
			}else{
				console.warn(_t.declaredClass, ' - Unsupported action "', action, '".');
			}
		},

		resize: function(/*int*/width, /*int*/height){
			var b = this._domNodeContentBox = { w: width, h: height };
			domGeometry.setContentSize(this._domNode, b);
			array.forEach(this.panes, function(p){ domGeometry.setContentSize(p.node, b); });
		},

		onManualChange: function(){
			// summary:
			//		Stub function that can be overriden or connected to.
		},

		_transitionWarn: function(bt, dt){
			console.warn(this.declaredClass, ' - Unable to find transition "', bt, '", defaulting to "', dt, '".');
		}
	});

	lang.setObject(_defaultTransition, function(/*Object*/args){
		// summary:
		//		The default rotator transition which swaps two panes.
		return new fx.Animation({ // dojo.Animation
			play: function(){
				domStyle.set(args.current.node, _displayStr, _noneStr);
				domStyle.set(args.next.node, _displayStr, "");
				this._fire("onEnd");
			}
		});
	});

	return Rotator;
});
