dojo.provide("dojox.grid.enhanced.plugins.DnD");

dojo.require("dojox.grid.enhanced._Plugin");
dojo.require("dojox.grid.enhanced.plugins.Selector");
dojo.require("dojox.grid.enhanced.plugins.Rearrange");
dojo.require("dojo.dnd.move");
dojo.require("dojo.dnd.Source");

(function(){
var _devideToArrays = function(a){
		a.sort(function(v1, v2){
			return v1 - v2;
		});
		var arr = [[a[0]]];
		for(var i = 1, j = 0; i < a.length; ++i){
			if(a[i] == a[i-1] + 1){
				arr[j].push(a[i]);
			}else{
				arr[++j] = [a[i]];
			}
		}
		return arr;
	},
	_joinToArray = function(arrays){
		var a = arrays[0];
		for(var i = 1; i < arrays.length; ++i){
			a = a.concat(arrays[i]);
		}
		return a;
	};
dojo.declare("dojox.grid.enhanced.plugins.DnD", dojox.grid.enhanced._Plugin, {
	// summary:
	//		Provide drag and drop for grid columns/rows/cells within grid and out of grid.
	//		The store of grid must implement dojo.data.api.Write.
	//		DnD selected columns:
	//			Support moving within grid, moving/copying out of grid to a non-grid DnD target.
	//		DnD selected rows:
	//			Support moving within grid, moving/copying out of grid to any DnD target.
	//		DnD selected cells (in rectangle shape only):
	//			Support moving/copying within grid, moving/copying out of grid to any DnD target.
	//
	
	// name: String,
	//		plugin name;
	name: "dnd",
	
	_targetAnchorBorderWidth: 2,
	_copyOnly: false,
	_config: {
		"row":{
			"within":true,
			"in":true,
			"out":true
		},
		"col":{
			"within":true,
			"in":true,
			"out":true
		},
		"cell":{
			"within":true,
			"in":true,
			"out":true
		}
	},
	constructor: function(grid, args){
		this.grid = grid;
		this._config = dojo.clone(this._config);
		args = dojo.isObject(args) ? args : {};
		this.setupConfig(args.dndConfig);
		this._copyOnly = !!args.copyOnly;
		
		//Get the plugins we are dependent on.
		this._mixinGrid();
		this.selector = grid.pluginMgr.getPlugin("selector");
		this.rearranger = grid.pluginMgr.getPlugin("rearrange");
		//TODO: waiting for a better plugin framework to pass args to dependent plugins.
		this.rearranger.setArgs(args);
		
		//Initialized the components we need.
		this._clear();
		this._elem = new dojox.grid.enhanced.plugins.GridDnDElement(this);
		this._source = new dojox.grid.enhanced.plugins.GridDnDSource(this._elem.node, {
			"grid": grid,
			"dndElem": this._elem,
			"dnd": this
		});
		this._container = dojo.query(".dojoxGridMasterView", this.grid.domNode)[0];
		this._initEvents();
	},
	destroy: function(){
		this.inherited(arguments);
		this._clear();
		this._source.destroy();
		this._elem.destroy();
		this._container = null;
		this.grid = null;
		this.selector = null;
		this.rearranger = null;
		this._config = null;
	},
	_mixinGrid: function(){
		// summary:
		//		Provide APIs for grid.
		this.grid.setupDnDConfig = dojo.hitch(this, "setupConfig");
		this.grid.dndCopyOnly = dojo.hitch(this, "copyOnly");
	},
	setupConfig: function(config){
		// summary:
		//		Configure which DnD functionalities are needed.
		//		Combination of any item from type set ("row", "col", "cell")
		//		and any item from mode set("within", "in", "out") is configurable.
		//
		//		"row", "col", "cell" are straitforward, while the other 3 are explained below:
		//		"within": DnD within grid, that is, column/row reordering and cell moving/copying.
		//		"in": Whether allowed to accept rows/cells (currently not support columns) from another grid.
		//		"out": Whether allowed to drag out of grid, to another grid or even to any other DnD target.
		//
		//		If not provided in the config, will use the default.
		//		When declared together, Mode set has higher priority than type set.
		// config: Object
		//		DnD configuration object.
		//		See the examples below.
		// example:
		//		The following code disables row DnD within grid,
		//		but still can drag rows out of grid or drag rows from other gird.
		//	|	setUpConfig({
		//	|		"row": {
		//	|			"within": false
		//	|		}
		//	|	});
		//
		//		The opposite way is also okay:
		//	|	setUpConfig({
		//	|		"within": {
		//	|			"row": false
		//	|		}
		//	|	});
		//
		//		And if you'd like to disable/enable a whole set, here's a shortcut:
		//	|	setUpConfig({
		//	|		"cell", true,
		//	|		"out": false
		//	|	});
		//
		//		Because mode has higher priority than type, the following will disable row dnd within grid:
		//	|	setUpConfig({
		//	|		"within", {
		//	|			"row": false;
		//	|		},
		//	|		"row", {
		//	|			"within": true
		//	|		}
		//	|	});
		if(config && dojo.isObject(config)){
			var firstLevel = ["row", "col", "cell"],
				secondLevel = ["within", "in", "out"],
				cfg = this._config;
			dojo.forEach(firstLevel, function(type){
				if(type in config){
					var t = config[type];
					if(t && dojo.isObject(t)){
						dojo.forEach(secondLevel, function(mode){
							if(mode in t){
								cfg[type][mode] = !!t[mode];
							}
						});
					}else{
						dojo.forEach(secondLevel, function(mode){
							cfg[type][mode] = !!t;
						});
					}
				}
			});
			dojo.forEach(secondLevel, function(mode){
				if(mode in config){
					var m = config[mode];
					if(m && dojo.isObject(m)){
						dojo.forEach(firstLevel, function(type){
							if(type in m){
								cfg[type][mode] = !!m[type];
							}
						});
					}else{
						dojo.forEach(firstLevel, function(type){
							cfg[type][mode] = !!m;
						});
					}
				}
			});
		}
	},
	copyOnly: function(isCopyOnly){
		// summary:
		//		Setter/getter of this._copyOnly.
		if(typeof isCopyOnly != "undefined"){
			this._copyOnly = !!isCopyOnly;
		}
		return this._copyOnly;
	},
	_isOutOfGrid: function(evt){
		var gridPos = dojo.position(this.grid.domNode), x = evt.clientX, y = evt.clientY;
		return y < gridPos.y || y > gridPos.y + gridPos.h ||
			x < gridPos.x || x > gridPos.x + gridPos.w;
	},
	_onMouseMove: function(evt){
		if(this._dndRegion && !this._dnding && !this._externalDnd){
			this._dnding = true;
			this._startDnd(evt);
		}else{
			if(this._isMouseDown && !this._dndRegion){
				delete this._isMouseDown;
				this._oldCursor = dojo.style(dojo.body(), "cursor");
				dojo.style(dojo.body(), "cursor", "not-allowed");
			}
			//TODO: should implement as mouseenter/mouseleave
			//But we have an avatar under mouse when dnd, and this will cause a lot of mouseenter in FF.
			var isOut = this._isOutOfGrid(evt);
			if(!this._alreadyOut && isOut){
				this._alreadyOut = true;
				if(this._dnding){
					this._destroyDnDUI(true, false);
				}
				this._moveEvent = evt;
				this._source.onOutEvent();
			}else if(this._alreadyOut && !isOut){
				this._alreadyOut = false;
				if(this._dnding){
					this._createDnDUI(evt, true);
				}
				this._moveEvent = evt;
				this._source.onOverEvent();
			}
		}
	},
	_onMouseUp: function(){
		if(!this._extDnding && !this._isSource){
			var isInner = this._dnding && !this._alreadyOut;
			if(isInner && this._config[this._dndRegion.type]["within"]){
				this._rearrange();
			}
			this._endDnd(isInner);
		}
		dojo.style(dojo.body(), "cursor", this._oldCursor || "");
		delete this._isMouseDown;
	},
	_initEvents: function(){
		var g = this.grid, s = this.selector;
		this.connect(dojo.doc, "onmousemove", "_onMouseMove");
		this.connect(dojo.doc, "onmouseup", "_onMouseUp");
		
		this.connect(g, "onCellMouseOver", function(evt){
			if(!this._dnding && !s.isSelecting() && !evt.ctrlKey){
				this._dndReady = s.isSelected("cell", evt.rowIndex, evt.cell.index);
				s.selectEnabled(!this._dndReady);
			}
		});
		this.connect(g, "onHeaderCellMouseOver", function(evt){
			if(this._dndReady){
				s.selectEnabled(true);
			}
		});
		this.connect(g, "onRowMouseOver", function(evt){
			if(this._dndReady && !evt.cell){
				s.selectEnabled(true);
			}
		});
		this.connect(g, "onCellMouseDown", function(evt){
			if(!evt.ctrlKey && this._dndReady){
				this._dndRegion = this._getDnDRegion(evt.rowIndex, evt.cell.index);
				this._isMouseDown = true;
			}
		});
		this.connect(g, "onCellMouseUp", function(evt){
			if(!this._dndReady && !s.isSelecting() && evt.cell){
				this._dndReady = s.isSelected("cell", evt.rowIndex, evt.cell.index);
				s.selectEnabled(!this._dndReady);
			}
		});
		this.connect(g, "onCellClick", function(evt){
			if(this._dndReady && !evt.ctrlKey && !evt.shiftKey){
				s.select("cell", evt.rowIndex, evt.cell.index);
			}
		});
		this.connect(g, "onEndAutoScroll", function(isVertical, isForward, view, target, evt){
			if(this._dnding){
				this._markTargetAnchor(evt);
			}
		});
		this.connect(dojo.doc, "onkeydown", function(evt){
			if(evt.keyCode == dojo.keys.ESCAPE){
				this._endDnd(false);
			}else if(evt.keyCode == dojo.keys.CTRL){
				s.selectEnabled(true);
				this._isCopy = true;
			}
		});
		this.connect(dojo.doc, "onkeyup", function(evt){
			if(evt.keyCode == dojo.keys.CTRL){
				s.selectEnabled(!this._dndReady);
				this._isCopy = false;
			}
		});
	},
	_clear: function(){
		this._dndRegion = null;
		this._target = null;
		this._moveEvent = null;
		this._targetAnchor = {};
		this._dnding = false;
		this._externalDnd = false;
		this._isSource = false;
		this._alreadyOut = false;
		this._extDnding = false;
	},
	_getDnDRegion: function(rowIndex, colIndex){
		var s = this.selector,
			selected = s._selected,
			flag = (!!selected.cell.length) | (!!selected.row.length << 1) | (!!selected.col.length << 2),
			type;
		switch(flag){
			case 1:
				type = "cell";
				if(!this._config[type]["within"] && !this._config[type]["out"]){
					return null;
				}
				var cells = this.grid.layout.cells,
					getCount = function(range){
						var hiddenColCnt = 0;
						for(var i = range.min.col; i <= range.max.col; ++i){
							if(cells[i].hidden){
								++hiddenColCnt;
							}
						}
						return (range.max.row - range.min.row + 1) * (range.max.col - range.min.col + 1 - hiddenColCnt);
					},
					inRange = function(item, range){
						return item.row >= range.min.row && item.row <= range.max.row &&
							item.col >= range.min.col && item.col <= range.max.col;
					},
					range = {
						max: {
							row: -1,
							col: -1
						},
						min: {
							row: Infinity,
							col: Infinity
						}
					};
				
				dojo.forEach(selected[type], function(item){
					if(item.row < range.min.row){
						range.min.row = item.row;
					}
					if(item.row > range.max.row){
						range.max.row = item.row;
					}
					if(item.col < range.min.col){
						range.min.col = item.col;
					}
					if(item.col > range.max.col){
						range.max.col = item.col;
					}
				});
				if(dojo.some(selected[type], function(item){
					return item.row == rowIndex && item.col == colIndex;
				})){
					if(getCount(range) == selected[type].length && dojo.every(selected[type], function(item){
						return inRange(item, range);
					})){
						return {
							"type": type,
							"selected": [range],
							"handle": {
								"row": rowIndex,
								"col": colIndex
							}
						};
					}
				}
				return null;
			case 2: case 4:
				type = flag == 2 ? "row" : "col";
				if(!this._config[type]["within"] && !this._config[type]["out"]){
					return null;
				}
				var res = s.getSelected(type);
				if(res.length){
					return {
						"type": type,
						"selected": _devideToArrays(res),
						"handle": flag == 2 ? rowIndex : colIndex
					};
				}
				return null;
		}
		return null;
	},
	_startDnd: function(evt){
		this._createDnDUI(evt);
	},
	_endDnd: function(destroySource){
		this._destroyDnDUI(false, destroySource);
		this._clear();
	},
	_createDnDUI: function(evt, isMovingIn){
		//By default the master view of grid do not have height, because the children in it are all positioned absolutely.
		//But we need it to contain avatars.
		var viewPos = dojo.position(this.grid.views.views[0].domNode);
		dojo.style(this._container, "height", viewPos.h + "px");
		try{
			//If moving in from out side, dnd source is already created.
			if(!isMovingIn){
				this._createSource(evt);
			}
			this._createMoveable(evt);
			this._oldCursor = dojo.style(dojo.body(), "cursor");
			dojo.style(dojo.body(), "cursor", "default");
		}catch(e){
			console.warn("DnD._createDnDUI() error:", e);
		}
	},
	_destroyDnDUI: function(isMovingOut, destroySource){
		try{
			if(destroySource){
				this._destroySource();
			}
			this._unmarkTargetAnchor();
			if(!isMovingOut){
				this._destroyMoveable();
			}
			dojo.style(dojo.body(), "cursor", this._oldCursor);
		}catch(e){
			console.warn("DnD._destroyDnDUI() error:", this.grid.id, e);
		}
	},
	_createSource: function(evt){
		this._elem.createDnDNodes(this._dndRegion);
		var m = dojo.dnd.manager();
		var oldMakeAvatar = m.makeAvatar;
		m._dndPlugin = this;
		m.makeAvatar = function(){
			var avatar = new dojox.grid.enhanced.plugins.GridDnDAvatar(m);
			delete m._dndPlugin;
			return avatar;
		};
		m.startDrag(this._source, this._elem.getDnDNodes(), evt.ctrlKey);
		m.makeAvatar = oldMakeAvatar;
		m.onMouseMove(evt);
	},
	_destroySource: function(){
		dojo.publish("/dnd/cancel");
		this._elem.destroyDnDNodes();
	},
	_createMoveable: function(evt){
		if(!this._markTagetAnchorHandler){
			this._markTagetAnchorHandler = this.connect(dojo.doc, "onmousemove", "_markTargetAnchor");
		}
	},
	_destroyMoveable: function(){
		this.disconnect(this._markTagetAnchorHandler);
		delete this._markTagetAnchorHandler;
	},
	_calcColTargetAnchorPos: function(evt, containerPos){
		// summary:
		//		Calculate the position of the column DnD avatar
		var i, headPos, left, target, ex = evt.clientX,
			cells = this.grid.layout.cells,
			ltr = dojo._isBodyLtr(),
			headers = this._getVisibleHeaders();
		for(i = 0; i < headers.length; ++i){
			headPos = dojo.position(headers[i].node);
			if(ltr ? ((i === 0 || ex >= headPos.x) && ex < headPos.x + headPos.w) :
				((i === 0 || ex < headPos.x + headPos.w) && ex >= headPos.x)){
				left = headPos.x + (ltr ? 0 : headPos.w);
				break;
			}else if(ltr ? (i === headers.length - 1 && ex >= headPos.x + headPos.w) :
				(i === headers.length - 1 && ex < headPos.x)){
				++i;
				left = headPos.x + (ltr ? headPos.w : 0);
				break;
			}
		}
		if(i < headers.length){
			target = headers[i].cell.index;
			if(this.selector.isSelected("col", target) && this.selector.isSelected("col", target - 1)){
				var ranges = this._dndRegion.selected;
				for(i = 0; i < ranges.length; ++i){
					if(dojo.indexOf(ranges[i], target) >= 0){
						target = ranges[i][0];
						headPos = dojo.position(cells[target].getHeaderNode());
						left = headPos.x + (ltr ? 0 : headPos.w);
						break;
					}
				}
			}
		}else{
			target = cells.length;
		}
		this._target = target;
		return left - containerPos.x;
	},
	_calcRowTargetAnchorPos: function(evt, containerPos){
		// summary:
		//		Calculate the position of the row DnD avatar
		var g = this.grid, top, i = 0,
			cells = g.layout.cells;
		while(cells[i].hidden){ ++i; }
		var cell = g.layout.cells[i],
			rowIndex = g.scroller.firstVisibleRow,
			nodePos = dojo.position(cell.getNode(rowIndex));
		while(nodePos.y + nodePos.h < evt.clientY){
			if(++rowIndex >= g.rowCount){
				break;
			}
			nodePos = dojo.position(cell.getNode(rowIndex));
		}
		if(rowIndex < g.rowCount){
			if(this.selector.isSelected("row", rowIndex) && this.selector.isSelected("row", rowIndex - 1)){
				var ranges = this._dndRegion.selected;
				for(i = 0; i < ranges.length; ++i){
					if(dojo.indexOf(ranges[i], rowIndex) >= 0){
						rowIndex = ranges[i][0];
						nodePos = dojo.position(cell.getNode(rowIndex));
						break;
					}
				}
			}
			top = nodePos.y;
		}else{
			top = nodePos.y + nodePos.h;
		}
		this._target = rowIndex;
		return top - containerPos.y;
	},
	_calcCellTargetAnchorPos: function(evt, containerPos, targetAnchor){
		// summary:
		//		Calculate the position of the cell DnD avatar
		var s = this._dndRegion.selected[0],
			origin = this._dndRegion.handle,
			g = this.grid, ltr = dojo._isBodyLtr(),
			cells = g.layout.cells, headPos,
			minPos, maxPos, headers,
			height, width, left, top,
			minCol, maxCol, i,
			preSpan = origin.col - s.min.col,
			postSpan = s.max.col - origin.col,
			leftTopDiv, rightBottomDiv;
		if(!targetAnchor.childNodes.length){
			leftTopDiv = dojo.create("div", {
				"class": "dojoxGridCellBorderLeftTopDIV"
			}, targetAnchor);
			rightBottomDiv = dojo.create("div", {
				"class": "dojoxGridCellBorderRightBottomDIV"
			}, targetAnchor);
		}else{
			leftTopDiv = dojo.query(".dojoxGridCellBorderLeftTopDIV", targetAnchor)[0];
			rightBottomDiv = dojo.query(".dojoxGridCellBorderRightBottomDIV", targetAnchor)[0];
		}
		for(i = s.min.col + 1; i < origin.col; ++i){
			if(cells[i].hidden){
				--preSpan;
			}
		}
		for(i = origin.col + 1; i < s.max.col; ++i){
			if(cells[i].hidden){
				--postSpan;
			}
		}
		headers = this._getVisibleHeaders();
		//calc width
		for(i = preSpan; i < headers.length - postSpan; ++i){
			headPos = dojo.position(headers[i].node);
			if((evt.clientX >= headPos.x && evt.clientX < headPos.x + headPos.w) || //within in this column
				//prior to this column, but within range
				(i == preSpan && (ltr ? evt.clientX < headPos.x : evt.clientX >= headPos.x + headPos.w)) ||
				//post to this column, but within range
				(i == headers.length - postSpan - 1 && (ltr ? evt.clientX >= headPos.x + headPos.w : evt < headPos.x))){
					minCol = headers[i - preSpan];
					maxCol = headers[i + postSpan];
					minPos = dojo.position(minCol.node);
					maxPos = dojo.position(maxCol.node);
					minCol = minCol.cell.index;
					maxCol = maxCol.cell.index;
					left = ltr ? minPos.x : maxPos.x;
					width = ltr ? (maxPos.x + maxPos.w - minPos.x) : (minPos.x + minPos.w - maxPos.x);
					break;
			}
		}
		//calc height
		i = 0;
		while(cells[i].hidden){ ++i; }
		var cell = cells[i],
			rowIndex = g.scroller.firstVisibleRow,
			nodePos = dojo.position(cell.getNode(rowIndex));
		while(nodePos.y + nodePos.h < evt.clientY){
			if(++rowIndex < g.rowCount){
				nodePos = dojo.position(cell.getNode(rowIndex));
			}else{
				break;
			}
		}
		var minRow = rowIndex >= origin.row - s.min.row ? rowIndex - origin.row + s.min.row : 0;
		var maxRow = minRow + s.max.row - s.min.row;
		if(maxRow >= g.rowCount){
			maxRow = g.rowCount - 1;
			minRow = maxRow - s.max.row + s.min.row;
		}
		minPos = dojo.position(cell.getNode(minRow));
		maxPos = dojo.position(cell.getNode(maxRow));
		top = minPos.y;
		height = maxPos.y + maxPos.h - minPos.y;
		this._target = {
			"min":{
				"row": minRow,
				"col": minCol
			},
			"max":{
				"row": maxRow,
				"col": maxCol
			}
		};
		var anchorBorderSize = (dojo.marginBox(leftTopDiv).w - dojo.contentBox(leftTopDiv).w) / 2;
		var leftTopCellPos = dojo.position(cells[minCol].getNode(minRow));
		dojo.style(leftTopDiv, {
			"width": (leftTopCellPos.w - anchorBorderSize) + "px",
			"height": (leftTopCellPos.h - anchorBorderSize) + "px"
		});
		var rightBottomCellPos = dojo.position(cells[maxCol].getNode(maxRow));
		dojo.style(rightBottomDiv, {
			"width": (rightBottomCellPos.w - anchorBorderSize) + "px",
			"height": (rightBottomCellPos.h - anchorBorderSize) + "px"
		});
		return {
			h: height,
			w: width,
			l: left - containerPos.x,
			t: top - containerPos.y
		};
	},
	_markTargetAnchor: function(evt){
		try{
		var t = this._dndRegion.type;
		if(this._alreadyOut || (this._dnding && !this._config[t]["within"]) || (this._extDnding && !this._config[t]["in"])){
			return;
		}
		var height, width, left, top,
			targetAnchor = this._targetAnchor[t],
			pos = dojo.position(this._container);
		if(!targetAnchor){
			targetAnchor = this._targetAnchor[t] = dojo.create("div", {
				"class": (t == "cell") ? "dojoxGridCellBorderDIV" : "dojoxGridBorderDIV"
			});
			dojo.style(targetAnchor, "display", "none");
			this._container.appendChild(targetAnchor);
		}
		switch(t){
			case "col":
				height = pos.h;
				width = this._targetAnchorBorderWidth;
				left = this._calcColTargetAnchorPos(evt, pos);
				top = 0;
				break;
			case "row":
				height = this._targetAnchorBorderWidth;
				width = pos.w;
				left = 0;
				top = this._calcRowTargetAnchorPos(evt, pos);
				break;
			case "cell":
				var cellPos = this._calcCellTargetAnchorPos(evt, pos, targetAnchor);
				height = cellPos.h;
				width = cellPos.w;
				left = cellPos.l;
				top = cellPos.t;
		}
		if(typeof height == "number" && typeof width == "number" && typeof left == "number" && typeof top == "number"){
			dojo.style(targetAnchor, {
				"height": height + "px",
				"width": width + "px",
				"left": left + "px",
				"top": top + "px"
			});
			dojo.style(targetAnchor, "display", "");
		}else{
			this._target = null;
		}
		}catch(e){
			console.warn("DnD._markTargetAnchor() error:",e);
		}
	},
	_unmarkTargetAnchor: function(){
		if(this._dndRegion){
			var targetAnchor = this._targetAnchor[this._dndRegion.type];
			if(targetAnchor){
				dojo.style(this._targetAnchor[this._dndRegion.type], "display", "none");
			}
		}
	},
	_getVisibleHeaders: function(){
		return dojo.map(dojo.filter(this.grid.layout.cells, function(cell){
			return !cell.hidden;
		}), function(cell){
			return {
				"node": cell.getHeaderNode(),
				"cell": cell
			};
		});
	},
	_rearrange: function(){
		if(this._target === null){
			return;
		}
		var t = this._dndRegion.type;
		var ranges = this._dndRegion.selected;
		if(t === "cell"){
			this.rearranger[(this._isCopy || this._copyOnly) ? "copyCells" : "moveCells"](ranges[0], this._target);
		}else{
			this.rearranger[t == "col" ? "moveColumns" : "moveRows"](_joinToArray(ranges), this._target);
		}
		this._target = null;
	},
	onDraggingOver: function(sourcePlugin){
		if(!this._dnding && sourcePlugin){
			sourcePlugin._isSource = true;
			this._extDnding = true;
			if(!this._externalDnd){
				this._externalDnd = true;
				this._dndRegion = this._mapRegion(sourcePlugin.grid, sourcePlugin._dndRegion);
			}
			this._createDnDUI(this._moveEvent,true);
			this.grid.pluginMgr.getPlugin("autoScroll").readyForAutoScroll = true;
		}
	},
	_mapRegion: function(srcGrid, dndRegion){
		if(dndRegion.type === "cell"){
			var srcRange = dndRegion.selected[0];
			var cells = this.grid.layout.cells;
			var srcCells = srcGrid.layout.cells;
			var c, cnt = 0;
			for(c = srcRange.min.col; c <= srcRange.max.col; ++c){
				if(!srcCells[c].hidden){
					++cnt;
				}
			}
			for(c = 0; cnt > 0; ++c){
				if(!cells[c].hidden){
					--cnt;
				}
			}
			var region = dojo.clone(dndRegion);
			region.selected[0].min.col = 0;
			region.selected[0].max.col = c - 1;
			for(c = srcRange.min.col; c <= dndRegion.handle.col; ++c){
				if(!srcCells[c].hidden){
					++cnt;
				}
			}
			for(c = 0; cnt > 0; ++c){
				if(!cells[c].hidden){
					--cnt;
				}
			}
			region.handle.col = c;
		}
		return dndRegion;
	},
	onDraggingOut: function(sourcePlugin){
		if(this._externalDnd){
			this._extDnding = false;
			this._destroyDnDUI(true, false);
			if(sourcePlugin){
				sourcePlugin._isSource = false;
			}
		}
	},
	onDragIn: function(sourcePlugin, isCopy){
		var success = false;
		if(this._target !== null){
			var type = sourcePlugin._dndRegion.type;
			var ranges = sourcePlugin._dndRegion.selected;
			switch(type){
				case "cell":
					this.rearranger.changeCells(sourcePlugin.grid, ranges[0], this._target);
					break;
				case "row":
					var range = _joinToArray(ranges);
					this.rearranger.insertRows(sourcePlugin.grid, range, this._target);
					break;
			}
			success = true;
		}
		this._endDnd(true);
		if(sourcePlugin.onDragOut){
			sourcePlugin.onDragOut(success && !isCopy);
		}
	},
	onDragOut: function(isMove){
		if(isMove && !this._copyOnly){
			var type = this._dndRegion.type;
			var ranges = this._dndRegion.selected;
			switch(type){
				case "cell":
					this.rearranger.clearCells(ranges[0]);
					break;
				case "row":
					this.rearranger.removeRows(_joinToArray(ranges));
					break;
			}
		}
		this._endDnd(true);
	},
	_canAccept: function(sourcePlugin){
		if(!sourcePlugin){
			return false;
		}
		var srcRegion = sourcePlugin._dndRegion;
		var type = srcRegion.type;
		if(!this._config[type]["in"] || !sourcePlugin._config[type]["out"]){
			return false;
		}
		var g = this.grid;
		var ranges = srcRegion.selected;
		var colCnt = dojo.filter(g.layout.cells, function(cell){
			return !cell.hidden;
		}).length;
		var rowCnt = g.rowCount;
		var res = true;
		switch(type){
			case "cell":
				ranges = ranges[0];
				res = g.store.getFeatures()["dojo.data.api.Write"] &&
					(ranges.max.row - ranges.min.row) <= rowCnt &&
					dojo.filter(sourcePlugin.grid.layout.cells, function(cell){
						return cell.index >= ranges.min.col && cell.index <= ranges.max.col && !cell.hidden;
					}).length <= colCnt;
				//intentional drop through - don't break
			case "row":
				if(sourcePlugin._allDnDItemsLoaded()){
					return res;
				}
		}
		return false;
	},
	_allDnDItemsLoaded: function(){
		if(this._dndRegion){
			var type = this._dndRegion.type,
				ranges = this._dndRegion.selected,
				rows = [];
			switch(type){
				case "cell":
					for(var i = ranges[0].min.row, max = ranges[0].max.row; i <= max; ++i){
						rows.push(i);
					}
					break;
				case "row":
					rows = _joinToArray(ranges);
					break;
				default:
					return false;
			}
			var cache = this.grid._by_idx;
			return dojo.every(rows, function(rowIndex){
				return !!cache[rowIndex];
			});
		}
		return false;
	}
});
dojo.declare("dojox.grid.enhanced.plugins.GridDnDElement", null, {
	constructor: function(dndPlugin){
		this.plugin = dndPlugin;
		this.node = dojo.create("div");
		this._items = {};
	},
	destroy: function(){
		this.plugin = null;
		dojo.destroy(this.node);
		this.node = null;
		this._items = null;
	},
	createDnDNodes: function(dndRegion){
		this.destroyDnDNodes();
		var acceptType = ["grid/" + dndRegion.type + "s"];
		var itemNodeIdBase = this.plugin.grid.id + "_dndItem";
		dojo.forEach(dndRegion.selected, function(range, i){
			var id = itemNodeIdBase + i;
			this._items[id] = {
				"type": acceptType,
				"data": range,
				"dndPlugin": this.plugin
			};
			this.node.appendChild(dojo.create("div", {
				"id": id
			}));
		}, this);
	},
	getDnDNodes: function(){
		return dojo.map(this.node.childNodes, function(node){
			return node;
		});
	},
	destroyDnDNodes: function(){
		dojo.empty(this.node);
		this._items = {};
	},
	getItem: function(nodeId){
		return this._items[nodeId];
	}
});
dojo.declare("dojox.grid.enhanced.plugins.GridDnDSource",dojo.dnd.Source,{
	accept: ["grid/cells", "grid/rows", "grid/cols"],
	constructor: function(node, param){
		this.grid = param.grid;
		this.dndElem = param.dndElem;
		this.dndPlugin = param.dnd;
		this.sourcePlugin = null;
	},
	destroy: function(){
		this.inherited(arguments);
		this.grid = null;
		this.dndElem = null;
		this.dndPlugin = null;
		this.sourcePlugin = null;
	},
	getItem: function(nodeId){
		return this.dndElem.getItem(nodeId);
	},
	checkAcceptance: function(source, nodes){
		if(this != source && nodes[0]){
			var item = source.getItem(nodes[0].id);
			if(item.dndPlugin){
				var type = item.type;
				for(var j = 0; j < type.length; ++j){
					if(type[j] in this.accept){
						if(this.dndPlugin._canAccept(item.dndPlugin)){
							this.sourcePlugin = item.dndPlugin;
						}else{
							return false;
						}
						break;
					}
				}
			}else if("grid/rows" in this.accept){
				var rows = [];
				dojo.forEach(nodes, function(node){
					var item = source.getItem(node.id);
					if(item.data && dojo.indexOf(item.type, "grid/rows") >= 0){
						var rowData = item.data;
						if(typeof item.data == "string"){
							rowData = dojo.fromJson(item.data);
						}
						if(rowData){
							rows.push(rowData);
						}
					}
				});
				if(rows.length){
					this.sourcePlugin = {
						_dndRegion: {
							type: "row",
							selected: [rows]
						}
					};
				}else{
					return false;
				}
			}
		}
		return this.inherited(arguments);
	},
	onDraggingOver: function(){
		this.dndPlugin.onDraggingOver(this.sourcePlugin);
	},
	onDraggingOut: function(){
		this.dndPlugin.onDraggingOut(this.sourcePlugin);
	},
	onDndDrop: function(source, nodes, copy, target){
		//this.inherited(arguments);
		this.onDndCancel();
		if(this != source && this == target){
			this.dndPlugin.onDragIn(this.sourcePlugin, copy);
		}
	}
});

dojo.declare("dojox.grid.enhanced.plugins.GridDnDAvatar", dojo.dnd.Avatar, {
	construct: function(){
		// summary:
		//		constructor function;
		//		it is separate so it can be (dynamically) overwritten in case of need
		this._itemType = this.manager._dndPlugin._dndRegion.type;
		this._itemCount = this._getItemCount();
			
		this.isA11y = dojo.hasClass(dojo.body(), "dijit_a11y");
		var a = dojo.create("table", {
				"border": "0",
				"cellspacing": "0",
				"class": "dojoxGridDndAvatar",
				"style": {
					position: "absolute",
					zIndex: "1999",
					margin: "0px"
				}
			}),
			source = this.manager.source,
			b = dojo.create("tbody", null, a),
			tr = dojo.create("tr", null, b),
			td = dojo.create("td", {
				"class": "dojoxGridDnDIcon"
			}, tr);
		if(this.isA11y){
			dojo.create("span", {
				"id" : "a11yIcon",
				"innerHTML" : this.manager.copy ? '+' : "<"
			}, td);
		}
		td = dojo.create("td", {
			"class" : "dojoxGridDnDItemIcon " + this._getGridDnDIconClass()
		}, tr);
		td = dojo.create("td", null, tr);
		dojo.create("span", {
			"class": "dojoxGridDnDItemCount",
			"innerHTML": source.generateText ? this._generateText() : ""
		}, td);
		// we have to set the opacity on IE only after the node is live
		dojo.style(tr, {
			"opacity": 0.9
		});
		this.node = a;
	},
	_getItemCount: function(){
		var selected = this.manager._dndPlugin._dndRegion.selected,
			count = 0;
		switch(this._itemType){
			case "cell":
				selected = selected[0];
				var cells = this.manager._dndPlugin.grid.layout.cells,
					colCount = selected.max.col - selected.min.col + 1,
					rowCount = selected.max.row - selected.min.row + 1;
				if(colCount > 1){
					for(var i = selected.min.col; i <= selected.max.col; ++i){
						if(cells[i].hidden){
							--colCount;
						}
					}
				}
				count = colCount * rowCount;
				break;
			case "row":
			case "col":
				count = _joinToArray(selected).length;
		}
		return count;
	},
	_getGridDnDIconClass: function(){
		return {
			"row": ["dojoxGridDnDIconRowSingle", "dojoxGridDnDIconRowMulti"],
			"col": ["dojoxGridDnDIconColSingle", "dojoxGridDnDIconColMulti"],
			"cell": ["dojoxGridDnDIconCellSingle", "dojoxGridDnDIconCellMulti"]
		}[this._itemType][this._itemCount == 1 ? 0 : 1];
	},
	_generateText: function(){
		// summary:
		//		generates a proper text to reflect copying or moving of items
		return "(" + this._itemCount + ")";
	}
});

dojox.grid.EnhancedGrid.registerPlugin(dojox.grid.enhanced.plugins.DnD/*name:'dnd'*/, {
	"dependency": ["selector", "rearrange"]
});
})();