/**
 * A control that allows selection of multiple items in a list.
 */
Ext.define('Ext.ux.form.MultiSelect', {

	extend: 'Ext.form.FieldContainer',

	mixins: [
		'Ext.util.StoreHolder',
		'Ext.form.field.Field'
	],

	alternateClassName: 'Ext.ux.Multiselect',
	alias: ['widget.multiselectfield', 'widget.multiselect'],

	uses: ['Ext.view.DragZone', 'Ext.view.DropZone'],

	layout: 'anchor',

	/**
	 * @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect DragZone.
	 */

	/**
	 * @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect DropZone.
	 */

	/**
	 * @cfg {String} [title=""] A title for the underlying panel.
	 */

	/**
	 * @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect list are drag/drop reorderable.
	 */
	ddReorder: false,

	/**
	 * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
	 * This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
	 * to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
	 */

	/**
	 * @cfg {String} [appendOnly=false] `true` if the list should only allow append drops when drag/drop is enabled.
	 * This is useful for lists which are sorted.
	 */
	appendOnly: false,

	/**
	 * @cfg {String} [displayField="text"] Name of the desired display field in the dataset.
	 */
	displayField: 'text',

	/**
	 * @cfg {String} [valueField="text"] Name of the desired value field in the dataset.
	 */

	/**
	 * @cfg {Boolean} [allowBlank=true] `false` to require at least one item in the list to be selected, `true` to allow no
	 * selection.
	 */
	allowBlank: true,

	/**
	 * @cfg {Number} [minSelections=0] Minimum number of selections allowed.
	 */
	minSelections: 0,

	/**
	 * @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of selections allowed.
	 */
	maxSelections: Number.MAX_VALUE,

	/**
	 * @cfg {String} [blankText="This field is required"] Default text displayed when the control contains no items.
	 */
	blankText: 'This field is required',

	/**
	 * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"]
	 * Validation message displayed when {@link #minSelections} is not met.
	 * The {0} token will be replaced by the value of {@link #minSelections}.
	 */
	minSelectionsText: 'Minimum {0} item(s) required',

	/**
	 * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"]
	 * Validation message displayed when {@link #maxSelections} is not met
	 * The {0} token will be replaced by the value of {@link #maxSelections}.
	 */
	maxSelectionsText: 'Maximum {0} item(s) required',

	/**
	 * @cfg {String} [delimiter=","] The string used to delimit the selected values when {@link #getSubmitValue submitting}
	 * the field as part of a form. If you wish to have the selected values submitted as separate
	 * parameters rather than a single delimited parameter, set this to `null`.
	 */
	delimiter: ',',

	/**
	 * @cfg {String} [dragText="{0} Item{1}"] The text to show while dragging items.
	 * {0} will be replaced by the number of items. {1} will be replaced by the plural
	 * form if there is more than 1 item.
	 */
	dragText: '{0} Item{1}',

	/**
	 * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to `undefined`).
	 * Acceptable values for this property are:
	 * <div class="mdetail-params"><ul>
	 * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
	 * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
	 * <div class="mdetail-params"><ul>
	 * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
	 * A 1-dimensional array will automatically be expanded (each array item will be the combo
	 * {@link #valueField value} and {@link #displayField text})</div></li>
	 * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
	 * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
	 * {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
	 * </div></li></ul></div></li></ul></div>
	 */

	ignoreSelectChange: 0,

	/**
	 * @cfg {Object} listConfig
	 * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor.
	 * Any configuration that is valid for BoundList can be included.
	 */

	initComponent: function() {
		var me = this;

		me.items = me.setupItems();

		me.bindStore(me.store, true);

		if (me.store.autoCreated) {
			me.valueField = me.displayField = 'field1';
			if (!me.store.expanded) {
				me.displayField = 'field2';
			}
		}

		if (!Ext.isDefined(me.valueField)) {
			me.valueField = me.displayField;
		}

		me.callParent();
		me.initField();
	},

	setupItems: function() {
		var me = this;

		me.boundList = Ext.create('Ext.view.BoundList', Ext.apply({
			anchor: 'none 100%',
			border: 1,
			multiSelect: true,
			store: me.store,
			displayField: me.displayField,
			disabled: me.disabled
		}, me.listConfig));

		me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me);

		// Boundlist expects a reference to its pickerField for when an item is selected (see Boundlist#onItemClick).
		me.boundList.pickerField = me;

		// Only need to wrap the BoundList in a Panel if we have a title.
		if (!me.title) {
			return me.boundList;
		}

		// Wrap to add a title
		me.boundList.border = false;
		return {
			border: true,
			anchor: 'none 100%',
			layout: 'anchor',
			title: me.title,
			tbar: me.tbar,
			items: me.boundList
		};
	},

	onSelectChange: function(selModel, selections) {
		if (!this.ignoreSelectChange) {
			this.setValue(selections);
		}
	},

	getSelected: function() {
		return this.boundList.getSelectionModel().getSelection();
	},

	// compare array values
	isEqual: function(v1, v2) {
		var fromArray = Ext.Array.from,
			i = 0,
			len;

		v1 = fromArray(v1);
		v2 = fromArray(v2);
		len = v1.length;

		if (len !== v2.length) {
			return false;
		}

		for(; i < len; i++) {
			if (v2[i] !== v1[i]) {
				return false;
			}
		}

		return true;
	},

	afterRender: function() {
		var me = this,
			records;

		me.callParent();
		if (me.selectOnRender) {
			records = me.getRecordsForValue(me.value);
			if (records.length) {
				++me.ignoreSelectChange;
				me.boundList.getSelectionModel().select(records);
				--me.ignoreSelectChange;
			}
			delete me.toSelect;
		}

		if (me.ddReorder && !me.dragGroup && !me.dropGroup) {
			me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
		}

		if (me.draggable || me.dragGroup) {
			me.dragZone = Ext.create('Ext.view.DragZone', {
				view: me.boundList,
				ddGroup: me.dragGroup,
				dragText: me.dragText
			});
		}
		if (me.droppable || me.dropGroup) {
			me.dropZone = Ext.create('Ext.view.DropZone', {
				view: me.boundList,
				ddGroup: me.dropGroup,
				handleNodeDrop: function(data, dropRecord, position) {
					var view = this.view,
						store = view.getStore(),
						records = data.records,
						index;

					// remove the Models from the source Store
					data.view.store.remove(records);

					index = store.indexOf(dropRecord);
					if (position === 'after') {
						index++;
					}
					store.insert(index, records);
					view.getSelectionModel().select(records);
					me.fireEvent('drop', me, records);
				}
			});
		}
	},

	isValid: function() {

		var me = this;
		var disabled = me.disabled;
		var validate = me.forceValidation || !disabled;

		return validate ? me.validateValue(me.value) : disabled;
	},

	validateValue: function(value) {

		var me = this;
		var errors = me.getErrors(value);
		var isValid = Ext.isEmpty(errors);

		return isValid;
	},

	markInvalid: function(errors) {
		// Save the message and fire the 'invalid' event
		var me = this;
		var oldMsg = me.getActiveError();

		me.setActiveErrors(Ext.Array.from(errors));
		if (oldMsg !== me.getActiveError()) {
			me.updateLayout();
		}
	},

	/**
	 * Clear any invalid styles/messages for this field.
	 *
	 * __Note:__ this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true`
	 * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow
	 * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
	 */
	clearInvalid: function() {
		// Clear the message and fire the 'valid' event
		var me = this,
			hadError = me.hasActiveError();
		me.unsetActiveError();
		if (hadError) {
			me.updateLayout();
		}
	},

	getSubmitData: function() {
		var me = this,
			data = null,
			val;
		if (!me.disabled && me.submitValue && !me.isFileUpload()) {
			val = me.getSubmitValue();
			if (val !== null) {
				data = {};
				data[me.getName()] = val;
			}
		}
		return data;
	},

	/**
	 * Returns the value that would be included in a standard form submit for this field.
	 *
	 * @return {String} The value to be submitted, or `null`.
	 */
	getSubmitValue: function() {
		var me = this,
			delimiter = me.delimiter,
			val = me.getValue();

		return Ext.isString(delimiter) ? val.join(delimiter) : val;
	},

	getValue: function() {
		return this.value || [];
	},

	getRecordsForValue: function(value) {
		var me = this,
			records = [],
			all = me.store.getRange(),
			valueField = me.valueField,
			i = 0,
			allLen = all.length,
			rec,
			j,
			valueLen;

		for (valueLen = value.length; i < valueLen; ++i) {
			for (j = 0; j < allLen; ++j) {
				rec = all[j];
				if (rec.get(valueField) == value[i]) {
					records.push(rec);
				}
			}
		}

		return records;
	},

	setupValue: function(value) {
		var delimiter = this.delimiter,
			valueField = this.valueField,
			i = 0,
			out,
			len,
			item;

		if (Ext.isDefined(value)) {
			if (delimiter && Ext.isString(value)) {
				value = value.split(delimiter);
			}
			else if (!Ext.isArray(value)) {
				value = [value];
			}

			for (len = value.length; i < len; ++i) {
				item = value[i];
				if (item && item.isModel) {
					value[i] = item.get(valueField);
				}
			}
			out = Ext.Array.unique(value);
		}
		else {
			out = [];
		}
		return out;
	},

	setValue: function(value) {
		var me = this,
			selModel = me.boundList.getSelectionModel(),
			store = me.store;

		// Store not loaded yet - we cannot set the value
		if (!store.getCount()) {
			store.on({
				load: Ext.Function.bind(me.setValue, me, [value]),
				single: true
			});
			return;
		}

		value = me.setupValue(value);
		me.mixins.field.setValue.call(me, value);

		if (me.rendered) {
			++me.ignoreSelectChange;
			selModel.deselectAll();
			if (value.length) {
				selModel.select(me.getRecordsForValue(value));
			}
			--me.ignoreSelectChange;
		}
		else {
			me.selectOnRender = true;
		}
	},

	clearValue: function() {
		this.setValue([]);
	},

	onEnable: function() {
		var list = this.boundList;
		this.callParent();
		if (list) {
			list.enable();
		}
	},

	onDisable: function() {
		var list = this.boundList;
		this.callParent();
		if (list) {
			list.disable();
		}
	},

	getErrors: function(value) {
		var me = this,
			format = Ext.String.format,
			errors = [],
			numSelected;

		value = Ext.Array.from(value || me.getValue());
		numSelected = value.length;

		if (!me.allowBlank && numSelected < 1) {
			errors.push(me.blankText);
		}
		if (numSelected < me.minSelections) {
			errors.push(format(me.minSelectionsText, me.minSelections));
		}
		if (numSelected > me.maxSelections) {
			errors.push(format(me.maxSelectionsText, me.maxSelections));
		}
		return errors;
	},

	onDestroy: function() {
		var me = this;

		me.bindStore(null);
		Ext.destroy(me.dragZone, me.dropZone);
		me.callParent();
	},

	onBindStore: function(store) {
		var boundList = this.boundList;

		if (boundList) {
			boundList.bindStore(store);
		}
	}

});
