/**
 * This class provides a flexible means to control the
 * `{@link Ext.util.Collection#cfg!sorters sorters}` of a
 * `{@link Ext.util.Collection Collection}`. Instances of this class are created
 * automatically when sorters are added to added to Collections.
 *
 * This collection can be directly manipulated by application code to gain full
 * control over the sorters of the owner collection. Be aware that some components
 * create sorters internally (such as grids) so be careful in such cases to not disturb
 * these additional sorters.
 *
 * Items in this collection are `Ext.util.Sorter` instances and can be managed
 * individually by their `id`. This is the recommended way to manage application
 * filters while preserving sorter applied from other sources.
 *
 * Bulk changes to this collection should be wrapped in
 * `{@link Ext.util.Collection#method!beginUpdate beginUpdate}` and
 * `{@link Ext.util.Collection#method!endUpdate endUpdate}` (as with any collection).
 * During these bulk updates all reactions to sorter changes will be suspended.
 */
Ext.define('Ext.util.SorterCollection', {
    extend: 'Ext.util.Collection',
 
    requires: [
        'Ext.util.Sorter'
    ],
 
    isSorterCollection: true,
 
    /**
     * @property {Ext.util.Sortable} sortable
     * The owning sortable instance. The sortable's configuration governs this
     * collection.
     * @private
     * @readonly
     */
    $sortable: null,
 
    /**
     * @property sortFn
     * This is the cached sorting function which is a generated function that calls all the
     * configured sorters in the correct order.
     * @readonly
     */
    sortFn: null,
    
    config: {
        /**
         * @cfg {Function} applySorterOptionsFn
         * A template method that can be used to apply options to a sorter during creation
         * @private
         */
        sorterOptionsFn: null,
        
        /**
         * @cfg {Object} applySorterOptionsScope
         * The scope to execute the {@link #applySorterOptionsFn}
         * @private
         */
        sorterOptionsScope: null
    },
 
    constructor: function(config) {
        var me = this;
 
        me.sortFn = Ext.util.Sorter.createComparator(me);
 
        me.callParent([config]);
        me.setDecoder(me.decodeSorter);
    },
 
    addSort: function(property, direction, mode) {
        var me = this,
            count, index, limit, options, primary, sorter, sorters;
 
        if (!property) {
            // nothing specified so just trigger a sort...
            me.beginUpdate();
            me.endUpdate();
        }
        else {
            options = me.getOptions();
 
            if (property instanceof Array) {
                sorters = property;
                mode = direction;
                direction = null;
            }
            else if (Ext.isString(property)) {
                if (!(sorter = me.get(property))) {
                    sorters = [{
                        property: property,
                        direction: direction || options.getDefaultSortDirection()
                    }];
                }
                else {
                    sorters = [sorter];
                }
            }
            else if (Ext.isFunction(property)) {
                sorters = [{
                    sorterFn: property,
                    direction: direction || options.getDefaultSortDirection()
                }];
            }
            else {
                //<debug>
                if (!Ext.isObject(property)) {
                    Ext.raise('Invalid sort descriptor: ' + property);
                }
                //</debug>
 
                sorters = [property];
                mode = direction;
                direction = null;
            }
 
            //<debug>
            if (mode && !me._sortModes[mode]) {
                Ext.raise('Sort mode should be "multi", "append", "prepend" or "replace", not "' +
                          mode + '"');
            }
            //</debug>
            
            mode = me._sortModes[mode || 'replace'];
 
            primary = me.getAt(0);
            count = me.length;
            index = mode.append ? count : 0;
 
            // We have multiple changes to make, so mark the sorters collection as updating
            // before we start.
            me.beginUpdate();
 
            // Leverage the decode logic wired to the collection to up-convert sorters to
            // real instances.
            me.splice(index, mode.replace ? count : 0, sorters);
 
            if (mode.multi) {
                count = me.length;
                limit = options.getMultiSortLimit();
 
                if (count > limit) {
                    me.removeAt(limit, count); // count will be truncated
                }
            }
 
            if (sorter && direction) {
                sorter.setDirection(direction);
            }
            else if (index === 0 && primary && primary === me.getAt(0)) {
                // If we just adjusted the sorters at the front and the primary sorter is
                // still the primary sorter, toggle its direction:
                primary.toggle();
            }
 
            me.endUpdate();
        }
    },
 
    clear: function() {
    // The owning Collection needs to have its onSortersEndUpdate called on sorter clear so that
    // it clears its sorted flag.
        this.beginUpdate();
        this.callParent();
        this.endUpdate(this.items);
    },
 
    /**
     * Returns an up to date sort function.
     * @return {Function} The sort function.
     */
    getSortFn: function() {
        return this.sortFn;
    },
    
    /**
     * Get the first matching sorter with a matching property.
     * @param {String} prop The property name
     * @return {Ext.util.Sorter} The sorter. `null` if not found.
     * @private
     */
    getByProperty: function(prop) {
        var items = this.items,
            len = items.length,
            i, item;
        
        for (= 0; i < len; ++i) {
            item = items[i];
 
            if (item.getProperty() === prop) {
                return item;
            }
        }
 
        return null;
    },
 
    //-------------------------------------------------------------------------
    // Private
 
    _sortModes: {
        append: { append: 1 },
        multi: { multi: 1 },
        prepend: { prepend: 1 },
        replace: { replace: 1 }
    },
 
    decodeSorter: function(sorter, xclass) {
        var me = this,
            options = me.getOptions(),
            root = options.getRootProperty(),
            sorterOptionsFn = me.getSorterOptionsFn(),
            currentSorter, sorterConfig, type;
 
        if (sorter.isSorter) {
            if (!sorter.getRoot()) {
                sorter.setRoot(root);
            }
        }
        else {
            sorterConfig = {
                direction: options.getDefaultSortDirection(),
                root: root
            };
            type = typeof sorter;
 
            // If we are dealing with a string we assume it is a property they want to sort on.
            if (type === 'string') {
                currentSorter = me.get(sorter);
 
                if (currentSorter) {
                    return currentSorter;
                }
 
                sorterConfig.property = sorter;
            }
            // If it is a function, we assume its a sorting function.
            else if (type === 'function') {
                sorterConfig.sorterFn = sorter;
            }
            // If we are dealing with an object, we assume its a Sorter configuration. In
            // this case we create an instance of Sorter passing this configuration.
            else {
                // Finally we get to the point where it has to be invalid
                // <debug>
                if (!Ext.isObject(sorter)) {
                    Ext.raise('Invalid sorter specified: ' + sorter);
                }
                // </debug>
 
                sorterConfig = Ext.apply(sorterConfig, sorter);
 
                if (sorterConfig.fn) {
                    sorterConfig.sorterFn = sorterConfig.fn;
                    delete sorterConfig.fn;
                }
            }
 
            // If a sorter config was created, make it an instance
            sorter = Ext.create(xclass || Ext.util.Sorter, sorterConfig);
        }
 
        if (sorterOptionsFn) {
            sorterOptionsFn.call(me.getSorterOptionsScope() || me, sorter);
        }
 
        return sorter;
    },
    
    setSorterConfigure: function(fn, scope) {
        this.setSorterOptionsFn(fn);
        this.setSorterOptionsScope(scope);
    },
 
    decodeRemoveItems: function(args, index) {
        var me = this,
            ret = (index === undefined) ? args : args[index];
 
        if (!ret || !ret.$cloned) {
            if (args.length > index + 1 || !Ext.isIterable(ret)) {
                ret = Ext.Array.slice(args, index);
            }
 
            // eslint-disable-next-line vars-on-top
            var currentSorters = me.items,
                ln = ret.length,
                remove = [],
                i, item, n, sorter, type;
 
            for (= 0; i < ln; i++) {
                sorter = ret[i];
 
                if (sorter && sorter.isSorter) {
                    remove.push(sorter);
                }
                else {
                    type = typeof sorter;
 
                    if (type === 'string') {
                        sorter = me.get(sorter);
 
                        if (sorter) {
                            remove.push(sorter);
                        }
                    }
                    else if (type === 'function') {
                        for (= currentSorters.length; n-- > 0;) {
                            item = currentSorters[n];
 
                            if (item.getSorterFn() === sorter) {
                                remove.push(item);
                            }
                        }
                    }
                    //<debug>
                    else {
                        Ext.raise('Invalid sorter specification: ' + sorter);
                    }
                    //</debug>
                }
            }
 
            ret = remove;
            ret.$cloned = true;
        }
 
        return ret;
    },
 
    getOptions: function() {
        // Odd thing this. We need a Sortable to know how to manage our collection, but
        // we may not have one. Of course as a Collection, we *are* one as well... just
        // that is not really useful to sort the sorters themselves, but we do have the
        // default options for Sortables baked in, so we'll do.
        return this.$sortable || this;
    }
});