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

dojo.require("dojox.grid.enhanced._Plugin");
dojo.require("dojox.grid.enhanced.plugins.AutoScroll");
dojo.require("dojox.grid.cells._base");

(function(){
/*=====
dojo.declare("__SelectItem", null,{
	// summary:
	//		An abstract representation of an item.
});
dojo.declare("__SelectCellItem", __SelectItem,{
	// summary:
	//		An abstract representation of a cell.
	
	// row: Integer
	//		Row index of this cell
	row: 0,
	
	// col: Integer
	//		Column index of this cell
	col: 0
});
dojo.declare("__SelectRowItem", __SelectItem,{
	// summary:
	//		An abstract representation of a row.
	
	// row: Integer
	//		Row index of this row
	row: 0,
	
	// except: Integer[]
	//		An array of column indexes of all the unselected cells in this row.
	except: []
});
dojo.declare("__SelectColItem", __SelectItem,{
	// summary:
	//		An abstract representation of a column.
	
	// col: Integer
	//		Column index of this column
	col: 0,
	
	// except: Integer[]
	//		An array of row indexes of all the unselected cells in this column.
	except: []
});
=====*/

var DISABLED = 0, SINGLE = 1, MULTI = 2,
	_theOther = { col: "row", row: "col" },
	_inRange = function(type, value, start, end, halfClose){
		if(type !== "cell"){
			value = value[type];
			start = start[type];
			end = end[type];
			if(typeof value !== "number" || typeof start !== "number" || typeof end !== "number"){
				return false;
			}
			return halfClose ? ((value >= start && value < end) || (value > end && value <= start))
							: ((value >= start && value <= end) || (value >= end && value <= start));
		}else{
			return _inRange("col", value, start, end, halfClose) && _inRange("row", value, start, end, halfClose);
		}
	},
	_isEqual = function(type, v1, v2){
		try{
			if(v1 && v2){
				switch(type){
					case "col": case "row":
						return v1[type] == v2[type] && typeof v1[type] == "number" &&
								!(_theOther[type] in v1) && !(_theOther[type] in v2);
					case "cell":
						return v1.col == v2.col && v1.row == v2.row && typeof v1.col == "number" && typeof v1.row == "number";
				}
			}
		}catch(e){}
		return false;
	},
	_stopEvent = function(evt){
		try{
			if(evt && evt.preventDefault){
				dojo.stopEvent(evt);
			}
		}catch(e){}
	},
	_createItem = function(type, rowIndex, colIndex){
		switch(type){
			case "col":
				return {
					"col": typeof colIndex == "undefined" ? rowIndex : colIndex,
					"except": []
				};
			case "row":
				return {
					"row": rowIndex,
					"except": []
				};
			case "cell":
				return {
					"row": rowIndex,
					"col": colIndex
				};
		}
		return null;
	};
dojo.declare("dojox.grid.enhanced.plugins.Selector", dojox.grid.enhanced._Plugin, {
	// summary:
	//		Provides standard extended selection for grid.
	//		Supports mouse/keyboard selection, multi-selection, and de-selection.
	//		Acceptable plugin parameters:
	//			The whole plugin parameter object is a config object passed to the setupConfig function.
	//
	//		Acceptable cell parameters defined in layout:
	//		1. notselectable: boolean
	//			Whether this column is (and all the cells in it are) selectable.
	
	// name: String
	//		plugin name
	name: "selector",
/*
	//	_config: null,
	//	_enabled: true,
	//	_selecting: {
	//		row: false,
	//		col: false,
	//		cell: false
	//	},
	//	_selected: {
	//		row: [],
	//		col: [],
	//		cell: []
	//	},
	//	_startPoint: {},
	//	_currentPoint: {},
	//	_lastAnchorPoint: {},
	//	_lastEndPoint: {},
	//	_lastSelectedAnchorPoint: {},
	//	_lastSelectedEndPoint: {},
	//	_keyboardSelect: {
	//		row: 0,
	//		col: 0,
	//		cell: 0
	//	},
	//	_curType: null,
	//	_lastType: null,
	//	_usingKeyboard: false,
	//	_toSelect: true,
*/

	constructor: function(grid, args){
		this.grid = grid;
		this._config = {
			row: MULTI,
			col: MULTI,
			cell: MULTI
		};
		this.setupConfig(args);
		if(grid.selectionMode === "single"){
			this._config.row = SINGLE;
		}
		this._enabled = true;
		this._selecting = {};
		this._selected = {
			"col": [],
			"row": [],
			"cell": []
		};
		this._startPoint = {};
		this._currentPoint = {};
		this._lastAnchorPoint = {};
		this._lastEndPoint = {};
		this._lastSelectedAnchorPoint = {};
		this._lastSelectedEndPoint = {};
		this._keyboardSelect = {};
		this._lastType = null;
		this._selectedRowModified = {};
		this._hacks();
		this._initEvents();
		this._initAreas();
		this._mixinGrid();
	},
	destroy: function(){
		this.inherited(arguments);
	},
	//------------public--------------------
	setupConfig: function(config){
		// summary:
		//		Set selection mode for row/col/cell.
		// config: Object
		//		An object with the following structure (all properties are optional):
		//		{
		//			//Default is "multi", all other values are same as "multi".
		//			row: false|"disabled"|"single",
		//			col: false|"disabled"|"single",
		//			cell: false|"disabled"|"single"
		//		}
		if(!config || !dojo.isObject(config)){
			return;
		}
		var types = ["row", "col", "cell"];
		for(var type in config){
			if(dojo.indexOf(types, type) >= 0){
				if(!config[type] || config[type] == "disabled"){
					this._config[type] = DISABLED;
				}else if(config[type] == "single"){
					this._config[type] = SINGLE;
				}else{
					this._config[type] = MULTI;
				}
			}
		}
		
		//Have to set mode to default grid selection.
		var mode = ["none","single","extended"][this._config.row];
		this.grid.selection.setMode(mode);
	},
	isSelected: function(type, rowIndex, colIndex){
		// summary:
		//		Check whether a location (a cell, a column or a row) is selected.
		// tag:
		//		public
		// type: String
		//		"row" or "col" or "cell"
		// rowIndex: Integer
		//		If type is "row" or "cell", this is the row index.
		//		If type if "col", this is the column index.
		// colIndex: Integer?
		//		Only valid when type is "cell"
		// return: Boolean
		//		true if selected, false if not. If cell is covered by a selected column, it's selected.
		return this._isSelected(type, _createItem(type, rowIndex, colIndex));
	},
	toggleSelect: function(type, rowIndex, colIndex){
		this._startSelect(type, _createItem(type, rowIndex, colIndex), this._config[type] === MULTI, false, false, !this.isSelected(type, rowIndex, colIndex));
		this._endSelect(type);
	},
	select: function(type, rowIndex, colIndex){
		// summary:
		//		Select a location (a cell, a column or a row).
		// tag:
		//		public
		// type: String
		//		"row" or "col" or "cell"
		// rowIndex: Integer
		//		If type is "row" or "cell", this is the row index.
		//		If type if "col", this is the column index.
		// colIndex: Integer?
		//		Only valid when type is "cell"
		if(!this.isSelected(type, rowIndex, colIndex)){
			this.toggleSelect(type, rowIndex, colIndex);
		}
	},
	deselect: function(type, rowIndex, colIndex){
		if(this.isSelected(type, rowIndex, colIndex)){
			this.toggleSelect(type, rowIndex, colIndex);
		}
	},
	selectRange: function(type, start, end, toSelect){
		// summary:
		//		Select a continuous range (a block of cells, a set of continuous columns or rows)
		// tag:
		//		public
		// type: String
		//		"row" or "col" or "cell"
		// start: Integer | Object
		//		If type is "row" or "col", this is the index of the starting row or column.
		//		If type if "cell", this is the left-top cell of the range.
		// end: Integer | Object
		//		If type is "row" or "col", this is the index of the ending row or column.
		//		If type if "cell", this is the right-bottom cell of the range.
		this.grid._selectingRange = true;
		var startPoint = type == "cell" ? _createItem(type, start.row, start.col) : _createItem(type, start),
			endPoint = type == "cell" ? _createItem(type, end.row, end.col) : _createItem(type, end);
		this._startSelect(type, startPoint, false, false, false, toSelect);
		this._highlight(type, endPoint, toSelect === undefined ? true : toSelect);
		this._endSelect(type);
		this.grid._selectingRange = false;
	},
	clear: function(type){
		// summary:
		//		Clear all selections.
		// tag:
		//		public
		// type: String?
		//		"row" or "col" or "cell". If omitted, clear all.
		this._clearSelection(type || "all");
	},
	isSelecting: function(type){
		// summary:
		//		Check whether the user is currently selecting something.
		// tag:
		//		public
		// type: String
		//		"row" or "col" or "cell"
		// return: Boolean
		//		true if is selection, false otherwise.
		if(typeof type == "undefined"){
			return this._selecting.col || this._selecting.row || this._selecting.cell;
		}
		return this._selecting[type];
	},
	selectEnabled: function(toEnable){
		// summary:
		//		Turn on/off this selection functionality if *toEnable* is provided.
		//		Check whether this selection functionality is enabled if nothing is passed in.
		// tag:
		//		public
		// toEnable: Boolean?
		//		To enable or not. Optional.
		// return: Boolean | undefined
		//		Enabled or not.
		if(typeof toEnable != "undefined" && !this.isSelecting()){
			this._enabled = !!toEnable;
		}
		return this._enabled;
	},
	getSelected: function(type, includeExceptions){
		// summary:
		//		Get an array of selected locations.
		// tag:
		//		public
		// type: String
		//		"row" or "col" or "cell"
		// includeExceptions: Boolean
		//		Only meaningful for rows/columns. If true, all selected rows/cols, even they are partly selected, are all returned.
		// return: __SelectItem[]
		switch(type){
			case "cell":
				return dojo.map(this._selected[type], function(item){ return item; });
			case "col": case "row":
				return dojo.map(includeExceptions ? this._selected[type]
				: dojo.filter(this._selected[type], function(item){
					return item.except.length === 0;
				}), function(item){
					return includeExceptions ? item : item[type];
				});
		}
		return [];
	},
	getSelectedCount: function(type, includeExceptions){
		// summary:
		//		Get the number of selected items.
		// tag:
		//		public
		// type: String
		//		"row" or "col" or "cell"
		// includeExceptions: Boolean
		//		Only meaningful for rows/columns. If true, all selected rows/cols, even they are partly selected, are all returned.
		// return: Integer
		//		The number of selected items.
		switch(type){
			case "cell":
				return this._selected[type].length;
			case "col": case "row":
				return (includeExceptions ? this._selected[type]
				: dojo.filter(this._selected[type], function(item){
					return item.except.length === 0;
				})).length;
		}
		return 0;
	},
	getSelectedType: function(){
		// summary:
		//		Get the type of selected items.
		// tag:
		//		public
		// return: String
		//		"row" or "col" or "cell", or any mix of these (separator is | ).
		var s = this._selected;
		return ["",		"cell",		"row",		"row|cell",
				"col",	"col|cell",	"col|row",	"col|row|cell"
			][(!!s.cell.length) | (!!s.row.length << 1) | (!!s.col.length << 2)];
	},
	getLastSelectedRange: function(type){
		// summary:
		//		Get last selected range of the given type.
		// tag:
		//		public
		// return: Object
		//		{start: __SelectItem, end: __SelectItem}
		//		return null if nothing is selected.
		return this._lastAnchorPoint[type] ? {
			"start": this._lastAnchorPoint[type],
			"end": this._lastEndPoint[type]
		} : null;
	},
	
	//--------------------------private----------------------------
	_hacks: function(){
		// summary:
		//		Complete the event system of grid, hack some grid functions to prevent default behavior.
		var g = this.grid;
		var doContentMouseUp = function(e){
			if(e.cellNode){
				g.onMouseUp(e);
			}
			g.onMouseUpRow(e);
		};
		var mouseUp = dojo.hitch(g, "onMouseUp");
		var mouseDown = dojo.hitch(g, "onMouseDown");
		var doRowSelectorFocus = function(e){
			e.cellNode.style.border = "solid 1px";
		};
		dojo.forEach(g.views.views, function(view){
			view.content.domouseup = doContentMouseUp;
			view.header.domouseup = mouseUp;
			if(view.declaredClass == "dojox.grid._RowSelector"){
				view.domousedown = mouseDown;
				view.domouseup = mouseUp;
				view.dofocus = doRowSelectorFocus;
			}
		});
		//Disable default selection.
		g.selection.clickSelect = function(){};
		
		this._oldDeselectAll = g.selection.deselectAll;
		var _this = this;
		g.selection.selectRange = function(from, to){
			_this.selectRange("row", from, to, true);
			if(g.selection.preserver){
				g.selection.preserver._updateMapping(true, true, false, from, to);
			}
			g.selection.onChanged();
		};
		g.selection.deselectRange = function(from, to){
			_this.selectRange("row", from, to, false);
			if(g.selection.preserver){
				g.selection.preserver._updateMapping(true, false, false, from, to);
			}
			g.selection.onChanged();
		};
		g.selection.deselectAll = function(){
			g._selectingRange = true;
			_this._oldDeselectAll.apply(g.selection, arguments);
			_this._clearSelection("row");
			g._selectingRange = false;
			if(g.selection.preserver){
				g.selection.preserver._updateMapping(true, false, true);
			}
			g.selection.onChanged();
		};
		
		var rowSelector = g.views.views[0];
		//The default function re-write the whole className, so can not insert any other classes.
		if(rowSelector instanceof dojox.grid._RowSelector){
			rowSelector.doStyleRowNode = function(inRowIndex, inRowNode){
				dojo.removeClass(inRowNode, "dojoxGridRow");
				dojo.addClass(inRowNode, "dojoxGridRowbar");
				dojo.addClass(inRowNode, "dojoxGridNonNormalizedCell");
				dojo.toggleClass(inRowNode, "dojoxGridRowbarOver", g.rows.isOver(inRowIndex));
				dojo.toggleClass(inRowNode, "dojoxGridRowbarSelected", !!g.selection.isSelected(inRowIndex));
			};
		}
		this.connect(g, "updateRow", function(rowIndex){
			dojo.forEach(g.layout.cells, function(cell){
				if(this.isSelected("cell", rowIndex, cell.index)){
					this._highlightNode(cell.getNode(rowIndex), true);
				}
			}, this);
		});
	},
	_mixinGrid: function(){
		// summary:
		//		Expose events to grid.
		var g = this.grid;
		g.setupSelectorConfig = dojo.hitch(this, this.setupConfig);
		g.onStartSelect = function(){};
		g.onEndSelect = function(){};
		g.onStartDeselect = function(){};
		g.onEndDeselect = function(){};
		g.onSelectCleared = function(){};
	},
	_initEvents: function(){
		// summary:
		//		Connect events, create event handlers.
		var g = this.grid,
			_this = this,
			dp = dojo.partial,
			starter = function(type, e){
				if(type === "row"){
					_this._isUsingRowSelector = true;
				}
				//only left mouse button can select.
				if(_this.selectEnabled() && _this._config[type] && e.button != 2){
					if(_this._keyboardSelect.col || _this._keyboardSelect.row || _this._keyboardSelect.cell){
						_this._endSelect("all");
						_this._keyboardSelect.col = _this._keyboardSelect.row = _this._keyboardSelect.cell = 0;
					}
					if(_this._usingKeyboard){
						_this._usingKeyboard = false;
					}
					var target = _createItem(type, e.rowIndex, e.cell && e.cell.index);
					_this._startSelect(type, target, e.ctrlKey, e.shiftKey);
				}
			},
			ender = dojo.hitch(this, "_endSelect");
		this.connect(g, "onHeaderCellMouseDown", dp(starter, "col"));
		this.connect(g, "onHeaderCellMouseUp", dp(ender, "col"));
		
		this.connect(g, "onRowSelectorMouseDown", dp(starter, "row"));
		this.connect(g, "onRowSelectorMouseUp", dp(ender, "row"));
		
		this.connect(g, "onCellMouseDown", function(e){
			if(e.cell && e.cell.isRowSelector){ return; }
			if(g.singleClickEdit){
				_this._singleClickEdit = true;
				g.singleClickEdit = false;
			}
			starter(_this._config["cell"] == DISABLED ? "row" : "cell", e);
		});
		this.connect(g, "onCellMouseUp", function(e){
			if(_this._singleClickEdit){
				delete _this._singleClickEdit;
				g.singleClickEdit = true;
			}
			ender("all", e);
		});
		
		this.connect(g, "onCellMouseOver", function(e){
			if(_this._curType != "row" && _this._selecting[_this._curType] && _this._config[_this._curType] == MULTI){
				_this._highlight("col", _createItem("col", e.cell.index), _this._toSelect);
				if(!_this._keyboardSelect.cell){
					_this._highlight("cell", _createItem("cell", e.rowIndex, e.cell.index), _this._toSelect);
				}
			}
		});
		this.connect(g, "onHeaderCellMouseOver", function(e){
			if(_this._selecting.col && _this._config.col == MULTI){
				_this._highlight("col", _createItem("col", e.cell.index), _this._toSelect);
			}
		});
		this.connect(g, "onRowMouseOver", function(e){
			if(_this._selecting.row && _this._config.row == MULTI){
				_this._highlight("row", _createItem("row", e.rowIndex), _this._toSelect);
			}
		});
		
		//When row order has changed in a unpredictable way (sorted or filtered), map the new rowindex.
		this.connect(g, "onSelectedById", "_onSelectedById");
		
		//When the grid refreshes, all those selected should still appear selected.
		this.connect(g, "_onFetchComplete", function(){
			//console.debug("refresh after buildPage:", g._notRefreshSelection);
			if(!g._notRefreshSelection){
				this._refreshSelected(true);
			}
		});

		//Small scroll might not refresh the grid.
		this.connect(g.scroller, "buildPage", function(){
			//console.debug("refresh after buildPage:", g._notRefreshSelection);
			if(!g._notRefreshSelection){
				this._refreshSelected(true);
			}
		});
		
		//Whenever the mouse is up, end selecting.
		this.connect(dojo.doc, "onmouseup", dp(ender, "all"));
		
		//If autoscroll is enabled, connect to it.
		this.connect(g, "onEndAutoScroll", function(isVertical, isForward, view, target){
			var selectCell = _this._selecting.cell,
				type, current, dir = isForward ? 1 : -1;
			if(isVertical && (selectCell || _this._selecting.row)){
				type = selectCell ? "cell" : "row";
				current = _this._currentPoint[type];
				_this._highlight(type, _createItem(type, current.row + dir, current.col), _this._toSelect);
			}else if(!isVertical && (selectCell || _this._selecting.col)){
				type = selectCell ? "cell" : "col";
				current = _this._currentPoint[type];
				_this._highlight(type, _createItem(type, current.row, target), _this._toSelect);
			}
		});
		//If the grid is changed, selection should be consistent.
		this.subscribe("dojox/grid/rearrange/move/" + g.id, "_onInternalRearrange");
		this.subscribe("dojox/grid/rearrange/copy/" + g.id, "_onInternalRearrange");
		this.subscribe("dojox/grid/rearrange/change/" + g.id, "_onExternalChange");
		this.subscribe("dojox/grid/rearrange/insert/" + g.id, "_onExternalChange");
		this.subscribe("dojox/grid/rearrange/remove/" + g.id, "clear");
		
		//have to also select when the grid's default select is used.
		this.connect(g, "onSelected", function(rowIndex){
			if(this._selectedRowModified && this._isUsingRowSelector){
				delete this._selectedRowModified;
			}else if(!this.grid._selectingRange){
				this.select("row", rowIndex);
			}
		});
		this.connect(g, "onDeselected", function(rowIndex){
			if(this._selectedRowModified && this._isUsingRowSelector){
				delete this._selectedRowModified;
			}else if(!this.grid._selectingRange){
				this.deselect("row", rowIndex);
			}
		});
	},
	_onSelectedById: function(id, newIndex, isSelected){
		if(this.grid._noInternalMapping){
			return;
		}
		var pointSet = [this._lastAnchorPoint.row, this._lastEndPoint.row,
			this._lastSelectedAnchorPoint.row, this._lastSelectedEndPoint.row];
		pointSet = pointSet.concat(this._selected.row);
		var found = false;
		dojo.forEach(pointSet, function(item){
			if(item){
				if(item.id === id){
					found = true;
					item.row = newIndex;
				}else if(item.row === newIndex && item.id){
					item.row = -1;
				}
			}
		});
		if(!found && isSelected){
			dojo.some(this._selected.row, function(item){
				if(item && !item.id && !item.except.length){
					item.id = id;
					item.row = newIndex;
					return true;
				}
				return false;
			});
		}
		found = false;
		pointSet = [this._lastAnchorPoint.cell, this._lastEndPoint.cell,
			this._lastSelectedAnchorPoint.cell, this._lastSelectedEndPoint.cell];
		pointSet = pointSet.concat(this._selected.cell);
		dojo.forEach(pointSet, function(item){
			if(item){
				if(item.id === id){
					found = true;
					item.row = newIndex;
				}else if(item.row === newIndex && item.id){
					item.row = -1;
				}
			}
		});
	},
	onSetStore: function(){
		this._clearSelection("all");
	},
	_onInternalRearrange: function(type, mapping){
		try{
		//The column can not refresh it self!
		this._refresh("col", false);
		
		dojo.forEach(this._selected.row, function(item){
			dojo.forEach(this.grid.layout.cells, function(cell){
				this._highlightNode(cell.getNode(item.row), false);
			}, this);
		}, this);
		//The rowbar must be cleaned manually
		dojo.query(".dojoxGridRowSelectorSelected").forEach(function(node){
			dojo.removeClass(node, "dojoxGridRowSelectorSelected");
			dojo.removeClass(node, "dojoxGridRowSelectorSelectedUp");
			dojo.removeClass(node, "dojoxGridRowSelectorSelectedDown");
		});
		
		var cleanUp = function(item){
			if(item){
				delete item.converted;
			}
		},
		pointSet = [this._lastAnchorPoint[type], this._lastEndPoint[type],
			this._lastSelectedAnchorPoint[type], this._lastSelectedEndPoint[type]];
		
		if(type === "cell"){
			this.selectRange("cell", mapping.to.min, mapping.to.max);
			var cells = this.grid.layout.cells;
			dojo.forEach(pointSet, function(item){
				if(item.converted){ return; }
				for(var r = mapping.from.min.row, tr = mapping.to.min.row; r <= mapping.from.max.row; ++r, ++tr){
					for(var c = mapping.from.min.col, tc = mapping.to.min.col; c <= mapping.from.max.col; ++c, ++tc){
						while(cells[c].hidden){ ++c; }
						while(cells[tc].hidden){ ++tc; }
						if(item.row == r && item.col == c){
							//console.log('mapping found: (', item.row, ",",item.col,") to (", tr, ",", tc,")");
							item.row = tr;
							item.col = tc;
							item.converted = true;
							return;
						}
					}
				}
			});
		}else{
			pointSet = this._selected.cell.concat(this._selected[type]).concat(pointSet).concat(
				[this._lastAnchorPoint.cell, this._lastEndPoint.cell,
				this._lastSelectedAnchorPoint.cell, this._lastSelectedEndPoint.cell]);
			dojo.forEach(pointSet, function(item){
				if(item && !item.converted){
					var from = item[type];
					if(from in mapping){
						item[type] = mapping[from];
					}
					item.converted = true;
				}
			});
			dojo.forEach(this._selected[_theOther[type]], function(item){
				for(var i = 0, len = item.except.length; i < len; ++i){
					var from = item.except[i];
					if(from in mapping){
						item.except[i] = mapping[from];
					}
				}
			});
		}
		
		dojo.forEach(pointSet, cleanUp);
		
		this._refreshSelected(true);
		this._focusPoint(type, this._lastEndPoint);
		}catch(e){
			console.warn("Selector._onInternalRearrange() error",e);
		}
	},
	_onExternalChange: function(type, target){
		var start = type == "cell" ? target.min : target[0],
			end = type == "cell" ? target.max : target[target.length - 1];
		this.selectRange(type, start, end);
	},
	_refresh: function(type, toHighlight){
		if(!this._keyboardSelect[type]){
			dojo.forEach(this._selected[type], function(item){
				this._highlightSingle(type, toHighlight, item, undefined, true);
			}, this);
		}
	},
	_refreshSelected: function(){
		this._refresh("col", true);
		this._refresh("row", true);
		this._refresh("cell", true);
	},
	_initAreas: function(){
		var g = this.grid, f = g.focus, _this = this, dk = dojo.keys,
			keyboardSelectReady = 1, duringKeyboardSelect = 2,
			onmove = function(type, createNewEnd, rowStep, colStep, evt){
				//Keyboard swipe selection is SHIFT + Direction Keys.
				var ks = _this._keyboardSelect;
				//Tricky, rely on valid status not being 0.
				if(evt.shiftKey && ks[type]){
					if(ks[type] === keyboardSelectReady){
						if(type === "cell"){
							var item = _this._lastEndPoint[type];
							if(f.cell != g.layout.cells[item.col + colStep] || f.rowIndex != item.row + rowStep){
								ks[type] = 0;
								return;
							}
						}
						//If selecting is not started, start it
						_this._startSelect(type, _this._lastAnchorPoint[type], true, false, true);
						_this._highlight(type, _this._lastEndPoint[type], _this._toSelect);
						ks[type] = duringKeyboardSelect;
					}
					//Highlight to the new end point.
					var newEnd = createNewEnd(type, rowStep, colStep, evt);
					if(_this._isValid(type, newEnd, g)){
						_this._highlight(type, newEnd, _this._toSelect);
					}
					_stopEvent(evt);
				}
			},
			onkeydown = function(type, getTarget, evt, isBubble){
				if(isBubble && _this.selectEnabled() && _this._config[type] != DISABLED){
					switch(evt.keyCode){
						case dk.SPACE:
							//Keyboard single point selection is SPACE.
							_this._startSelect(type, getTarget(), evt.ctrlKey, evt.shiftKey);
							_this._endSelect(type);
							break;
						case dk.SHIFT:
							//Keyboard swipe selection starts with SHIFT.
							if(_this._config[type] == MULTI && _this._isValid(type, _this._lastAnchorPoint[type], g)){
								//End last selection if any.
								_this._endSelect(type);
								_this._keyboardSelect[type] = keyboardSelectReady;
								_this._usingKeyboard = true;
							}
					}
				}
			},
			onkeyup = function(type, evt, isBubble){
				if(isBubble && evt.keyCode == dojo.keys.SHIFT && _this._keyboardSelect[type]){
					_this._endSelect(type);
					_this._keyboardSelect[type] = 0;
				}
			};
		//TODO: this area "rowHeader" should be put outside, same level as header/content.
		if(g.views.views[0] instanceof dojox.grid._RowSelector){
			this._lastFocusedRowBarIdx = 0;
			f.addArea({
				name:"rowHeader",
				onFocus: function(evt, step){
					var view = g.views.views[0];
					if(view instanceof dojox.grid._RowSelector){
						var rowBarNode = view.getCellNode(_this._lastFocusedRowBarIdx, 0);
						if(rowBarNode){
							dojo.toggleClass(rowBarNode, f.focusClass, false);
						}
						//evt might not be real event, it may be a mock object instead.
						if(evt && "rowIndex" in evt){
							if(evt.rowIndex >= 0){
								_this._lastFocusedRowBarIdx = evt.rowIndex;
							}else if(!_this._lastFocusedRowBarIdx){
								_this._lastFocusedRowBarIdx = 0;
							}
						}
						rowBarNode = view.getCellNode(_this._lastFocusedRowBarIdx, 0);
						if(rowBarNode){
							dijit.focus(rowBarNode);
							dojo.toggleClass(rowBarNode, f.focusClass, true);
						}
						f.rowIndex = _this._lastFocusedRowBarIdx;
						_stopEvent(evt);
						return true;
					}
					return false;
				},
				onBlur: function(evt, step){
					var view = g.views.views[0];
					if(view instanceof dojox.grid._RowSelector){
						var rowBarNode = view.getCellNode(_this._lastFocusedRowBarIdx, 0);
						if(rowBarNode){
							dojo.toggleClass(rowBarNode, f.focusClass, false);
						}
						_stopEvent(evt);
					}
					return true;
				},
				onMove: function(rowStep, colStep, evt){
					var view = g.views.views[0];
					if(rowStep && view instanceof dojox.grid._RowSelector){
						var next = _this._lastFocusedRowBarIdx + rowStep;
						if(next >= 0 && next < g.rowCount){
							//TODO: these logic require a better Scroller.
							_stopEvent(evt);
							var rowBarNode = view.getCellNode(_this._lastFocusedRowBarIdx, 0);
							dojo.toggleClass(rowBarNode, f.focusClass, false);
							//If the row is not fetched, fetch it.
							var sc = g.scroller;
							var lastPageRow = sc.getLastPageRow(sc.page);
							var rc = g.rowCount - 1, row = Math.min(rc, next);
							if(next > lastPageRow){
								g.setScrollTop(g.scrollTop + sc.findScrollTop(row) - sc.findScrollTop(_this._lastFocusedRowBarIdx));
							}
							//Now we have fetched the row.
							rowBarNode = view.getCellNode(next, 0);
							dijit.focus(rowBarNode);
							dojo.toggleClass(rowBarNode, f.focusClass, true);
							_this._lastFocusedRowBarIdx = next;
							//If the row is out of view, scroll to it.
							f.cell = rowBarNode;
							f.cell.view = view;
							f.cell.getNode = function(index){
								return f.cell;
							};
							f.rowIndex = _this._lastFocusedRowBarIdx;
							f.scrollIntoView();
							f.cell = null;
						}
					}
				}
			});
			f.placeArea("rowHeader","before","content");
		}
		//Support keyboard selection.
		f.addArea({
			name:"cellselect",
			onMove: dojo.partial(onmove, "cell", function(type, rowStep, colStep, evt){
				var current = _this._currentPoint[type];
				return _createItem("cell", current.row + rowStep, current.col + colStep);
			}),
			onKeyDown: dojo.partial(onkeydown, "cell", function(){
				return _createItem("cell", f.rowIndex, f.cell.index);
			}),
			onKeyUp: dojo.partial(onkeyup, "cell")
		});
		f.placeArea("cellselect","below","content");
		f.addArea({
			name:"colselect",
			onMove: dojo.partial(onmove, "col", function(type, rowStep, colStep, evt){
				var current = _this._currentPoint[type];
				return _createItem("col", current.col + colStep);
			}),
			onKeyDown: dojo.partial(onkeydown, "col", function(){
				return _createItem("col", f.getHeaderIndex());
			}),
			onKeyUp: dojo.partial(onkeyup, "col")
		});
		f.placeArea("colselect","below","header");
		f.addArea({
			name:"rowselect",
			onMove: dojo.partial(onmove, "row", function(type, rowStep, colStep, evt){
				return _createItem("row", f.rowIndex);
			}),
			onKeyDown: dojo.partial(onkeydown, "row", function(){
				return _createItem("row", f.rowIndex);
			}),
			onKeyUp: dojo.partial(onkeyup, "row")
		});
		f.placeArea("rowselect","below","rowHeader");
	},
	_clearSelection: function(type, reservedItem){
		// summary:
		//		Clear selection for given type and fire events, but retain the highlight for *reservedItem*,
		//		thus avoid "flashing".
		// tag:
		//		private
		// type: String
		//		"row", "col", or "cell
		// reservedItem: __SelectItem
		//		The item to retain highlight.
		if(type == "all"){
			this._clearSelection("cell", reservedItem);
			this._clearSelection("col", reservedItem);
			this._clearSelection("row", reservedItem);
			return;
		}
		this._isUsingRowSelector = true;
		dojo.forEach(this._selected[type], function(item){
			if(!_isEqual(type, reservedItem, item)){
				this._highlightSingle(type, false, item);
			}
		}, this);
		this._blurPoint(type, this._currentPoint);
		this._selecting[type] = false;
		this._startPoint[type] = this._currentPoint[type] = null;
		this._selected[type] = [];
		
		//Have to also deselect default grid selection.
		if(type == "row" && !this.grid._selectingRange){
			this._oldDeselectAll.call(this.grid.selection);
			this.grid.selection._selectedById = {};
		}
		
		//Fire events.
		this.grid.onEndDeselect(type, null, null, this._selected);
		this.grid.onSelectCleared(type);
	},
	_startSelect: function(type, start, extending, isRange, mandatarySelect, toSelect){
		// summary:
		//		Start selection, setup start point and current point, fire events.
		// tag:
		//		private
		// type: String
		//		"row", "col", or "cell"
		// extending: Boolean
		//		Whether this is a multi selection
		// isRange: Boolean
		//		Whether this is a range selection (i.e. select from the last end point to this point)
		// start: __SelectItem
		//		The start point
		// mandatarySelect: Boolean
		//		If true, toSelect will be same as the original selection status.
		if(!this._isValid(type, start)){
			return;
		}
		var lastIsSelected = this._isSelected(type, this._lastEndPoint[type]),
			isSelected = this._isSelected(type, start);
		
		//If we are modifying the selection using keyboard, retain the old status.
		this._toSelect = mandatarySelect ? isSelected : !isSelected;
		
		//If CTRL is not pressed or it's SINGLE mode, this is a brand new selection.
		if(!extending || (!isSelected && this._config[type] == SINGLE)){
			this._clearSelection("all", start);
			this._toSelect = toSelect === undefined ? true : toSelect;
		}
		
		this._selecting[type] = true;
		this._currentPoint[type] = null;
		
		//We're holding SHIFT while clicking, it's a Click-Range selection.
		if(isRange && this._lastType == type && lastIsSelected == this._toSelect){
			if(type === "row"){
				this._isUsingRowSelector = true;
			}
			this._startPoint[type] = this._lastEndPoint[type];
			this._highlight(type, this._startPoint[type]);
			this._isUsingRowSelector = false;
		}else{
			this._startPoint[type] = start;
		}
		//Now start selection
		this._curType = type;
		this._fireEvent("start", type);
		this._isStartFocus = true;
		this._isUsingRowSelector = true;
		this._highlight(type, start, this._toSelect);
		this._isStartFocus = false;
	},
	_endSelect: function(type){
		// summary:
		//		End selection. Keep records, fire events and cleanup status.
		// tag:
		//		private
		// type: String
		//		"row", "col", or "cell"
		if(type === "row"){
			delete this._isUsingRowSelector;
		}
		if(type == "all"){
			this._endSelect("col");
			this._endSelect("row");
			this._endSelect("cell");
		}else if(this._selecting[type]){
			this._addToSelected(type);
			this._lastAnchorPoint[type] = this._startPoint[type];
			this._lastEndPoint[type] = this._currentPoint[type];
			if(this._toSelect){
				this._lastSelectedAnchorPoint[type] = this._lastAnchorPoint[type];
				this._lastSelectedEndPoint[type] = this._lastEndPoint[type];
			}
			this._startPoint[type] = this._currentPoint[type] = null;
			this._selecting[type] = false;
			this._lastType = type;
			this._fireEvent("end", type);
		}
	},
	_fireEvent: function(evtName, type){
		switch(evtName){
			case "start":
				this.grid[this._toSelect ? "onStartSelect" : "onStartDeselect"](type, this._startPoint[type], this._selected);
				break;
			case "end":
				this.grid[this._toSelect ? "onEndSelect" : "onEndDeselect"](type, this._lastAnchorPoint[type], this._lastEndPoint[type], this._selected);
				break;
		}
	},
	_calcToHighlight: function(type, target, toHighlight, toSelect){
		// summary:
		//		Calculate what status should *target* have.
		//		If *toSelect* is not provided, this is a no op.
		// This function is time-critical!!
		if(toSelect !== undefined){
			var sltd;
			if(this._usingKeyboard && !toHighlight){
				var last = this._isInLastRange(this._lastType, target);
				if(last){
					sltd = this._isSelected(type, target);
					//This 2 cases makes the keyboard swipe selection valid!
					if(toSelect && sltd){
						return false;
					}
					if(!toSelect && !sltd && this._isInLastRange(this._lastType, target, true)){
						return true;
					}
				}
			}
			return toHighlight ? toSelect : (sltd || this._isSelected(type, target));
		}
		return toHighlight;
	},
	_highlightNode: function(node, toHighlight){
		// summary:
		//		Do the actual highlight work.
		if(node){
			var selectCSSClass = "dojoxGridRowSelected";
			var selectCellClass = "dojoxGridCellSelected";
			dojo.toggleClass(node, selectCSSClass, toHighlight);
			dojo.toggleClass(node, selectCellClass, toHighlight);
		}
	},
	_highlightHeader: function(colIdx, toHighlight){
		var cells = this.grid.layout.cells;
		var node = cells[colIdx].getHeaderNode();
		var selectedClass = "dojoxGridHeaderSelected";
		dojo.toggleClass(node, selectedClass, toHighlight);
	},
	_highlightRowSelector: function(rowIdx, toHighlight){
		//var t1 = (new Date()).getTime();
		var rowSelector = this.grid.views.views[0];
		if(rowSelector instanceof dojox.grid._RowSelector){
			var node = rowSelector.getRowNode(rowIdx);
			if(node){
				var selectedClass = "dojoxGridRowSelectorSelected";
				dojo.toggleClass(node, selectedClass, toHighlight);
			}
		}
		//console.log((new Date()).getTime() - t1);
	},
	_highlightSingle: function(type, toHighlight, target, toSelect, isRefresh){
		// summary:
		//		Highlight a single item.
		// This function is time critical!!
		var _this = this, toHL, g = _this.grid, cells = g.layout.cells;
		switch(type){
			case "cell":
				toHL = this._calcToHighlight(type, target, toHighlight, toSelect);
				var c = cells[target.col];
				if(!c.hidden && !c.notselectable){
					this._highlightNode(target.node || c.getNode(target.row), toHL);
				}
				break;
			case "col":
				toHL = this._calcToHighlight(type, target, toHighlight, toSelect);
				this._highlightHeader(target.col, toHL);
				dojo.query("td[idx='" + target.col + "']", g.domNode).forEach(function(cellNode){
					var rowNode = cells[target.col].view.content.findRowTarget(cellNode);
					if(rowNode){
						var rowIndex = rowNode[dojox.grid.util.rowIndexTag];
						_this._highlightSingle("cell", toHL, {
							"row": rowIndex,
							"col": target.col,
							"node": cellNode
						});
					}
				});
				break;
			case "row":
				toHL = this._calcToHighlight(type, target, toHighlight, toSelect);
				this._highlightRowSelector(target.row, toHL);
				dojo.forEach(cells, function(cell){
					_this._highlightSingle("cell", toHL, {
						"row": target.row,
						"col": cell.index,
						"node": cell.getNode(target.row)
					});
				});
				//To avoid dead lock
				this._selectedRowModified = true;
				if(!isRefresh){
					g.selection.setSelected(target.row, toHL);
				}
		}
	},
	_highlight: function(type, target, toSelect){
		// summary:
		//		Highlight from start point to target.
		// toSelect: Boolean
		//		Whether we are selecting or deselecting.
		// This function is time critical!!
		if(this._selecting[type] && target !== null){
			var start = this._startPoint[type],
				current = this._currentPoint[type],
				_this = this,
				highlight = function(from, to, toHL){
					_this._forEach(type, from, to, function(item){
						_this._highlightSingle(type, toHL, item, toSelect);
					}, true);
				};
			switch(type){
				case "col": case "row":
					if(current !== null){
						if(_inRange(type, target, start, current, true)){
							//target is between start and current, some selected should be deselected.
							highlight(current, target, false);
						}else{
							if(_inRange(type, start, target, current, true)){
								//selection has jumped to different direction, all should be deselected.
								highlight(current, start, false);
								current = start;
							}
							highlight(target, current, true);
						}
					}else{
						//First time select.
						this._highlightSingle(type, true, target, toSelect);
					}
					break;
				case "cell":
					if(current !== null){
						if(_inRange("row", target, start, current, true) ||
							_inRange("col", target, start, current, true) ||
							_inRange("row", start, target, current, true) ||
							_inRange("col", start, target, current, true)){
							highlight(start, current, false);
						}
					}
					highlight(start, target, true);
			}
			this._currentPoint[type] = target;
			this._focusPoint(type, this._currentPoint);
		}
	},
	_focusPoint: function(type, point){
		// summary:
		//		Focus the current point, so when you move mouse, the focus indicator follows you.
		if(!this._isStartFocus){
			var current = point[type],
				f = this.grid.focus;
			if(type == "col"){
				f._colHeadFocusIdx = current.col;
				f.focusArea("header");
			}else if(type == "row"){
				f.focusArea("rowHeader", {
					"rowIndex": current.row
				});
			}else if(type == "cell"){
				f.setFocusIndex(current.row, current.col);
			}
		}
	},
	_blurPoint: function(type, point){
		// summary:
		//		Blur the current point.
		var f = this.grid.focus;
		if(type == "col"){
			f._blurHeader();
		}else if(type == "cell"){
			f._blurContent();
		}
	},
	_addToSelected: function(type){
		// summary:
		//		Record the selected items.
		var toSelect = this._toSelect, _this = this,
			toAdd = [], toRemove = [],
			start = this._startPoint[type],
			end = this._currentPoint[type];
		if(this._usingKeyboard){
			//If using keyboard, selection will be ended after every move. But we have to remember the original selection status,
			//so as to return to correct status when we shrink the selection region.
			this._forEach(type, this._lastAnchorPoint[type], this._lastEndPoint[type], function(item){
				//If the original selected item is not in current range, change its status.
				if(!_inRange(type, item, start, end)){
					(toSelect ? toRemove : toAdd).push(item);
				}
			});
		}
		this._forEach(type, start, end, function(item){
			var isSelected = _this._isSelected(type, item);
			if(toSelect && !isSelected){
				//Add new selected items
				toAdd.push(item);
			}else if(!toSelect){
				//Remove deselected items.
				toRemove.push(item);
			}
		});
		this._add(type, toAdd);
		this._remove(type, toRemove);
		
		// have to keep record in original grid selection
		dojo.forEach(this._selected.row, function(item){
			if(item.except.length > 0){
				//to avoid dead lock
				this._selectedRowModified = true;
				this.grid.selection.setSelected(item.row, false);
			}
		}, this);
	},
	_forEach: function(type, start, end, func, halfClose){
		// summary:
		//		Go through items from *start* point to *end* point.
		// This function is time critical!!
		if(!this._isValid(type, start, true) || !this._isValid(type, end, true)){
			return;
		}
		switch(type){
			case "col": case "row":
				start = start[type];
				end = end[type];
				var dir = end > start ? 1 : -1;
				if(!halfClose){
					end += dir;
				}
				for(; start != end; start += dir){
					func(_createItem(type, start));
				}
				break;
			case "cell":
				var colDir = end.col > start.col ? 1 : -1,
					rowDir = end.row > start.row ? 1 : -1;
				for(var i = start.row, p = end.row + rowDir; i != p; i += rowDir){
					for(var j = start.col, q = end.col + colDir; j != q; j += colDir){
						func(_createItem(type, i, j));
					}
				}
		}
	},
	_makeupForExceptions: function(type, newCellItems){
		// summary:
		//		When new cells is selected, maybe they will fill in the "holes" in selected rows and columns.
		var makedUps = [];
		dojo.forEach(this._selected[type], function(v1){
			dojo.forEach(newCellItems, function(v2){
				if(v1[type] == v2[type]){
					var pos = dojo.indexOf(v1.except, v2[_theOther[type]]);
					if(pos >= 0){
						v1.except.splice(pos, 1);
					}
					makedUps.push(v2);
				}
			});
		});
		return makedUps;
	},
	_makeupForCells: function(type, newItems){
		// summary:
		//		When some rows/cols are selected, maybe they can cover some of the selected cells,
		//		and fill some of the "holes" in the selected cols/rows.
		var toRemove = [];
		dojo.forEach(this._selected.cell, function(v1){
			dojo.some(newItems, function(v2){
				if(v1[type] == v2[type]){
					toRemove.push(v1);
					return true;
				}
				return false;
			});
		});
		this._remove("cell", toRemove);
		dojo.forEach(this._selected[_theOther[type]], function(v1){
			dojo.forEach(newItems, function(v2){
				var pos = dojo.indexOf(v1.except, v2[type]);
				if(pos >= 0){
					v1.except.splice(pos, 1);
				}
			});
		});
	},
	_addException: function(type, items){
		// summary:
		//		If some rows/cols are deselected, maybe they have created "holes" in selected cols/rows.
		dojo.forEach(this._selected[type], function(v1){
			dojo.forEach(items, function(v2){
				v1.except.push(v2[_theOther[type]]);
			});
		});
	},
	_addCellException: function(type, items){
		// summary:
		//		If some cells are deselected, maybe they have created "holes" in selected rows/cols.
		dojo.forEach(this._selected[type], function(v1){
			dojo.forEach(items, function(v2){
				if(v1[type] == v2[type]){
					v1.except.push(v2[_theOther[type]]);
				}
			});
		});
	},
	_add: function(type, items){
		// summary:
		//		Add to the selection record.
		var cells = this.grid.layout.cells;
		if(type == "cell"){
			var colMakedup = this._makeupForExceptions("col", items);
			var rowMakedup = this._makeupForExceptions("row", items);
			//Step over hidden columns.
			items = dojo.filter(items, function(item){
				return dojo.indexOf(colMakedup, item) < 0 && dojo.indexOf(rowMakedup, item) < 0 &&
					!cells[item.col].hidden && !cells[item.col].notselectable;
			});
		}else{
			if(type == "col"){
				//Step over hidden columns.
				items = dojo.filter(items, function(item){
					return !cells[item.col].hidden && !cells[item.col].notselectable;
				});
			}
			this._makeupForCells(type, items);
			this._selected[type] = dojo.filter(this._selected[type], function(v){
				return dojo.every(items, function(item){
					return v[type] !== item[type];
				});
			});
		}
		if(type != "col" && this.grid._hasIdentity){
			dojo.forEach(items, function(item){
				var record = this.grid._by_idx[item.row];
				if(record){
					item.id = record.idty;
				}
			}, this);
		}
		this._selected[type] = this._selected[type].concat(items);
	},
	_remove: function(type, items){
		// summary:
		//		Remove from the selection record.
		var comp = dojo.partial(_isEqual, type);
		this._selected[type] = dojo.filter(this._selected[type], function(v1){
			return !dojo.some(items, function(v2){
				return comp(v1, v2);
			});
		});
		if(type == "cell"){
			this._addCellException("col", items);
			this._addCellException("row", items);
		}else{
			this._addException(_theOther[type], items);
		}
	},
	_isCellNotInExcept: function(type, item){
		// summary:
		//		Return true only when a cell is covered by selected row/col, and its not a "hole".
		var attr = item[type], corres = item[_theOther[type]];
		return dojo.some(this._selected[type], function(v){
			return v[type] == attr && dojo.indexOf(v.except, corres) < 0;
		});
	},
	_isSelected: function(type, item){
		// summary:
		//		Return true when the item is selected. (or logically selected, i.e, covered by a row/col).
		if(!item){ return false; }
		var res = dojo.some(this._selected[type], function(v){
			var ret = _isEqual(type, item, v);
			if(ret && type !== "cell"){
				return v.except.length === 0;
			}
			return ret;
		});
		if(!res && type === "cell"){
			res = (this._isCellNotInExcept("col", item) || this._isCellNotInExcept("row", item));
			if(type === "cell"){
				res = res && !this.grid.layout.cells[item.col].notselectable;
			}
		}
		return res;
	},
	_isInLastRange: function(type, item, isSelected){
		// summary:
		//		Return true only when the item is in the last seletion/deseletion range.
		var start = this[isSelected ? "_lastSelectedAnchorPoint" : "_lastAnchorPoint"][type],
			end = this[isSelected ? "_lastSelectedEndPoint" : "_lastEndPoint"][type];
		if(!item || !start || !end){ return false; }
		return _inRange(type, item, start, end);
	},
	_isValid: function(type, item, allowNotSelectable){
		// summary:
		//		Check whether the item is a valid __SelectItem for the given type.
		if(!item){ return false; }
		try{
			var g = this.grid, index = item[type];
			switch(type){
				case "col":
					return index >= 0 && index < g.layout.cells.length && dojo.isArray(item.except) &&
							(allowNotSelectable || !g.layout.cells[index].notselectable);
				case "row":
					return index >= 0 && index < g.rowCount && dojo.isArray(item.except);
				case "cell":
					return item.col >= 0 && item.col < g.layout.cells.length &&
							item.row >= 0 && item.row < g.rowCount &&
							(allowNotSelectable || !g.layout.cells[item.col].notselectable);
			}
		}catch(e){}
		return false;
	}
});
dojox.grid.EnhancedGrid.registerPlugin(dojox.grid.enhanced.plugins.Selector/*name:'selector'*/, {
	"dependency": ["autoScroll"]
});
})();
