/**
 * Represents a read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}.
 * Operation objects are used to enable communication between Stores and Proxies.
 * Application developers should rarely need to interact with Operation objects directly.
 *
 * Several Operations can be batched together in a {@link Ext.data.Batch batch}.
 */
Ext.define('Ext.data.operation.Operation', {
    alternateClassName: 'Ext.data.Operation',
    
    isOperation: true,
    
    config: {
        /**
         * @cfg {Boolean} synchronous
         * True if this Operation is to be executed synchronously. This property is inspected by a
         * {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel
         * or not.
         */
        synchronous: false,
 
        /**
         * @cfg {String} url
         * The url for this operation. Typically this will be provided by a proxy and not configured
         * here.
         */
        url: '',
        
        /**
         * @cfg {Object} params
         * Parameters to pass along with the request when performing the operation.
         */
        params: undefined,
    
        /**
         * @cfg {Function} callback
         * Function to execute when operation completed.
         * @cfg {Ext.data.Model[]} callback.records Array of records.
         * @cfg {Ext.data.operation.Operation} callback.operation The Operation itself.
         * @cfg {Boolean} callback.success True when operation completed successfully.
         */
        callback: undefined,
    
        /**
         * @cfg {Object} scope
         * Scope for the {@link #callback} function.
         */
        scope: undefined,
        
        /**
         * @cfg {Ext.data.ResultSet} resultSet
         * The ResultSet for this operation.
         * @accessor
         */
        resultSet: null,
        
        /**
         * @private
         * @cfg {Object} response
         * The response for this operation.
         */
        response: null,
        
        /**
         * @cfg {Ext.data.Request} request
         * The request for this operation.
         */
        request: null,
        
        /**
         * @cfg {Ext.data.Model[]} records
         * The records associated with this operation. If this is a `read` operation, this will be
         * `null` until data is returned from the {@link Ext.data.proxy.Proxy}.
         */
        records: null,
        
        /**
         * @cfg {Object} id
         * The id of the operation.
         */
        id: undefined,
        
        /**
         * @cfg {Ext.data.proxy.Proxy} proxy
         * The proxy for this operation
         */
        proxy: null,
        
        /**
         * @cfg {Ext.data.Batch} 
         * The batch for this operation, if applicable
         */
        batch: null,
        
        /**
         * @cfg {Function} recordCreator
         * Passed to the reader, see {@link Ext.data.reader.Reader#read}
         * @private
         */
        recordCreator: null,
        
        // We use this because in a lot of cases the developer can indirectly pass
        // a callback/scope and that will get pushed on to the operation. As such,
        // create our own hook for the callback that will fire first
        /**
         * @cfg {Function} internalCallback
         * A callback to run before the {@link #callback}
         * @private
         */
        internalCallback: null,
        
        /**
         * @cfg {Object} internalScope
         * Scope to run the {@link #internalCallback}
         * @private
         */
        internalScope: null
    },
 
    /**
     * @property {Number} order
     * This number is used by `{@link Ext.data.Batch#sort}` to order operations. Order of
     * operations of the same type is determined by foreign-key dependencies. The array of
     * operations is sorted by ascending (increasing) values of `order`.
     * @private
     * @readonly
     * @since 5.0.0
     */
    order: 0,
 
    /**
     * @property {Number} foreignKeyDirection
     * This number is used by `{@link Ext.data.Batch#sort}` to order operations of the
     * same type. This value is multipled by the "entity rank" (which is determined by
     * foreign-key dependencies) to invert the direction of the sort based on the type of
     * operation. Only `Ext.data.operation.Destroy` overrides this value.
     * @private
     * @readonly
     * @since 5.0.0
     */
    foreignKeyDirection: 1,
 
    /**
     * @property {Boolean} started
     * The start status of this Operation. Use {@link #isStarted}.
     * @readonly
     * @private
     */
    started: false,
 
    /**
     * @property {Boolean} running
     * The run status of this Operation. Use {@link #isRunning}.
     * @readonly
     * @private
     */
    running: false,
 
    /**
     * @property {Boolean} complete
     * The completion status of this Operation. Use {@link #isComplete}.
     * @readonly
     * @private
     */
    complete: false,
 
    /**
     * @property {Boolean} success
     * Whether the Operation was successful or not. This starts as undefined and is set to true
     * or false by the Proxy that is executing the Operation. It is also set to false by
     * {@link #setException}. Use {@link #wasSuccessful} to query success status.
     * @readonly
     * @private
     */
    success: undefined,
 
    /**
     * @property {Boolean} exception
     * The exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
     * @readonly
     * @private
     */
    exception: false,
 
    /**
     * @property {String/Object} error
     * The error object passed when {@link #setException} was called. This could be any object or
     * primitive.
     * @private
     */
    error: undefined,
    
    idPrefix: 'ext-operation-',
 
    /**
     * Creates new Operation object.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        // This ugliness is here to prevent an issue when specifying scope
        // as an object literal. The object will be pulled in as part of
        // the merge() during initConfig which will change the object 
        // reference. As such, we attempt to fudge it here until we come
        // up with a better solution for describing how specific config
        // objects should behave during init() time.
        var scope = config && config.scope;
 
        this.initConfig(config);
 
        if (config) {
            config.scope = scope;
        }
        
        if (scope) {
            this.setScope(scope);
            this.initialConfig.scope = scope;
        }
        
        // We need an internal id to track operations in Proxy
        this._internalId = Ext.id(this, this.idPrefix);
    },
    
    getAction: function() {
        return this.action;
    },
    
    /**
     * @private
     * Executes the operation on the configured {@link #proxy}.
     * @return {Ext.data.Request} The request object
     */
    execute: function() {
        var me = this,
            request;
 
        delete me.error;
        delete me.success;
        me.complete = me.exception = false;
        
        me.setStarted();
        me.request = request = me.doExecute();
        
        if (request) {
            request.setOperation(me);
        }
        
        return request;
    },
    
    doExecute: Ext.emptyFn,
    
    /**
     * Aborts the processing of this operation on the {@link #proxy}.
     * This is only valid for proxies that make asynchronous requests.
     */
    abort: function() {
        var me = this,
            request = me.request,
            proxy;
        
        me.aborted = true;
            
        if (me.running && request) {
            proxy = me.getProxy();
            
            if (proxy && !proxy.destroyed) {
                proxy.abort(request);
            }
            
            me.request = null;
        }
        
        me.running = false;
    },
    
    process: function(resultSet, request, response, autoComplete) {
        var me = this;
        
        autoComplete = autoComplete !== false;
        
        me.setResponse(response);
        me.setResultSet(resultSet);
        
        if (resultSet.getSuccess()) {
            me.doProcess(resultSet, request, response);
            me.setSuccessful(autoComplete);
        }
        else if (autoComplete) {
            me.setException(resultSet.getMessage());
        }
    },
 
    /**
     * @private
     * This private object is used to save on memory allocation. This instance is used to
     * apply server record updates as part of a record commit performed by calling the
     * set() method on the record. See doProcess.
     */
    _commitSetOptions: { convert: true, commit: true },
 
    /**
     * Process records in the operation after the response is successful and the result
     * set is parsed correctly. The base class implementation of this method is used by
     * "create" and "update" operations to allow the server response to update the client
     * side records.
     * 
     * @param {Ext.data.ResultSet} resultSet The result set
     * @param {Ext.data.Request} request The request
     * @param {Object} response The response
     * @protected
     */
    doProcess: function(resultSet, request, response) {
        var me = this,
            commitSetOptions = me._commitSetOptions,
            clientRecords = me.getRecords(),
            clientLen = clientRecords.length,
            clientIdProperty = clientRecords[0].clientIdProperty,
            serverRecords = resultSet.getRecords(), // a data array, not records yet
            serverLen = serverRecords ? serverRecords.length : 0,
            clientMap, serverRecord, clientRecord, i;
 
        if (serverLen && clientIdProperty) {
            // Linear pass over clientRecords to map them by their idProperty
            clientMap = Ext.Array.toValueMap(clientRecords, 'id');
 
            // Linear pass over serverRecords to match them by clientIdProperty to the
            // corresponding clientRecord (if one exists).
            for (= 0; i < serverLen; ++i) {
                serverRecord = serverRecords[i];
                clientRecord = clientMap[serverRecord[clientIdProperty]];
 
                if (clientRecord) {
                    // Remove this one so we don't commit() on it next
                    delete clientMap[clientRecord.id];
                    // Remove the clientIdProperty value since we don't want to store it
                    delete serverRecord[clientIdProperty];
 
                    clientRecord.set(serverRecord, commitSetOptions); // set & commit
                }
                //<debug>
                else {
                    Ext.log.warn('Ignoring server record: ' + Ext.encode(serverRecord));
                }
                //</debug>
            }
 
            // Linear pass over any remaining client records.
            for (in clientMap) {
                clientMap[i].commit();
            }
        }
        else {
            // Either no serverRecords or no clientIdProperty, so index correspondence is
            // all we have to go on. If there is no serverRecord at a given index we just
            // commit() the record.
            for (= 0; i < clientLen; ++i) {
                clientRecord = clientRecords[i];
 
                if (serverLen === 0 || !(serverRecord = serverRecords[i])) {
                    // once i > serverLen then serverRecords[i] will be undefined...
                    clientRecord.commit();
                }
                else {
                    clientRecord.set(serverRecord, commitSetOptions);
                }
            }
        }
    },
 
    /**
     * Marks the Operation as started.
     */
    setStarted: function() {
        this.started = this.running = true;
    },
 
    /**
     * Marks the Operation as completed.
     */
    setCompleted: function() {
        var me = this,
            proxy;
        
        me.complete = true;
        me.running = false;
        
        if (!me.destroying) {
            me.triggerCallbacks();
        }
        
        // Operation can be destroyed in callback
        if (me.destroyed) {
            return;
        }
        
        proxy = me.getProxy();
        
        // Store and proxy could be destroyed in callbacks
        if (proxy && !proxy.destroyed) {
            proxy.completeOperation(me);
        }
    },
 
    /**
     * Marks the Operation as successful.
     * @param {Boolean} [complete] `true` to also mark this operation
     * as being complete See {@link #setCompleted}.
     */
    setSuccessful: function(complete) {
        this.success = true;
        
        if (complete) {
            this.setCompleted();
        }
    },
 
    /**
     * Marks the Operation as having experienced an exception. Can be supplied with an option
     * error message/object.
     * @param {String/Object} error (optional) error string/object
     */
    setException: function(error) {
        var me = this;
        
        me.exception = true;
        me.success = me.running = false;
        me.error = error;
        
        me.setCompleted();
    },
 
    triggerCallbacks: function() {
        var me = this,
            callback = me.getInternalCallback();
 
        // Call internal callback first (usually the Store's onProxyLoad method)
        if (callback) {
            callback.call(me.getInternalScope() || me, me);
            
            // Operation callback can cause it to be destroyed
            if (me.destroyed) {
                return;
            }
            
            me.setInternalCallback(null);
            me.setInternalScope(null);
        }
 
        // Call the user's callback as passed to Store's read/write
        callback = me.getCallback();
        
        if (callback) {
            // Maintain the public API for callback
            callback.call(me.getScope() || me, me.getRecords(), me, me.wasSuccessful());
            
            if (me.destroyed) {
                return;
            }
            
            me.setCallback(null);
            me.setScope(null);
        }
    },
 
    /**
     * Returns true if this Operation encountered an exception (see also {@link #getError})
     * @return {Boolean} True if there was an exception
     */
    hasException: function() {
        return this.exception;
    },
 
    /**
     * Returns the error string or object that was set using {@link #setException}
     * @return {String/Object} The error object
     */
    getError: function() {
        return this.error;
    },
 
    /**
     * Returns the {@link Ext.data.Model record}s associated with this operation. For read
     * operations the records as set by the {@link Ext.data.proxy.Proxy Proxy} will be
     * returned (returns `null` if the proxy has not yet set the records).
     *
     * For create, update, and destroy operations the operation's initially configured
     * records will be returned, although the proxy may modify these records' data at some
     * point after the operation is initialized.
     *
     * @return {Ext.data.Model[]} 
     */
    getRecords: function() {
        var resultSet;
        
        /* eslint-disable-next-line no-cond-assign */
        return this._records || ((resultSet = this.getResultSet()) ? resultSet.getRecords() : null);
    },
 
    /**
     * Returns true if the Operation has been started. Note that the Operation may have started
     * AND completed, see {@link #isRunning} to test if the Operation is currently running.
     * @return {Boolean} True if the Operation has started
     */
    isStarted: function() {
        return this.started;
    },
 
    /**
     * Returns true if the Operation has been started but has not yet completed.
     * @return {Boolean} True if the Operation is currently running
     */
    isRunning: function() {
        return this.running;
    },
 
    /**
     * Returns true if the Operation has been completed
     * @return {Boolean} True if the Operation is complete
     */
    isComplete: function() {
        return this.complete;
    },
 
    /**
     * Returns true if the Operation has completed and was successful
     * @return {Boolean} True if successful
     */        
    wasSuccessful: function() {
        return this.isComplete() && this.success === true; // success can be undefined
    },
 
    /**
     * Checks whether this operation should cause writing to occur.
     * @return {Boolean} Whether the operation should cause a write to occur.
     */
    allowWrite: function() {
        return true;
    },
    
    destroy: function() {
        var me = this;
        
        me.destroying = true;
        
        if (me.running) {
            me.abort();
        }
        
        // Cleanup upon destruction can be turned off
        me._params = me._callback = me._scope = me._resultSet = me._response = null;
        me.request = me._request = me._records = me._proxy = me._batch = null;
        me._recordCreator = me._internalCallback = me._internalScope = null;
        
        me.callParent();
    }
});