/**
 * This class is used to send requests to the server using {@link Ext.direct.Manager Ext Direct}.
 * When a request is made, the transport mechanism is handed off to the appropriate
 * {@link Ext.direct.RemotingProvider Provider} to complete the call.
 *
 * # Specifying the functions
 *
 * This proxy expects Direct remoting method to be passed in order to be able to complete requests,
 * one Direct function per CRUD method. This is done via {@link #api} configuration:
 *
 *      api: {
 *          read: 'MyApp.readRecords',
 *          create: 'MyApp.createRecords',
 *          update: 'MyApp.updateRecords',
 *          destroy: 'MyApp.destroyRecords'
 *      }
 *
 * You can also use a `prefix` config to avoid duplicating full namespaces for Direct functions:
 *
 *      api: {
 *          prefix: 'MyApp',
 *          read: 'readRecords',
 *          create: 'createRecords',
 *          update: 'updateRecords',
 *          destroy: 'destroyRecords'
 *      }
 *
 * The preferred way is to specify function names to allow late resolution, however you can
 * pass function references instead if desired:
 *
 *      api: {
 *          read: MyApp.readRecords,
 *          create: MyApp.createRecords,
 *          update: MyApp.updateRecords,
 *          destroy: MyApp.destroyRecords
 *      }
 *
 * This method of configuring API is not recommended because this way the Direct functions
 * need to be created very early in the application lifecycle, long before {@link Ext.app.Application}
 * instance is initialized.
 *
 * You can also use the {@link #directFn} configuration instead of {@link #api}. This will use
 * the same Direct function for all types of requests.
 *
 * # Server API
 *
 * The server side methods are expected to conform to the following calling conventions:
 *
 * ## `read`
 *
 * Accept one argument which is either named arguments in an object (default), or an array
 * of values depending on the {@link #paramsAsHash} configuration. Return an array of records
 * or an object with format recognizable by the configured {@link Ext.data.reader.Reader}
 * instance.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: {
 *          MyApp: [{
 *              name: 'readRecords',
 *              params: [],
 *              strict: false
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.readRecords(
 *          {
 *              start: 0,
 *              limit: 10
 *          },
 *          // Results are passed to the callback function
 *          function(records) {
 *              console.log(records);
 *              // Logs:  [{ id: 'r0', text: 'foo' }, { id: 'r1', text: 'bar' }]
 *          }
 *      );
 *
 * ## `create`
 *
 * Accept one ordered argument which is either an object with data for the new record,
 * or an array of objects for multiple records. Return an array of identifiers for actually
 * created records. See {@link Ext.data.Model#clientIdProperty} for more information.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: [
 *          MyApp: [{
 *              name: 'createRecords',
 *              len: 1
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.createRecords(
 *          [
 *              { id: 0, text: 'foo' },
 *              { id: 1, text: 'bar' }
 *          ],
 *          // Results are passed to the callback function
 *          function(records) {
 *              console.log(records);
 *              // Logs: [{ clientId: 0, id: 'r0' }, { clientId: 1, id: 'r1' }]
 *          }
 *      );
 *
 * ## `update`
 *
 * Accept one ordered argument which is either an object with updated data and valid
 * record identifier, or an array of objects for multiple records. Return an array of
 * objects with updated record data.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: [
 *          MyApp: [{
 *              name: 'updateRecords',
 *              len: 1
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.updateRecords(
 *          [
 *              { id: 'r0', text: 'blerg' },
 *              { id: 'r1', text: 'throbbe' }
 *          ],
 *          // Results are passed to the callback function
 *          function(records) {
 *              console.log(records);
 *              // Logs: [{ id: 'r0', text: 'blerg' }, { id: 'r1', text: 'throbbe }]
 *          }
 *      );
 *
 * ## `destroy`
 *
 * Accept one ordered argument which is an array of record identifiers to be deleted.
 * Return an object with at least one {@link Ext.data.reader.Json#successProperty}
 * property set to `true` or `false`, with more optional properties recognizable by configured
 * {@link Ext.data.reader.Reader} instance.
 *
 * Example {@link Ext.direct.RemotingProvider#cfg-actions Direct API declaration}:
 *
 *      actions: [
 *          MyApp: [{
 *              name: 'destroyRecords',
 *              len: 1
 *          }]
 *      }
 *
 * Example function invocation:
 *
 *      MyApp.destroyRecords(
 *          [
 *              { id: 'r0' },
 *              { id: 'r1' }
 *          ],
 *          // Results are passed to the callback function
 *          function(result) {
 *              // Default successProperty is `success`
 *              if (!result.success) {
 *                  // Handle the error
 *              }
 *          }
 *      );
 *
 * ## Read method parameters
 *
 * Direct proxy provides options to help configure which parameters will be sent to the server
 * for Read operations. By setting the {@link #paramsAsHash} option to `true`, the proxy will
 * send an object literal containing each of the passed parameters. This is the default. When
 * {@link #paramsAsHash} is set to `false`, Proxy will pass the Read function an array of values
 * instead of an object, with the order determined by {@link #paramOrder} value.
 *
 * Setting {@link #paramOrder} to any value other than `undefined` will automatically reset
 * {@link #paramsAsHash} to `false`.
 *
 * # Example Usage
 *
 *      Ext.define('User', {
 *          extend: 'Ext.data.Model',
 *          fields: ['firstName', 'lastName']
 *      });
 *      
 *      Ext.define('Users', {
 *          extend: 'Ext.data.Store',
 *          model: 'User',
 *          proxy: {
 *              type: 'direct',
 *              directFn: 'MyApp.getUsers',
 *              // Tells the proxy to pass `start` and `limit` as two by-position arguments:
 *              paramOrder: 'start,limit'
 *          }
 *      });
 *      
 *      var store = new Users();
 *      store.load();
 */
