/**
 * 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.
 * @protected
 */
Ext.define('Ext.data.proxy.Server', {
    extend: 'Ext.data.proxy.Proxy',
    alias: 'proxy.server',
    alternateClassName: 'Ext.data.ServerProxy',
    uses: ['Ext.data.Request'],
    
    isRemote: true,
 
    config: {
        /**
         * @cfg {String} url
         * The URL from which to request the data object.
         */
        url: '',
    
        /**
         * @cfg {String} [pageParam="page"]
         * The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to
         * `''` if you don't want to send a page parameter.
         */
        pageParam: 'page',
    
        /**
         * @cfg {String} [startParam="start"]
         * The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this to
         * `''` if you don't want to send a start parameter.
         */
        startParam: 'start',
    
        /**
         * @cfg {String} [limitParam="limit"]
         * The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this to
         * `''` if you don't want to send a limit parameter.
         */
        limitParam: 'limit',
    
        /**
         * @cfg {String} [groupParam="group"]
         * The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this to
         * `''` if you don't want to send a group parameter.
         */
        groupParam: 'group',
    
        /**
         * @cfg {String} [groupDirectionParam="groupDir"]
         * The name of the direction parameter to send in a request. **This is only used when
         * simpleGroupMode is set to true.**
         * If this is set to the same value as the {@link #groupParam}, then the group property
         * name *and* direction of each grouper is passed as a single, space separated parameter,
         * looking like a database `group by` specification.
         *
         * So if there are multiple groupers, the single group parameter will look like this:
         *
         *     ?group=name%20ASC&group=age%20DESC
         */
        groupDirectionParam: 'groupDir',
    
        /**
         * @cfg {String} [sortParam="sort"]
         * The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this to
         * `''` if you don't want to send a sort parameter.
         */
        sortParam: 'sort',
    
        /**
         * @cfg {String} [filterParam="filter"]
         * The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set this
         * to `''` if you don't want to send a filter parameter.
         */
        filterParam: 'filter',
    
        /**
         * @cfg {String} [directionParam="dir"]
         * The name of the direction parameter to send in a request. **This is only used when
         * simpleSortMode is set to true.**
         * 
         * If this is set to the same value as the {@link #sortParam}, then the sort property name
         * *and* direction of each sorter is passed as a single, space separated parameter, looking
         * like a database `order by` specification.
         *
         * So if there are multiple sorters, the single sort parameter will look like this:
         *
         *     ?sort=name%20ASC&sort=age%20DESC
         */
        directionParam: 'dir',
    
        /**
         * @cfg {String} [idParam="id"]
         * The name of the parameter which carries the id of the entity being operated upon.
         */
        idParam: 'id',
    
        /**
         * @cfg {Boolean} [simpleSortMode=false]
         * Enabling simpleSortMode in conjunction with remoteSort will send the sorted field names
         * in the parameter named by {@link #sortParam}, and the directions for each sorted field
         * in a parameter named by {@link #directionParam}.
         *
         * In the simplest case, with one Sorter, this will result in HTTP parameters like this:
         *
         *     ?sort=name&dir=ASC
         *
         * If there are multiple sorters, the parameters will be encoded like this:
         *
         *     ?sort=name&sort=age&dir=ASC&dir=DESC
         */
        simpleSortMode: false,
    
        /**
         * @cfg {Boolean} [simpleGroupMode=false]
         * Enabling simpleGroupMode in conjunction with remoteGroup will only send one group
         * property and a direction when a remote group is requested. The
         * {@link #groupDirectionParam} and {@link #groupParam} will be sent with the property name
         * and either 'ASC' or 'DESC'.
         */
        simpleGroupMode: false,
    
        /**
         * @cfg {Boolean} [noCache=true]
         * Disable caching by adding a unique parameter name to the request. Set to false to allow
         * caching. Defaults to true.
         */
        noCache: true,
    
        /**
         * @cfg {String} [cacheString="_dc"]
         * The name of the cache param added to the url when using noCache. Defaults to "_dc".
         */
        cacheString: "_dc",
    
        /**
         * @cfg {Number} timeout
         * The number of milliseconds to wait for a response. Defaults to 30000 milliseconds
         * (30 seconds).
         */
        timeout: 30000,
    
        /**
         * @cfg {Object} api
         * Specific urls to call on CRUD action methods "create", "read", "update" and "destroy".
         * Defaults to:
         *
         *     api: {
         *         create  : undefined,
         *         read    : undefined,
         *         update  : undefined,
         *         destroy : undefined
         *     }
         *
         * The url is built based upon the action being executed [create|read|update|destroy] using
         * the commensurate {@link #api} property, or if undefined default to the configured
         * {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
         *
         * For example:
         *
         *     api: {
         *         create  : '/controller/new',
         *         read    : '/controller/load',
         *         update  : '/controller/update',
         *         destroy : '/controller/destroy_action'
         *     }
         *
         * If the specific URL for a given CRUD action is undefined, the CRUD action request will
         * be directed to the configured {@link Ext.data.proxy.Server#url url}.
         */
        api: {
            create: undefined,
            read: undefined,
            update: undefined,
            destroy: undefined
        },
 
        /**
         * @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.
         */
        extraParams: {}
    },
 
    /**
     * @event exception
     * Fires when the server returns an exception. This event may also be listened
     * to in the event that a request has timed out or has been aborted.
     * @param {Ext.data.proxy.Proxy} this 
     * @param {Ext.data.Response} response The response that was received
     * @param {Ext.data.operation.Operation} operation The operation that triggered the request
     */
 
    // 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);
    },
 
    erase: function() {
        return this.doRequest.apply(this, arguments);
    },
 
    /**
     * Sets a value in the underlying {@link #extraParams}.
     * @param {String} name The key for the new value
     * @param {Object} value The value
     */
    setExtraParam: function(name, value) {
        var extraParams = this.getExtraParams();
        
        extraParams[name] = value;
        
        this.fireEvent('extraparamschanged', extraParams);
    },
 
    updateExtraParams: function(newExtraParams, oldExtraParams) {
        this.fireEvent('extraparamschanged', newExtraParams);
    },
 
    /**
     * Creates an {@link Ext.data.Request Request} object from
     * {@link Ext.data.operation.Operation Operation}.
     *
     * This gets called from doRequest methods in subclasses of Server proxy.
     * 
     * @param {Ext.data.operation.Operation} operation The operation to execute
     * @return {Ext.data.Request} The request object
     */
    buildRequest: function(operation) {
        var me = this,
            initialParams = Ext.apply({}, operation.getParams()),
            // Clone params right now so that they can be mutated at any point further down the
            // call stack
            params = Ext.applyIf(initialParams, me.getExtraParams() || {}),
            request,
            operationId,
            idParam;
 
        // copy any sorters, filters etc into the params so they can be sent over the wire
        Ext.applyIf(params, me.getParams(operation));
 
        // Set up the entity id parameter according to the configured name.
        // This defaults to "id". But TreeStore has a "nodeParam" configuration which
        // specifies the id parameter name of the node being loaded.
        operationId = operation.getId();
        idParam = me.getIdParam();
        
        if (operationId !== undefined && params[idParam] === undefined) {
            params[idParam] = operationId;
        }
 
        request = new Ext.data.Request({
            params: params,
            action: operation.getAction(),
            records: operation.getRecords(),
            url: operation.getUrl(),
            operation: operation,
 
            // this is needed by JsonSimlet in order to properly construct responses for
            // requests from this proxy
            proxy: me
        });
 
        request.setUrl(me.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.setRequest(request);
 
        return request;
    },
 
    /**
     * Processes response, which may involve updating or committing records, each of which
     * will inform the owning stores and their interested views. Finally, we may perform
     * an additional layout if the data shape has changed. 
     *
     * @protected
     */
    processResponse: function(success, operation, request, response) {
        var me = this,
            exception, reader, resultSet, meta, destroyOp;
        
        // Async callback could have landed at any time, including during and after
        // destruction. We don't want to unravel the whole response chain in such case.
        if (me.destroying || me.destroyed) {
            return;
        }
 
        // Processing a response may involve updating or committing many records
        // each of which will inform the owning stores, which will ultimately
        // inform interested views which will most likely have to do a layout
        // assuming that the data shape has changed.
        // Bracketing the processing with this event gives owning stores the ability
        // to fire their own beginupdate/endupdate events which can be used by interested
        // views to suspend layouts.
        me.fireEvent('beginprocessresponse', me, response, operation);
 
        if (success === true) {
            reader = me.getReader();
 
            if (response.status === 204) {
                resultSet = reader.getNullResultSet();
            }
            else {
                resultSet = reader.read(me.extractResponseData(response), {
                    // If we're doing an update, we want to construct the models ourselves.
                    recordCreator: operation.getRecordCreator() ||
                    reader.defaultRecordCreatorFromServer
                });
            }
 
            if (!operation.$destroyOwner) {
                operation.$destroyOwner = me;
                destroyOp = true;
            }
            
            operation.process(resultSet, request, response);
            exception = !operation.wasSuccessful();
        }
        else {
            me.setException(operation, response);
            exception = true;
        }
        
        // It is possible that exception callback destroyed the store and owning proxy,
        // in which case we can't do nothing except punt.
        if (me.destroyed) {
            if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
                operation.destroy();
            }
            
            return;
        }
        
        if (exception) {
            me.fireEvent('exception', me, response, operation);
        }
        // If a JsonReader detected metadata, process it now.
        // This will fire the 'metachange' event which the Store processes to fire its own
        // 'metachange'
        else {
            meta = resultSet.getMetadata();
            
            if (meta) {
                me.onMetaChange(meta);
            }
        }
 
        // Ditto
        if (me.destroyed) {
            if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
                operation.destroy();
            }
            
            return;
        }
 
        me.afterRequest(request, success);
 
        // Tell owning store processing has finished.
        // It will fire its endupdate event which will cause interested views to 
        // resume layouts.
        me.fireEvent('endprocessresponse', me, response, operation);
        
        if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
            operation.destroy();
        }
    },
    
    /**
     * Sets up an exception on the operation
     * @private
     * @param {Ext.data.operation.Operation} operation The operation
     * @param {Object} response The response
     */
    setException: function(operation, response) {
        operation.setException({
            status: response.status,
            statusText: response.statusText,
            response: response
        });
    },
 
    /**
     * @method
     * Template method to allow subclasses to specify how to get the response for the reader.
     * @template
     * @private
     * @param {Object} response The server response
     * @return {Object} The response data to be used by the reader
     */
    extractResponseData: Ext.identityFn,
 
    /**
     * Encode any values being sent to the server. Can be overridden in subclasses.
     * @protected
     * @param {Array} value An array of sorters/filters.
     * @return {Object} 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 {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
     * @param {Boolean} [preventArray=false] Prevents the items from being output as an array.
     * @return {String} The encoded sorters
     */
    encodeSorters: function(sorters, preventArray) {
        var out = [],
            length = sorters.length,
            i;
        
        for (= 0; i < length; i++) {
            out[i] = sorters[i].serialize();
        }
 
        return this.applyEncoding(preventArray ? out[0] : out);
    },
 
    /**
     * 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 {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
     * @return {String} The encoded filters
     */
    encodeFilters: function(filters) {
        var out = [],
            length = filters.length,
            encode, i;
 
        for (= 0; i < length; i++) {
            encode |= filters[i].serializeTo(out);
        }
 
        // If any Filters return Objects encapsulating their full state, then the parameters
        // needs JSON encoding.
        return encode ? this.applyEncoding(out) : out;
    },
 
    /**
     * @private
     * Copy any sorters, filters etc into the params so they can be sent over the wire
     */
    getParams: function(operation) {
        if (!operation.isReadOperation) {
            return {};
        }
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            params = {},
            grouper = operation.getGrouper(),
            sorters = operation.getSorters(),
            filters = operation.getFilters(),
            page = operation.getPage(),
            start = operation.getStart(),
            limit = operation.getLimit(),
            simpleSortMode = me.getSimpleSortMode(),
            simpleGroupMode = me.getSimpleGroupMode(),
            pageParam = me.getPageParam(),
            startParam = me.getStartParam(),
            limitParam = me.getLimitParam(),
            groupParam = me.getGroupParam(),
            groupDirectionParam = me.getGroupDirectionParam(),
            sortParam = me.getSortParam(),
            filterParam = me.getFilterParam(),
            directionParam = me.getDirectionParam(),
            hasGroups, index;
 
        if (pageParam && page) {
            params[pageParam] = page;
        }
 
        if (startParam && (start || start === 0)) {
            params[startParam] = start;
        }
 
        if (limitParam && limit) {
            params[limitParam] = limit;
        }
 
        hasGroups = groupParam && grouper;
        
        if (hasGroups) {
            // Grouper is a subclass of sorter, so we can just use the sorter method
            if (simpleGroupMode) {
                params[groupParam] = grouper.getProperty();
 
                // Allow for direction to be encoded into the same parameter
                if (groupDirectionParam === groupParam) {
                    params[groupParam] += ' ' + grouper.getDirection();
                }
                else {
                    params[groupDirectionParam] = grouper.getDirection();
                }
            }
            else {
                params[groupParam] = me.encodeSorters([grouper], true);
            }
        }
 
        /* eslint-disable max-len */
        if (sortParam && sorters && sorters.length > 0) {
            if (simpleSortMode) {
                // Group will be included in sorters, so skip sorter 0 if groups
                for (index = (sorters.length > 1 && hasGroups) ? 1 : 0; index < sorters.length; index++) {
                    // Allow for direction to be encoded into the same parameter
                    if (directionParam === sortParam) {
                        params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty() + ' ' + sorters[index].getDirection());
                    }
                    else {
                        params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty());
                        params[directionParam] = Ext.Array.push(params[directionParam] || [], sorters[index].getDirection());
                    }
                }
            }
            else {
                params[sortParam] = me.encodeSorters(sorters);
            }
        }
        /* eslint-enable max-len */
 
        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.raise("You are using a ServerProxy but have not supplied it with a url.");
        }
        //</debug>
 
        if (me.getNoCache()) {
            url = Ext.urlAppend(
                url,
                Ext.String.format("{0}={1}", me.getCacheString(), 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) {
        var url;
        
        if (request) {
            url = request.getUrl() || this.getApi()[request.getAction()];
        }
        
        return url ? url : this.callParent();
    },
 
    /**
     * In ServerProxy subclasses, the {@link #method-create}{@link #method-read},
     * {@link #method-update}, and {@link #method-erase} 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} operation The Ext.data.operation.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.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
     * @protected
     * @template
     * @method
     */
    afterRequest: Ext.emptyFn,
 
    destroy: function() {
        var me = this;
        
        me.destroying = true;
        
        // Don't force Reader and Writer creation if they weren't yet instantiated
        me.reader = me.writer = Ext.destroy(me.reader, me.writer);
        
        me.callParent();
        
        me.destroying = false;
        me.destroyed = true;
    }
});