/**
 * Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of
 * {@link Ext.data.Model Model} data. Usually developers will not need to create or interact
 * with proxies directly.
 *
 * # Types of Proxy
 *
 * There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and
 * {@link Ext.data.proxy.Server Server}. The Client proxies save their data locally and include
 * the following subclasses:
 *
 * - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage
 *   if the browser supports it
 * - {@link Ext.data.proxy.SessionStorage SessionStorageProxy} - saves its data to sessionStorage
 *   if the browsers supports it
 * - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost
 *   when the page is refreshed
 *
 * The Server proxies save their data by sending requests to some remote server. These proxies
 * include:
 *
 * - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
 * - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different
 *   domain
 * - {@link Ext.data.proxy.Rest Rest} - uses RESTful HTTP methods (GET/PUT/POST/DELETE)
 *   to communicate with server
 * - {@link Ext.data.proxy.Direct Direct} - uses {@link Ext.direct.Manager} to send requests
 *
 * Proxies operate on the principle that all operations performed are either Create, Read, Update
 * or Delete. These four operations are mapped to the methods {@link #method!create},
 * {@link #method!read}{@link #method!update} and {@link #method!erase} respectively. Each Proxy
 * subclass implements these functions.
 *
 * The CRUD methods each expect an {@link Ext.data.operation.Operation Operation} object as the only
 * argument. The Operation encapsulates information about the action the Store wishes to perform,
 * the {@link Ext.data.Model model} instances that are to be modified, etc. See the
 * {@link Ext.data.operation.Operation Operation} documentation for more details. Each CRUD
 * method also accepts a callback function to be called asynchronously on completion.
 *
 * Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked
 * by the {@link #batch}
 * method.
 */
