/**
 * Provides a mechanism to run one or more {@link Ext.data.operation.Operation operations}
 * in a given order. Fires the `operationcomplete` event after the completion of each
 * Operation, and the `complete` event when all Operations have been successfully executed.
 * Fires an `exception` event if any of the Operations encounter an exception.
 *
 * Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes.
 */
Ext.define('Ext.data.Batch', {
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
 
    config: {
        /**
        * @cfg {Boolean} pauseOnException
        * True to pause the execution of the batch if any operation encounters an exception
        * (defaults to false). If you set this to true you are responsible for implementing
        * the appropriate handling logic and restarting or discarding the batch as needed.
        * There are different ways you could  do this, e.g. by handling the batch's
        * {@link #event-exception} event directly, or perhaps by overriding
        * {@link Ext.data.ProxyStore#onBatchException onBatchException} at the store level.
        * If you do pause and attempt to handle the exception you can call {@link #retry} to
        * process the same operation again. 
        * 
        * Note that {@link Ext.data.operation.Operation operations} are atomic, so any operations
        * that may have succeeded prior to an exception (and up until pausing the batch) will be
        * finalized at the server level and will not be automatically reversible. Any transactional
        * / rollback behavior that might be desired would have to be implemented at the application
        * level. Pausing on exception will likely be most beneficial when used in coordination with
        * such a scheme, where an exception might actually affect subsequent operations in the same
        * batch and so should be handled before continuing with the next operation.
        * 
        * If you have not implemented transactional operation handling then this option should
        * typically be left  to the default of false (e.g. process as many operations as possible,
        * and handle any exceptions  asynchronously without holding up the rest of the batch).
        */
        pauseOnException: false
    },
 
    /**
     * @property {Number} current
     * The index of the current operation being executed.
     * @private
     */
    current: -1,
 
    /**
     * @property {Number} total
     * The total number of operations in this batch.
     * @private
     */
    total: 0,
 
    /**
     * @property {Boolean} running
     * True if the batch is currently running.
     * @private
     */
    running: false,
 
    /**
     * @property {Boolean} complete
     * True if this batch has been executed completely.
     * @private
     */
    complete: false,
 
    /**
     * @property {Boolean} exception
     * True if this batch has encountered an exception. This is cleared at the start of each
     * operation.
     * @private
     */
    exception: false,
 
    /**
     * Creates new Batch object.
     * @param {Object} [config] Config object
     */
    constructor: function(config) {
        var me = this;
 
        me.mixins.observable.constructor.call(me, config);
 
        /**
         * @event complete
         * Fired when all operations of this batch have been completed
         * @param {Ext.data.Batch} batch The batch object
         * @param {Object} operation The last operation that was executed
         */
 
        /**
         * @event exception
         * Fired when a operation encountered an exception
         * @param {Ext.data.Batch} batch The batch object
         * @param {Object} operation The operation that encountered the exception
         */
 
        /**
         * @event operationcomplete
         * Fired when each operation of the batch completes
         * @param {Ext.data.Batch} batch The batch object
         * @param {Object} operation The operation that just completed
         */
 
        /**
         * Ordered array of operations that will be executed by this batch
         * @property {Ext.data.operation.Operation[]} operations
         * @private
         */
        me.operations = [];
        
        /**
         * Ordered array of operations that raised an exception during the most recent
         * batch execution and did not successfully complete
         * @property {Ext.data.operation.Operation[]} exceptions
         */
        me.exceptions = [];
    },
 
    /**
     * Adds a new operation to this batch at the end of the {@link #operations} array
     * @param {Ext.data.operation.Operation/Ext.data.operation.Operation[]} operation 
     * The {@link Ext.data.operation.Operation Operation} object or an array of operations.
     * @return {Ext.data.Batch} this
     */
    add: function(operation) {
        var me = this,
            i, len;
            
        if (Ext.isArray(operation)) {
            for (= 0, len = operation.length; i < len; ++i) {
                me.add(operation[i]);
            }
        }
        else {
            me.total++;
    
            operation.setBatch(me);
 
            me.operations.push(operation);
        }
        
        return me;
    },
 
    /**
     * Sorts the `{@link Ext.data.operation.Operation operations}` based on their type and
     * the foreign key dependencies of the entities. Consider a simple Parent and Child
     * case where the Child has a "parentId" field. If this batch contains two `create`
     * operations, one of a Parent and one for its Child, the server must receive and
     * process the `create` of the Parent before the Child can be created.
     *
     * In the case of `destroy` operations this order is reversed. The Child entity must be
     * destroyed before the Parent to avoid any foreign key constraints (a Child with an
     * invalid parentId field).
     *
     * Further, `create` operations must all occur before `update` operations to ensure
     * that all entities exist that might be now referenced by the updates. The created
     * entities can safely reference already existing entities.
     *
     * Finally, `destroy` operations are sorted after `update` operations to allow those
     * updates to remove references to the soon-to-be-deleted entities.
     */
    sort: function() {
        this.operations.sort(this.sortFn);
    },
 
    sortFn: function(operation1, operation2) {
        var ret = operation1.order - operation2.order;
        
        if (ret) {
            return ret;
        }
 
        /* eslint-disable-next-line vars-on-top, one-var */
        var entityType1 = operation1.entityType,
            entityType2 = operation2.entityType,
            rank;
 
        // Since the orders are equal, the operations are the same type. Read operations
        // have no records, so report equality.
        if (!entityType1 || !entityType2) {
            return 0;
        }
 
        // Otherwise, determine the entity rank for the entities involved in the two
        // operations.
        if (!(rank = entityType1.rank)) {
            // Time to perform the topo-sort based on foreign-key references.
            entityType1.schema.rankEntities();
 
            // Now the rank is available for all entities.
            rank = entityType1.rank;
        }
 
        return (rank - entityType2.rank) * operation1.foreignKeyDirection;
    },
 
    /**
     * Kicks off execution of the batch, continuing from the next operation if the previous
     * operation encountered an exception, or if execution was paused. Use this method to start
     * the batch for the first time or to restart a paused batch by skipping the current
     * unsuccessful operation.
     * 
     * To retry processing the current operation before continuing to the rest of the batch (e.g.
     * because you explicitly handled the operation's exception), call {@link #retry} instead.
     * 
     * Note that if the batch is already running any call to start will be ignored.
     * @param {Number} [index] (private)
     * @return {Ext.data.Batch} this
     */
    start: function(index) {
        var me = this;
        
        if (me.destroyed || !me.operations.length || me.running) {
            return me;
        }
        
        me.exceptions.length = 0;
        me.exception = false;
        me.running = true;
 
        return me.runOperation(Ext.isDefined(index) ? index : me.current + 1);
    },
    
    abort: function() {
        var me = this,
            op;
        
        if (me.running) {
            op = me.getCurrent();
            
            if (!op.destroyed) {
                op.abort();
            }
        }
        
        me.running = false;
        me.aborted = true;
        me.current = undefined;
    },
    
    /**
     * Kicks off execution of the batch, continuing from the current operation. This is intended
     * for restarting a {@link #pause paused} batch after an exception, and the operation that
     * raised the exception will now be retried. The batch will then continue with its normal
     * processing until all operations are complete or another exception is encountered.
     * 
     * Note that if the batch is already running any call to retry will be ignored.
     * 
     * @return {Ext.data.Batch} this
     */
    retry: function() {
        return this.start(this.current);
    },
 
    /**
     * @private
     * Runs the next operation, relative to this.current.
     * @return {Ext.data.Batch} this
     */
    runNextOperation: function() {
        var me = this;
        
        if (me.running) {
            me.runOperation(me.current + 1);
        }
        
        return me;
    },
 
    /**
     * Pauses execution of the batch, but does not cancel the current operation
     * @return {Ext.data.Batch} this
     */
    pause: function() {
        this.running = false;
        
        return this;
    },
    
    /**
     * Gets the operations for this batch.
     * @return {Ext.data.operation.Operation[]} The operations.
     */
    getOperations: function() {
        return this.operations;
    },
    
    /**
     * Gets any operations that have returned without success in this batch.
     * @return {Ext.data.operation.Operation[]} The exceptions
     */
    getExceptions: function() {
        return this.exceptions;
    },
    
    /**
     * Gets the currently running operation. Will return null if the batch has
     * not started or is completed.
     * @return {Ext.data.operation.Operation} The operation
     */
    getCurrent: function() {
        var out = null,
            current = this.current;
            
        if (!(current === -1 || this.complete)) {
            out = this.operations[current];
        }
        
        return out;
    },
    
    /**
     * Gets the total number of operations in this batch.
     * @return {Number} The total
     */
    getTotal: function() {
        return this.total;
    },
    
    /**
     * Checks if this batch is running.
     * @return {Boolean} `true` if this batch is running.
     */
    isRunning: function() {
        return this.running;
    },
    
    /**
     * Checks if this batch is complete.
     * @return {Boolean} `true` if this batch is complete.
     */
    isComplete: function() {
        return this.complete;
    },
    
    /**
     * Checks if this batch has any exceptions.
     * @return {Boolean} `true` if this batch has any exceptions.
     */
    hasException: function() {
        return this.exception;
    },
 
    /**
     * Executes an operation by its numeric index in the {@link #operations} array
     * @param {Number} index The operation index to run
     * @return {Ext.data.Batch} this
     * 
     * @private
     */
    runOperation: function(index) {
        var me = this,
            operations = me.operations,
            operation = operations[index];
 
        if (operation === undefined) {
            me.running = false;
            me.complete = true;
            me.fireEvent('complete', me, operations[operations.length - 1]);
        }
        else {
            me.current = index;
            operation.setInternalCallback(me.onOperationComplete);
            operation.setInternalScope(me);
            operation.execute();
        }
        
        return me;
    },
    
    onOperationComplete: function(operation) {
        var me = this,
            exception = operation.hasException();
            
        if (exception) {
            me.exception = true;
            me.exceptions.push(operation);
            me.fireEvent('exception', me, operation);
        }
 
        if (exception && me.getPauseOnException()) {
            me.pause();
        }
        else {
            me.fireEvent('operationcomplete', me, operation);
            me.runNextOperation();
        }
    },
    
    destroy: function() {
        var me = this,
            operations = me.operations,
            op, i, len;
        
        if (me.running) {
            me.abort();
        }
        
        for (= 0, len = me.operations.length; i < len; i++) {
            op = operations[i];
            
            if (op) {
                if (!op.destroyed && !op.$destroyOwner) {
                    op.destroy();
                }
                
                op[i] = null;
            }
        }
        
        // Global cleanup can be turned off
        me.operations = me.exceptions = null;
        
        me.callParent();
    }
});