/**
 * @author Ed Spencer
 * @class Ext.data.proxy.Server
 * @extends Ext.data.proxy.Proxy
 * 
 * <p>ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and {@link Ext.data.proxy.Ajax AjaxProxy},
 * and would not usually be used directly.</p>
 * 
 * <p>ServerProxy should ideally be named HttpProxy as it is a superclass for all HTTP proxies - for Ext JS 4.x it has been 
 * called ServerProxy to enable any 3.x applications that reference the HttpProxy to continue to work (HttpProxy is now an 
 * alias of AjaxProxy).</p>
 */
Ext.define('Ext.data.proxy.Server', {
    extend: 'Ext.data.proxy.Proxy',
    alias : 'proxy.server',
    alternateClassName: 'Ext.data.ServerProxy',
    uses  : ['Ext.data.Request'],
    
    /**
     * @cfg {String} url The URL from which to request the data object.
     */
    
    /**
     * @cfg {Object/String/Ext.data.reader.Reader} reader The Ext.data.reader.Reader to use to decode the server's response. This can
     * either be a Reader instance, a config object or just a valid Reader type name (e.g. 'json', 'xml').
     */
    
    /**
     * @cfg {Object/String/Ext.data.writer.Writer} writer The Ext.data.writer.Writer to use to encode any request sent to the server.
     * This can either be a Writer instance, a config object or just a valid Writer type name (e.g. 'json', 'xml').
     */
    
    /**
     * @cfg {String} pageParam The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to
     * undefined if you don't want to send a page parameter
     */
    pageParam: 'page',
    
    /**
     * @cfg {String} startParam The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this
     * to undefined if you don't want to send a start parameter
     */
    startParam: 'start',

    /**
     * @cfg {String} limitParam The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this
     * to undefined if you don't want to send a limit parameter
     */
    limitParam: 'limit',
    
    /**
     * @cfg {String} groupParam The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this
     * to undefined if you don't want to send a group parameter
     */
    groupParam: 'group',
    
    /**
     * @cfg {String} sortParam The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this
     * to undefined if you don't want to send a sort parameter
     */
    sortParam: 'sort',
    
    /**
     * @cfg {String} filterParam The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set 
     * this to undefined if you don't want to send a filter parameter
     */
    filterParam: 'filter',
    
    /**
     * @cfg {String} directionParam The name of the direction parameter to send in a request. <strong>This is only used when simpleSortMode is set to true.</strong>
     * Defaults to 'dir'.
     */
    directionParam: 'dir',
    
    /**
     * @cfg {Boolean} simpleSortMode Enabling simpleSortMode in conjunction with remoteSort will only send one sort property and a direction when a remote sort is requested.
     * The directionParam and sortParam will be sent with the property name and either 'ASC' or 'DESC'
     */
    simpleSortMode: false,
    
    /**
     * @cfg {Boolean} noCache (optional) Defaults to true. Disable caching by adding a unique parameter
     * name to the request.
     */
    noCache : true,
    
    /**
     * @cfg {String} cacheString The name of the cache param added to the url when using noCache (defaults to "_dc")
     */
    cacheString: "_dc",
    
    /**
     * @cfg {Number} timeout (optional) The number of milliseconds to wait for a response. Defaults to 30 seconds.
     */
    timeout : 30000,
    
    /**
     * @cfg {Object} api
     * Specific urls to call on CRUD action methods "read", "create", "update" and "destroy".
     * Defaults to:<pre><code>
api: {
    read    : undefined,
    create  : undefined,
    update  : undefined,
    destroy : undefined
}
     * </code></pre>
     * <p>The url is built based upon the action being executed <tt>[load|create|save|destroy]</tt>
     * using the commensurate <tt>{@link #api}</tt> property, or if undefined default to the
     * configured {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.</p><br>
     * <p>For example:</p>
     * <pre><code>
api: {
    load :    '/controller/load',
    create :  '/controller/new',
    save :    '/controller/update',
    destroy : '/controller/destroy_action'
}
     * </code></pre>
     * <p>If the specific URL for a given CRUD action is undefined, the CRUD action request
     * will be directed to the configured <tt>{@link Ext.data.proxy.Server#url url}</tt>.</p>
     */
    
    /**
     * @ignore
     */
    constructor: function(config) {
        var me = this;
        
        config = config || {};
        this.addEvents(
            /**
             * @event exception
             * Fires when the server returns an exception
             * @param {Ext.data.proxy.Proxy} this
             * @param {Object} response The response from the AJAX request
             * @param {Ext.data.Operation} operation The operation that triggered request
             */
            'exception'
        );
        me.callParent([config]);
        
        /**
         * @cfg {Object} extraParams Extra parameters that will be included on every request. Individual requests with params
         * of the same name will override these params when they are in conflict.
         */
        me.extraParams = config.extraParams || {};
        
        me.api = config.api || {};
        
        //backwards compatibility, will be deprecated in 5.0
        me.nocache = me.noCache;
    },
    
    //in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to doRequest in each case
    create: function() {
        return this.doRequest.apply(this, arguments);
    },
    
    read: function() {
        return this.doRequest.apply(this, arguments);
    },
    
    update: function() {
        return this.doRequest.apply(this, arguments);
    },
    
    destroy: function() {
        return this.doRequest.apply(this, arguments);
    },
    
    /**
     * Creates and returns an Ext.data.Request object based on the options passed by the {@link Ext.data.Store Store}
     * that this Proxy is attached to.
     * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
     * @return {Ext.data.Request} The request object
     */
    buildRequest: function(operation) {
        var params = Ext.applyIf(operation.params || {}, this.extraParams || {}),
            request;
        
        //copy any sorters, filters etc into the params so they can be sent over the wire
        params = Ext.applyIf(params, this.getParams(params, operation));
        
        if (operation.id && !params.id) {
            params.id = operation.id;
        }
        
        request = Ext.create('Ext.data.Request', {
            params   : params,
            action   : operation.action,
            records  : operation.records,
            operation: operation,
            url      : operation.url
        });
        
        request.url = this.buildUrl(request);
        
        /*
         * Save the request on the Operation. Operations don't usually care about Request and Response data, but in the
         * ServerProxy and any of its subclasses we add both request and response as they may be useful for further processing
         */
        operation.request = request;
        
        return request;
    },
    
    /**
     * 
     */
    processResponse: function(success, operation, request, response, callback, scope){
        var me = this,
            reader,
            result,
            records,
            length,
            mc,
            record,
            i;
            
        if (success === true) {
            reader = me.getReader();
            result = reader.read(me.extractResponseData(response));
            records = result.records;
            length = records.length;
            
            if (result.success !== false) {
                mc = Ext.create('Ext.util.MixedCollection', true, function(r) {return r.getId();});
                mc.addAll(operation.records);
                for (i = 0; i < length; i++) {
                    record = mc.get(records[i].getId());
                    
                    if (record) {
                        record.beginEdit();
                        record.set(record.data);
                        record.endEdit(true);
                    }
                }
                
                //see comment in buildRequest for why we include the response object here
                Ext.apply(operation, {
                    response: response,
                    resultSet: result
                });
                
                operation.setCompleted();
                operation.setSuccessful();
            } else {
                operation.setException(result.message);
                me.fireEvent('exception', this, response, operation);
            }
        } else {
            me.setException(operation, response);
            me.fireEvent('exception', this, response, operation);              
        }
            
        //this callback is the one that was passed to the 'read' or 'write' function above
        if (typeof callback == 'function') {
            callback.call(scope || me, operation);
        }
            
        me.afterRequest(request, success);
    },
    
    /**
     * Sets up an exception on the operation
     * @private
     * @param {Ext.data.Operation} operation The operation
     * @param {Object} response The response
     */
    setException: function(operation, response){
        operation.setException({
            status: response.status,
            statusText: response.statusText
        });     
    },
    
    /**
     * Template method to allow subclasses to specify how to get the response for the reader.
     * @private
     * @param {Object} response The server response
     * @return {Mixed} The response data to be used by the reader
     */
    extractResponseData: function(response){
        return response; 
    },
    
    /**
     * Encode any values being sent to the server. Can be overridden in subclasses.
     * @private
     * @param {Array} An array of sorters/filters.
     * @return {Mixed} The encoded value
     */
    applyEncoding: function(value){
        return Ext.encode(value);
    },
    
    /**
     * Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request url. By default, 
     * this simply JSON-encodes the sorter data
     * @param {Array} sorters The array of {@link Ext.util.Sorter Sorter} objects
     * @return {String} The encoded sorters
     */
    encodeSorters: function(sorters) {
        var min = [],
            length = sorters.length,
            i = 0;
        
        for (; i < length; i++) {
            min[i] = {
                property : sorters[i].property,
                direction: sorters[i].direction
            };
        }
        return this.applyEncoding(min);
        
    },
    
    /**
     * Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request url. By default, 
     * this simply JSON-encodes the filter data
     * @param {Array} sorters The array of {@link Ext.util.Filter Filter} objects
     * @return {String} The encoded filters
     */
    encodeFilters: function(filters) {
        var min = [],
            length = filters.length,
            i = 0;
        
        for (; i < length; i++) {
            min[i] = {
                property: filters[i].property,
                value   : filters[i].value
            };
        }
        return this.applyEncoding(min);
    },
    
    /**
     * @private
     * Copy any sorters, filters etc into the params so they can be sent over the wire
     */
    getParams: function(params, operation) {
        params = params || {};
        
        var me             = this,
            isDef          = Ext.isDefined,
            groupers       = operation.groupers,
            sorters        = operation.sorters,
            filters        = operation.filters,
            page           = operation.page,
            start          = operation.start,
            limit          = operation.limit,
            
            simpleSortMode = me.simpleSortMode,
            
            pageParam      = me.pageParam,
            startParam     = me.startParam,
            limitParam     = me.limitParam,
            groupParam     = me.groupParam,
            sortParam      = me.sortParam,
            filterParam    = me.filterParam,
            directionParam       = me.directionParam;
        
        if (pageParam && isDef(page)) {
            params[pageParam] = page;
        }
        
        if (startParam && isDef(start)) {
            params[startParam] = start;
        }
        
        if (limitParam && isDef(limit)) {
            params[limitParam] = limit;
        }
        
        if (groupParam && groupers && groupers.length > 0) {
            // Grouper is a subclass of sorter, so we can just use the sorter method
            params[groupParam] = me.encodeSorters(groupers);
        }
        
        if (sortParam && sorters && sorters.length > 0) {
            if (simpleSortMode) {
                params[sortParam] = sorters[0].property;
                params[directionParam] = sorters[0].direction;
            } else {
                params[sortParam] = me.encodeSorters(sorters);
            }
            
        }
        
        if (filterParam && filters && filters.length > 0) {
            params[filterParam] = me.encodeFilters(filters);
        }
        
        return params;
    },
    
    /**
     * Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl will
     * add the cache-buster param to the end of the url. Subclasses may need to perform additional modifications
     * to the url.
     * @param {Ext.data.Request} request The request object
     * @return {String} The url
     */
    buildUrl: function(request) {
        var me = this,
            url = me.getUrl(request);
        
        //<debug>
        if (!url) {
            Ext.Error.raise("You are using a ServerProxy but have not supplied it with a url.");
        }
        //</debug>
        
        if (me.noCache) {
            url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.cacheString, Ext.Date.now()));
        }
        
        return url;
    },
    
    /**
     * Get the url for the request taking into account the order of priority,
     * - The request
     * - The api
     * - The url
     * @private
     * @param {Ext.data.Request} request The request
     * @return {String} The url
     */
    getUrl: function(request){
        return request.url || this.api[request.action] || this.url;
    },
    
    /**
     * In ServerProxy subclasses, the {@link #create}, {@link #read}, {@link #update} and {@link #destroy} methods all pass
     * through to doRequest. Each ServerProxy subclass must implement the doRequest method - see {@link Ext.data.proxy.JsonP}
     * and {@link Ext.data.proxy.Ajax} for examples. This method carries the same signature as each of the methods that delegate to it.
     * @param {Ext.data.Operation} operation The Ext.data.Operation object
     * @param {Function} callback The callback function to call when the Operation has completed
     * @param {Object} scope The scope in which to execute the callback
     */
    doRequest: function(operation, callback, scope) {
        //<debug>
        Ext.Error.raise("The doRequest function has not been implemented on your Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
        //</debug>
    },
    
    /**
     * Optional callback function which can be used to clean up after a request has been completed.
     * @param {Ext.data.Request} request The Request object
     * @param {Boolean} success True if the request was successful
     * @method
     */
    afterRequest: Ext.emptyFn,
    
    onDestroy: function() {
        Ext.destroy(this.reader, this.writer);
    }
});
