/**
 * @author Ed Spencer
 * @aside guide models
 *
 * A Model represents some object that your application manages. For example, one might define a Model for Users,
 * Products, Cars, or any other real-world object that we want to model in the system. Models are registered via the
 * {@link Ext.data.ModelManager model manager}, and are used by {@link Ext.data.Store stores}, which are in turn used by many
 * of the data-bound components in Ext.
 *
 * Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: [
 *                 {name: 'name',  type: 'string'},
 *                 {name: 'age',   type: 'int'},
 *                 {name: 'phone', type: 'string'},
 *                 {name: 'alive', type: 'boolean', defaultValue: true}
 *             ]
 *         },
 *
 *         changeName: function() {
 *             var oldName = this.get('name'),
 *                 newName = oldName + " The Barbarian";
 *
 *             this.set('name', newName);
 *         }
 *     });
 *
 * The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link
 * Ext.data.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype.
 *
 * Now we can create instances of our User model and call any model logic we defined:
 *
 *     var user = Ext.create('User', {
 *         name : 'Conan',
 *         age  : 24,
 *         phone: '555-555-5555'
 *     });
 *
 *     user.changeName();
 *     user.get('name'); //returns "Conan The Barbarian"
 *
 * # Validations
 *
 * Models have built-in support for validations, which are executed against the validator functions in {@link
 * Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to
 * models:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: [
 *                 {name: 'name',     type: 'string'},
 *                 {name: 'age',      type: 'int'},
 *                 {name: 'phone',    type: 'string'},
 *                 {name: 'gender',   type: 'string'},
 *                 {name: 'username', type: 'string'},
 *                 {name: 'alive',    type: 'boolean', defaultValue: true}
 *             ],
 *
 *             validations: [
 *                 {type: 'presence',  field: 'age'},
 *                 {type: 'length',    field: 'name',     min: 2},
 *                 {type: 'inclusion', field: 'gender',   list: ['Male', 'Female']},
 *                 {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
 *                 {type: 'format',    field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
 *             ]
 *         }
 *     });
 *
 * The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors}
 * object:
 *
 *     var instance = Ext.create('User', {
 *         name: 'Ed',
 *         gender: 'Male',
 *         username: 'edspencer'
 *     });
 *
 *     var errors = instance.validate();
 *
 * # Associations
 *
 * Models can have associations with other Models via {@link Ext.data.association.HasOne},
 * {@link Ext.data.association.BelongsTo belongsTo} and {@link Ext.data.association.HasMany hasMany} associations.
 * For example, let's say we're writing a blog administration application which deals with Users, Posts and Comments.
 * We can express the relationships between these models like this:
 *
 *     Ext.define('Post', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: ['id', 'user_id'],
 *             belongsTo: 'User',
 *             hasMany  : {model: 'Comment', name: 'comments'}
 *         }
 *     });
 *
 *     Ext.define('Comment', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: ['id', 'user_id', 'post_id'],
 *             belongsTo: 'Post'
 *         }
 *     });
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: ['id'],
 *             hasMany: [
 *                 'Post',
 *                 {model: 'Comment', name: 'comments'}
 *             ]
 *         }
 *     });
 *
 * See the docs for {@link Ext.data.association.HasOne}, {@link Ext.data.association.BelongsTo} and
 * {@link Ext.data.association.HasMany} for details on the usage and configuration of associations.
 * Note that associations can also be specified like this:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: ['id'],
 *             associations: [
 *                 {type: 'hasMany', model: 'Post',    name: 'posts'},
 *                 {type: 'hasMany', model: 'Comment', name: 'comments'}
 *             ]
 *         }
 *     });
 *
 * # Using a Proxy
 *
 * Models are great for representing types of data and relationships, but sooner or later we're going to want to load or
 * save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which
 * can be set directly on the Model:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *
 *         config: {
 *             fields: ['id', 'name', 'email'],
 *             proxy: {
 *                 type: 'rest',
 *                 url : '/users'
 *             }
 *         }
 *     });
 *
 * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a
 * RESTful backend. Let's see how this works:
 *
 *     var user = Ext.create('User', {name: 'Ed Spencer', email: '[email protected]'});
 *
 *     user.save(); //POST /users
 *
 * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's
 * data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id,
 * and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We
 * configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
 *
 * Loading data via the Proxy is equally easy:
 *
 *     //get a reference to the User model class
 *     var User = Ext.ModelManager.getModel('User');
 *
 *     //Uses the configured RestProxy to make a GET request to /users/123
 *     User.load(123, {
 *         success: function(user) {
 *             console.log(user.getId()); //logs 123
 *         }
 *     });
 *
 * Models can also be updated and destroyed easily:
 *
 *     //the user Model we loaded in the last snippet:
 *     user.set('name', 'Edward Spencer');
 *
 *     //tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id
 *     user.save({
 *         success: function() {
 *             console.log('The User was updated');
 *         }
 *     });
 *
 *     //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
 *     user.erase({
 *         success: function() {
 *             console.log('The User was destroyed!');
 *         }
 *     });
 *
 * # Usage in Stores
 *
 * It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this by
 * creating a {@link Ext.data.Store Store}:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User'
 *     });
 *
 *     //uses the Proxy we set up on Model to load the Store data
 *     store.load();
 *
 * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a
 * set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link
 * Ext.data.Store Store docs} for more information on Stores.
 *
 * @constructor
 * Creates new Model instance.
 * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values
 * @param {Number} id (optional) Unique ID to assign to this model instance
 */
