dojo.provide("dojo.selection.Selection");
dojo.require("dojo.lang.array");
dojo.require("dojo.lang.func");
dojo.require("dojo.math");

dojo.selection.Selection = function(items, isCollection) {
	this.items = [];
	this.selection = [];
	this._pivotItems = [];
	this.clearItems();

	if(items) {
		if(isCollection) {
			this.setItemsCollection(items);
		} else {
			this.setItems(items);
		}
	}
}
dojo.lang.extend(dojo.selection.Selection, {
	items: null, // items to select from, order matters for growable selections

	selection: null, // items selected, aren't stored in order (see sorted())
	lastSelected: null, // last item selected

	allowImplicit: true, // if true, grow selection will start from 0th item when nothing is selected
	length: 0, // number of *selected* items

	// if true, the selection is treated as an in-order and can grow by ranges, not just by single item
	isGrowable: true,

	_pivotItems: null, // stack of pivot items
	_pivotItem: null, // item we grow selections from, top of stack

	// event handlers
	onSelect: function(item) {},
	onDeselect: function(item) {},
	onSelectChange: function(item, selected) {},

	_find: function(item, inSelection) {
		if(inSelection) {
			return dojo.lang.find(item, this.selection);
		} else {
			return dojo.lang.find(item, this.items);
		}
	},

	isSelectable: function(item) {
		// user-customizable, will filter items through this
		return true;
	},

	setItems: function(/* ... */) {
		this.clearItems();
		this.addItems.call(this, arguments);
	},

	// this is in case you have an active collection array-like object
	// (i.e. getElementsByTagName collection) that manages its own order
	// and item list
	setItemsCollection: function(collection) {
		this.items = collection;
	},

	addItems: function(/* ... */) {
		var args = dojo.lang.unnest(arguments);
		for(var i = 0; i < args.length; i++) {
			this.items.push(args[i]);
		}
	},

	addItemsAt: function(item, before /* ... */) {
		if(this.items.length == 0) { // work for empy case
			return this.addItems(dojo.lang.toArray(arguments, 2));
		}

		if(!this.isItem(item)) {
			item = this.items[item];
		}
		if(!item) { throw new Error("addItemsAt: item doesn't exist"); }
		var idx = this._find(item);
		if(idx > 0 && before) { idx--; }
		for(var i = 2; i < arguments.length; i++) {
			if(!this.isItem(arguments[i])) {
				this.items.splice(idx++, 0, arguments[i]);
			}
		}
	},

	removeItem: function(item) {
		// remove item
		var idx = this._find(item);
		if(idx > -1) {
			this.items.splice(idx, 1);
		}
		// remove from selection
		// FIXME: do we call deselect? I don't think so because this isn't how
		// you usually want to deselect an item. For example, if you deleted an
		// item, you don't really want to deselect it -- you want it gone. -DS
		idx = this._find(item, true);
		if(idx > -1) {
			this.selection.splice(idx, 1);
		}
	},

	clearItems: function() {
		this.items = [];
		this.deselectAll();
	},

	isItem: function(item) {
		return this._find(item) > -1;
	},

	isSelected: function(item) {
		return this._find(item, true) > -1;
	},

	/**
	 * allows you to filter item in or out of the selection
	 * depending on the current selection and action to be taken
	**/
	selectFilter: function(item, selection, add, grow) {
		return true;
	},

	/**
	 * update -- manages selections, most selecting should be done here
	 *  item => item which may be added/grown to/only selected/deselected
	 *  add => behaves like ctrl in windows selection world
	 *  grow => behaves like shift
	 *  noToggle => if true, don't toggle selection on item
	**/
	update: function(item, add, grow, noToggle) {
		if(!this.isItem(item)) { return false; }

		if(this.isGrowable && grow) {
			if(!this.isSelected(item)
				&& this.selectFilter(item, this.selection, false, true)) {
				this.grow(item);
				this.lastSelected = item;
			}
		} else if(add) {
			if(this.selectFilter(item, this.selection, true, false)) {
				if(noToggle) {
					if(this.select(item)) {
						this.lastSelected = item;
					}
				} else if(this.toggleSelected(item)) {
					this.lastSelected = item;
				}
			}
		} else {
			this.deselectAll();
			this.select(item);
		}

		this.length = this.selection.length;
	},

	/**
	 * Grow a selection.
	 *  toItem => which item to grow selection to
	 *  fromItem => which item to start the growth from (it won't be selected)
	 *
	 * Any items in (fromItem, lastSelected] that aren't part of
	 * (fromItem, toItem] will be deselected
	**/
	grow: function(toItem, fromItem) {
		if(!this.isGrowable) { return; }

		if(arguments.length == 1) {
			fromItem = this._pivotItem;
			if(!fromItem && this.allowImplicit) {
				fromItem = this.items[0];
			}
		}
		if(!toItem || !fromItem) { return false; }

		var fromIdx = this._find(fromItem);

		// get items to deselect (fromItem, lastSelected]
		var toDeselect = {};
		var lastIdx = -1;
		if(this.lastSelected) {
			lastIdx = this._find(this.lastSelected);
			var step = fromIdx < lastIdx ? -1 : 1;
			var range = dojo.math.range(lastIdx, fromIdx, step);
			for(var i = 0; i < range.length; i++) {
				toDeselect[range[i]] = true;
			}
		}

		// add selection (fromItem, toItem]
		var toIdx = this._find(toItem);
		var step = fromIdx < toIdx ? -1 : 1;
		var shrink = lastIdx >= 0 && step == 1 ? lastIdx < toIdx : lastIdx > toIdx;
		var range = dojo.math.range(toIdx, fromIdx, step);
		if(range.length) {
			for(var i = range.length-1; i >= 0; i--) {
				var item = this.items[range[i]];
				if(this.selectFilter(item, this.selection, false, true)) {
					if(this.select(item, true) || shrink) {
						this.lastSelected = item;
					}
					if(range[i] in toDeselect) {
						delete toDeselect[range[i]];
					}
				}
			}
		} else {
			this.lastSelected = fromItem;
		}

		// now deselect...
		for(var i in toDeselect) {
			if(this.items[i] == this.lastSelected) {
				//dojo.debug("oops!");
			}
			this.deselect(this.items[i]);
		}

		// make sure everything is all kosher after selections+deselections
		this._updatePivot();
	},

	/**
	 * Grow selection upwards one item from lastSelected
	**/
	growUp: function() {
		if(!this.isGrowable) { return; }

		var idx = this._find(this.lastSelected) - 1;
		while(idx >= 0) {
			if(this.selectFilter(this.items[idx], this.selection, false, true)) {
				this.grow(this.items[idx]);
				break;
			}
			idx--;
		}
	},

	/**
	 * Grow selection downwards one item from lastSelected
	**/
	growDown: function() {
		if(!this.isGrowable) { return; }

		var idx = this._find(this.lastSelected);
		if(idx < 0 && this.allowImplicit) {
			this.select(this.items[0]);
			idx = 0;
		}
		idx++;
		while(idx > 0 && idx < this.items.length) {
			if(this.selectFilter(this.items[idx], this.selection, false, true)) {
				this.grow(this.items[idx]);
				break;
			}
			idx++;
		}
	},

	toggleSelected: function(item, noPivot) {
		if(this.isItem(item)) {
			if(this.select(item, noPivot)) { return 1; }
			if(this.deselect(item)) { return -1; }
		}
		return 0;
	},

	select: function(item, noPivot) {
		if(this.isItem(item) && !this.isSelected(item)
			&& this.isSelectable(item)) {
			this.selection.push(item);
			this.lastSelected = item;
			this.onSelect(item);
			this.onSelectChange(item, true);
			if(!noPivot) {
				this._addPivot(item);
			}
			return true;
		}
		return false;
	},

	deselect: function(item) {
		var idx = this._find(item, true);
		if(idx > -1) {
			this.selection.splice(idx, 1);
			this.onDeselect(item);
			this.onSelectChange(item, false);
			if(item == this.lastSelected) {
				this.lastSelected = null;
			}

			this._removePivot(item);

			return true;
		}
		return false;
	},

	selectAll: function() {
		for(var i = 0; i < this.items.length; i++) {
			this.select(this.items[i]);
		}
	},

	deselectAll: function() {
		while(this.selection && this.selection.length) {
			this.deselect(this.selection[0]);
		}
	},

	selectNext: function() {
		var idx = this._find(this.lastSelected);
		while(idx > -1 && ++idx < this.items.length) {
			if(this.isSelectable(this.items[idx])) {
				this.deselectAll();
				this.select(this.items[idx]);
				return true;
			}
		}
		return false;
	},

	selectPrevious: function() {
		//debugger;
		var idx = this._find(this.lastSelected);
		while(idx-- > 0) {
			if(this.isSelectable(this.items[idx])) {
				this.deselectAll();
				this.select(this.items[idx]);
				return true;
			}
		}
		return false;
	},

	// select first selectable item
	selectFirst: function() {
		this.deselectAll();
		var idx = 0;
		while(this.items[idx] && !this.select(this.items[idx])) {
			idx++;
		}
		return this.items[idx] ? true : false;
	},

	// select last selectable item
	selectLast: function() {
		this.deselectAll();
		var idx = this.items.length-1;
		while(this.items[idx] && !this.select(this.items[idx])) {
			idx--;
		}
		return this.items[idx] ? true : false;
	},

	_addPivot: function(item, andClear) {
		this._pivotItem = item;
		if(andClear) {
			this._pivotItems = [item];
		} else {
			this._pivotItems.push(item);
		}
	},

	_removePivot: function(item) {
		var i = dojo.lang.find(item, this._pivotItems);
		if(i > -1) {
			this._pivotItems.splice(i, 1);
			this._pivotItem = this._pivotItems[this._pivotItems.length-1];
		}

		this._updatePivot();
	},

	_updatePivot: function() {
		if(this._pivotItems.length == 0) {
			if(this.lastSelected) {
				this._addPivot(this.lastSelected);
			}
		}
	},

	sorted: function() {
		return dojo.lang.toArray(this.selection).sort(
			dojo.lang.hitch(this, function(a, b) {
				var A = this._find(a), B = this._find(b);
				if(A > B) {
					return 1;
				} else if(A < B) {
					return -1;
				} else {
					return 0;
				}
			})
		);
	},

	// remove any items from the selection that are no longer in this.items
	updateSelected: function() {
		for(var i = 0; i < this.selection.length; i++) {
			if(this._find(this.selection[i]) < 0) {
				var removed = this.selection.splice(i, 1);

				this._removePivot(removed[0]);
			}
		}

		this.length = this.selection.length;
	}
});
