/**
 * A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store}
 * and {@link Ext.data.TreeStore}.
 *
 * **NOTE**: This mixin is mainly for internal use and most users should not need to use it
 * directly. It is more likely you will want to use one of the component classes that import
 * this mixin, such as {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
 */
Ext.define("Ext.util.Sortable", {
    /**
     * @property {Boolean} isSortable
     * `true` in this class to identify an object as an instantiated Sortable, or subclass thereof.
     */
    isSortable: true,
 
    $configPrefixed: false,
    $configStrict: false,
 
    config: {
        /**
         * @cfg {Ext.util.Sorter[]/Object[]} sorters
         * The initial set of {@link Ext.util.Sorter Sorters}.
         *
         *     sorters: [{
         *         property: 'age',
         *         direction: 'DESC'
         *     }, {
         *         property: 'firstName',
         *         direction: 'ASC'
         *     }]
         */
        sorters: null
    },
 
    /**
     * @cfg {String} defaultSortDirection
     * The default sort direction to use if one is not specified.
     */
    defaultSortDirection: "ASC",
 
    requires: [
        'Ext.util.Sorter'
    ],
 
    /**
     * @event beforesort
     * Fires before a sort occurs.
     * @param {Ext.util.Sortable} me This object.
     * @param {Ext.util.Sorter[]} sorters The collection of Sorters being used to generate
     * the comparator function.
     */
 
    /**
     * @cfg {Number} [multiSortLimit=3]
     * The maximum number of sorters which may be applied to this Sortable when using the "multi"
     * insertion position when adding sorters.
     *
     * New sorters added using the "multi" insertion position are inserted at the top of the
     * sorters list becoming the new primary sort key.
     *
     * If the sorters collection has grown to longer then **`multiSortLimit`**, then it is trimmed.
     *
     */
    multiSortLimit: 3,
 
    statics: {
        /**
         * Creates a single comparator function which encapsulates the passed Sorter array.
         * @param {Ext.util.Sorter[]} sorters The sorter set for which to create a comparator
         * function
         * @return {Function} a function, which when passed two comparable objects returns
         * the result of the whole sorter comparator functions.
         */
        createComparator: function(sorters) {
            return sorters && sorters.length
                ? function(r1, r2) {
                    var result = sorters[0].sort(r1, r2),
                        length = sorters.length,
                        i = 1;
 
                    // While we have not established a comparison value,
                    // loop through subsequent sorters asking for a comparison value
                    for (; !result && i < length; i++) {
                        result = sorters[i].sort.call(sorters[i], r1, r2);
                    }
 
                    return result;
                }
                : function() {
                    return 0;
                };
        }
    },
 
    /**
     * @cfg {String} sortRoot
     * The property in each item that contains the data to sort.
     */
 
    applySorters: function(sorters) {
        var me = this,
            sortersCollection;
 
        sortersCollection = me.getSorters() || new Ext.util.MixedCollection(false, Ext.returnId);
 
        // We have been configured with a non-default value.
        if (sorters) {
            sortersCollection.addAll(me.decodeSorters(sorters));
        }
 
        return sortersCollection;
    },
 
    /**
     * Updates the sorters collection and triggers sorting of this Sortable. Example usage:
     *
     *     //sort by a single field
     *     myStore.sort('myField', 'DESC');
     *
     *     //sorting by multiple fields
     *     myStore.sort([{
     *         property : 'age',
     *         direction: 'ASC'
     *     }, {
     *         property : 'name',
     *         direction: 'DESC'
     *     }]);
     *
     * Classes which use this mixin must implement a **`soSort`** method which accepts a comparator
     * function computed from the full sorter set which performs the sort
     * in an implementation-specific way.
     *
     * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
     * so this code:
     *
     *     store.sort('myField');
     *     store.sort('myField');
     *
     * Is equivalent to this code, because Store handles the toggling automatically:
     *
     *     store.sort('myField', 'ASC');
     *     store.sort('myField', 'DESC');
     *
     * @param {String/Ext.util.Sorter[]} [sorters] Either a string name of one of the fields
     * in this Store's configured {@link Ext.data.Model Model}, or an array of sorter
     * configurations.
     * @param {String} [direction="ASC"] The overall direction to sort the data by.
     * @param {String} [insertionPosition="replace"] Where to put the new sorter in the collection
     * of sorters. This may take the following values:
     *
     * * `replace`: This means that the new sorter(s) becomes the sole sorter set for this Sortable.
     * This is the most useful call mode to programatically sort by multiple fields.
     *
     * * `prepend`: This means that the new sorters are inserted as the primary sorters, unchanged,
     * and the sorter list length must be controlled by the developer.
     *
     * * `multi`:  This is mainly useful for implementing intuitive "Sort by this" user interfaces
     * such as the {@link Ext.grid.Panel GridPanel}'s column sorting UI. This mode is only 
     * supported when passing a property name and a direction. This means that the new sorter
     * becomes the primary sorter. If the sorter was **already** the primary sorter, the direction
     * of sort is toggled if no direction parameter is specified. The number of sorters maintained
     * is limited by the {@link #multiSortLimit} configuration.
     *
     * * `append` : This means that the new sorter becomes the last sorter.
     * @param {Boolean} doSort True to sort using a generated sorter function that combines all
     * of the Sorters passed
     * @return {Ext.util.Sorter[]} The new sorters.
     */
    sort: function(sorters, direction, insertionPosition, doSort) {
        var me = this,
            sorter,
            overFlow,
            currentSorters = me.getSorters();
 
        if (!currentSorters) {
            me.setSorters(null);
            currentSorters = me.getSorters();
        }
 
        if (Ext.isArray(sorters)) {
            doSort = insertionPosition;
            insertionPosition = direction;
        }
        else if (Ext.isObject(sorters)) {
            sorters = [sorters];
            doSort = insertionPosition;
            insertionPosition = direction;
        }
        else if (Ext.isString(sorters)) {
            sorter = currentSorters.get(sorters);
 
            if (!sorter) {
                sorter = {
                    property: sorters,
                    direction: direction
                };
            }
            else if (direction == null) {
                sorter.toggle();
            }
            else {
                sorter.setDirection(direction);
            }
 
            sorters = [sorter];
        }
 
        if (sorters && sorters.length) {
            sorters = me.decodeSorters(sorters);
 
            switch (insertionPosition) {
                // multi sorting means always inserting the specified sorters
                // at the top.
                // If we are asked to sort by what is already the primary sorter
                // then toggle its direction.
                case "multi":
                    // Insert the new sorter at the beginning.
                    currentSorters.insert(0, sorters[0]);
 
                    // If we now are oversize, trim our sorters collection
                    overFlow = currentSorters.getCount() - me.multiSortLimit;
 
                    if (overFlow > 0) {
                        currentSorters.removeRange(me.multiSortLimit, overFlow);
                    }
 
                    break;
 
                case "prepend":
                    currentSorters.insert(0, sorters);
                    break;
 
                case "append":
                    currentSorters.addAll(sorters);
                    break;
 
                case undefined:
                case null:
                case "replace":
                    currentSorters.clear();
                    currentSorters.addAll(sorters);
                    break;
 
                default:
                    //<debug>
                    Ext.raise('Sorter insertion point must be "multi", "prepend", ' +
                              '"append" or "replace"');
                    //</debug>
            }
        }
 
        if (doSort !== false) {
            me.fireEvent('beforesort', me, sorters);
            me.onBeforeSort(sorters);
 
            if (me.getSorterCount()) {
                // Sort using a generated sorter function which combines all of the Sorters passed
                me.doSort(me.generateComparator());
            }
        }
 
        return sorters;
    },
 
    /**
     * @protected
     * Returns the number of Sorters which apply to this Sortable.
     *
     * May be overridden in subclasses. {@link Ext.data.Store Store} in particlar overrides
     * this because its groupers must contribute to the sorter count so that the sort method above
     * executes doSort.
     */
    getSorterCount: function() {
        return this.getSorters().items.length;
    },
 
    /**
     * Returns a comparator function which compares two items and returns -1, 0, or 1 depending
     * on the currently defined set of {@link #cfg-sorters}.
     *
     * If there are no {@link #cfg-sorters} defined, it returns a function which returns `0` meaning
     * that no sorting will occur.
     */
    generateComparator: function() {
        var sorters = this.getSorters().getRange();
 
        return sorters.length ? this.createComparator(sorters) : this.emptyComparator;
    },
 
    emptyComparator: function() {
        return 0;
    },
 
    onBeforeSort: Ext.emptyFn,
 
    /**
     * @private
     * Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances
     * @param {Object[]} sorters The sorters array
     * @return {Ext.util.Sorter[]} Array of Ext.util.Sorter objects
     */
    decodeSorters: function(sorters) {
        if (!Ext.isArray(sorters)) {
            if (sorters === undefined) {
                sorters = [];
            }
            else {
                sorters = [sorters];
            }
        }
 
        // eslint-disable-next-line vars-on-top
        var length = sorters.length,
            Sorter = Ext.util.Sorter,
            model = this.getModel ? this.getModel() : this.model,
            field,
            config, i;
 
        for (= 0; i < length; i++) {
            config = sorters[i];
 
            if (!(config instanceof Sorter)) {
                if (Ext.isString(config)) {
                    config = {
                        property: config
                    };
                }
 
                Ext.applyIf(config, {
                    root: this.sortRoot,
                    direction: "ASC"
                });
 
                // support for 3.x style sorters where a function can be defined as 'fn'
                if (config.fn) {
                    config.sorterFn = config.fn;
                }
 
                // support a function to be passed as a sorter definition
                if (typeof config === 'function') {
                    config = {
                        sorterFn: config
                    };
                }
 
                // ensure sortType gets pushed on if necessary
                if (model && !config.transform) {
                    field = model.getField(config.property);
                    config.transform = field && field.sortType !== Ext.identityFn
                        ? field.sortType
                        : undefined;
                }
 
                sorters[i] = new Ext.util.Sorter(config);
            }
        }
 
        return sorters;
    },
 
    /**
     * Gets the first sorter from the sorters collection, excluding
     * any groupers that may be in place
     * @protected
     * @return {Ext.util.Sorter} The sorter, null if none exist
     */
    getFirstSorter: function() {
        var sorters = this.getSorters().items,
            len = sorters.length,
            i = 0,
            sorter;
 
        for (; i < len; ++i) {
            sorter = sorters[i];
 
            if (!sorter.isGrouper) {
                return sorter;
            }
        }
 
        return null;
    }
}, function() {
    // Reference the static implementation in prototype
    this.prototype.createComparator = this.createComparator;
});