Ext.define('Ext.data.proxy.Proxy', {
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Observable'
    ],
 
    $configPrefixed: false,
 
    alias: 'proxy.proxy', // also configures Factoryable
 
    alternateClassName: [
        'Ext.data.DataProxy',
        'Ext.data.Proxy'
    ],
 
    requires: [
        'Ext.data.schema.Schema',
        'Ext.data.reader.Reader',
        'Ext.data.writer.Writer'
    ],
 
    uses: [
        'Ext.data.Batch',
        'Ext.data.operation.*',
        'Ext.data.Model'
    ],
 
    config: {
        /**
         * @cfg {String} batchOrder
         * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching.
         * Override this to set a different order for the batched CRUD actions to be executed in.
         * Defaults to 'create,update,destroy'.
         */
        batchOrder: 'create,update,destroy',
 
        /**
         * @cfg {Boolean} batchActions
         * True to batch actions of a particular type when synchronizing the store. Defaults to
         * `true`.
         */
        batchActions: true,
 
        /**
         * @cfg {String/Ext.data.Model} model
         * The name of the Model to tie to this Proxy. Can be either the string name of the Model,
         * or a reference to the Model constructor. Required.
         */
        model: undefined,
 
        // @cmd-auto-dependency {aliasPrefix : "reader.", defaultPropertyName : "defaultReaderType"}
        /**
         * @cfg {Object/String/Ext.data.reader.Reader} reader
         * The Ext.data.reader.Reader to use to decode the server's response or data read
         * from client. This can either be a Reader instance, a config object or just a
         * valid Reader type name (e.g. 'json', 'xml').
         */
        reader: {
            type: 'json'
        },
 
        // @cmd-auto-dependency {aliasPrefix : "writer.", defaultPropertyName : "defaultWriterType"}
        /**
         * @cfg {Object/String/Ext.data.writer.Writer} writer
         * The Ext.data.writer.Writer to use to encode any request sent to the server or
         * saved to client. This can either be a Writer instance, a config object or just
         * a valid Writer type name (e.g. 'json', 'xml').
         */
        writer: {
            type: 'json'
        }
    },
 
    /**
     * @property {Boolean} isProxy
     * `true` in this class to identify an object as an instantiated Proxy, or subclass thereof.
     */
    isProxy: true,
 
    /**
     * @property {Boolean} [isSynchronous=false]
     * Identifies the proxy as (a)synchronous.
     */
    isSynchronous: false,
 
    /**
     * @event metachange
     * Fires when this proxy's reader provides new metadata. Metadata usually consists
     * of new field definitions, but can include any configuration data required by an
     * application, and can be processed as needed in the event handler.
     * This event is currently only fired for JsonReaders. Note that this event is also
     * propagated by {@link Ext.data.Store}, which is typically where it would be handled.
     * @param {Ext.data.proxy.Proxy} this 
     * @param {Object} meta The JSON metadata
     */
 
    /**
     * Creates the Proxy
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        // Will call initConfig
        this.mixins.observable.constructor.call(this, config);
 
        // We need to abort all pending operations when destroying
        this.pendingOperations = {};
    },
 
    applyModel: function(model) {
        return Ext.data.schema.Schema.lookupEntity(model);
    },
 
    updateModel: function(model) {
        var reader;
 
        if (model) {
            reader = this.getReader();
 
            if (reader && !reader.getModel()) {
                reader.setModel(model);
            }
        }
    },
 
    applyReader: function(reader) {
        // Synchronous proxies need to force keepRawData to allow Grid features
        // like Summary and Grouping access rawData after the Reader processed records.
        // It doesn't do much harm since synchronous proxies are Client side ones,
        // which will keep their datasets in memory or local storage anyway.
        if (this.isSynchronous) {
            reader = reader || {};
            reader.keepRawData = true;
        }
 
        return Ext.Factory.reader(reader);
    },
 
    updateReader: function(reader) {
        var me = this,
            model;
 
        if (reader) {
            model = me.getModel();
 
            if (!model) {
                model = reader.getModel();
 
                if (model) {
                    me.setModel(model);
                }
            }
            else {
                reader.setModel(model);
            }
 
            if (reader.responseType != null) {
                me.responseType = reader.responseType;
            }
        }
    },
 
    applyWriter: function(writer) {
        var reader = this.getReader();
 
        writer = Ext.Factory.writer(writer);
 
        // XML Writers may have a record config to define the node name of each record tag.
        // If not set, but the Reader has a record config, use the Reader's record config.
        if (writer.getRecord && !writer.getRecord() && reader && reader.getRecord) {
            reader = reader.getRecord();
 
            if (reader) {
                writer.setRecord(reader);
            }
        }
 
        return writer;
    },
 
    abort: Ext.emptyFn,
 
    /**
     * @private
     * Called each time the reader's onMetaChange is called so that the proxy can fire the
     * metachange event
     */
    onMetaChange: function(meta) {
        this.fireEvent('metachange', this, meta);
    },
 
    /**
     * Performs the given create operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    create: Ext.emptyFn,
 
    /**
     * Performs the given read operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    read: Ext.emptyFn,
 
    /**
     * Performs the given update operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    update: Ext.emptyFn,
 
    /**
     * Performs the given destroy operation.
     * @param {Ext.data.operation.Operation} operation The Operation to perform
     * @method
     */
    erase: Ext.emptyFn,
 
    /**
     * Performs a batch of {@link Ext.data.operation.Operation Operations}, in the order specified
     * by {@link #batchOrder}. Used internally by {@link Ext.data.Store}'s
     * {@link Ext.data.Store#sync sync} method. Example usage:
     *
     *     myProxy.batch({
     *         create : [myModel1, myModel2],
     *         update : [myModel3],
     *         destroy: [myModel4, myModel5]
     *     });
     *
     * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2
     * are new instances and have not been saved before, 3 has been saved previously but needs to be
     * updated, and 4 and 5 have already been saved but should now be destroyed.
     * 
     * Note that the previous version of this method took 2 arguments (operations and listeners).
     * While this is still supported for now, the current signature is now a single `options`
     * argument that can contain both operations and listeners, in addition to other options.
     * The multi-argument signature will likely be deprecated in a future release.
     *
     * @param {Object} options Object containing one or more properties supported by the batch
     * method:
     * 
     * @param {Object} options.operations Object containing the Model instances to act upon, keyed
     * by action name
     * 
     * @param {Object} [options.listeners] Event listeners object passed straight through to the
     * Batch - see {@link Ext.data.Batch} for details
     * 
     * @param {Ext.data.Batch/Object} [options.batch] A {@link Ext.data.Batch} object (or batch
     * config to apply  to the created batch). If unspecified a default batch will be auto-created.
     * 
     * @param {Function} [options.callback] The function to be called upon completion of processing
     * the batch. The callback is called regardless of success or failure and is passed the
     * following parameters:
     * @param {Ext.data.Batch} options.callback.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.callback.options The options argument that was originally passed
     * into batch
     * 
     * @param {Function} [options.success] The function to be called upon successful completion
     * of the batch. The  success function is called only if no exceptions were reported in any
     * operations. If one or more exceptions occurred then the `failure` function will be called
     * instead. The success function is called  with the following parameters:
     * @param {Ext.data.Batch} options.success.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.success.options The options argument that was originally passed into
     * batch
     * 
     * @param {Function} [options.failure] The function to be called upon unsuccessful completion
     * of the batch. The  failure function is called when one or more operations returns an
     * exception during processing (even if some operations were also successful). In this case you
     * can check the batch's {@link Ext.data.Batch#exceptions exceptions} array to see exactly
     * which operations had exceptions. The failure function is called with the following
     * parameters:
     * @param {Ext.data.Batch} options.failure.batch The {@link Ext.data.Batch batch} that was
     * processed, containing all operations in their current state after processing
     * @param {Object} options.failure.options The options argument that was originally passed into
     * batch
     * 
     * @param {Object} [options.scope] The scope in which to execute any callbacks (i.e. the `this`
     * object inside the callback, success and/or failure functions). Defaults to the proxy.
     *
     * @param {Object} [listeners] (deprecated) If `options` is the `operations`, this
     * parameter is the listeners. Instead of passing these two arguments, the proper form
     * is to pass them as:
     *
     *      batch({
     *          operations: ...
     *          listeners: ...
     *      });
     *
     * @return {Ext.data.Batch} The newly created Batch
     */
    batch: function(options, listeners) {
        var me = this,
            useBatch = me.getBatchActions(),
            batch, records, actions, aLen, action, a, r, rLen, record;
 
        if (options.operations === undefined) {
            // the old-style (operations, listeners) signature was called
            // so convert to the single options argument syntax
            options = {
                operations: options,
                listeners: listeners
            };
        }
 
        if (options.batch) {
            if (Ext.isDefined(options.batch.runOperation)) {
                batch = Ext.applyIf(options.batch, {
                    proxy: me,
                    listeners: {}
                });
            }
        }
        else {
            options.batch = {
                proxy: me,
                listeners: options.listeners || {}
            };
        }
 
        if (!batch) {
            batch = new Ext.data.Batch(options.batch);
        }
 
        // Use single so that the listener gets removed upon completion.
        batch.on('complete', Ext.bind(me.onBatchComplete, me, [options], 0), null, {
            single: true,
            priority: 1000
        });
 
        batch.$destroyOwner = options.$destroyOwner;
 
        actions = me.getBatchOrder().split(',');
        aLen = actions.length;
 
        for (= 0; a < aLen; a++) {
            action = actions[a];
            records = options.operations[action];
 
            if (records) {
                if (useBatch) {
                    batch.add(me.createOperation(action, {
                        records: records,
                        // Relay any additional params through to the Operation (and Request).
                        params: options.params
                    }));
                }
                else {
                    rLen = records.length;
 
                    for (= 0; r < rLen; r++) {
                        record = records[r];
 
                        batch.add(me.createOperation(action, {
                            records: [record],
                            // Relay any additional params through to the Operation (and Request).
                            params: options.params
                        }));
                    }
                }
            }
        }
 
        batch.start();
 
        return batch;
    },
 
    /**
     * @private
     * The internal callback that the proxy uses to call any specified user callbacks after
     * completion of a batch
     */
    onBatchComplete: function(batchOptions, batch) {
        var scope = batchOptions.scope || this;
 
        if (batch.hasException()) {
            if (Ext.isFunction(batchOptions.failure)) {
                Ext.callback(batchOptions.failure, scope, [batch, batchOptions]);
            }
        }
        else if (Ext.isFunction(batchOptions.success)) {
            Ext.callback(batchOptions.success, scope, [batch, batchOptions]);
        }
 
        if (Ext.isFunction(batchOptions.callback)) {
            Ext.callback(batchOptions.callback, scope, [batch, batchOptions]);
        }
 
        // In certain cases when the batch was created by a ProxyStore we need to
        // defer destruction until the store can process the batch results.
        // The store will then destroy the batch.
        if (!batch.$destroyOwner) {
            batch.destroy();
        }
    },
 
    createOperation: function(action, config) {
        var operation = Ext.createByAlias('data.operation.' + action, config);
 
        operation.setProxy(this);
 
        this.pendingOperations[operation._internalId] = operation;
 
        return operation;
    },
 
    completeOperation: function(operation) {
        delete this.pendingOperations[operation._internalId];
    },
 
    clone: function() {
        return new this.self(this.getInitialConfig());
    },
 
    destroy: function() {
        var ops = this.pendingOperations,
            opId, op;
 
        for (opId in ops) {
            op = ops[opId];
 
            if (op && op.isRunning()) {
                op.abort();
            }
 
            op.destroy();
        }
 
        this.pendingOperations = null;
 
        this.callParent();
    }
});