define([
	"dojo/_base/array", // array.forEach array.indexOf array.map
	"dojo/_base/declare", // declare
	"dojo/dnd/common",
	"dojo/dom-class", // domClass.add
	"dojo/dom-geometry", // domGeometry.position
	"dojo/_base/lang", // lang.mixin lang.hitch
	"dojo/mouse",
	"dojo/on", // subscribe
	"dojo/touch",
	"dojo/topic",
	"dojo/dnd/Manager", // DNDManager.manager
	"./_dndSelector"
], function(array, declare, dndCommon, domClass, domGeometry, lang, mouse, on, touch, topic, DNDManager, _dndSelector){

	// module:
	//		dijit/tree/dndSource

	/*=====
	var __Item = {
		// summary:
		//		New item to be added to the Tree, like:
		// id: Anything
		id: "",
		// name: String
		name: ""
	};
	=====*/

	var dndSource = declare("dijit.tree.dndSource", _dndSelector, {
		// summary:
		//		Handles drag and drop operations (as a source or a target) for `dijit.Tree`

		// isSource: Boolean
		//		Can be used as a DnD source.
		isSource: true,

		// accept: String[]
		//		List of accepted types (text strings) for the Tree; defaults to
		//		["text"]
		accept: ["text", "treeNode"],

		// copyOnly: [private] Boolean
		//		Copy items, if true, use a state of Ctrl key otherwise
		copyOnly: false,

		// dragThreshold: Number
		//		The move delay in pixels before detecting a drag; 5 by default
		dragThreshold: 5,

		// betweenThreshold: Integer
		//		Distance from upper/lower edge of node to allow drop to reorder nodes
		betweenThreshold: 0,

		// Flag used by Avatar.js to signal to generate text node when dragging
		generateText: true,

		constructor: function(/*dijit/Tree*/ tree, /*dijit/tree/dndSource*/ params){
			// summary:
			//		a constructor of the Tree DnD Source
			// tags:
			//		private
			if(!params){
				params = {};
			}
			lang.mixin(this, params);
			var type = params.accept instanceof Array ? params.accept : ["text", "treeNode"];
			this.accept = null;
			if(type.length){
				this.accept = {};
				for(var i = 0; i < type.length; ++i){
					this.accept[type[i]] = 1;
				}
			}

			// class-specific variables
			this.isDragging = false;
			this.mouseDown = false;
			this.targetAnchor = null;	// DOMNode corresponding to the currently moused over TreeNode
			this.targetBox = null;	// coordinates of this.targetAnchor
			this.dropPosition = "";	// whether mouse is over/after/before this.targetAnchor
			this._lastX = 0;
			this._lastY = 0;

			// states
			this.sourceState = "";
			if(this.isSource){
				domClass.add(this.node, "dojoDndSource");
			}
			this.targetState = "";
			if(this.accept){
				domClass.add(this.node, "dojoDndTarget");
			}

			// set up events
			this.topics = [
				topic.subscribe("/dnd/source/over", lang.hitch(this, "onDndSourceOver")),
				topic.subscribe("/dnd/start", lang.hitch(this, "onDndStart")),
				topic.subscribe("/dnd/drop", lang.hitch(this, "onDndDrop")),
				topic.subscribe("/dnd/cancel", lang.hitch(this, "onDndCancel"))
			];
		},

		// methods
		checkAcceptance: function(/*===== source, nodes =====*/){
			// summary:
			//		Checks if the target can accept nodes from this source
			// source: dijit/tree/dndSource
			//		The source which provides items
			// nodes: DOMNode[]
			//		Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
			//		source is a dijit/Tree.
			// tags:
			//		extension
			return true;	// Boolean
		},

		copyState: function(keyPressed){
			// summary:
			//		Returns true, if we need to copy items, false to move.
			//		It is separated to be overwritten dynamically, if needed.
			// keyPressed: Boolean
			//		The "copy" control key was pressed
			// tags:
			//		protected
			return this.copyOnly || keyPressed;	// Boolean
		},
		destroy: function(){
			// summary:
			//		Prepares the object to be garbage-collected.
			this.inherited(arguments);
			var h;
			while(h = this.topics.pop()){
				h.remove();
			}
			this.targetAnchor = null;
		},

		_onDragMouse: function(e, firstTime){
			// summary:
			//		Helper method for processing onmousemove/onmouseover events while drag is in progress.
			//		Keeps track of current drop target.
			// e: Event
			//		The mousemove event.
			// firstTime: Boolean?
			//		If this flag is set, this is the first mouse move event of the drag, so call m.canDrop() etc.
			//		even if newTarget == null because the user quickly dragged a node in the Tree to a position
			//		over Tree.containerNode but not over any TreeNode (#7971)

			var m = DNDManager.manager(),
				oldTarget = this.targetAnchor, // the TreeNode corresponding to TreeNode mouse was previously over
				newTarget = this.current, // TreeNode corresponding to TreeNode mouse is currently over
				oldDropPosition = this.dropPosition;	// the previous drop position (over/before/after)

			// calculate if user is indicating to drop the dragged node before, after, or over
			// (i.e., to become a child of) the target node
			var newDropPosition = "Over";
			if(newTarget && this.betweenThreshold > 0){
				// If mouse is over a new TreeNode, then get new TreeNode's position and size
				if(!this.targetBox || oldTarget != newTarget){
					this.targetBox = domGeometry.position(newTarget.rowNode, true);
				}
				if((e.pageY - this.targetBox.y) <= this.betweenThreshold){
					newDropPosition = "Before";
				}else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){
					newDropPosition = "After";
				}
			}

			if(firstTime || newTarget != oldTarget || newDropPosition != oldDropPosition){
				if(oldTarget){
					this._removeItemClass(oldTarget.rowNode, oldDropPosition);
				}
				if(newTarget){
					this._addItemClass(newTarget.rowNode, newDropPosition);
				}

				// Check if it's ok to drop the dragged node on/before/after the target node.
				if(!newTarget){
					m.canDrop(false);
				}else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){
					// Can't drop before or after tree's root node; the dropped node would just disappear (at least visually)
					m.canDrop(false);
				}else{
					// Guard against dropping onto yourself or your parent.
					// But when dragging multiple objects, it's OK if some of them are being dropped onto own parent.
					var dropOntoSelf = false,
						dropOntoParent = false;
					if(m.source == this){
						dropOntoParent = (newDropPosition === "Over");
						for(var dragId in this.selection){
							var dragNode = this.selection[dragId];
							if(dragNode.item === newTarget.item){
								dropOntoSelf = true;
								break;
							}
							if(dragNode.getParent().id !== newTarget.id){
								dropOntoParent = false;
							}
						}
					}
					m.canDrop(
						!dropOntoSelf && !dropOntoParent &&
						!this._isParentChildDrop(m.source, newTarget.rowNode) &&
						this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase())
					);
				}

				this.targetAnchor = newTarget;
				this.dropPosition = newDropPosition;
			}
		},

		onMouseMove: function(e){
			// summary:
			//		Called for any onmousemove/ontouchmove events over the Tree
			// e: Event
			//		onmousemouse/ontouchmove event
			// tags:
			//		private
			if(this.isDragging && this.targetState == "Disabled"){
				return;
			}
			this.inherited(arguments);
			var m = DNDManager.manager();
			if(this.isDragging){
				this._onDragMouse(e);
			}else{
				if(this.mouseDown && this.isSource &&
					(Math.abs(e.pageX - this._lastX) >= this.dragThreshold || Math.abs(e.pageY - this._lastY) >= this.dragThreshold)){
					var nodes = this.getSelectedTreeNodes();
					if(nodes.length){
						if(nodes.length > 1){
							//filter out all selected items which has one of their ancestor selected as well
							var seen = this.selection, i = 0, r = [], n, p;
							nextitem: while((n = nodes[i++])){
								for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){
									if(seen[p.id]){ //parent is already selected, skip this node
										continue nextitem;
									}
								}
								//this node does not have any ancestors selected, add it
								r.push(n);
							}
							nodes = r;
						}
						nodes = array.map(nodes, function(n){
							return n.domNode
						});
						m.startDrag(this, nodes, this.copyState(dndCommon.getCopyKeyState(e)));
						this._onDragMouse(e, true);	// because this may be the only mousemove event we get before the drop
					}
				}
			}
		},

		onMouseDown: function(e){
			// summary:
			//		Event processor for onmousedown/ontouchstart
			// e: Event
			//		onmousedown/ontouchend event
			// tags:
			//		private

			if(e.type == "touchstart" || mouse.isLeft(e)){	// ignore right click
				this.mouseDown = true;
				this.mouseButton = e.button;
				this._lastX = e.pageX;
				this._lastY = e.pageY;
			}

			this.inherited(arguments);
		},

		onMouseUp: function(e){
			// summary:
			//		Event processor for onmouseup/ontouchend
			// e: Event
			//		onmouseup/ontouchend event
			// tags:
			//		private
			if(this.mouseDown){
				this.mouseDown = false;
				this.inherited(arguments);
			}
		},

		onMouseOut: function(){
			// summary:
			//		Event processor for when mouse is moved away from a TreeNode
			// tags:
			//		private
			this.inherited(arguments);
			this._unmarkTargetAnchor();
		},

		checkItemAcceptance: function(/*===== target, source, position =====*/){
			// summary:
			//		Stub function to be overridden if one wants to check for the ability to drop at the node/item level
			// description:
			//		In the base case, this is called to check if target can become a child of source.
			//		When betweenThreshold is set, position="before" or "after" means that we
			//		are asking if the source node can be dropped before/after the target node.
			// target: DOMNode
			//		The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
			//		Use dijit.getEnclosingWidget(target) to get the TreeNode.
			// source: dijit/tree/dndSource
			//		The (set of) nodes we are dropping
			// position: String
			//		"over", "before", or "after"
			// tags:
			//		extension
			return true;
		},

		// topic event processors
		onDndSourceOver: function(source){
			// summary:
			//		Topic event processor for /dnd/source/over, called when detected a current source.
			// source: Object
			//		The dijit/tree/dndSource / dojo/dnd/Source which has the mouse over it
			// tags:
			//		private
			if(this != source){
				this.mouseDown = false;
				this._unmarkTargetAnchor();
			}else if(this.isDragging){
				var m = DNDManager.manager();
				m.canDrop(false);
			}
		},
		onDndStart: function(source, nodes, copy){
			// summary:
			//		Topic event processor for /dnd/start, called to initiate the DnD operation
			// source: Object
			//		The dijit/tree/dndSource / dojo/dnd/Source which is providing the items
			// nodes: DomNode[]
			//		The list of transferred items, dndTreeNode nodes if dragging from a Tree
			// copy: Boolean
			//		Copy items, if true, move items otherwise
			// tags:
			//		private

			if(this.isSource){
				this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : "");
			}
			var accepted = this.checkAcceptance(source, nodes);

			this._changeState("Target", accepted ? "" : "Disabled");

			if(this == source){
				DNDManager.manager().overSource(this);
			}

			this.isDragging = true;
		},

		itemCreator: function(nodes /*===== , target, source =====*/){
			// summary:
			//		Returns objects passed to `Tree.model.newItem()` based on DnD nodes
			//		dropped onto the tree.   Developer must override this method to enable
			//		dropping from external sources onto this Tree, unless the Tree.model's items
			//		happen to look like {id: 123, name: "Apple" } with no other attributes.
			// description:
			//		For each node in nodes[], which came from source, create a hash of name/value
			//		pairs to be passed to Tree.model.newItem().  Returns array of those hashes.
			// nodes: DomNode[]
			// target: DomNode
			// source: dojo/dnd/Source
			// returns: __Item[]
			//		Array of name/value hashes for each new item to be added to the Tree
			// tags:
			//		extension

			// TODO: for 2.0 refactor so itemCreator() is called once per drag node, and
			// make signature itemCreator(sourceItem, node, target) (or similar).

			return array.map(nodes, function(node){
				return {
					"id": node.id,
					"name": node.textContent || node.innerText || ""
				};
			}); // Object[]
		},

		onDndDrop: function(source, nodes, copy){
			// summary:
			//		Topic event processor for /dnd/drop, called to finish the DnD operation.
			// description:
			//		Updates data store items according to where node was dragged from and dropped
			//		to.   The tree will then respond to those data store updates and redraw itself.
			// source: Object
			//		The dijit/tree/dndSource / dojo/dnd/Source which is providing the items
			// nodes: DomNode[]
			//		The list of transferred items, dndTreeNode nodes if dragging from a Tree
			// copy: Boolean
			//		Copy items, if true, move items otherwise
			// tags:
			//		protected
			if(this.containerState == "Over"){
				var tree = this.tree,
					model = tree.model,
					target = this.targetAnchor,
					doExpand = false; // this is so we don't expand the sibling above

				this.isDragging = false;

				// Compute the new parent item
				var newParentItem;
				var insertIndex;
				var before;		// drop source before (aka previous sibling) of target
				newParentItem = (target && target.item) || tree.item;
				if(this.dropPosition == "Before" || this.dropPosition == "After"){
					// TODO: if there is no parent item then disallow the drop.
					// Actually this should be checked during onMouseMove too, to make the drag icon red.
					newParentItem = (target.getParent() && target.getParent().item) || tree.item;
					// Compute the insert index for reordering
					insertIndex = target.getIndexInParent();
					if(this.dropPosition == "After"){
						insertIndex = target.getIndexInParent() + 1;
						before = target.getNextSibling() && target.getNextSibling().item;
					}else{
						before = target.item;
					}
				}else{
					newParentItem = (target && target.item) || tree.item;
					doExpand = true;
				}

				// If necessary, use this variable to hold array of hashes to pass to model.newItem()
				// (one entry in the array for each dragged node).
				var newItemsParams;

				array.forEach(nodes, function(node, idx){
					// dojo/dnd/Item representing the thing being dropped.
					// Don't confuse the use of item here (meaning a DnD item) with the
					// uses below where item means dojo.data item.
					var sourceItem = source.getItem(node.id);

					// Information that's available if the source is another Tree
					// (possibly but not necessarily this tree, possibly but not
					// necessarily the same model as this Tree)
					if(array.indexOf(sourceItem.type, "treeNode") != -1){
						var childTreeNode = sourceItem.data,
							childItem = childTreeNode.item,
							oldParentItem = childTreeNode.getParent().item;
					}

					if(source == this){
						// This is a node from my own tree, and we are moving it, not copying.
						// Remove item from old parent's children attribute.
						// TODO: dijit/tree/dndSelector should implement deleteSelectedNodes()
						// and this code should go there.

						if(typeof insertIndex == "number"){
							if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){
								insertIndex -= 1;
							}
						}
						model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex, before);
					}else if(model.isItem(childItem)){
						// Item from same model
						// (maybe we should only do this branch if the source is a tree?)
						model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex, before);
					}else{
						// Get the hash to pass to model.newItem().  A single call to
						// itemCreator() returns an array of hashes, one for each drag source node.
						if(!newItemsParams){
							newItemsParams = this.itemCreator(nodes, target.rowNode, source);
						}

						// Create new item in the tree, based on the drag source.
						model.newItem(newItemsParams[idx], newParentItem, insertIndex, before);
					}
				}, this);

				// Expand the target node (if it's currently collapsed) so the user can see
				// where their node was dropped.   In particular since that node is still selected.
				if(doExpand) {
					this.tree._expandNode(target);
				}
			}
			this.onDndCancel();
		},

		onDndCancel: function(){
			// summary:
			//		Topic event processor for /dnd/cancel, called to cancel the DnD operation
			// tags:
			//		private
			this._unmarkTargetAnchor();
			this.isDragging = false;
			this.mouseDown = false;
			delete this.mouseButton;
			this._changeState("Source", "");
			this._changeState("Target", "");
		},

		// When focus moves in/out of the entire Tree
		onOverEvent: function(){
			// summary:
			//		This method is called when mouse is moved over our container (like onmouseenter)
			// tags:
			//		private
			this.inherited(arguments);
			DNDManager.manager().overSource(this);
		},
		onOutEvent: function(){
			// summary:
			//		This method is called when mouse is moved out of our container (like onmouseleave)
			// tags:
			//		private
			this._unmarkTargetAnchor();
			var m = DNDManager.manager();
			if(this.isDragging){
				m.canDrop(false);
			}
			m.outSource(this);

			this.inherited(arguments);
		},

		_isParentChildDrop: function(source, targetRow){
			// summary:
			//		Checks whether the dragged items are parent rows in the tree which are being
			//		dragged into their own children.
			//
			// source:
			//		The DragSource object.
			//
			// targetRow:
			//		The tree row onto which the dragged nodes are being dropped.
			//
			// tags:
			//		private

			// If the dragged object is not coming from the tree this widget belongs to,
			// it cannot be invalid.
			if(!source.tree || source.tree != this.tree){
				return false;
			}


			var root = source.tree.domNode;
			var ids = source.selection;

			var node = targetRow.parentNode;

			// Iterate up the DOM hierarchy from the target drop row,
			// checking of any of the dragged nodes have the same ID.
			while(node != root && !ids[node.id]){
				node = node.parentNode;
			}

			return node.id && ids[node.id];
		},

		_unmarkTargetAnchor: function(){
			// summary:
			//		Removes hover class of the current target anchor
			// tags:
			//		private
			if(!this.targetAnchor){
				return;
			}
			this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition);
			this.targetAnchor = null;
			this.targetBox = null;
			this.dropPosition = null;
		},

		_markDndStatus: function(copy){
			// summary:
			//		Changes source's state based on "copy" status
			this._changeState("Source", copy ? "Copied" : "Moved");
		}
	});

	/*=====
	dndSource.__Item = __Item;
	=====*/

	return dndSource;
});
