/**
 * @private
 */
Ext.define('Ext.mixin.Sortable', {
    extend: 'Ext.mixin.Mixin',

    requires: [
        'Ext.util.Sorter'
    ],

    mixinConfig: {
        id: 'sortable'
    },

    config: {
        /**
         * @cfg {Array} sorters
         * An array with sorters. A sorter can be an instance of {@link Ext.util.Sorter}, a string
         * indicating a property name, an object representing an Ext.util.Sorter configuration,
         * or a sort function.
         */
        sorters: null,

        /**
         * @cfg {String} defaultSortDirection
         * The default sort direction to use if one is not specified.
         */
        defaultSortDirection: "ASC",

        /**
         * @cfg {String} sortRoot
         * The root inside each item in which the properties exist that we want to sort on.
         * This is useful for sorting records in which the data exists inside a `data` property.
         */
        sortRoot: null
    },

    /**
     * @property {Boolean} dirtySortFn
     * A flag indicating whether the currently cashed sort function is still valid.
     * @readonly
     */
    dirtySortFn: false,

    /**
     * @property currentSortFn
     * This is the cached sorting function which is a generated function that calls all the
     * configured sorters in the correct order.
     * @readonly
     */
    sortFn: null,

    /**
     * @property {Boolean} sorted
     * A read-only flag indicating if this object is sorted.
     * @readonly
     */
    sorted: false,

    applySorters: function(sorters, collection) {
        if (!collection) {
            collection = this.createSortersCollection();
        }

        collection.clear();
        this.sorted = false;

        if (sorters) {
            this.addSorters(sorters);
        }

        return collection;
    },

    createSortersCollection: function() {
        this._sorters = Ext.create('Ext.util.Collection', function(sorter) {
            return sorter.getId();
        });
        return this._sorters;
    },

    /**
     * This method adds a sorter.
     * @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of
     * Ext.util.Sorter, a string indicating a property name, an object representing an Ext.util.Sorter
     * configuration, or a sort function.
     * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
     * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
     */
    addSorter: function(sorter, defaultDirection) {
        this.addSorters([sorter], defaultDirection);
    },

    /**
     * This method adds all the sorters in a passed array.
     * @param {Array} sorters An array with sorters. A sorter can be an instance of Ext.util.Sorter, a string
     * indicating a property name, an object representing an Ext.util.Sorter configuration,
     * or a sort function.
     * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
     * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
     */
    addSorters: function(sorters, defaultDirection) {
        var currentSorters = this.getSorters();
        return this.insertSorters(currentSorters ? currentSorters.length : 0, sorters, defaultDirection);
    },

    /**
     * This method adds a sorter at a given index.
     * @param {Number} index The index at which to insert the sorter.
     * @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of Ext.util.Sorter,
     * a string indicating a property name, an object representing an Ext.util.Sorter configuration,
     * or a sort function.
     * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
     * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
     */
    insertSorter: function(index, sorter, defaultDirection) {
        return this.insertSorters(index, [sorter], defaultDirection);
    },

    /**
     * This method inserts all the sorters in the passed array at the given index.
     * @param {Number} index The index at which to insert the sorters.
     * @param {Array} sorters Can be an instance of Ext.util.Sorter, a string indicating a property name,
     * an object representing an Ext.util.Sorter configuration, or a sort function.
     * @param {String} defaultDirection The default direction for each sorter in the array. Defaults
     * to the value of {@link #defaultSortDirection}. Can be either 'ASC' or 'DESC'.
     */
    insertSorters: function(index, sorters, defaultDirection) {
        // We begin by making sure we are dealing with an array of sorters
        if (!Ext.isArray(sorters)) {
            sorters = [sorters];
        }

        var ln = sorters.length,
            direction = defaultDirection || this.getDefaultSortDirection(),
            sortRoot = this.getSortRoot(),
            currentSorters = this.getSorters(),
            newSorters = [],
            sorterConfig, i, sorter, currentSorter;

        if (!currentSorters) {
            // This will guarantee that we get the collection
            currentSorters = this.createSortersCollection();
        }

        // We first have to convert every sorter into a proper Sorter instance
        for (i = 0; i < ln; i++) {
            sorter = sorters[i];
            sorterConfig = {
                direction: direction,
                root: sortRoot
            };

            // If we are dealing with a string we assume it is a property they want to sort on.
            if (typeof sorter === 'string') {
                currentSorter = currentSorters.get(sorter);

                if (!currentSorter) {
                    sorterConfig.property = sorter;
                } else {
                    if (defaultDirection) {
                        currentSorter.setDirection(defaultDirection);
                    } else {
                        // If we already have a sorter for this property we just toggle its direction.
                        currentSorter.toggle();
                    }
                    continue;
                }
            }
            // If it is a function, we assume its a sorting function.
            else if (Ext.isFunction(sorter)) {
                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 if (Ext.isObject(sorter)) {
                if (!sorter.isSorter) {
                    if (sorter.fn) {
                        sorter.sorterFn = sorter.fn;
                        delete sorter.fn;
                    }

                    sorterConfig = Ext.apply(sorterConfig, sorter);
                }
                else {
                    newSorters.push(sorter);
                    if (!sorter.getRoot()) {
                        sorter.setRoot(sortRoot);
                    }
                    continue;
                }
            }
            // Finally we get to the point where it has to be invalid
            // <debug>
            else {
                Ext.Logger.warn('Invalid sorter specified:', sorter);
            }
            // </debug>

            // If a sorter config was created, make it an instance
            sorter = Ext.create('Ext.util.Sorter', sorterConfig);
            newSorters.push(sorter);
        }

        // Now lets add the newly created sorters.
        for (i = 0, ln = newSorters.length; i < ln; i++) {
            currentSorters.insert(index + i, newSorters[i]);
        }

        this.dirtySortFn = true;

        if (currentSorters.length) {
            this.sorted = true;
        }
        return currentSorters;
    },

    /**
     * This method removes a sorter.
     * @param {Ext.util.Sorter/String/Function/Object} sorter Can be an instance of Ext.util.Sorter,
     * a string indicating a property name, an object representing an Ext.util.Sorter configuration,
     * or a sort function.
     */
    removeSorter: function(sorter) {
        return this.removeSorters([sorter]);
    },

    /**
     * This method removes all the sorters in a passed array.
     * @param {Array} sorters Each value in the array can be a string (property name),
     * function (sorterFn) or {@link Ext.util.Sorter Sorter} instance.
     */
    removeSorters: function(sorters) {
        // We begin by making sure we are dealing with an array of sorters
        if (!Ext.isArray(sorters)) {
            sorters = [sorters];
        }

        var ln = sorters.length,
            currentSorters = this.getSorters(),
            i, sorter;

        for (i = 0; i < ln; i++) {
            sorter = sorters[i];

            if (typeof sorter === 'string') {
                currentSorters.removeAtKey(sorter);
            }
            else if (typeof sorter === 'function') {
                currentSorters.each(function(item) {
                    if (item.getSorterFn() === sorter) {
                        currentSorters.remove(item);
                    }
                });
            }
            else if (sorter.isSorter) {
                currentSorters.remove(sorter);
            }
        }

        if (!currentSorters.length) {
            this.sorted = false;
        }
    },

    /**
     * This updates the cached sortFn based on the current sorters.
     * @return {Function} The generated sort function.
     * @private
     */
    updateSortFn: function() {
        var sorters = this.getSorters().items;

        this.sortFn = function(r1, r2) {
            var ln = sorters.length,
                result, i;

            // We loop over each sorter and check if r1 should be before or after r2
            for (i = 0; i < ln; i++) {
                result = sorters[i].sort.call(this, r1, r2);

                // If the result is -1 or 1 at this point it means that the sort is done.
                // Only if they are equal (0) we continue to see if a next sort function
                // actually might find a winner.
                if (result !== 0) {
                    break;
                }
            }

            return result;
        };

        this.dirtySortFn = false;
        return this.sortFn;
    },

    /**
     * Returns an up to date sort function.
     * @return {Function} The sort function.
     */
    getSortFn: function() {
        if (this.dirtySortFn) {
            return this.updateSortFn();
        }
        return this.sortFn;
    },

    /**
     * This method will sort an array based on the currently configured {@link #sorters}.
     * @param {Array} data The array you want to have sorted.
     * @return {Array} The array you passed after it is sorted.
     */
    sort: function(data) {
        Ext.Array.sort(data, this.getSortFn());
        return data;
    },

    /**
     * This method returns the index that a given item would be inserted into a
     * given array based on the current sorters.
     * @param {Array} items The array that you want to insert the item into.
     * @param {Mixed} item The item that you want to insert into the items array.
     * @return {Number} The index for the given item in the given array based on
     * the current sorters.
     */
    findInsertionIndex: function(items, item, sortFn) {
        var start = 0,
            end   = items.length - 1,
            sorterFn = sortFn || this.getSortFn(),
            middle,
            comparison;

        while (start <= end) {
            middle = (start + end) >> 1;
            comparison = sorterFn(item, items[middle]);
            if (comparison >= 0) {
                start = middle + 1;
            } else if (comparison < 0) {
                end = middle - 1;
            }
        }

        return start;
    }
});
