/**
 * Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance
 * or a {@link Ext.data.Store Store} - often in response to an AJAX request. In general there is
 * usually no need to create a Reader instance directly, since a Reader is almost always used
 * together with a {@link Ext.data.proxy.Proxy Proxy}, and is configured using the Proxy's
 * {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
 * 
 *     Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'ajax',
 *             url: 'users.json',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'users'
 *             }
 *         },
 *     });
 *     
 * The above reader is configured to consume a JSON string that looks something like this:
 *  
 *     {
 *         "success": true,
 *         "users": [
 *             { "name": "User 1" },
 *             { "name": "User 2" }
 *         ]
 *     }
 * 
 *
 * # Loading Nested Data
 *
 * Readers have the ability to automatically load deeply-nested data objects based on the
 * {@link Ext.data.schema.Association associations} configured on each Model. Below is an example
 * demonstrating the flexibility of these associations in a fictional CRM system which manages
 * a User, their Orders, OrderItems and Products. First we'll define the models:
 *
 *     Ext.define("User", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'name'
 *         ],
 *
 *         hasMany: {model: 'Order', name: 'orders'},
 *
 *         proxy: {
 *             type: 'rest',
 *             url : 'users.json',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'users'
 *             }
 *         }
 *     });
 *
 *     Ext.define("Order", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'total'
 *         ],
 *
 *         hasMany  : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
 *         belongsTo: 'User'
 *     });
 *
 *     Ext.define("OrderItem", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'price', 'quantity', 'order_id', 'product_id'
 *         ],
 *
 *         belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
 *     });
 *
 *     Ext.define("Product", {
 *         extend: 'Ext.data.Model',
 *         fields: [
 *             'id', 'name'
 *         ],
 *
 *         hasMany: 'OrderItem'
 *     });
 *
 * This may be a lot to take in - basically a User has many Orders, each of which is composed of
 * several OrderItems. Finally, each OrderItem has a single Product. This allows us to consume data
 * like this:
 *
 *     {
 *         "users": [
 *             {
 *                 "id": 123,
 *                 "name": "Ed",
 *                 "orders": [
 *                     {
 *                         "id": 50,
 *                         "total": 100,
 *                         "order_items": [
 *                             {
 *                                 "id": 20,
 *                                 "price": 40,
 *                                 "quantity": 2,
 *                                 "product": {
 *                                     "id": 1000,
 *                                     "name": "MacBook Pro"
 *                                 }
 *                             },
 *                             {
 *                                 "id": 21,
 *                                 "price": 20,
 *                                 "quantity": 3,
 *                                 "product": {
 *                                     "id": 1001,
 *                                     "name": "iPhone"
 *                                 }
 *                             }
 *                         ]
 *                     }
 *                 ]
 *             }
 *         ]
 *     }
 *
 * The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's
 * sake), all of the Orders for each User (again just 1 in this case), all of the OrderItems
 * for each Order (2 order items in this case), and finally the Product associated with each
 * OrderItem. Now we can read the data and use it as follows:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: "User"
 *     });
 *
 *     store.load({
 *         callback: function() {
 *             // the user that was loaded
 *             var user = store.first();
 *
 *             console.log("Orders for " + user.get('name') + ":")
 *
 *             // iterate over the Orders for each User
 *             user.orders().each(function(order) {
 *                 console.log("Order ID: " + order.getId() + ", which contains items:");
 *
 *                 // iterate over the OrderItems for each Order
 *                 order.orderItems().each(function(orderItem) {
 *                     // we know that the Product data is already loaded,
 *                     // so we can use the synchronous getProduct
 *                     // usually, we would use the asynchronous version (see #belongsTo)
 *                     var product = orderItem.getProduct();
 *
 *                     console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
 *                 });
 *             });
 *         }
 *     });
 *
 * Running the code above results in the following:
 *
 *     Orders for Ed:
 *     Order ID: 50, which contains items:
 *     2 orders of MacBook Pro
 *     3 orders of iPhone
 */
