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

        extend: 'Ext.form.FieldContainer',

        mixins: {
            bindable: 'Ext.util.Bindable',
            field: 'Ext.form.field.Field'
        },

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

        requires: ['Ext.panel.Panel', 'Ext.view.BoundList'],

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

        /**
         * @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: 'Minimum {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 <tt>null</tt>.
         */
        delimiter: ',',

        /**
         * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to <tt>undefined</tt>).
         * 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,

        initComponent: function(){
            var me = this;

            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;
            }
            Ext.apply(me, me.setupItems());

            me.callParent();
            me.initField();
            me.addEvents('drop');
        },

        setupItems: function() {
            var me = this;

            me.boundList = Ext.create('Ext.view.BoundList', {
                deferInitialRefresh: false,
                multiSelect: true,
                store: me.store,
                displayField: me.displayField,
                disabled: me.disabled
            });

            me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me);
            return {
                layout: 'fit',
                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;

            me.callParent();
            if (me.selectOnRender) {
                ++me.ignoreSelectChange;
                me.boundList.getSelectionModel().select(me.getRecordsForValue(me.value));
                --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: '{0} Item{1}'
                });
            }
            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,
                disabled = me.disabled,
                validate = me.forceValidation || !disabled;

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

        validateValue: function(value) {
            var me = this,
                errors = me.getErrors(value),
                isValid = Ext.isEmpty(errors);

            if (!me.preventMark) {
                if (isValid) {
                    me.clearInvalid();
                } else {
                    me.markInvalid(errors);
                }
            }

            return isValid;
        },

        markInvalid : function(errors) {
            // Save the message and fire the 'invalid' event
            var me = this,
                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 not loaded yet - we cannot set the value
            if (!me.store.getCount()) {
                me.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();
                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);
            }
        }

    });