/**
 * 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'],
    
    requires: ['Ext.panel.Panel', 'Ext.view.BoundList', 'Ext.layout.container.Fit'],
    
    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., `[['f','Foo'],['b','Bar']]`)<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.
     */
 
    /**
     * @cfg {Number} [pageSize=10] The number of items to advance on pageUp and pageDown
     */
    pageSize: 10,
    
    initComponent: function() {
        var me = this;
 
        me.items = me.setupItems();
 
        me.bindStore(me.store, true);
 
        me.callParent();
        me.initField();
    },
 
    setupItems: function() {
        var me = this;
        
        me.boundList = new Ext.view.BoundList(Ext.apply({
            anchor: 'none 100%',
            border: 1,
            multiSelect: true,
            store: me.store,
            displayField: me.displayField,
            disabled: me.disabled,
            tabIndex: 0,
            navigationModel: {
                type: 'default'
            }
        }, me.listConfig));
        
        me.boundList.getNavigationModel().addKeyBindings({
            pageUp: me.onKeyPageUp,
            pageDown: me.onKeyPageDown,
            scope: me
        });
 
        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 {
            xtype: 'panel',
            isAriaRegion: false,
            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,
            boundList, scrollable, records, panel;
        
        me.callParent();
 
        boundList = me.boundList;
        scrollable = boundList && boundList.getScrollable();
 
        if (me.selectOnRender) {
            records = me.getRecordsForValue(me.value);
 
            if (records.length) {
                ++me.ignoreSelectChange;
                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: boundList,
                ddGroup: me.dragGroup,
                dragText: me.dragText,
                containerScroll: !!scrollable,
                scrollEl: scrollable && scrollable.getElement()
            });
        }
 
        if (me.droppable || me.dropGroup) {
            me.dropZone = Ext.create('Ext.view.DropZone', {
                view: 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);
                }
            });
        }
        
        panel = me.down('panel');
        
        if (panel && boundList) {
            boundList.ariaEl.dom.setAttribute('aria-labelledby', panel.header.id + '-title-textEl');
        }
    },
    
    onKeyPageUp: function(e) {
        var me = this,
            pageSize = me.pageSize,
            boundList = me.boundList,
            nm = boundList.getNavigationModel(),
            oldIdx, newIdx;
        
        oldIdx = nm.recordIndex;
        
        // Unlike up arrow, pgUp does not wrap but goes to the first item
        newIdx = oldIdx > pageSize ? oldIdx - pageSize : 0;
        
        nm.setPosition(newIdx, e);
    },
    
    onKeyPageDown: function(e) {
        var me = this,
            pageSize = me.pageSize,
            boundList = me.boundList,
            nm = boundList.getNavigationModel(),
            count, oldIdx, newIdx;
        
        count = boundList.getStore().getCount();
        oldIdx = nm.recordIndex;
        
        // Unlike down arrow, pgDown does not wrap but goes to the last item
        newIdx = oldIdx < (count - pageSize) ? oldIdx + pageSize : count - 1;
        
        nm.setPosition(newIdx, e);
    },
    
    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 (= 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;
    },
    
    doDestroy: function() {
        var me = this;
        
        me.bindStore(null);
        Ext.destroy(me.dragZone, me.dropZone, me.keyNav);
        me.callParent();
    },
    
    onBindStore: function(store) {
        var me = this,
            boundList = this.boundList;
 
        if (store.autoCreated) {
            me.resolveDisplayField();
        }
 
        if (!Ext.isDefined(me.valueField)) {
            me.valueField = me.displayField;
        }
 
        if (boundList) {
            boundList.bindStore(store);
        }
    },
 
    /**
     * Applies auto-created store fields to field and boundlist
     * @private
     */
    resolveDisplayField: function() {
        var me = this,
            boundList = me.boundList,
            store = me.getStore();
 
        me.valueField = me.displayField = 'field1';
 
        if (!store.expanded) {
            me.displayField = 'field2';
        }
 
        if (boundList) {
            boundList.setDisplayField(me.displayField);
        }
    }
});