Ext.define('Ext.data.reader.Reader', {
    alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
 
    requires: [
        'Ext.data.ResultSet',
        'Ext.XTemplate',
        'Ext.util.LruCache'
    ],
 
    mixins: [
        'Ext.mixin.Observable',
        'Ext.mixin.Factoryable'
    ],
 
    alias: 'reader.base',
    factoryConfig: {
        defaultType: null
    },
 
    config: {
        /**
         * @cfg {String/Function} [groupRootProperty]
         * Name of the property from which to retrieve remote grouping summary information.
         * There should be an item for each group.
         *
         * The remote summary data should be parseable as a {@link #model} used by this reader.
         */
        groupRootProperty: '',
 
        /**
        * @cfg {Boolean} [implicitIncludes]
        * True to automatically parse models nested within other models in a response object.
        * See the Ext.data.reader.Reader intro docs for full explanation.
        */
        implicitIncludes: true,
 
        /**
         * @cfg {Boolean} [keepRawData] Determines if the Reader will keep raw data
         * received from the server in the {@link #rawData} property.
         *
         * While this might seem useful to do additional data processing, keeping raw data
         * might cause adverse effects such as memory leaks. It is recommended to set
         * `keepRawData` to `false` if you do not need the raw data.
         *
         * If you need to process data packet to extract additional data such as row summaries,
         * it is recommended to use {@link #transform} function for that purpose.
         *
         * Note that starting with Ext JS 6.0 the default behavior has been changed to
         * **not** keep the raw data because of the high potential for memory leaks.
         * @since 5.1.1
         */
        keepRawData: null,
 
        /**
        * @cfg {String/Function} messageProperty
        * The name of the property which contains a response message for exception handling. If you
        * want to return a false success response from the server, maybe due to some server-side
        * validation, the messageProperty can hold the error message. For example:
        *
        *     {
        *         "success": false,
        *         "error": "There was an error with your request"
        *     }
        *
        * You can retrieve this error message in a callback when loading a
        * {@link Ext.data.Store Store} or {@link Ext.data.Model Model} like:
        *
        *     var store = new Ext.data.Store({
        *         fields : ['foo'],
        *         proxy  : {
        *             type   : 'ajax',
        *             url    : 'data.json',
        *             reader : {
        *                 type            : 'json',
        *                 rootProperty    : 'data',
        *                 messageProperty : 'error'
        *             }
        *         }
        *     });
        *
        *     store.load({
        *         callback: function(records, operation, success) {
        *             if (success) {
        *                 // ...
        *             } else {
        *                 var error = operation.getError();
        *
        *                 Ext.Msg.alert('Error', error);
        *             }
        *         }
        *     });
        *
        * In this example, the callback will execute with `success` being `false` and will
        * therefore show the {@link Ext.MessageBox#alert Ext.Msg.alert} with the error string
        * returned in the response.
        */
        messageProperty: '',
 
        /**
         * @cfg {String/Ext.data.Model} [model]
         * The model to use for this reader. This config is only required if the reader is being
         * used without a proxy, otherwise the proxy will automatically set the model.
         */
        model: null,
 
        /**
         * @cfg {Ext.data.proxy.Proxy} [proxy]
         * The proxy attached to this reader. Typically only needed onMetaChange so that
         * we can set the new model on the proxy.
         * @private
         */
        proxy: null,
    
        /**
        * @cfg {Boolean} [readRecordsOnFailure]
        * True to extract the records from a data packet even if the {@link #successProperty}
        * returns false.
        */
        readRecordsOnFailure: true,
 
        /**
         * @cfg {String/Function} rootProperty
         * The property that contains data items corresponding to the 
         * Model(s) of the configured Reader. `rootProperty` varies by Reader type.
         * 
         * ##JSON Reader 
         * `rootProperty` is a property name. It may also be a dot-separated 
         * list of property names if the root is nested. The root JSON array will be 
         * used by default.
         * 
         *     // rootPropety config
         *     rootProperty: 'embedded.myresults'
         *     
         *     // server response
         *     {
         *         embedded: {
         *             myresults: [{
         *                 name: 'Scott',
         *                 age: 22
         *             }, {
         *                 name: 'Ramona',
         *                 age: 24
         *             }]
         *         },
         *         success: true
         *     }
         * 
         * ##XML Reader 
         * `rootProperty` is a CSS selector. The root XML element will be used
         * by default.
         * 
         *     // rootProperty config (plus record config)
         *     rootProperty: 'myresults',
         *     record: 'user'
         *     
         *     // server response
         *     <?xml version="1.0" encoding="UTF-8"?>
         *     <embedded>
         *         <myresults>
         *             <user>
         *                 <name>Scott</name>
         *                 <age>22</age>
         *             </user>
         *             <user>
         *                 <name>Ramona</name>
         *                 <age>24</age>
         *             </user>
         *         </myresults>
         *     </embedded>
         * 
         * ##Array Reader 
         * `rootProperty` is not typically applicable since the data is assumed to be a
         * single-level array of arrays.  However, if the array of records is returned 
         * within a JSON response a `rootProperty` config may be used:
         * 
         *     // rootProperty config
         *     rootProperty: 'embedded.myresults'
         *     
         *     // server response
         *     {
         *         embedded: {
         *             myresults: [['Scott', 22], ['Ramona', 24]]
         *         },
         *         success: true
         *     }
         * 
         * ##rootProperty as a function
         * The `rootProperty` may also be a function that returns the root node from 
         * the dataset. For example:
         *
         *     var store = Ext.create('Ext.data.TreeStore', {
         *         proxy: {
         *             type: 'memory',
         *             reader: {
         *                 type: 'json',
         *                 rootProperty: function(data){
         *                     // Extract child nodes from the items or children property
         *                     // in the dataset
         *                     return data.items || data.children;
         *                 }
         *             }
         *         }, 
         *         data: {
         *             items: [{
         *                 text: 'item 1',
         *                 children: [{
         *                     text: 'child A',
         *                     leaf: true
         *                 }]
         *             }]
         *         }
         *     });
         *
         *     Ext.create('Ext.tree.Panel', {
         *         title: 'rootProperty as a function',
         *         width: 200,
         *         height:150,
         *         store: store,
         *         rootVisible: false,
         *         renderTo: Ext.getBody()
         *     });
         */
        rootProperty: '',
 
        /**
        * @cfg {String} [successProperty]
        * Name of the property from which to retrieve the `success` attribute, the value of which
        * indicates whether a given request succeeded or failed (typically a boolean or
        * 'true'|'false'). See
        * {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for
        * additional information.
        */
        successProperty: 'success',
 
        /**
         * @cfg {String/Function} [summaryRootProperty]
         * Name of the property from which to retrieve remote summary information.
         *
         * The remote summary data should be parseable as a {@link #model} used by this reader.
         */
        summaryRootProperty: '',
 
        /**
        * @cfg {String} [totalProperty]
        * Name of the property from which to retrieve the total number of records in the dataset.
        * This is only needed if the whole dataset is not passed in one go, but is being paged from
        * the remote server.
        */
        totalProperty: 'total',
 
        /**
         * @cfg {Function|String|Object} [transform]
         * If a transform function is set, it will be invoked just before {@link #readRecords}
         * executes. It is passed the raw (deserialized) data object. The transform function
         * returns a data object, which can be a modified version of the original data object,
         * or a completely new data object. The transform can be a function, or a method name
         * on the Reader instance, or an object with a 'fn' key and an optional 'scope' key.
         *
         * Example usage:
         *
         *     Ext.create('Ext.data.Store', {
         *         model: 'User',
         *         proxy: {
         *             type: 'ajax',
         *             url : 'users.json',
         *             reader: {
         *                 type: 'json',
         *                 transform: {
         *                     fn: function(data) {
         *                         // do some manipulation of the raw data object
         *                         return data;
         *                     },
         *                     scope: this
         *                 }
         *             }
         *         },
         *     });
         *
         */ 
        transform: null,
 
        /**
        * @cfg {String} [typeProperty]
        * The name of the property in a node raw data block which indicates the type of the model
        * to be created from that raw data. Useful for heterogeneous trees.
        *
        * For example, hierarchical geographical data may look like this:
        *
        *     {
        *         nodeType: 'Territory',
        *         name: 'EMEA',
        *         children: [{
        *             nodeType: 'Country',
        *             name: 'United Kingdon',
        *             children: [{
        *                 nodeType: 'City',
        *                 name: 'London'
        *             }]
        *         }]
        *     }
        *
        * You would configure the typeProperty in this case to be `"nodeType"` which would cause
        * the models named "Territory", "Country" and "City" to be used.
        */
        typeProperty: ''
    },
    
    /**
     * @property {Object} rawData
     * The raw data object that was last passed to {@link #readRecords}. rawData is populated 
     * based on the results of {@link Ext.data.proxy.Server#processResponse}. rawData will 
     * maintain a cached copy of the last successfully returned records. In other words, 
     * if processResponse is unsuccessful, the records from the last successful response 
     * will remain cached in rawData.
     *
     * Since Ext JS 5.1.1 you can use the {@link #keepRawData} config option to
     * control this behavior.
     */
    
    /**
     * @property {Object} metaData
     * The raw meta data that was most recently read, if any. Meta data can include existing
     * Reader config options like {@link #totalProperty}, etc. that get
     * automatically applied to the Reader, and those can still be accessed directly from the Reader
     * if needed. However, meta data is also often used to pass other custom data to be processed
     * by application code. For example, it is common when reconfiguring the data model of a grid to
     * also pass a corresponding column model config to be applied to the grid. Any such data will
     * not get applied to the Reader directly (it just gets passed through and is ignored by Ext).
     * This metaData property gives you access to all meta data that was passed, including any such
     * custom data ignored by the reader.
     * 
     * This is a read-only property, and it will get replaced each time a new meta data object is
     * passed to the reader. Note that typically you would handle proxy's
     * {@link Ext.data.proxy.Proxy#metachange metachange} event which passes this exact same meta
     * object to listeners. However this property is available if it's more convenient to access it
     * via the reader directly in certain cases.
     * @readonly
     */
    
    /**
     * @property {Boolean} isReader
     * `true` in this class to identify an object as an instantiated Reader, or subclass thereof.
     */
    isReader: true,
    
    /**
     * @event exception
     * Fires when the reader receives improperly encoded data from the server
     * @param {Ext.data.reader.Reader} reader A reference to this reader
     * @param {XMLHttpRequest} response The XMLHttpRequest response object
     * @param {Ext.data.ResultSet} error The error object
     */
 
    /**
     * Creates new Reader.
     * @param {Object} [config] Config object.
     */
    constructor: function(config) {
        var me = this;
        
        if (config && config.hasOwnProperty('root')) {
            config = Ext.apply({}, config);
            config.rootProperty = config.root;
            delete config.root;
            
            //<debug>
            Ext.log.error('Ext.data.reader.Reader: Using the deprecated "root" configuration. ' +
                          'Use "rootProperty" instead.');
            //</debug>
        }
 
        me.duringInit = 1;
        // Will call initConfig
        me.mixins.observable.constructor.call(me, config);
        --me.duringInit;
        
        me.buildExtractors();
    },
    
    forceBuildExtractors: function() {
        if (!this.duringInit) {
            this.buildExtractors(true);
        }
    },
 
    updateGroupRootProperty: function() {
        this.forceBuildExtractors();
    },
    
    updateMessageProperty: function() {
        this.forceBuildExtractors();
    },
 
    applyModel: function(model) {
        return Ext.data.schema.Schema.lookupEntity(model);
    },
 
    updateSuccessProperty: function() {
        this.forceBuildExtractors();
    },
    
    updateTotalProperty: function() {
        this.forceBuildExtractors();
    },
 
    applyTransform: function(transform) {
        if (transform) {
            if (Ext.isFunction(transform)) {
                transform = { fn: transform };
            }
            else if (transform.charAt) { // faster than Ext.isString()
                transform = { fn: this[transform] };
            }
            
            return transform.fn.bind(transform.scope || this);
        }
        
        return transform;
    },
 
    /**
     * Reads the given response object. This method normalizes the different types of response
     * object that may be passed to it. If it's an XMLHttpRequest object, hand off to the subclass'
     * {@link #getResponseData} method. Else, hand off the reading of records to the
     * {@link #readRecords} method.
     * @param {Object} response The response object. This may be either an XMLHttpRequest object or
     * a plain JS object
     * @param {Object} [readOptions] Various options that instruct the reader on how to read the
     * data
     * @param {Function} [readOptions.recordCreator] A function to construct the model based on the
     * processed data. By default, this just calls the model constructor and passes the raw data.
     * @return {Ext.data.ResultSet} The parsed or default ResultSet object
     */
    read: function(response, readOptions) {
        var data, result, responseText;
 
        if (response) {
            responseText = response.responseText;
            
            if (response.responseType || responseText) {
                result = this.getResponseData(response);
                
                if (result && result.__$isError) {
                    return new Ext.data.ResultSet({
                        total: 0,
                        count: 0,
                        records: [],
                        success: false,
                        message: result.msg
                    });
                }
                else {
                    data = this.readRecords(result, readOptions);
                }
            }
            else if (responseText !== '') {
                data = this.readRecords(response, readOptions);
            }
        }
 
        return data || this.nullResultSet;
    },
 
    /**
     * Returns the shared null result set.
     * @return {Ext.data.ResultSet} The null result set.
     * 
     * @private
     */
    getNullResultSet: function() {
        return this.nullResultSet;
    },
 
    /**
     * Creates an object that identifies a read error occurred.
     * @param {String} msg An error message to include
     * @return {Object} An error object
     * 
     * @private
     */
    createReadError: function(msg) {
        return {
            __$isError: true,
            msg: msg
        };
    },
 
    /**
     * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to
     * call this function before running its own logic and returning the Ext.data.ResultSet
     * instance. For most Readers additional processing should not be needed.
     * @param {Object} data The raw data object
     * @param {Object} [readOptions] See {@link #read} for details.
     * @param {Object} [internalReadOptions] (private)
     * @return {Ext.data.ResultSet} A ResultSet object
     */
    readRecords: function(data, readOptions, internalReadOptions) {
        var me = this,
            recordsOnly = internalReadOptions && internalReadOptions.recordsOnly,
            asRoot = internalReadOptions && internalReadOptions.asRoot,
            groupData = null,
            summaryData = null,
            success, recordCount, records, root, remoteTotal,
            total, value, message, transform, meta, summaryOptions;
 
        // Extract the metadata to return with the ResultSet.
        // If found reconfigure accordingly.
        // The calling Proxy fires its metachange event if it finds metadata in the ResultSet.
        meta = me.getMeta ? me.getMeta(data) : data.metaData;
        
        if (meta) {
            me.onMetaChange(meta);
        }
 
        transform = me.getTransform();
        
        if (transform) {
            data = transform(data);
        }
          
        me.buildExtractors();
        
        if (me.getKeepRawData()) {
            me.rawData = data;
        }
        
        if (me.hasListeners.rawdata) {
            me.fireEventArgs('rawdata', [data]);
        }
 
        data = me.getData(data);
        
        success = true;
        recordCount = 0;
        records = [];
            
        if (me.getSuccessProperty()) {
            value = me.getSuccess(data);
            
            if (value === false || value === 'false') {
                success = false;
            }
        }
        
        if (me.getMessageProperty()) {
            message = me.getMessage(data);
        }
 
        
        // Only try and extract other data if call was successful
        if (success || me.getReadRecordsOnFailure()) {
            // If we pass an array as the data, we don't use getRoot on the data.
            // Instead the root equals to the data.
            root = (asRoot || Ext.isArray(data)) ? data : me.getRoot(data);
 
            if (root) {
                total = root.length;
            }
 
            if (me.getTotalProperty()) {
                value = parseInt(me.getTotal(data), 10);
                
                if (!isNaN(value)) {
                    remoteTotal = total = value;
                }
            }
 
            if (root) {
                records = me.extractData(root, readOptions);
                recordCount = records.length;
            }
 
            if (me.getGroupRootProperty()) {
                root = me.getGroupRoot(data);
                
                if (root) {
                    summaryOptions = {
                        includes: false,
                        model: me.getModel().getSummaryModel()
                    };
                    
                    groupData = me.extractData(root, summaryOptions) || null;
                }
            }
 
            if (me.getSummaryRootProperty()) {
                root = me.getSummaryRoot(data);
                
                if (root) {
                    summaryOptions = summaryOptions || {
                        includes: false,
                        model: me.getModel().getSummaryModel()
                    };
                    
                    summaryData = me.extractData(root, summaryOptions) || null;
                    
                    // This always returns an array, so transform it
                    if (summaryData) {
                        summaryData = summaryData[0];
                    }
                }
            }
        }
 
        /* eslint-disable-next-line multiline-ternary */
        return recordsOnly ? records : new Ext.data.ResultSet({
            total: total || recordCount,
            remoteTotal: remoteTotal,
            metadata: meta,
            count: recordCount,
            records: records,
            success: success,
            message: message,
            groupData: groupData,
            summaryData: summaryData
        });
    },
 
    /**
     * Returns extracted, type-cast rows of data.
     * @param {Object[]/Object} root from server response
     * @param {Object} [readOptions] An object containing extra options.
     * @param {Function} [readOptions.model] The Model constructor to use.
     * @param {Function} [readOptions.recordCreator] A function to use to create and initialize
     * records. By default a function is supplied which creates *non-phantom* records on the
     * assumnption that a Reader is going to be used to read server-supplied data.
     * @param {Object} [readOptions.recordCreator.data] The raw data used to create a record.
     * @param {Function} [readOptions.recordCreator.Model] The Model constructor to use to create
     * the record.
     * @return {Array} An array of records containing the extracted data
     * @private
     */
    extractData: function(root, readOptions) {
        var me = this,
            entityType = readOptions && readOptions.model ? Ext.data.schema.Schema.lookupEntity(readOptions.model) : me.getModel(), // eslint-disable-line max-len
            schema = entityType.schema,
            includes = readOptions && 'includes' in readOptions ? readOptions.includes : schema.hasAssociations(entityType) && me.getImplicitIncludes(), // eslint-disable-line max-len
            fieldExtractorInfo = me.getFieldExtractorInfo(entityType),
            length = root.length,
            records = new Array(length),
            typeProperty = me.getTypeProperty(),
            reader, node, nodeType, record, i;
            
        if (!length && Ext.isObject(root)) {
            root = [root];
            length = 1;
        }
 
        for (= 0; i < length; i++) {
            record = root[i];
            
            if (!record.isModel) {
                // If we're given a model instance in the data, just push it on
                // without doing any conversion. Otherwise, create a record.
                node = record;
 
                // This Reader may be configured to produce different model types based on
                // a differentiator field in the incoming data:
                // typeProperty name be a string, a function which yields the child type, or an
                // object: {
                //     name: 'mtype',
                //     namespace: 'MyApp'
                // }
                if (typeProperty && (nodeType = me.getChildType(schema, node, typeProperty))) {
 
                    reader = nodeType.getProxy().getReader();
 
                    record = reader.extractRecord(
                        node, readOptions, nodeType,
                        schema.hasAssociations(nodeType) && reader.getImplicitIncludes(),
                        reader.getFieldExtractorInfo(nodeType)
                    );
                }
                else {
                    record = me.extractRecord(node, readOptions, entityType, includes,
                                              fieldExtractorInfo);
                }
                
                // Generally we don't want to have references to XML documents
                // or XML nodes to hang around in memory but Trees need to be able
                // to access the raw XML node data in order to process its children.
                // See https://sencha.jira.com/browse/EXTJS-15785 and
                // https://sencha.jira.com/browse/EXTJS-14286
                if (record.isModel && record.isNode) {
                    record.raw = node;
                }
            }
            
            if (record.onLoad) {
                record.onLoad();
            }
            
            records[i] = record;
        }
 
        return records;
    },
 
    // Based upon a Reader's typeProperty config, determine the type of child node to create from
    // the raw data
    getChildType: function(schema, rawNode, typeProperty) {
        var namespace;
 
        switch (typeof typeProperty) {
            case 'string':
                return schema.getEntity(rawNode[typeProperty]);
            
            case 'object':
                namespace = typeProperty.namespace;
                
                return schema.getEntity((namespace ? namespace + '.' : '') +
                       rawNode[typeProperty.name]);
            
            case 'function':
                return schema.getEntity(typeProperty(rawNode));
        }
    },
 
    extractRecordData: function(node, readOptions) {
        /* eslint-disable-next-line max-len */
        var entityType = readOptions && readOptions.model ? Ext.data.schema.Schema.lookupEntity(readOptions.model) : this.getModel(),
            fieldExtractorInfo = this.getFieldExtractorInfo(entityType);
 
        return this.extractRecord(node, readOptions, entityType, false, fieldExtractorInfo);
    },
 
    extractRecord: function(node, readOptions, entityType, includes, fieldExtractorInfo) {
        var me = this,
            creatorFn = (readOptions && readOptions.recordCreator) || me.defaultRecordCreator,
            modelData, record;
            
        // Create a record with an empty data object.
        // Populate that data object by extracting and converting field values from raw data.
        // Must pass the ID to use because we pass no data for the constructor to pluck an ID from
        modelData = me.extractModelData(node, fieldExtractorInfo);
        record = creatorFn.call(me, modelData, entityType || me.getModel(), readOptions);
        
        if (includes && record.isModel) {
            me.readAssociated(record, node, readOptions);
        }
        
        return record;
    },
    
    getFieldExtractorInfo: function(entityType) {
        var extractors = entityType.fieldExtractors,
            type, extractor;
        
        // If the base Ext.data.Model class is being used, there will be no extractor info
        // The raw data block will be imported unchanged.
        if (!extractors) {
            return;
        }
 
        type = this.$className;
        extractor = extractors[type];
            
        // If we have no extractors, buildFieldExtractors will return null,
        // so we never need to rebuild them
        if (extractor === undefined) {
            extractors[type] = extractor = this.buildFieldExtractors(entityType);
        }
        
        return extractor;
    },
    
    buildFieldExtractors: function(entityType) {
        var fields = entityType.getFields(),
            len = fields.length,
            buffer = [],
            extractors = [],
            out = null,
            cnt = 0,
            field, name, i, extractor;
        
        for (= 0; i < len; ++i) {
            field = fields[i];
            extractor = this.createFieldAccessor(field);
            
            if (extractor) {
                name = field.name;
                // Use [] property access since we may have non-JS looking field names
                buffer.push('val = extractors[' + cnt +
                            '](raw, self); if (val !== undefined) { data[\'' + name +
                            '\'] = val; }');
                extractors.push(extractor);
                ++cnt;
            }
        }
        
        if (buffer.length) {
            out = {
                extractors: extractors,
                fn: new Function(
                    'raw', 'data', 'extractors', 'self', 'var val;' + buffer.join('\n')
                )
            };
        }
        
        return out;
    },
    
    defaultRecordCreator: function(data, Model) {
        return new Model(data);
    },
 
    defaultRecordCreatorFromServer: function(data, Model) {
        var record = new Model(data);
        
        // If the server did not include an id in the response data, the Model constructor will
        // mark the record as phantom. We need to set phantom to false here because records created
        // from a server response using a reader by definition are not phantom records.
        record.phantom = false;
        
        return record;
    },
    
    getModelData: function(raw) {
        return {};
    },
    
    extractModelData: function(raw, fieldExtractorInfo) {
        var data = this.getModelData(raw),
            fn;
            
        // We may not have any mappings to process
        if (fieldExtractorInfo) {
            fn = fieldExtractorInfo.fn;
            fn(raw, data, fieldExtractorInfo.extractors, this);
        }
        
        return data;
    },
 
    /**
     * Loads the record associations from the data object.
     * @param {Ext.data.Model} record The record to load associations for.
     * @param {Object} data The raw data object.
     * @param {Object} readOptions See {@link #read}.
     *
     * @private
     */
    readAssociated: function(record, data, readOptions) {
        var roles = record.associations,
            key, role;
            
        for (key in roles) {
            if (roles.hasOwnProperty(key)) {
                role = roles[key];
                
                // The class for the other role may not have loaded yet
                if (role.cls) {
                    role.read(record, data, this, readOptions);
                }
            }
        }
    },
 
    /**
     * @method
     * This method provides a hook to do any data transformation before the reading process
     * begins. By default this function just returns what is passed to it. It can be
     * overridden in a subclass to return something else.
     * See {@link Ext.data.reader.Xml XmlReader} for an example.
     * 
     * @param {Object} data The data object
     * @return {Object} The normalized data object
     * @protected
     * @template
     */
    getData: Ext.identityFn,
 
    /**
     * @method
     * This will usually need to be implemented in a subclass. Given a generic data object
     * (the type depends on the type of data we are reading), this function should return the
     * object as configured by the Reader's 'root' meta data config. See XmlReader's getRoot
     * implementation for an example. By default the same data object will simply be returned.
     *
     * @param {Object} data The data object
     * @return {Object} The same data object
     * @private
     */
    getRoot: Ext.identityFn,
 
    /**
     * Takes a raw response object (as passed to the {@link #read} method) and returns the useful
     * data segment from it. This must be implemented by each subclass.
     * @param {Object} response The response object
     * @return {Object} The extracted data from the response. For example, a JSON object or an XML
     * document.
     */
    getResponseData: function(response) {
        //<debug>
        Ext.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass");
        //</debug>
    },
 
    /**
     * @private
     * Reconfigures the meta data tied to this Reader
     */
    onMetaChange: function(meta) {
        var me = this,
            fields = meta.fields,
            model, newModel, clientIdProperty, proxy;
        
        // save off the raw meta data
        me.metaData = meta;
        
        // set any reader-specific configs from meta if available
        if (meta.root) {
            me.setRootProperty(meta.root);
        }
        
        if (meta.totalProperty) {
            me.setTotalProperty(meta.totalProperty);
        }
        
        if (meta.successProperty) {
            me.setSuccessProperty(meta.successProperty);
        }
        
        if (meta.messageProperty) {
            me.setMessageProperty(meta.messageProperty);
        }
 
        clientIdProperty = meta.clientIdProperty;
        
        if (fields) {
            newModel = Ext.define(null, {
                extend: 'Ext.data.Model',
                fields: fields,
                clientIdProperty: clientIdProperty
            });
            
            me.setModel(newModel);
            proxy = me.getProxy();
            
            if (proxy) {
                proxy.setModel(newModel);
            }
        }
        else if (clientIdProperty) {
            model = me.getModel();
            
            if (model) {
                model.self.prototype.clientIdProperty = clientIdProperty;
            }
        }
    },
 
    /**
     * @private
     * This builds optimized functions for retrieving record data and meta data from an object.
     * Subclasses may need to implement their own getRoot function.
     * @param {Boolean} [force=false] True to automatically remove existing extractor functions
     * first
     */
    buildExtractors: function(force) {
        var me = this,
            totalProp, successProp, messageProp;
            
        if (force || !me.hasExtractors) {
            totalProp = me.getTotalProperty();
            successProp = me.getSuccessProperty();
            messageProp = me.getMessageProperty();
 
            // build the extractors for all the meta data
            if (totalProp) {
                me.getTotal = me.getAccessor(totalProp);
            }
 
            if (successProp) {
                me.getSuccess = me.getAccessor(successProp);
            }
 
            if (messageProp) {
                me.getMessage = me.getAccessor(messageProp);
            }
            
            me.hasExtractors = true;
            
            return true;
        }
    },
 
    getAccessor: function(prop) {
        var me = this,
            cache = me.extractorCache,
            ret, key;
 
        if (typeof prop === 'string') {
            key = me.getAccessorKey(prop);
            
            if (key) {
                ret = cache.get(key);
            }
            
            if (!ret) {
                ret = me.createAccessor(prop);
                
                if (key) {
                    cache.add(key, ret);
                }
            }
        }
        else {
            ret = me.createAccessor(prop);
        }
        
        return ret;
    },
 
    getAccessorKey: function(prop) {
        var className = this.$className;
        
        return className ? className + prop : '';
    },
    
    createAccessor: Ext.emptyFn,
    
    createFieldAccessor: Ext.emptyFn,
 
    destroy: function() {
        var me = this;
 
        me.model = me.getTotal = me.getSuccess = me.getMessage = me.rawData = null;
        
        // Proxy could have created a sequence
        me.onMetaChange = null;
        
        // Transform function can be bound
        me.transform = null;
        
        me.callParent();
    },
 
    privates: {
        copyFrom: function(reader) {
            var me = this;
 
            reader.buildExtractors();
            me.getTotal = reader.getTotal;
            me.getSuccess = reader.getSuccess;
            me.getMessage = reader.getMessage;
            ++me.duringInit;
            me.setConfig(reader.getConfig());
            --me.duringInit;
            me.hasExtractors = true;
        },
 
        getGroupRoot: Ext.privateFn,
 
        getSummaryRoot: Ext.privateFn
    }
}, function(Cls) {
    var proto = Cls.prototype;
    
    Ext.apply(proto, {
        // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
        nullResultSet: new Ext.data.ResultSet({
            total: 0,
            count: 0,
            records: [],
            success: true,
            message: ''
        })
    });
 
    proto.extractorCache = new Ext.util.LruCache();
});