dojo.provide("dijit.form._FormSelectWidget");

dojo.require("dijit.form._FormWidget");
dojo.require("dojo.data.util.sorter");

/*=====
dijit.form.__SelectOption = function(){
	// value: String
	//		The value of the option.  Setting to empty (or missing) will
	//		place a separator at that location
	// label: String
	//		The label for our option.  It can contain html tags.
	//  selected: Boolean
	//		Whether or not we are a selected option
	// disabled: Boolean
	//		Whether or not this specific option is disabled
	this.value = value;
	this.label = label;
	this.selected = selected;
	this.disabled = disabled;
}
=====*/

dojo.declare("dijit.form._FormSelectWidget", dijit.form._FormValueWidget, {
	// summary:
	//		Extends _FormValueWidget in order to provide "select-specific"
	//		values - i.e., those values that are unique to <select> elements.
	//		This also provides the mechanism for reading the elements from
	//		a store, if desired.

	// multiple: Boolean
	//		Whether or not we are multi-valued
	multiple: false,

	// options: dijit.form.__SelectOption[]
	//		The set of options for our select item.  Roughly corresponds to
	//      the html <option> tag.
	options: null,

	// store: dojo.data.api.Identity
	//		A store which, at the very least impelements dojo.data.api.Identity
	//		to use for getting our list of options - rather than reading them
	//		from the <option> html tags.
	store: null,

	// query: object
	//		A query to use when fetching items from our store
	query: null,

	// queryOptions: object
	//		Query options to use when fetching from the store
	queryOptions: null,

	// onFetch: Function
	//		A callback to do with an onFetch - but before any items are actually
	//		iterated over (i.e. to filter even futher what you want to add)
	onFetch: null,

	// sortByLabel: boolean
	//		Flag to sort the options returned from a store by the label of
	//		the store.
	sortByLabel: true,


	// loadChildrenOnOpen: boolean
	//		By default loadChildren is called when the items are fetched from the
	//		store.  This property allows delaying loadChildren (and the creation
	//		of the options/menuitems) until the user opens the click the button.
	//		dropdown
	loadChildrenOnOpen: false,

	getOptions: function(/* anything */ valueOrIdx){
		// summary:
		//		Returns a given option (or options).
		// valueOrIdx:
		//		If passed in as a string, that string is used to look up the option
		//		in the array of options - based on the value property.
		//		(See dijit.form.__SelectOption).
		//
		//		If passed in a number, then the option with the given index (0-based)
		//		within this select will be returned.
		//
		//		If passed in a dijit.form.__SelectOption, the same option will be
		//		returned if and only if it exists within this select.
		//
		//		If passed an array, then an array will be returned with each element
		//		in the array being looked up.
		//
		//		If not passed a value, then all options will be returned
		//
		// returns:
		//		The option corresponding with the given value or index.  null
		//		is returned if any of the following are true:
		//			- A string value is passed in which doesn't exist
		//			- An index is passed in which is outside the bounds of the array of options
		//			- A dijit.form.__SelectOption is passed in which is not a part of the select

		// NOTE: the compare for passing in a dijit.form.__SelectOption checks
		//		if the value property matches - NOT if the exact option exists
		// NOTE: if passing in an array, null elements will be placed in the returned
		//		array when a value is not found.
		var lookupValue = valueOrIdx, opts = this.options || [], l = opts.length;

		if(lookupValue === undefined){
			return opts; // dijit.form.__SelectOption[]
		}
		if(dojo.isArray(lookupValue)){
			return dojo.map(lookupValue, "return this.getOptions(item);", this); // dijit.form.__SelectOption[]
		}
		if(dojo.isObject(valueOrIdx)){
			// We were passed an option - so see if it's in our array (directly),
			// and if it's not, try and find it by value.
			if(!dojo.some(this.options, function(o, idx){
				if(o === lookupValue ||
					(o.value && o.value === lookupValue.value)){
					lookupValue = idx;
					return true;
				}
				return false;
			})){
				lookupValue = -1;
			}
		}
		if(typeof lookupValue == "string"){
			for(var i=0; i<l; i++){
				if(opts[i].value === lookupValue){
					lookupValue = i;
					break;
				}
			}
		}
		if(typeof lookupValue == "number" && lookupValue >= 0 && lookupValue < l){
			return this.options[lookupValue] // dijit.form.__SelectOption
		}
		return null; // null
	},

	addOption: function(/* dijit.form.__SelectOption, dijit.form.__SelectOption[] */ option){
		// summary:
		//		Adds an option or options to the end of the select.  If value
		//		of the option is empty or missing, a separator is created instead.
		//		Passing in an array of options will yield slightly better performance
		//		since the children are only loaded once.
		if(!dojo.isArray(option)){ option = [option]; }
		dojo.forEach(option, function(i){
			if(i && dojo.isObject(i)){
				this.options.push(i);
			}
		}, this);
		this._loadChildren();
	},

	removeOption: function(/* string, dijit.form.__SelectOption, number, or array */ valueOrIdx){
		// summary:
		//		Removes the given option or options.  You can remove by string
		//		(in which case the value is removed), number (in which case the
		//		index in the options array is removed), or select option (in
		//		which case, the select option with a matching value is removed).
		//		You can also pass in an array of those values for a slightly
		//		better performance since the children are only loaded once.
		if(!dojo.isArray(valueOrIdx)){ valueOrIdx = [valueOrIdx]; }
		var oldOpts = this.getOptions(valueOrIdx);
		dojo.forEach(oldOpts, function(i){
			// We can get null back in our array - if our option was not found.  In
			// that case, we don't want to blow up...
			if(i){
				this.options = dojo.filter(this.options, function(node, idx){
					return (node.value !== i.value);
				});
				this._removeOptionItem(i);
			}
		}, this);
		this._loadChildren();
	},

	updateOption: function(/* dijit.form.__SelectOption, dijit.form.__SelectOption[] */ newOption){
		// summary:
		//		Updates the values of the given option.  The option to update
		//		is matched based on the value of the entered option.  Passing
		//		in an array of new options will yeild better performance since
		//		the children will only be loaded once.
		if(!dojo.isArray(newOption)){ newOption = [newOption]; }
		dojo.forEach(newOption, function(i){
			var oldOpt = this.getOptions(i), k;
			if(oldOpt){
				for(k in i){ oldOpt[k] = i[k]; }
			}
		}, this);
		this._loadChildren();
	},

	setStore: function(/* dojo.data.api.Identity */ store,
						/* anything? */ selectedValue,
						/* Object? */ fetchArgs){
		// summary:
		//		Sets the store you would like to use with this select widget.
		//		The selected value is the value of the new store to set.  This
		//		function returns the original store, in case you want to reuse
		//		it or something.
		// store: dojo.data.api.Identity
		//		The store you would like to use - it MUST implement Identity,
		//		and MAY implement Notification.
		// selectedValue: anything?
		//		The value that this widget should set itself to *after* the store
		//		has been loaded
		// fetchArgs: Object?
		//		The arguments that will be passed to the store's fetch() function
		var oStore = this.store;
		fetchArgs = fetchArgs || {};
		if(oStore !== store){
			// Our store has changed, so update our notifications
			dojo.forEach(this._notifyConnections || [], dojo.disconnect);
			delete this._notifyConnections;
			if(store && store.getFeatures()["dojo.data.api.Notification"]){
				this._notifyConnections = [
					dojo.connect(store, "onNew", this, "_onNewItem"),
					dojo.connect(store, "onDelete", this, "_onDeleteItem"),
					dojo.connect(store, "onSet", this, "_onSetItem")
				];
			}
			this.store = store;
		}

		// Turn off change notifications while we make all these changes
		this._onChangeActive = false;

		// Remove existing options (if there are any)
		if(this.options && this.options.length){
			this.removeOption(this.options);
		}

		// Add our new options
		if(store){
			var cb = function(items){
				if(this.sortByLabel && !fetchArgs.sort && items.length){
					items.sort(dojo.data.util.sorter.createSortFunction([{
						attribute: store.getLabelAttributes(items[0])[0]
					}], store));
				}

				if(fetchArgs.onFetch){
					items = fetchArgs.onFetch(items);
				}
				// TODO: Add these guys as a batch, instead of separately
				dojo.forEach(items, function(i){
					this._addOptionForItem(i);
				}, this);

				// Set our value (which might be undefined), and then tweak
				// it to send a change event with the real value
				this._loadingStore = false;
				this.set("value", (("_pendingValue" in this) ? this._pendingValue : selectedValue));
				delete this._pendingValue;

				if(!this.loadChildrenOnOpen){
					this._loadChildren();
				}else{
					this._pseudoLoadChildren(items);
				}
				this._fetchedWith = opts;
				this._lastValueReported = this.multiple ? [] : null;
				this._onChangeActive = true;
				this.onSetStore();
				this._handleOnChange(this.value);
			};
			var opts = dojo.mixin({onComplete:cb, scope: this}, fetchArgs);
			this._loadingStore = true;
			store.fetch(opts);
		}else{
			delete this._fetchedWith;
		}
		return oStore;	// dojo.data.api.Identity
	},

	_setValueAttr: function(/*anything*/ newValue, /*Boolean, optional*/ priorityChange){
		// summary:
		//		set the value of the widget.
		//		If a string is passed, then we set our value from looking it up.
		if(this._loadingStore){
			// Our store is loading - so save our value, and we'll set it when
			// we're done
			this._pendingValue = newValue;
			return;
		}
		var opts = this.getOptions() || [];
		if(!dojo.isArray(newValue)){
			newValue = [newValue];
		}
		dojo.forEach(newValue, function(i, idx){
			if(!dojo.isObject(i)){
				i = i + "";
			}
			if(typeof i === "string"){
				newValue[idx] = dojo.filter(opts, function(node){
					return node.value === i;
				})[0] || {value: "", label: ""};
			}
		}, this);

		// Make sure some sane default is set
		newValue = dojo.filter(newValue, function(i){ return i && i.value; });
		if(!this.multiple && (!newValue[0] || !newValue[0].value) && opts.length){
			newValue[0] = opts[0];
		}
		dojo.forEach(opts, function(i){
			i.selected = dojo.some(newValue, function(v){ return v.value === i.value; });
		});
		var val = dojo.map(newValue, function(i){ return i.value; }),
			disp = dojo.map(newValue, function(i){ return i.label; });

		this.value = this.multiple ? val : val[0];
		this._setDisplay(this.multiple ? disp : disp[0]);
		this._updateSelection();
		this._handleOnChange(this.value, priorityChange);
	},

	_getDisplayedValueAttr: function(){
		// summary:
		//		returns the displayed value of the widget
		var val = this.get("value");
		if(!dojo.isArray(val)){
			val = [val];
		}
		var ret = dojo.map(this.getOptions(val), function(v){
			if(v && "label" in v){
				return v.label;
			}else if(v){
				return v.value;
			}
			return null;
		}, this);
		return this.multiple ? ret : ret[0];
	},

	_getValueDeprecated: false, // remove when _FormWidget:getValue is removed
	getValue: function(){
		// summary:
		//		get the value of the widget.
		return this._lastValue;
	},

	undo: function(){
		// summary:
		//		restore the value to the last value passed to onChange
		this._setValueAttr(this._lastValueReported, false);
	},

	_loadChildren: function(){
		// summary:
		//		Loads the children represented by this widget's options.
		//		reset the menu to make it "populatable on the next click
		if(this._loadingStore){ return; }
		dojo.forEach(this._getChildren(), function(child){
			child.destroyRecursive();
		});
		// Add each menu item
		dojo.forEach(this.options, this._addOptionItem, this);

		// Update states
		this._updateSelection();
	},

	_updateSelection: function(){
		// summary:
		//		Sets the "selected" class on the item for styling purposes
		this.value = this._getValueFromOpts();
		var val = this.value;
		if(!dojo.isArray(val)){
			val = [val];
		}
		if(val && val[0]){
			dojo.forEach(this._getChildren(), function(child){
				var isSelected = dojo.some(val, function(v){
					return child.option && (v === child.option.value);
				});
				dojo.toggleClass(child.domNode, this.baseClass + "SelectedOption", isSelected);
				dijit.setWaiState(child.domNode, "selected", isSelected);
			}, this);
		}
		this._handleOnChange(this.value);
	},

	_getValueFromOpts: function(){
		// summary:
		//		Returns the value of the widget by reading the options for
		//		the selected flag
		var opts = this.getOptions() || [];
		if(!this.multiple && opts.length){
			// Mirror what a select does - choose the first one
			var opt = dojo.filter(opts, function(i){
				return i.selected;
			})[0];
			if(opt && opt.value){
				return opt.value
			}else{
				opts[0].selected = true;
				return opts[0].value;
			}
		}else if(this.multiple){
			// Set value to be the sum of all selected
			return dojo.map(dojo.filter(opts, function(i){
				return i.selected;
			}), function(i){
				return i.value;
			}) || [];
		}
		return "";
	},

	// Internal functions to call when we have store notifications come in
	_onNewItem: function(/* item */ item, /* Object? */ parentInfo){
		if(!parentInfo || !parentInfo.parent){
			// Only add it if we are top-level
			this._addOptionForItem(item);
		}
	},
	_onDeleteItem: function(/* item */ item){
		var store = this.store;
		this.removeOption(store.getIdentity(item));
	},
	_onSetItem: function(/* item */ item){
		this.updateOption(this._getOptionObjForItem(item));
	},

	_getOptionObjForItem: function(item){
		// summary:
		//		Returns an option object based off the given item.  The "value"
		//		of the option item will be the identity of the item, the "label"
		//		of the option will be the label of the item.  If the item contains
		//		children, the children value of the item will be set
		var store = this.store, label = store.getLabel(item),
			value = (label ? store.getIdentity(item) : null);
		return {value: value, label: label, item:item}; // dijit.form.__SelectOption
	},

	_addOptionForItem: function(/* item */ item){
		// summary:
		//		Creates (and adds) the option for the given item
		var store = this.store;
		if(!store.isItemLoaded(item)){
			// We are not loaded - so let's load it and add later
			store.loadItem({item: item, onComplete: function(i){
				this._addOptionForItem(item);
			},
			scope: this});
			return;
		}
		var newOpt = this._getOptionObjForItem(item);
		this.addOption(newOpt);
	},

	constructor: function(/* Object */ keywordArgs){
		// summary:
		//		Saves off our value, if we have an initial one set so we
		//		can use it if we have a store as well (see startup())
		this._oValue = (keywordArgs || {}).value || null;
	},

	_fillContent: function(){
		// summary:
		//		Loads our options and sets up our dropdown correctly.  We
		//		don't want any content, so we don't call any inherit chain
		//		function.
		var opts = this.options;
		if(!opts){
			opts = this.options = this.srcNodeRef ? dojo.query(">",
						this.srcNodeRef).map(function(node){
							if(node.getAttribute("type") === "separator"){
								return { value: "", label: "", selected: false, disabled: false };
							}
							return { value: node.getAttribute("value"),
										label: String(node.innerHTML),
										selected: node.getAttribute("selected") || false,
										disabled: node.getAttribute("disabled") || false };
						}, this) : [];
		}
		if(!this.value){
			this.value = this._getValueFromOpts();
		}else if(this.multiple && typeof this.value == "string"){
			this.value = this.value.split(",");
		}
	},

	postCreate: function(){
		// summary:
		//		sets up our event handling that we need for functioning
		//		as a select
		dojo.setSelectable(this.focusNode, false);
		this.inherited(arguments);

		// Make our event connections for updating state
		this.connect(this, "onChange", "_updateSelection");
		this.connect(this, "startup", "_loadChildren");

		this._setValueAttr(this.value, null);
	},

	startup: function(){
		// summary:
		//		Connects in our store, if we have one defined
		this.inherited(arguments);
		var store = this.store, fetchArgs = {};
		dojo.forEach(["query", "queryOptions", "onFetch"], function(i){
			if(this[i]){
				fetchArgs[i] = this[i];
			}
			delete this[i];
		}, this);
		if(store && store.getFeatures()["dojo.data.api.Identity"]){
			// Temporarily set our store to null so that it will get set
			// and connected appropriately
			this.store = null;
			this.setStore(store, this._oValue, fetchArgs);
		}
	},

	destroy: function(){
		// summary:
		//		Clean up our connections
		dojo.forEach(this._notifyConnections || [], dojo.disconnect);
		this.inherited(arguments);
	},

	_addOptionItem: function(/* dijit.form.__SelectOption */ option){
		// summary:
		//		User-overridable function which, for the given option, adds an
		//		item to the select.  If the option doesn't have a value, then a
		//		separator is added in that place.  Make sure to store the option
		//		in the created option widget.
	},

	_removeOptionItem: function(/* dijit.form.__SelectOption */ option){
		// summary:
		//		User-overridable function which, for the given option, removes
		//		its item from the select.
	},

	_setDisplay: function(/*String or String[]*/ newDisplay){
		// summary:
		//		Overridable function which will set the display for the
		//		widget.  newDisplay is either a string (in the case of
		//		single selects) or array of strings (in the case of multi-selects)
	},

	_getChildren: function(){
		// summary:
		//		Overridable function to return the children that this widget contains.
		return [];
	},

	_getSelectedOptionsAttr: function(){
		// summary:
		//		hooks into this.attr to provide a mechanism for getting the
		//		option items for the current value of the widget.
		return this.getOptions(this.get("value"));
	},

	_pseudoLoadChildren: function(/* item[] */ items){
		// summary:
		//		a function that will "fake" loading children, if needed, and
		//		if we have set to not load children until the widget opens.
		// items:
		//		An array of items that will be loaded, when needed
	},

	onSetStore: function(){
		// summary:
		//		a function that can be connected to in order to receive a
		//		notification that the store has finished loading and all options
		//		from that store are available
	}
});
