/**
 * @author Ed Spencer
 *
 * Represents a single 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.
 *
 * Note that when you define an Operation directly, you need to specify at least the {@link #model} configuration.
 *
 * Several Operations can be batched together in a {@link Ext.data.Batch batch}.
 */
Ext.define('Ext.data.Operation', {
    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.
         * @accessor
         */
        synchronous: true,

        /**
         * @cfg {String} action
         * The action being performed by this Operation. Should be one of 'create', 'read', 'update' or 'destroy'.
         * @accessor
         */
        action: null,

        /**
         * @cfg {Ext.util.Filter[]} filters
         * Optional array of filter objects. Only applies to 'read' actions.
         * @accessor
         */
        filters: null,

        /**
         * @cfg {Ext.util.Sorter[]} sorters
         * Optional array of sorter objects. Only applies to 'read' actions.
         * @accessor
         */
        sorters: null,

        /**
         * @cfg {Ext.util.Grouper} grouper
         * Optional grouping configuration. Only applies to 'read' actions where grouping is desired.
         * @accessor
         */
        grouper: null,

        /**
         * @cfg {Number} start
         * The start index (offset), used in paging when running a 'read' action.
         * @accessor
         */
        start: null,

        /**
         * @cfg {Number} limit
         * The number of records to load. Used on 'read' actions when paging is being used.
         * @accessor
         */
        limit: null,

        /**
         * @cfg {Ext.data.Batch} batch
         * The batch that this Operation is a part of.
         * @accessor
         */
        batch: null,

        /**
         * @cfg {Function} callback
         * Function to execute when operation completed.
         * @cfg {Ext.data.Model[]} callback.records Array of records.
         * @cfg {Ext.data.Operation} callback.operation The Operation itself.
         * @accessor
         */
        callback: null,

        /**
         * @cfg {Object} scope
         * Scope for the {@link #callback} function.
         * @accessor
         */
        scope: null,

        /**
         * @cfg {Ext.data.ResultSet} resultSet
         * The ResultSet for this operation.
         * @accessor
         */
        resultSet: null,

        /**
         * @cfg {Array} records
         * The records associated to this operation. Before an operation starts, these
         * are the records you are updating, creating, or destroying. After an operation
         * is completed, a Proxy usually sets these records on the Operation to become
         * the processed records. If you don't set these records on your operation in
         * your proxy, then the getter will return the ones defined on the {@link #resultSet}
         * @accessor
         */
        records: null,

        /**
         * @cfg {Ext.data.Request} request
         * The request used for this Operation. Operations don't usually care about Request and Response data, but in the
         * ServerProxy and any of its subclasses they may be useful for further processing.
         * @accessor
         */
        request: null,

        /**
         * @cfg {Object} response
         * The response that was gotten from the server if there was one.
         * @accessor
         */
        response: null,

        /**
         * @cfg {Boolean} withCredentials
         * This field is necessary when using cross-origin resource sharing.
         * @accessor
         */
        withCredentials: null,

        /**
         * @cfg {Object} params
         * The params send along with this operation. These usually apply to a Server proxy if you are
         * creating your own custom proxy,
         * @accessor
         */
        params: null,
        url: null,
        page: null,
        node: null,

        /**
         * @cfg {Ext.data.Model} model
         * The Model that this Operation will be dealing with. This configuration is required when defining any Operation.
         * Since Operations take care of creating, updating, destroying and reading records, it needs access to the Model.
         * @accessor
         */
        model: undefined,

        addRecords: false
    },

    /**
     * @property {Boolean} started
     * Property tracking the start status of this Operation. Use {@link #isStarted}.
     * @private
     * @readonly
     */
    started: false,

    /**
     * @property {Boolean} running
     * Property tracking the run status of this Operation. Use {@link #isRunning}.
     * @private
     * @readonly
     */
    running: false,

    /**
     * @property {Boolean} complete
     * Property tracking the completion status of this Operation. Use {@link #isComplete}.
     * @private
     * @readonly
     */
    complete: false,

    /**
     * @property {Boolean} success
     * Property tracking 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.
     * @private
     * @readonly
     */
    success: undefined,

    /**
     * @property {Boolean} exception
     * Property tracking the exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
     * @private
     * @readonly
     */
    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,

    /**
     * Creates new Operation object.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        this.initConfig(config);
    },

    applyModel: function(model) {
        if (typeof model == 'string') {
            model = Ext.data.ModelManager.getModel(model);

            if (!model) {
                Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
            }
        }

        if (model && !model.prototype.isModel && Ext.isObject(model)) {
            model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
        }

        // <debug>
        if (!model) {
            Ext.Logger.warn('Unless you define your model using metadata, an Operation needs to have a model defined.');
        }
        // </debug>

        return model;
    },

    getRecords: function() {
        var resultSet = this.getResultSet();
        return this._records || (resultSet ? resultSet.getRecords() : []);
    },

    /**
     * Marks the Operation as started.
     */
    setStarted: function() {
        this.started = true;
        this.running = true;
    },

    /**
     * Marks the Operation as completed.
     */
    setCompleted: function() {
        this.complete = true;
        this.running  = false;
    },

    /**
     * Marks the Operation as successful.
     */
    setSuccessful: function() {
        this.success = true;
    },

    /**
     * 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) {
        this.exception = true;
        this.success = false;
        this.running = false;
        this.error = error;
    },

    /**
     * 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 === true;
    },

    /**
     * Returns the error string or object that was set using {@link #setException}.
     * @return {String/Object} The error object.
     */
    getError: function() {
        return this.error;
    },

    /**
     * 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 === true;
    },

    /**
     * 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 === true;
    },

    /**
     * Returns `true` if the Operation has been completed
     * @return {Boolean} `true` if the Operation is complete
     */
    isComplete: function() {
        return this.complete === true;
    },

    /**
     * Returns `true` if the Operation has completed and was successful
     * @return {Boolean} `true` if successful
     */
    wasSuccessful: function() {
        return this.isComplete() && this.success === true;
    },

    /**
     * Checks whether this operation should cause writing to occur.
     * @return {Boolean} Whether the operation should cause a write to occur.
     */
    allowWrite: function() {
        return this.getAction() != 'read';
    },

    process: function(action, resultSet, request, response) {
        if (resultSet.getSuccess() !== false) {
            this.setResponse(response);
            this.setResultSet(resultSet);
            this.setCompleted();
            this.setSuccessful();
        } else {
            this.setResponse(response);
            this.setResultSet(resultSet);
            return false;
        }

        return this['process' + Ext.String.capitalize(action)].call(this, resultSet, request, response);
    },

    processRead: function(resultSet) {
        var records = resultSet.getRecords(),
            processedRecords = [],
            Model = this.getModel(),
            ln = records.length,
            i, record;

        for (i = 0; i < ln; i++) {
            record = records[i];
            processedRecords.push(new Model(record.data, record.id, record.node));
        }

        this.setRecords(processedRecords);
        resultSet.setRecords(processedRecords);
        return true;
    },

    processCreate: function(resultSet) {
        var updatedRecords = resultSet.getRecords(),
            currentRecords = this.getRecords(),
            ln = updatedRecords.length,
            i, currentRecord, updatedRecord;

        for (i = 0; i < ln; i++) {
            updatedRecord = updatedRecords[i];

            if (updatedRecord.clientId === null && currentRecords.length == 1 && updatedRecords.length == 1) {
                currentRecord = currentRecords[i];
            } else {
                currentRecord = this.findCurrentRecord(updatedRecord.clientId);
            }

            if (currentRecord) {
                this.updateRecord(currentRecord, updatedRecord);
            }
            // <debug>
            else {
                Ext.Logger.warn('Unable to match the record that came back from the server.');
            }
            // </debug>
        }

        return true;
    },

    processUpdate: function(resultSet) {
        var updatedRecords = resultSet.getRecords(),
            currentRecords = this.getRecords(),
            ln = updatedRecords.length,
            i, currentRecord, updatedRecord;

        for (i = 0; i < ln; i++) {
            updatedRecord = updatedRecords[i];
            currentRecord = currentRecords[i];

            if (currentRecord) {
                this.updateRecord(currentRecord, updatedRecord);
            }
            // <debug>
            else {
                Ext.Logger.warn('Unable to match the updated record that came back from the server.');
            }
            // </debug>
        }

        return true;
    },

    processDestroy: function(resultSet) {
        var updatedRecords = resultSet.getRecords(),
            ln = updatedRecords.length,
            i, currentRecord, updatedRecord;

        for (i = 0; i < ln; i++) {
            updatedRecord = updatedRecords[i];
            currentRecord = this.findCurrentRecord(updatedRecord.id);

            if (currentRecord) {
                currentRecord.setIsErased(true);
                currentRecord.notifyStores('afterErase', currentRecord);
            }
            // <debug>
            else {
                Ext.Logger.warn('Unable to match the destroyed record that came back from the server.');
            }
            // </debug>
        }
    },

    findCurrentRecord: function(clientId) {
        var currentRecords = this.getRecords(),
            ln = currentRecords.length,
            i, currentRecord;

        for (i = 0; i < ln; i++) {
            currentRecord = currentRecords[i];
            if (currentRecord.getId() === clientId) {
                return currentRecord;
            }
        }
    },

    updateRecord: function(currentRecord, updatedRecord) {
        var recordData = updatedRecord.data,
            recordId = updatedRecord.id;

        currentRecord.beginEdit();

        currentRecord.set(recordData);
        if (recordId !== null) {
            currentRecord.setId(recordId);
        }

        // We call endEdit with silent: true because the commit below already makes
        // sure any store is notified of the record being updated.
        currentRecord.endEdit(true);

        currentRecord.commit();
    }
    // <deprecated product=touch since=2.0>
}, function() {
    /**
     * @member Ext.data.Operation
     * @cfg {Boolean} group
     * @inheritdoc Ext.data.Operation#grouper
     * @deprecated 2.0.0 Please use {@link #grouper} instead.
     */
    Ext.deprecateProperty(this, 'group', 'grouper');
    // </deprecated>
});
