/**
 * @author Ed Spencer
 *
 * 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',
 *                 root: '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.association.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',
 *                 root: '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 {@link Ext.data.association.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', {
    requires: ['Ext.data.ResultSet'],
    alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],

    mixins: {
        observable: 'Ext.util.Observable'
    },

    /**
     * @cfg {String} idProperty
     * Name of the property within a row object that contains a record identifier value. Defaults to the id of the
     * model. If an idProperty is explicitly specified it will override the idProperty defined on the model.
     */

    /**
     * @cfg {String} [totalProperty="total"]
     * 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 {String} [successProperty="success"]
     * 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} root
     * The name of the property which contains the data items corresponding to the Model(s) for which this
     * Reader is configured.  For JSON reader it's a property name (or a dot-separated list of property names
     * if the root is nested).  For XML reader it's a CSS selector.  For Array reader the root is not applicable
     * since the data is assumed to be a single-level array of arrays.
     * 
     * By default the natural root of the data will be used: the root JSON array, the root XML element, or the array.
     *
     * The data packet value for this property should be an empty array to clear the data or show no data.
     */
    root: '',
    
    /**
     * @cfg {String} messageProperty
     * The name of the property which contains a response message. This property is optional.
     */
    
    /**
     * @cfg {Boolean} [implicitIncludes=true]
     * 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} [readRecordsOnFailure=true]
     * True to extract the records from a data packet even if the {@link #success} property returns false.
     */
    readRecordsOnFailure: true,
    
    /**
     * @property {Object} metaData
     * The raw meta data that was most recently read, if any. Meta data can include existing
     * Reader config options like {@link #idProperty}, {@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,

    // Private flag to the generated convertRecordData function to indicate whether to apply Field default
    // values to fields for which no value is present in the raw data.
    // This is set to false by a Server Proxy which is reading the response from a "create" or "update" operation.
    applyDefaults: true,
    
    /**
     * Creates new Reader.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        var me = this;
        
        me.mixins.observable.constructor.call(me, config);
        me.fieldCount = 0;
        me.model = Ext.ModelManager.getModel(me.model);

        // Extractors can only be calculated if the fields MixedCollection has been set.
        // A Model may only complete its setup (set the prototype properties) after asynchronous loading
        // which would mean that there may be no "fields"
        // If this happens, the load callback will call proxy.setModel which calls reader.setModel which
        // triggers buildExtractors.
        if (me.model && me.model.prototype.fields) {
            me.buildExtractors();
        }

        this.addEvents(
            /**
             * @event
             * 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
             */
            'exception'
        );
    },

    /**
     * Sets a new model for the reader.
     * @private
     * @param {Object} model The model to set.
     * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured
     */
    setModel: function(model, setOnProxy) {
        var me = this;
        
        me.model = Ext.ModelManager.getModel(model);
        me.buildExtractors(true);
        
        if (setOnProxy && me.proxy) {
            me.proxy.setModel(me.model, true);
        }
    },

    /**
     * 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
     * @return {Ext.data.ResultSet} The parsed or default ResultSet object
     */
    read: function(response) {
        var data;

        if (response) {
            data = response.responseText ? this.getResponseData(response) : this.readRecords(response);
        }

        return data || this.nullResultSet;
    },

    /**
     * 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
     * @return {Ext.data.ResultSet} A ResultSet object
     */
    readRecords: function(data) {
        var me = this,
            success,
            recordCount,
            records,
            root,
            total,
            value,
            message;
        
        /*
         * We check here whether the number of fields has changed since the last read.
         * This works around an issue when a Model is used for both a Tree and another
         * source, because the tree decorates the model with extra fields and it causes
         * issues because the readers aren't notified.
         */
        if (me.fieldCount !== me.getFields().length) {
            me.buildExtractors(true);
        }
        
        /**
         * @property {Object} rawData
         * The raw data object that was last passed to {@link #readRecords}. Stored for further processing if needed.
         */
        me.rawData = data;

        data = me.getData(data);
        
        success = true;
        recordCount = 0;
        records = [];
            
        if (me.successProperty) {
            value = me.getSuccess(data);
            if (value === false || value === 'false') {
                success = false;
            }
        }
        
        if (me.messageProperty) {
            message = me.getMessage(data);
        }

        
        // Only try and extract other data if call was successful
        if (me.readRecordsOnFailure || success) {
            // If we pass an array as the data, we dont use getRoot on the data.
            // Instead the root equals to the data.
            root = Ext.isArray(data) ? data : me.getRoot(data);
            
            if (root) {
                total = root.length;
            }

          if (me.totalProperty) {
                value = parseInt(me.getTotal(data), 10);
                if (!isNaN(value)) {
                    total = value;
                }
            }

           if (root) {
                records = me.extractData(root);
                recordCount = records.length;
            }
        }

        return new Ext.data.ResultSet({
            total  : total || recordCount,
            count  : recordCount,
            records: records,
            success: success,
            message: message
        });
    },

    /**
     * Returns extracted, type-cast rows of data.
     * @param {Object[]/Object} root from server response
     * @return {Array} An array of records containing the extracted data
     * @private
     */
    extractData : function(root) {
        var me = this,
            records = [],
            Model   = me.model,
            length  = root.length,
            convertedValues, node, record, i;
            
        if (!root.length && Ext.isObject(root)) {
            root = [root];
            length = 1;
        }

        for (i = 0; i < length; i++) {
            node = root[i];
            // Create a record with an empty data object.
            // Populate that data object by extracting and converting field values from raw data
            record = new Model(undefined, me.getId(node), node, convertedValues = {});

            // 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;

            // Use generated function to extract all fields at once
            me.convertRecordData(convertedValues, node, record);

            records.push(record);
                
            if (me.implicitIncludes) {
                me.readAssociated(record, node);
            }
        }

        return records;
    },
    
    /**
     * @private
     * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations
     * on the record provided.
     * @param {Ext.data.Model} record The record to load associations for
     * @param {Object} data The data object
     * @return {String} Return value description
     */
    readAssociated: function(record, data) {
        var associations = record.associations.items,
            i            = 0,
            length       = associations.length,
            association, associationData, proxy, reader;
        
        for (; i < length; i++) {
            association     = associations[i];
            associationData = this.getAssociatedDataRoot(data, association.associationKey || association.name);
            
            if (associationData) {
                reader = association.getReader();
                if (!reader) {
                    proxy = association.associatedModel.proxy;
                    // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
                    if (proxy) {
                        reader = proxy.getReader();
                    } else {
                        reader = new this.constructor({
                            model: association.associatedName
                        });
                    }
                }
                association.read(record, reader, associationData);
            }  
        }
    },
    
    /**
     * @private
     * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific
     * record, this should return the relevant part of that data for the given association name. This is only really
     * needed to support the XML Reader, which has to do a query to get the associated data object
     * @param {Object} data The raw data object
     * @param {String} associationName The name of the association to get data for (uses associationKey if present)
     * @return {Object} The root
     */
    getAssociatedDataRoot: function(data, associationName) {
        return data[associationName];
    },
    
    getFields: function() {
        return this.model.prototype.fields.items;
    },

    /**
     * @private
     * By default this function just returns what is passed to it. It can be overridden in a subclass
     * to return something else. See XmlReader for an example.
     * @param {Object} data The data object
     * @return {Object} The normalized data object
     */
    getData: function(data) {
        return data;
    },

    /**
     * @private
     * 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
     */
    getRoot: function(data) {
        return data;
    },

    /**
     * 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 {Ext.data.ResultSet} A ResultSet object
     */
    getResponseData: function(response) {
        //<debug>
        Ext.Error.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 || me.getFields(),
            newModel,
            clientIdProperty;
        
        // save off the raw meta data
        me.metaData = meta;
        
        // set any reader-specific configs from meta if available
        me.root = meta.root || me.root;
        me.idProperty = meta.idProperty || me.idProperty;
        me.totalProperty = meta.totalProperty || me.totalProperty;
        me.successProperty = meta.successProperty || me.successProperty;
        me.messageProperty = meta.messageProperty || me.messageProperty;
        clientIdProperty = meta.clientIdProperty;

        if (me.model) {
            me.model.setFields(fields, me.idProperty, clientIdProperty);
            me.setModel(me.model, true);
        }
        else {
            newModel = Ext.define("Ext.data.reader.Json-Model" + Ext.id(), {
                extend: 'Ext.data.Model',
                fields: fields,
                clientIdProperty: clientIdProperty
            });
            if (me.idProperty) {
                // We only do this if the reader actually has a custom idProperty set,
                // otherwise let the model use its own default value. It is valid for
                // the reader idProperty to be undefined, in which case it will use the
                // model's idProperty (in getIdProperty()).
                newModel.idProperty = me.idProperty;
            }
            me.setModel(newModel, true);
        }
    },
    
    /**
     * Get the idProperty to use for extracting data
     * @private
     * @return {String} The id property
     */
    getIdProperty: function(){
        return this.idProperty || this.model.prototype.idProperty;
    },

    /**
     * @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,
            idProp      = me.getIdProperty(),
            totalProp   = me.totalProperty,
            successProp = me.successProperty,
            messageProp = me.messageProperty,
            accessor,
            idField,
            map;
            
        if (force === true) {
            delete me.convertRecordData;
        }
        
        if (me.convertRecordData) {
            return;
        }   

        //build the extractors for all the meta data
        if (totalProp) {
            me.getTotal = me.createAccessor(totalProp);
        }

        if (successProp) {
            me.getSuccess = me.createAccessor(successProp);
        }

        if (messageProp) {
            me.getMessage = me.createAccessor(messageProp);
        }

        if (idProp) {
            idField = me.model.prototype.fields.get(idProp);
            if (idField) {
                map = idField.mapping;
                idProp = (map !== undefined && map !== null) ? map : idProp;
            }
            accessor = me.createAccessor(idProp);

            me.getId = function(record) {
                var id = accessor.call(me, record);
                return (id === undefined || id === '') ? null : id;
            };
        } else {
            me.getId = function() {
                return null;
            };
        }
        me.convertRecordData = me.buildRecordDataExtractor();
    },

    /**
     * @private
     * Return a function which will read a raw row object in the format this Reader accepts, and populates
     * a record's data object with converted data values.
     *
     * The returned function must be passed the following parameters:
     *
     * - dest A record's empty data object into which the new field value properties are injected.
     * - source A raw row data object of whatever type this Reader consumes
     * - record The record which is being populated.
     *
     */
    buildRecordDataExtractor: function() {
        var me = this,
            modelProto = me.model.prototype,
            clientIdProp = modelProto.clientIdProperty,
            fields = modelProto.fields.items,
            numFields = fields.length,
            fieldVarName = [],
            varName,
            i = 0,
            field,
            code = [
                'var me = this,\n',
                '    fields = me.model.prototype.fields,\n',
                '    value,\n',
                '    internalId'
            ];

        for (i = 0; i < numFields; i++) {
            field = fields[i];
            fieldVarName[i] = '__field' + i;
            code.push(',\n    ', fieldVarName[i], ' = fields.get("', field.name, '")');
        }
        code.push(';\n\n    return function(dest, source, record) {\n');

        for (i = 0; i < numFields; i++) {
            field = fields[i];
            varName = fieldVarName[i];
            // createFieldAccessExpression must be implemented in subclasses to extract data from the source object in the correct way.
            code.push('        value = ', me.createFieldAccessExpression(field, varName, 'source'), ';\n');

            // Code for processing a source property value when there is a default value.
            if (field.defaultValue !== undefined) {
                code.push('        if (value === undefined) {\n');
                code.push('            if (me.applyDefaults) {\n');
                code.push('                dest["' + field.name + '"] = ', (field.convert ? fieldVarName[i] + '.convert(' + fieldVarName[i] + '.defaultValue, record)' : (fieldVarName[i] + '.defaultValue')), ';\n');
                code.push('            }\n');
                code.push('        } else {\n');
                code.push('            dest["' + field.name + '"] = ', (field.convert ? fieldVarName[i] + '.convert(value, record)' : 'value'), ';\n');
                code.push('        }\n');
            }
            
            // Code for processing a source property value when there is no default value.
            else {
                code.push('        if (value !== undefined) {\n');
                code.push('            dest["' + field.name + '"] = ', (field.convert ? fieldVarName[i] + '.convert(value, record)' : 'value'), ';\n');
                code.push('        }\n');
            }
        }

        // set the client id as the internalId of the record.
        // clientId handles the case where a client side record did not previously exist on the server,
        // so the server is passing back a client id that can be used to pair the server side record up with the client record
        if (clientIdProp) {
            code.push('        if (record && (internalId = ' + me.createFieldAccessExpression({mapping: clientIdProp}, null, 'source') + ')) {\n');
            code.push('            record.internalId = internalId;\n        }\n');
        }

        code.push('    };');

        // Here we are creating a new Function and invoking it immediately in the scope of this Reader
        // It declares several vars capturing the configured context of this Reader, and returns a function
        // which, when passed a record data object, a raw data row in the format this Reader is configured to read,
        // and the record which is being created, will populate the record's data object from the raw row data.
        return Ext.functionFactory(code.join('')).call(me);
    },

    destroyReader: function() {
        var me = this;
        delete me.proxy;
        delete me.model;
        delete me.convertRecordData;
        delete me.getId;
        delete me.getTotal;
        delete me.getSuccess;
        delete me.getMessage;
    }
}, function() {
    Ext.apply(this.prototype, {
        // 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
        })
    });
});