Ext.define('Ext.data.Model', {
    alternateClassName: 'Ext.data.Record',

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

    /**
     * Provides an easy way to quickly determine if a given class is a Model
     * @property isModel
     * @type Boolean
     * @private
     */
    isModel: true,

    requires: [
        'Ext.util.Collection',
        'Ext.data.Field',
        'Ext.data.identifier.Simple',
        'Ext.data.ModelManager',
        'Ext.data.proxy.Ajax',
        'Ext.data.association.HasMany',
        'Ext.data.association.BelongsTo',
        'Ext.data.association.HasOne',
        'Ext.data.Errors'
    ],

    config: {
        /**
         * @cfg {String} idProperty
         * The name of the field treated as this Model's unique id. Defaults to 'id'. Note that this field
         * needs to have a type of 'auto'. Setting the field type to anything else will be undone by the
         * framework. This is because new records that are created without an id, will have one generated.
         * This generated id is a string.
         */
        idProperty: 'id',

        data: null,

        /**
         * @cfg {Object[]/String[]} fields
         * The field definitions for all instances of this Model. **Note:** this does not set the *values* of each
         * field on an instance, it sets the collection of fields itself. Sample usage:
         *
         *     Ext.define('MyApp.model.User', {
         *         extend: 'Ext.data.Model',
         *
         *         config: {
         *             fields: [
         *                 'id',
         *                 {name: 'age', type: 'int'},
         *                 {name: 'taxRate', type: 'float'}
         *             ]
         *         }
         *     });
         * @accessor
         */
        fields: undefined,

        /**
         * @cfg {Object[]} validations
         * An array of {@link Ext.data.Validations validations} for this model.
         */
        validations: null,

        /**
         * @cfg {Object[]} associations
         * An array of {@link Ext.data.association.Association associations} for this model.
         */
        associations: null,

        /**
         * @cfg {String/Object/String[]/Object[]} hasMany
         * One or more {@link Ext.data.association.HasMany HasMany associations} for this model.
         */
        hasMany: null,

        /**
         * @cfg {String/Object/String[]/Object[]} hasOne
         * One or more {@link Ext.data.association.HasOne HasOne associations} for this model.
         */
        hasOne: null,

        /**
         * @cfg {String/Object/String[]/Object[]} belongsTo
         * One or more {@link Ext.data.association.BelongsTo BelongsTo associations} for this model.
         */
        belongsTo: null,

        /**
         * @cfg {Object/Ext.data.Proxy} proxy
         * The string type of the default Model Proxy. Defaults to null.
         * @accessor
         */
        proxy: null,


        /**
         * @cfg {Object/String} identifier
         * The identifier strategy used when creating new instances of this Model that don't have an id defined.
         * By default this uses the simple identifier strategy that generates id's like 'ext-record-12'. If you are
         * saving these records in localstorage using a LocalStorage proxy you need to ensure that this identifier
         * strategy is set to something that always generates unique id's. We provide one strategy by default that
         * generates these unique id's which is the uuid strategy.
         */
        identifier: {
            type: 'simple'
        },

        /**
         * @cfg {String} clientIdProperty
         * The name of a property that is used for submitting this Model's unique client-side identifier
         * to the server when multiple phantom records are saved as part of the same {@link Ext.data.Operation Operation}.
         * In such a case, the server response should include the client id for each record
         * so that the server response data can be used to update the client-side records if necessary.
         * This property cannot have the same name as any of this Model's fields.
         * Defaults to 'clientId'.
         * @accessor
         */
        clientIdProperty: 'clientId',

        /**
         * @method getIsErased Returns true if the record has been erased on the server.
         */
        isErased: false
    },

    staticConfigs: [
        'idProperty',
        'fields',
        'validations',
        'associations',
        'hasMany',
        'hasOne',
        'belongsTo',
        'clientIdProperty',
        'identifier',
        'proxy'
    ],

    statics: {
        EDIT   : 'edit',
        REJECT : 'reject',
        COMMIT : 'commit',

        cache: {},

        generateProxyMethod: function(name) {
            return function() {
                var prototype = this.prototype;

                return prototype[name].apply(prototype, arguments);
            };
        },

        generateCacheId: function(record, id) {
            var modelName;

            if (record && record.isModel) {
                modelName = record.modelName;
                if (id === undefined) {
                    id = record.getId();
                }
            } else {
                modelName = record;
            }

            return modelName.replace(/\./g, '-').toLowerCase() + '-' + id;
        }
    },

    inheritableStatics: {
        /**
         * Asynchronously loads a model instance by id. Sample usage:
         *
         *     MyApp.User = Ext.define('User', {
         *         extend: 'Ext.data.Model',
         *         fields: [
         *             {name: 'id', type: 'int'},
         *             {name: 'name', type: 'string'}
         *         ]
         *     });
         *
         *     MyApp.User.load(10, {
         *         scope: this,
         *         failure: function(record, operation) {
         *             //do something if the load failed
         *         },
         *         success: function(record, operation) {
         *             //do something if the load succeeded
         *         },
         *         callback: function(record, operation) {
         *             //do something whether the load succeeded or failed
         *         }
         *     });
         *
         * @param {Number} id The id of the model to load
         * @param {Object} config (optional) config object containing success, failure and callback functions, plus
         * optional scope
         * @static
         * @inheritable
         */
        load: function(id, config, scope) {
            var proxy = this.getProxy(),
                idProperty = this.getIdProperty(),
                record = null,
                params = {},
                callback, operation;

            scope = scope || (config && config.scope) || this;
            if (Ext.isFunction(config)) {
                config = {
                    callback: config,
                    scope: scope
                };
            }

            params[idProperty] = id;
            config = Ext.apply({}, config);
            config = Ext.applyIf(config, {
                action: 'read',
                params: params,
                model: this
            });

            operation  = Ext.create('Ext.data.Operation', config);

            if (!proxy) {
                Ext.Logger.error('You are trying to load a model that doesn\'t have a Proxy specified');
            }

            callback = function(operation) {
                if (operation.wasSuccessful()) {
                    record = operation.getRecords()[0] || null;
                    Ext.callback(config.success, scope, [record, operation]);
                } else {
                    Ext.callback(config.failure, scope, [record, operation]);
                }
                Ext.callback(config.callback, scope, [record, operation]);
            };

            proxy.read(operation, callback, this);
        }
    },

    /**
     * @property {Boolean} editing
     * Internal flag used to track whether or not the model instance is currently being edited. Read-only.
     */
    editing : false,

    /**
     * @property {Boolean} dirty
     * True if this Record has been modified. Read-only.
     */
    dirty : false,

    /**
     * @property {Boolean} phantom
     * True when the record does not yet exist in a server-side database (see {@link #setDirty}).
     * Any record which has a real database pk set as its id property is NOT a phantom -- it's real.
     */
    phantom : false,

    constructor: function(data, id, raw, convertedData) {
        var me = this,
            cached = null,
            idProperty = this.getIdProperty();

        /**
         * @property {Object} modified key/value pairs of all fields whose values have changed.
         * The value is the original value for the field.
         */
        me.modified = {};

        /**
         * @property {Object} raw The raw data used to create this model if created via a reader.
         */
        me.raw = raw || data || {};

        /**
         * @property {Array} stores
         * An array of {@link Ext.data.Store} objects that this record is bound to.
         */
        me.stores = [];

        data = data || convertedData || {};

        // We begin by checking if an id is passed to the constructor. If this is the case we override
        // any possible id value that was passed in the data.
        if (id || id === 0) {
            // Lets skip using set here since it's so much faster
            data[idProperty] = me.internalId = id;
        }

        id = data[idProperty];
        if (id || id === 0) {
            cached = Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, id)];
            if (cached) {
                return cached.mergeData(convertedData || data || {});
            }
        }

        if (convertedData) {
            me.setConvertedData(data);
        } else {
            me.setData(data);
        }

        // If it does not have an id at this point, we generate it using the id strategy. This means
        // that we will treat this record as a phantom record from now on
        id = me.data[idProperty];
        if (!id && id !== 0) {
            me.data[idProperty] = me.internalId = me.id = me.getIdentifier().generate(me);
            me.phantom = true;

            if (this.associations.length) {
                this.handleInlineAssociationData(data);
            }
        } else {
            me.id = me.getIdentifier().generate(me);
        }

        Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)] = me;

        if (this.init && typeof this.init == 'function') {
            this.init();
        }
    },

    /**
     * Private function that is used when you create a record that already exists in the model cache.
     * In this case we loop over each field, and apply any data to the current instance that is not already
     * marked as being dirty on that instance.
     * @param data
     * @private
     */
    mergeData: function(rawData) {
        var me = this,
            fields = me.getFields().items,
            ln = fields.length,
            data = me.data,
            i, field, fieldName, value, convert;

        for (i = 0; i < ln; i++) {
            field = fields[i];
            fieldName = field.getName();
            convert = field.getConvert();
            value = rawData[fieldName];

            if (value !== undefined && !me.isModified(fieldName)) {
                if (convert) {
                    value = convert.call(field, value, me);
                }

                data[fieldName] = value;
            }
        }

        return this;
    },

    /**
     * This method is used to set the data for this Record instance.
     * Note that the existing data is removed. If a field is not specified
     * in the passed data it will use the field's default value. If a convert
     * method is specified for the field it will be called on the value.
     * @param rawData
     * @return {Ext.data.Model} this This Record
     */
    setData: function(rawData) {
        var fields = this.fields.items,
            ln = fields.length,
            isArray = Ext.isArray(rawData),
            data = this._data = this.data = {},
            i, field, name, value, convert, id;

        if (!rawData) {
            return this;
        }

        for (i = 0; i < ln; i++) {
            field = fields[i];
            name = field.getName();
            convert = field.getConvert();

            if (isArray) {
                value = rawData[i];
            }
            else {
                value = rawData[name];
                if (typeof value == 'undefined') {
                    value = field.getDefaultValue();
                }
            }

            if (convert) {
                value = convert.call(field, value, this);
            }

            data[name] = value;
        }

        id = this.getId();
        if (this.associations.length && (id || id === 0)) {
            this.handleInlineAssociationData(rawData);
        }

        return this;
    },

    handleInlineAssociationData: function(data) {
        var associations = this.associations.items,
            ln = associations.length,
            i, association, associationData, reader, proxy, associationKey;

        for (i = 0; i < ln; i++) {
            association = associations[i];
            associationKey = association.getAssociationKey();
            associationData = data[associationKey];

            if (associationData) {
                reader = association.getReader();
                if (!reader) {
                    proxy = association.getAssociatedModel().getProxy();
                    // 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 Ext.data.JsonReader({
                            model: association.getAssociatedModel()
                        });
                    }
                }
                association.read(this, reader, associationData);
            }
        }
    },

    /**
     * Sets the model instance's id field to the given id.
     * @param {Number/String} id The new id
     */
    setId: function(id) {
        var currentId = this.getId();

        // Lets use the direct property instead of getter here
        this.set(this.getIdProperty(), id);

        // We don't update the this.id since we don't want to break listeners that already
        // exist on the record instance.
        this.internalId = id;

        delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(this, currentId)];
        Ext.data.Model.cache[Ext.data.Model.generateCacheId(this)] = this;
    },

    /**
     * Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
     * @return {Number/String} The id
     */
    getId: function() {
        // Lets use the direct property instead of getter here
        return this.get(this.getIdProperty());
    },

    /**
     * This sets the data directly without converting and applying default values.
     * This method is used when a Record gets instantiated by a Reader. Only use
     * this when you are sure you are passing correctly converted data.
     * @param data
     * @return this This Record
     */
    setConvertedData: function(data) {
        this._data = this.data = data;
        return this;
    },

    /**
     * Returns the value of the given field
     * @param {String} fieldName The field to fetch the value for
     * @return {Object} The value
     */
    get: function(fieldName) {
        return this.data[fieldName];
    },

    /**
     * Sets the given field to the given value, marks the instance as dirty
     * @param {String/Object} fieldName The field to set, or an object containing key/value pairs
     * @param {Object} value The value to set
     */
    set: function(fieldName, value) {
        var me = this,
            // We are using the fields map since it saves lots of function calls
            fieldMap = me.fields.map,
            modified = me.modified,
            notEditing = !me.editing,
            associations = me.associations.items,
            modifiedCount = 0,
            modifiedFieldNames = [],
            field, key, i, currentValue, ln, convert;

        /*
         * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and
         * set those last so that all other possible data is set before the convert function is called
         */
        if (arguments.length == 1) {
            for (key in fieldName) {
                if (fieldName.hasOwnProperty(key)) {
                    //here we check for the custom convert function. Note that if a field doesn't have a convert function,
                    //we default it to its type's convert function, so we have to check that here. This feels rather dirty.
                    field = fieldMap[key];
                    if (field && field.hasCustomConvert()) {
                        modifiedFieldNames.push(key);
                        continue;
                    }

                    if (!modifiedCount && notEditing) {
                        me.beginEdit();
                    }
                    ++modifiedCount;
                    me.set(key, fieldName[key]);
                }
            }

            ln = modifiedFieldNames.length;
            if (ln) {
                if (!modifiedCount && notEditing) {
                    me.beginEdit();
                }
                modifiedCount += ln;
                for (i = 0; i < ln; i++) {
                    field = modifiedFieldNames[i];
                    me.set(field, fieldName[field]);
                }
            }

            if (notEditing && modifiedCount) {
                me.endEdit(false, modifiedFieldNames);
            }
        } else {
            field = fieldMap[fieldName];
            convert = field && field.getConvert();
            if (convert) {
                value = convert.call(field, value, me);
            }

            currentValue = me.data[fieldName];
            me.data[fieldName] = value;

            if (field && !me.isEqual(currentValue, value)) {
                if (modified.hasOwnProperty(fieldName)) {
                    if (me.isEqual(modified[fieldName], value)) {
                        // the original value in me.modified equals the new value, so the
                        // field is no longer modified
                        delete modified[fieldName];
                        // we might have removed the last modified field, so check to see if
                        // there are any modified fields remaining and correct me.dirty:
                        me.dirty = false;
                        for (key in modified) {
                            if (modified.hasOwnProperty(key)) {
                                me.dirty = true;
                                break;
                            }
                        }
                    }
                } else {
                    me.dirty = true;
                    // We only go one level back?
                    modified[fieldName] = currentValue;
                }
            }

            if (notEditing) {
                me.afterEdit([fieldName], modified);
            }
        }
    },

    /**
     * Checks if two values are equal, taking into account certain
     * special factors, for example dates.
     * @private
     * @param {Object} a The first value
     * @param {Object} b The second value
     * @return {Boolean} True if the values are equal
     */
    isEqual: function(a, b){
        if (Ext.isDate(a) && Ext.isDate(b)) {
            return a.getTime() === b.getTime();
        }
        return a === b;
    },

    /**
     * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are relayed to the containing store.
     * When an edit has begun, it must be followed by either {@link #endEdit} or {@link #cancelEdit}.
     */
    beginEdit: function() {
        var me = this;
        if (!me.editing) {
            me.editing      = true;

            // We save the current states of dirty, data and modified so that when we
            // cancel the edit, we can put it back to this state
            me.dirtySave    = me.dirty;
            me.dataSave     = Ext.apply({}, me.data);
            me.modifiedSave = Ext.apply({}, me.modified);
        }
    },

    /**
     * Cancels all changes made in the current edit operation.
     */
    cancelEdit: function() {
        var me = this;
        if (me.editing) {
            me.editing = false;

            // Reset the modified state, nothing changed since the edit began
            me.modified = me.modifiedSave;
            me.data = me.dataSave;
            me.dirty = me.dirtySave;

            // Delete the saved states
            delete me.modifiedSave;
            delete me.dataSave;
            delete me.dirtySave;
        }
    },

    /**
     * Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will
     * fire).
     * @param {Boolean} silent True to not notify the store of the change
     * @param {String[]} modifiedFieldNames Array of field names changed during edit.
     */
    endEdit: function(silent, modifiedFieldNames) {
        var me = this;

        if (me.editing) {
            me.editing = false;

            if (silent !== true && (me.changedWhileEditing())) {
                me.afterEdit(modifiedFieldNames || Ext.Object.getKeys(this.modified), this.modified);
            }

            delete me.modifiedSave;
            delete me.dataSave;
            delete me.dirtySave;
        }
    },

    /**
     * Checks if the underlying data has changed during an edit. This doesn't necessarily
     * mean the record is dirty, however we still need to notify the store since it may need
     * to update any views.
     * @private
     * @return {Boolean} True if the underlying data has changed during an edit.
     */
    changedWhileEditing: function() {
        var me = this,
            saved = me.dataSave,
            data = me.data,
            key;

        for (key in data) {
            if (data.hasOwnProperty(key)) {
                if (!me.isEqual(data[key], saved[key])) {
                    return true;
                }
            }
        }

        return false;
    },

    /**
     * Gets a hash of only the fields that have been modified since this Model was created or commited.
     * @return {Object}
     */
    getChanges : function() {
        var modified = this.modified,
            changes  = {},
            field;

        for (field in modified) {
            if (modified.hasOwnProperty(field)) {
                changes[field] = this.get(field);
            }
        }

        return changes;
    },

    /**
     * Returns true if the passed field name has been `{@link #modified}` since the load or last commit.
     * @param {String} fieldName {@link Ext.data.Field#name}
     * @return {Boolean}
     */
    isModified : function(fieldName) {
        return this.modified.hasOwnProperty(fieldName);
    },

    /**
     * Saves the model instance using the configured proxy.
     *
     * @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
     * If you pass a function, this will automatically become the callback method. For convenience the config
     * object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
     * with the Model and Operation as arguments.
     * @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
     * as the first argument.
     * @return {Ext.data.Model} The Model instance
     */
    save: function(options, scope) {
        var me     = this,
            action = me.phantom ? 'create' : 'update',
            proxy  = me.getProxy(),
            operation,
            callback;

        if (!proxy) {
            Ext.Logger.error('You are trying to save a model instance that doesn\'t have a Proxy specified');
        }

        options = options || {};
        scope = scope || me;

        if (Ext.isFunction(options)) {
            options = {
                callback: options,
                scope: scope
            };
        }

        Ext.applyIf(options, {
            records: [me],
            action : action,
            model: me.self
        });

        operation = Ext.create('Ext.data.Operation', options);

        callback = function(operation) {
            if (operation.wasSuccessful()) {
                Ext.callback(options.success, scope, [me, operation]);
            } else {
                Ext.callback(options.failure, scope, [me, operation]);
            }

            Ext.callback(options.callback, scope, [me, operation]);
        };

        proxy[action](operation, callback, me);

        return me;
    },

    /**
     * Destroys the record using the configured proxy. This will create a 'destroy' operation.
     * Note that this doesn't destroy this instance after the server comes back with a response.
     * It will however call afterErase on any Stores it is joined to. Stores by default will
     * automatically remove this instance from their data collection.
     *
     * @param {Object/Function} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
     * If you pass a function, this will automatically become the callback method. For convenience the config
     * object may also contain `success` and `failure` methods in addition to `callback` - they will all be invoked
     * with the Model and Operation as arguments.
     * @param {Object} scope The scope to run your callback method in. This is only used if you passed a function
     * as the first argument.
     * @return {Ext.data.Model} The Model instance
     */
    erase: function(options, scope) {
        var me     = this,
            proxy  = this.getProxy(),
            operation,
            callback;

        if (!proxy) {
            Ext.Logger.error('You are trying to erase a model instance that doesn\'t have a Proxy specified');
        }

        options = options || {};
        scope = scope || me;

        if (Ext.isFunction(options)) {
            options = {
                callback: options,
                scope: scope
            };
        }

        Ext.applyIf(options, {
            records: [me],
            action : 'destroy',
            model: this.self
        });

        operation = Ext.create('Ext.data.Operation', options);

        callback = function(operation) {
            if (operation.wasSuccessful()) {
                Ext.callback(options.success, scope, [me, operation]);
            } else {
                Ext.callback(options.failure, scope, [me, operation]);
            }

            Ext.callback(options.callback, scope, [me, operation]);
        };

        proxy.destroy(operation, callback, me);

        return me;
    },

    /**
     * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects
     * all changes made to the model instance since either creation, or the last commit operation. Modified fields are
     * reverted to their original values.
     *
     * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject
     * operations.
     *
     * @param {Boolean} silent (optional) True to skip notification of the owning store of the change.
     * Defaults to false.
     */
    reject: function(silent) {
        var me = this,
            modified = me.modified,
            field;

        for (field in modified) {
            if (modified.hasOwnProperty(field)) {
                if (typeof modified[field] != "function") {
                    me.data[field] = modified[field];
                }
            }
        }

        me.dirty = false;
        me.editing = false;
        me.modified = {};

        if (silent !== true) {
            me.afterReject();
        }
    },

    /**
     * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the
     * instance since either creation or the last commit operation.
     *
     * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit
     * operations.
     *
     * @param {Boolean} silent (optional) True to skip notification of the owning store of the change.
     * Defaults to false.
     */
    commit: function(silent) {
        var me = this,
            modified = this.modified;

        me.phantom = me.dirty = me.editing = false;
        me.modified = {};

        if (silent !== true) {
            me.afterCommit(modified);
        }
    },

    /**
     * @private
     * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
     * afterEdit method is called
     * @param {String[]} modifiedFieldNames Array of field names changed during edit.
     */
    afterEdit : function(modifiedFieldNames, modified) {
        this.notifyStores('afterEdit', modifiedFieldNames, modified);
    },

    /**
     * @private
     * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
     * afterReject method is called
     */
    afterReject : function() {
        this.notifyStores("afterReject");
    },

    /**
     * @private
     * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
     * afterCommit method is called
     */
    afterCommit: function(modified) {
        this.notifyStores('afterCommit', Ext.Object.getKeys(modified || {}), modified);
    },

    /**
     * @private
     * Helper function used by afterEdit, afterReject and afterCommit. Calls the given method on the
     * {@link Ext.data.Store store} that this instance has {@link #join joined}, if any. The store function
     * will always be called with the model instance as its single argument.
     * @param {String} fn The function to call on the store
     */
    notifyStores: function(fn) {
        var args = Ext.Array.clone(arguments),
            stores = this.stores,
            ln = stores.length,
            i, store;

        args[0] = this;
        for (i = 0; i < ln; ++i) {
            store = stores[i];
            if (store !== undefined && typeof store[fn] == "function") {
                store[fn].apply(store, args);
            }
        }
    },

    /**
     * Creates a copy (clone) of this Model instance.
     *
     * @param {String} id A new id. If you don't specify this a new id will be generated for you.
     * To generate a phantom instance with a new id use:
     *
     *     var rec = record.copy(); // clone the record with a new id
     *
     * @return {Ext.data.Model}
     */
    copy: function(newId) {
        var me = this,
            idProperty = me.getIdProperty(),
            raw = Ext.apply({}, me.raw),
            data = Ext.apply({}, me.data);

        delete raw[idProperty];
        delete data[idProperty];

        return new me.self(null, newId, raw, data);
    },

    /**
     * Returns an object containing the data set on this record. This method also allows you to
     * retrieve all the associated data. Note that if you should always use this method if you
     * need all the associated data, since the data property on the record instance is not
     * ensured to be updated at all times.
     * @param {Boolean} includeAssociated True to include the associated data.
     * @return {Object} The data
     */
    getData: function(includeAssociated) {
        var data = this.data;

        if (includeAssociated === true) {
            Ext.apply(data, this.getAssociatedData());
        }

        return data;
    },

    /**
     * Gets all of the data from this Models *loaded* associations. It does this recursively - for example if we have a
     * User which hasMany Orders, and each Order hasMany OrderItems, it will return an object like this:
     *
     *     {
     *         orders: [
     *             {
     *                 id: 123,
     *                 status: 'shipped',
     *                 orderItems: [
     *                     ...
     *                 ]
     *             }
     *         ]
     *     }
     *
     * @return {Object} The nested data set for the Model's loaded associations
     */
    getAssociatedData: function() {
        return this.prepareAssociatedData(this, [], null);
    },

    /**
     * @private
     * This complex-looking method takes a given Model instance and returns an object containing all data from
     * all of that Model's *loaded* associations. See {@link #getAssociatedData}
     * @param {Ext.data.Model} record The Model instance
     * @param {String[]} ids PRIVATE. The set of Model instance internalIds that have already been loaded
     * @param {String} associationType (optional) The name of the type of association to limit to.
     * @return {Object} The nested data set for the Model's loaded associations
     */
    prepareAssociatedData: function(record, ids, associationType) {
        //we keep track of all of the internalIds of the models that we have loaded so far in here
        var associations     = record.associations.items,
            associationCount = associations.length,
            associationData  = {},
            associatedStore, associationName, associatedRecords, associatedRecord,
            associatedRecordCount, association, id, i, j, type, allow;

        for (i = 0; i < associationCount; i++) {
            association = associations[i];
            associationName = association.getName();
            type = association.getType();
            allow = true;

            if (associationType) {
                allow = type == associationType;
            }

            if (allow && type.toLowerCase() == 'hasmany') {
                //this is the hasMany store filled with the associated data
                associatedStore = record[association.getStoreName()];

                //we will use this to contain each associated record's data
                associationData[associationName] = [];

                //if it's loaded, put it into the association data
                if (associatedStore && associatedStore.getCount() > 0) {
                    associatedRecords = associatedStore.data.items;
                    associatedRecordCount = associatedRecords.length;

                    //now we're finally iterating over the records in the association. We do this recursively
                    for (j = 0; j < associatedRecordCount; j++) {
                        associatedRecord = associatedRecords[j];
                        // Use the id, since it is prefixed with the model name, guaranteed to be unique
                        id = associatedRecord.id;

                        //when we load the associations for a specific model instance we add it to the set of loaded ids so that
                        //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures.
                        if (Ext.Array.indexOf(ids, id) == -1) {
                            ids.push(id);

                            associationData[associationName][j] = associatedRecord.getData();
                            Ext.apply(associationData[associationName][j], this.prepareAssociatedData(associatedRecord, ids, associationType));
                        }
                    }
                }
            } else if (allow && (type.toLowerCase() == 'belongsto' || type.toLowerCase() == 'hasone')) {
                associatedRecord = record[association.getInstanceName()];
                if (associatedRecord !== undefined) {
                    id = associatedRecord.id;
                    if (Ext.Array.indexOf(ids, id) === -1) {
                        ids.push(id);
                        associationData[associationName] = associatedRecord.getData();
                        Ext.apply(associationData[associationName], this.prepareAssociatedData(associatedRecord, ids, associationType));
                    }
                }
            }
        }

        return associationData;
    },

    /**
     * By joining this model to an instance of a class, this model will automatically try to
     * call certain template methods on that instance (afterEdit, afterCommit, afterErase).
     * For example, a Store calls join and unjoin whenever you add or remove a record to it's data collection.
     * This way a Store can get notified of any changes made to this record.
     * This functionality is usually only required when creating custom components.
     * @param {Ext.data.Store} store The store to which this model has been added.
     */
    join: function(store) {
        Ext.Array.include(this.stores, store);
    },

    /**
     * This unjoins this record from an instance of a class. Look at the documentation for {@link #join}
     * for more information about joining records to class instances.
     * @param {Ext.data.Store} store The store from which this model has been removed.
     */
    unjoin: function(store) {
        Ext.Array.remove(this.stores, store);
    },

    /**
     * Marks this **Record** as `{@link #dirty}`. This method is used interally when adding `{@link #phantom}` records
     * to a {@link Ext.data.proxy.Server#writer writer enabled store}.
     *
     * Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords}
     * where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations.
     */
    setDirty : function() {
        var me = this,
            name;

        me.dirty = true;

        me.fields.each(function(field) {
            if (field.getPersist()) {
                name = field.getName();
                me.modified[name] = me.get(name);
            }
        });
    },

    /**
     * Validates the current data against all of its configured {@link #cfg-validations}.
     * @return {Ext.data.Errors} The errors object
     */
    validate: function() {
        var errors      = Ext.create('Ext.data.Errors'),
            validations = this.getValidations().items,
            validators  = Ext.data.Validations,
            length, validation, field, valid, type, i;

        if (validations) {
            length = validations.length;

            for (i = 0; i < length; i++) {
                validation = validations[i];
                field = validation.field || validation.name;
                type  = validation.type;
                valid = validators[type](validation, this.get(field));

                if (!valid) {
                    errors.add(Ext.create('Ext.data.Error', {
                        field  : field,
                        message: validation.message || validators.getMessage(type)
                    }));
                }
            }
        }

        return errors;
    },

    /**
     * Checks if the model is valid. See {@link #validate}.
     * @return {Boolean} True if the model is valid.
     */
    isValid: function(){
        return this.validate().isValid();
    },

    /**
     * Returns a url-suitable string for this model instance. By default this just returns the name of the Model class
     * followed by the instance ID - for example an instance of MyApp.model.User with ID 123 will return 'user/123'.
     * @return {String} The url string for this model instance
     */
    toUrl: function() {
        var pieces = this.$className.split('.'),
            name   = pieces[pieces.length - 1].toLowerCase();

        return name + '/' + this.getId();
    },

    /**
     * Destroys this model instance. Note that this doesn't do a 'destroy' operation. If you want to destroy
     * the record in your localstorage or on the server you should use the {@link #erase} method.
     */
    destroy: function() {
        var me = this;
        me.notifyStores('afterErase', me);
        delete Ext.data.Model.cache[Ext.data.Model.generateCacheId(me)];
        me.raw = me.stores = me.modified = null;
        me.callParent(arguments);
    },

    //<debug>
    markDirty : function() {
        if (Ext.isDefined(Ext.Logger)) {
            Ext.Logger.deprecate('Ext.data.Model: markDirty has been deprecated. Use setDirty instead.');
        }
        return this.setDirty.apply(this, arguments);
    },
    //</debug>

    applyProxy: function(proxy, currentProxy) {
        return Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
    },

    updateProxy: function(proxy) {
        if (proxy) {
            proxy.setModel(this.self);
        }
    },

    applyAssociations: function(associations) {
        if (associations) {
            this.addAssociations(associations, 'hasMany');
        }
    },

    applyBelongsTo: function(belongsTo) {
        if (belongsTo) {
            this.addAssociations(belongsTo, 'belongsTo');
        }
    },

    applyHasMany: function(hasMany) {
        if (hasMany) {
            this.addAssociations(hasMany, 'hasMany');
        }
    },

    applyHasOne: function(hasOne) {
        if (hasOne) {
            this.addAssociations(hasOne, 'hasOne');
        }
    },

    addAssociations: function(associations, defaultType) {
        var ln, i, association,
            name = this.self.modelName,
            associationsCollection = this.self.associations,
            onCreatedFn;

        associations = Ext.Array.from(associations);

        for (i = 0, ln = associations.length; i < ln; i++) {
            association = associations[i];
            if (!Ext.isObject(association)) {
                association = {model: association};
            }

            Ext.applyIf(association, {
                type: defaultType,
                ownerModel: name,
                associatedModel: association.model
            });

            delete association.model;

            onCreatedFn = Ext.Function.bind(function(associationName) {
                associationsCollection.add(Ext.data.association.Association.create(this));
            }, association);

            Ext.ClassManager.onCreated(onCreatedFn, this, (typeof association.associatedModel === 'string') ? association.associatedModel : Ext.getClassName(association.associatedModel));
        }
    },

    applyValidations: function(validations) {
        if (validations) {
            if (!Ext.isArray(validations)) {
                validations = [validations];
            }
            this.addValidations(validations);
        }
    },

    addValidations: function(validations) {
        this.self.validations.addAll(validations);
    },

    /**
     * @method setFields
     * Updates the collection of Fields that all instances of this Model use. **Does not** update field values in a Model
     * instance (use {@link #set} for that), instead this updates which fields are available on the Model class. This
     * is normally used when creating or updating Model definitions dynamically, for example if you allow your users to
     * define their own Models and save the fields configuration to a database, this method allows you to change those
     * fields later.
     */

    applyFields: function(fields) {
        var superFields = this.superclass.fields;
        if (superFields) {
            fields = superFields.items.concat(fields || []);
        }
        return fields || [];
    },

    updateFields: function(fields) {
        var ln = fields.length,
            me = this,
            prototype = me.self.prototype,
            idProperty = this.getIdProperty(),
            idField, fieldsCollection, field, i;

        /**
         * @property {Ext.util.MixedCollection} fields
         * The fields defined on this model.
         */
        fieldsCollection = me._fields = me.fields = new Ext.util.Collection(prototype.getFieldName);

        for (i = 0; i < ln; i++) {
            field = fields[i];
            if (!field.isField) {
                field = new Ext.data.Field(fields[i]);
            }
            fieldsCollection.add(field);
        }

        // We want every Model to have an id property field
        idField = fieldsCollection.get(idProperty);
        if (!idField) {
            fieldsCollection.add(new Ext.data.Field(idProperty));
        } else {
            idField.setType('auto');
        }

        fieldsCollection.addSorter(prototype.sortConvertFields);
    },

    applyIdentifier: function(identifier) {
        if (typeof identifier === 'string') {
            identifier = {
                type: identifier
            };
        }
        return Ext.factory(identifier, Ext.data.identifier.Simple, this.getIdentifier(), 'data.identifier');
    },

    /**
     * This method is used by the fields collection to retrieve the key for a field
     * based on it's name.
     * @param field
     * @private
     */
    getFieldName: function(field) {
        return field.getName();
    },

    /**
     * This method is being used to sort the fields based on their convert method. If
     * a field has a custom convert method, we ensure its more to the bottom of the collection.
     * @param field1
     * @param field2
     * @private
     */
    sortConvertFields: function(field1, field2) {
        var f1SpecialConvert = field1.hasCustomConvert(),
            f2SpecialConvert = field2.hasCustomConvert();

        if (f1SpecialConvert && !f2SpecialConvert) {
            return 1;
        }
        if (!f1SpecialConvert && f2SpecialConvert) {
            return -1;
        }
        return 0;
    },

    /**
     * @private
     */
    onClassExtended: function(cls, data, hooks) {
        var onBeforeClassCreated = hooks.onBeforeCreated,
            Model = this,
            prototype = Model.prototype,
            configNameCache = Ext.Class.configNameCache,
            staticConfigs = prototype.staticConfigs.concat(data.staticConfigs || []),
            defaultConfig = prototype.config,
            config = data.config || {},
            key;

        // Convert old properties in data into a config object
        // <deprecated product=touch since=2.0>
        if (data.idgen || config.idgen) {
            config.identifier = data.idgen || config.idgen;
            // <debug warn>
            Ext.Logger.deprecate('idgen is deprecated as a property. Please put it inside the config object' +
                ' under the new "identifier" configuration');
            // </debug>
        }

        for (key in defaultConfig) {
            if (key in data) {
                config[key] = data[key];
                delete data[key];
                // <debug warn>
                Ext.Logger.deprecate(key + ' is deprecated as a property directly on the Model prototype. ' +
                    'Please put it inside the config object.');
                // </debug>
            }
        }
        // </deprecated>
        data.config = config;

        hooks.onBeforeCreated = function(cls, data) {
            var dependencies = [],
                prototype = cls.prototype,
                statics = {},
                config = prototype.config,
                staticConfigsLn = staticConfigs.length,
                copyMethods = ['set', 'get'],
                copyMethodsLn = copyMethods.length,
                associations = config.associations || [],
                name = Ext.getClassName(cls),
                key, methodName, i, j, ln;

            // Create static setters and getters for each config option
            for (i = 0; i < staticConfigsLn; i++) {
                key = staticConfigs[i];

                for (j = 0; j < copyMethodsLn; j++) {
                    methodName = configNameCache[key][copyMethods[j]];
                    if (methodName in prototype) {
                        statics[methodName] = Model.generateProxyMethod(methodName);
                    }
                }
            }

            cls.addStatics(statics);

            // Save modelName on class and its prototype
            cls.modelName = name;
            prototype.modelName = name;

            // Take out dependencies on other associations and the proxy type
            if (config.belongsTo) {
                dependencies.push('association.belongsto');
            }
            if (config.hasMany) {
                dependencies.push('association.hasmany');
            }
            if (config.hasOne) {
                dependencies.push('association.hasone');
            }

            for (i = 0,ln = associations.length; i < ln; ++i) {
                dependencies.push('association.' + associations[i].type.toLowerCase());
            }

            if (config.proxy) {
                if (typeof config.proxy === 'string') {
                    dependencies.push('proxy.' + config.proxy);
                }
                else if (typeof config.proxy.type === 'string') {
                    dependencies.push('proxy.' + config.proxy.type);
                }
            }

            if (config.validations) {
                dependencies.push('Ext.data.Validations');
            }

            Ext.require(dependencies, function() {
                Ext.Function.interceptBefore(hooks, 'onCreated', function() {
                    Ext.data.ModelManager.registerType(name, cls);

                    var superCls = cls.prototype.superclass;

                    /**
                     * @property {Ext.util.Collection} associations
                     * The associations defined on this model.
                     */
                    cls.prototype.associations = cls.associations = cls.prototype._associations = (superCls && superCls.associations)
                        ? superCls.associations.clone()
                        : new Ext.util.Collection(function(association) {
                            return association.getName();
                        });

                    /**
                     * @property {Ext.util.Collection} validations
                     * The validations defined on this model.
                     */
                    cls.prototype.validations = cls.validations = cls.prototype._validations = (superCls && superCls.validations)
                        ? superCls.validations.clone()
                        : new Ext.util.Collection(function(validation) {
                            return validation.field ? (validation.field + '-' + validation.type) : (validation.name + '-' + validation.type);
                        });

                    cls.prototype = Ext.Object.chain(cls.prototype);
                    cls.prototype.initConfig.call(cls.prototype, config);

                    delete cls.prototype.initConfig;
                });

                onBeforeClassCreated.call(Model, cls, data, hooks);
            });
        };
    }
});