Ext.define('Ext.data.proxy.Direct', {
    /* Begin Definitions */
 
    extend: 'Ext.data.proxy.Server',
    alternateClassName: 'Ext.data.DirectProxy',
 
    alias: 'proxy.direct',
 
    requires: ['Ext.direct.Manager'],
 
    /* End Definitions */
 
    /**
     * @cfg url
     * @hide
     */
 
    config: {
        /**
         * @cfg {String/String[]} paramOrder
         * A list of params to be passed to server side Read function. Specify the params
         * in the order in which they must be executed on the server-side as either (1)
         * an Array of String values, or (2) a String of params delimited by either
         * whitespace, comma, or pipe. For example, any of the following would be
         * acceptable:
         *
         *     paramOrder: ['param1','param2','param3']
         *     paramOrder: 'param1 param2 param3'
         *     paramOrder: 'param1,param2,param3'
         *     paramOrder: 'param1|param2|param'
         */
        paramOrder: undefined,
 
        /**
         * @cfg {Boolean} paramsAsHash 
         * Send Read function parameters as a collection of named arguments. Providing a
         * {@link #paramOrder} nullifies this configuration.
         */
        paramsAsHash: true,
 
        /**
         * @cfg {Function/String} directFn
         * Function to call when executing a request. `directFn` is a simple alternative
         * to defining the api configuration parameter for Stores which will not
         * implement a full CRUD api. The `directFn` may also be a string reference to
         * the fully qualified name of the function, for example:
         * `'MyApp.company.GetProfile'`. This can be useful when using dynamic loading.
         * The string will be resolved before calling the function for the first time.
         */
        directFn: undefined,
 
        /**
         * @cfg {Object} api 
         * The same as {@link Ext.data.proxy.Server#api}, however instead of providing
         * urls you should provide a Direct function name for each CRUD method.
         *
         * Instead of providing fully qualified names for each function, you can use
         * `prefix` property to provide a common prefix for all functions:
         *
         *   api: {
         *       prefix: 'MyApp',
         *       read: 'readRecords',
         *       create: 'createRecords',
         *       update: 'updateRecords',
         *       destroy: 'destroyRecords'
         *   }
         *
         * This way function names will be resolved to `'MyApp.readRecords'`, 
         * `'MyApp.createRecords'`, etc. Note that using `prefix` and fully qualified
         * function names is **not** supported, and prefix will be used for every
         * function name when configured.
         *
         * See also {@link #directFn}.
         */
        api: undefined,
        
        /**
         * @cfg {Object/Array} metadata
         * Optional set of fixed parameters to send with every Proxy request, similar to
         * {@link #extraParams} but available with all CRUD requests. Also unlike
         * {@link #extraParams}, metadata is not mixed with the ordinary data but sent
         * separately in the data packet.
         * You may need to update your server side Ext Direct stack to use this feature.
         */
        metadata: undefined
    },
 
    /**
     * @private
     */
    paramOrderRe: /[\s,|]/,
    
    constructor: function(config) {
        this.callParent([config]);
        this.canceledOperations = {};
    },
    
    applyParamOrder: function(paramOrder) {
        if (Ext.isString(paramOrder)) {
            paramOrder = paramOrder.split(this.paramOrderRe);
        }
        return paramOrder;
    },
 
    updateApi: function() {
        this.methodsResolved = false;
    },
 
    updateDirectFn: function() {
        this.methodsResolved = false;
    },
    
    resolveMethods: function() {
        var me = this,
            fn = me.getDirectFn(),
            api = me.getApi(),
            method;
        
        if (fn) {
            me.setDirectFn(method = Ext.direct.Manager.parseMethod(fn));
            
            if (!Ext.isFunction(method)) {
                Ext.raise('Cannot resolve directFn ' + fn);
            }
        }
        
        if (api) {
            api = Ext.direct.Manager.resolveApi(api, me);
            me.setApi(api);
        }
        
        me.methodsResolved = true;
    },
 
    doRequest: function(operation) {
        var me = this,
            writer, request, action, params, args, api, fn, callback;
        
        if (!me.methodsResolved) {
            me.resolveMethods();
        }
        
        request = me.buildRequest(operation);
        action  = request.getAction();
        api     = me.getApi();
 
        if (api) {
            fn = api[action];
        }
        
        fn = fn || me.getDirectFn();
        
        //<debug> 
        if (!fn || !fn.directCfg) {
            Ext.raise({
                msg: 'No Ext Direct function specified for Direct proxy "' + action + '" operation',
                proxy: me
            });
        }
        
        // This might lead to exceptions so bail out early 
        if (!me.paramOrder && fn.directCfg.method.len > 1) {
            Ext.raise({
                msg: 'Incorrect parameters for Direct proxy "' + action + '" operation',
                proxy: me
            });
        }
        //</debug> 
        
        writer = me.getWriter();
 
        if (writer && operation.allowWrite()) {
            request = writer.write(request);
        }
        
        // The weird construct below is due to historical way of handling extraParams; 
        // they were mixed in with request data in ServerProxy.buildRequest() and were 
        // inseparable after that point. This does not work well with CUD operations 
        // so instead of using potentially poisoned request params we took the raw 
        // JSON data as Direct function argument payload (but only for CUD!). A side 
        // effect of that was that the request metadata (extraParams) was only available 
        // for read operations. 
        // We keep this craziness for backwards compatibility. 
        if (action === 'read') {
            params = request.getParams();
        }
        else {
            params = request.getJsonData();
        }
        
        args = fn.directCfg.method.getArgs({
            params: params,
            allowSingle: writer.getAllowSingle(),
            paramOrder: me.getParamOrder(),
            paramsAsHash: me.getParamsAsHash(),
            paramsAsArray: true,
            metadata: me.getMetadata(),
            callback: me.createRequestCallback(request, operation),
            scope: me
        });
        
        request.setConfig({
            args: args,
            directFn: fn
        });
        
        fn.apply(window, args);
        
        // Store expects us to return something to indicate that the request 
        // is pending; not doing so will make a buffered Store repeat the 
        // requests over and over. 
        return request;
    },
    
    /**
     * Aborts a running request by operation.
     *
     * @param {Ext.data.Operation} operation The operation to abort. This parameter
     * is mandatory.
     */
    abort: function(operation) {
        var id;
        
        // Assume this can be called with request instead of operation, a la Ajax proxy 
        if (operation && operation.isDataRequest) {
            operation = operation.getOperation();
        }
        
        // Check definedness again, the above could have returned null 
        if (operation && operation.isOperation) {
            id = operation.id;
        }
        
        // We cannot abort a running request but we can ignore the data when it comes back. 
        if (id != null) {
            this.canceledOperations[id] = true;
        }
    },
 
    /**
     * @method applyEncoding
     * @inheritdoc
     */
    applyEncoding: Ext.identityFn,
 
    createRequestCallback: function(request, operation) {
        var me = this;
 
        return function(data, event) {
            if (!me.canceledOperations[operation.id]) {
                me.processResponse(event.status, operation, request, event);
            }
            
            delete me.canceledOperations[operation.id];
        };
    },
 
    /**
     * @method extractResponseData
     * @inheritdoc
     */
    extractResponseData: function(response) {
        return Ext.isDefined(response.result) ? response.result : response.data;
    },
 
    /**
     * @method setException
     * @inheritdoc
     */
    setException: function(operation, response) {
        operation.setException(response.message);
    },
 
    /**
     * @method buildUrl
     * @inheritdoc
     */
    buildUrl: function() {
        return '';
    }
});