/*

This file is part of Ext JS 4

Copyright (c) 2011 Sencha Inc

Contact:  http://www.sencha.com/contact

GNU General Public License Usage
This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.

If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.

*/
/**
 * @author Ed Spencer
 *
 * 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.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',
 *         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.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',
 *         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.BelongsToAssociation belongsTo} and {@link
 * Ext.data.HasManyAssociation 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',
 *         fields: ['id', 'user_id'],
 *
 *         belongsTo: 'User',
 *         hasMany  : {model: 'Comment', name: 'comments'}
 *     });
 *
 *     Ext.define('Comment', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'user_id', 'post_id'],
 *
 *         belongsTo: 'Post'
 *     });
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id'],
 *
 *         hasMany: [
 *             'Post',
 *             {model: 'Comment', name: 'comments'}
 *         ]
 *     });
 *
 * See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} 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',
 *         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',
 *         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: 'ed@sencha.com'});
 *
 *     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.destroy({
 *         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.util.Observable'
    },

    requires: [
        'Ext.ModelManager',
        'Ext.data.IdGenerator',
        'Ext.data.Field',
        'Ext.data.Errors',
        'Ext.data.Operation',
        'Ext.data.validations',
        'Ext.data.proxy.Ajax',
        'Ext.util.MixedCollection'
    ],

    onClassExtended: function(cls, data) {
        var onBeforeClassCreated = data.onBeforeClassCreated;

        data.onBeforeClassCreated = function(cls, data) {
            var me = this,
                name = Ext.getClassName(cls),
                prototype = cls.prototype,
                superCls = cls.prototype.superclass,

                validations = data.validations || [],
                fields = data.fields || [],
                associations = data.associations || [],
                belongsTo = data.belongsTo,
                hasMany = data.hasMany,
                idgen = data.idgen,

                fieldsMixedCollection = new Ext.util.MixedCollection(false, function(field) {
                    return field.name;
                }),

                associationsMixedCollection = new Ext.util.MixedCollection(false, function(association) {
                    return association.name;
                }),

                superValidations = superCls.validations,
                superFields = superCls.fields,
                superAssociations = superCls.associations,

                association, i, ln,
                dependencies = [];

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

            // Merge the validations of the superclass and the new subclass
            if (superValidations) {
                validations = superValidations.concat(validations);
            }

            data.validations = validations;

            // Merge the fields of the superclass and the new subclass
            if (superFields) {
                fields = superFields.items.concat(fields);
            }

            for (i = 0, ln = fields.length; i < ln; ++i) {
                fieldsMixedCollection.add(new Ext.data.Field(fields[i]));
            }

            data.fields = fieldsMixedCollection;

            if (idgen) {
                data.idgen = Ext.data.IdGenerator.get(idgen);
            }

            //associations can be specified in the more convenient format (e.g. not inside an 'associations' array).
            //we support that here
            if (belongsTo) {
                belongsTo = Ext.Array.from(belongsTo);

                for (i = 0, ln = belongsTo.length; i < ln; ++i) {
                    association = belongsTo[i];

                    if (!Ext.isObject(association)) {
                        association = {model: association};
                    }

                    association.type = 'belongsTo';
                    associations.push(association);
                }

                delete data.belongsTo;
            }

            if (hasMany) {
                hasMany = Ext.Array.from(hasMany);
                for (i = 0, ln = hasMany.length; i < ln; ++i) {
                    association = hasMany[i];

                    if (!Ext.isObject(association)) {
                        association = {model: association};
                    }

                    association.type = 'hasMany';
                    associations.push(association);
                }

                delete data.hasMany;
            }

            if (superAssociations) {
                associations = superAssociations.items.concat(associations);
            }

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

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

            Ext.require(dependencies, function() {
                Ext.ModelManager.registerType(name, cls);

                for (i = 0, ln = associations.length; i < ln; ++i) {
                    association = associations[i];

                    Ext.apply(association, {
                        ownerModel: name,
                        associatedModel: association.model
                    });

                    if (Ext.ModelManager.getModel(association.model) === undefined) {
                        Ext.ModelManager.registerDeferredAssociation(association);
                    } else {
                        associationsMixedCollection.add(Ext.data.Association.create(association));
                    }
                }

                data.associations = associationsMixedCollection;

                onBeforeClassCreated.call(me, cls, data);

                cls.setProxy(cls.prototype.proxy || cls.prototype.defaultProxyType);

                // Fire the onModelDefined template method on ModelManager
                Ext.ModelManager.onModelDefined(cls);
            });
        };
    },

    inheritableStatics: {
        /**
         * Sets the Proxy to use for this model. Accepts any options that can be accepted by
         * {@link Ext#createByAlias Ext.createByAlias}.
         * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
         * @return {Ext.data.proxy.Proxy}
         * @static
         * @inheritable
         */
        setProxy: function(proxy) {
            //make sure we have an Ext.data.proxy.Proxy object
            if (!proxy.isProxy) {
                if (typeof proxy == "string") {
                    proxy = {
                        type: proxy
                    };
                }
                proxy = Ext.createByAlias("proxy." + proxy.type, proxy);
            }
            proxy.setModel(this);
            this.proxy = this.prototype.proxy = proxy;

            return proxy;
        },

        /**
         * Returns the configured Proxy for this Model
         * @return {Ext.data.proxy.Proxy} The proxy
         * @static
         * @inheritable
         */
        getProxy: function() {
            return this.proxy;
        },

        /**
         * 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) {
            config = Ext.apply({}, config);
            config = Ext.applyIf(config, {
                action: 'read',
                id    : id
            });

            var operation  = Ext.create('Ext.data.Operation', config),
                scope      = config.scope || this,
                record     = null,
                callback;

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

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

    statics: {
        PREFIX : 'ext-record',
        AUTO_ID: 1,
        EDIT   : 'edit',
        REJECT : 'reject',
        COMMIT : 'commit',

        /**
         * Generates a sequential id. This method is typically called when a record is {@link Ext#create
         * create}d and {@link #constructor no id has been specified}. The id will automatically be assigned to the
         * record. The returned id takes the form: {PREFIX}-{AUTO_ID}.
         *
         * - **PREFIX** : String - Ext.data.Model.PREFIX (defaults to 'ext-record')
         * - **AUTO_ID** : String - Ext.data.Model.AUTO_ID (defaults to 1 initially)
         *
         * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}.
         * @return {String} auto-generated string id, `"ext-record-i++"`;
         * @static
         */
        id: function(rec) {
            var id = [this.PREFIX, '-', this.AUTO_ID++].join('');
            rec.phantom = true;
            rec.internalId = id;
            return id;
        }
    },

    /**
     * @cfg {String/Object} idgen
     * The id generator to use for this model. The default id generator does not generate
     * values for the {@link #idProperty}.
     *
     * This can be overridden at the model level to provide a custom generator for a model.
     * The simplest form of this would be:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.SequentialIdGenerator'],
     *          idgen: 'sequential',
     *          ...
     *      });
     *
     * The above would generate {@link Ext.data.SequentialIdGenerator sequential} id's such
     * as 1, 2, 3 etc..
     *
     * Another useful id generator is {@link Ext.data.UuidGenerator}:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.UuidGenerator'],
     *          idgen: 'uuid',
     *          ...
     *      });
     *
     * An id generation can also be further configured:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          idgen: {
     *              type: 'sequential',
     *              seed: 1000,
     *              prefix: 'ID_'
     *          }
     *      });
     *
     * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
     *
     * If multiple models share an id space, a single generator can be shared:
     *
     *      Ext.define('MyApp.data.MyModelX', {
     *          extend: 'Ext.data.Model',
     *          idgen: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     *      Ext.define('MyApp.data.MyModelY', {
     *          extend: 'Ext.data.Model',
     *          idgen: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     * For more complex, shared id generators, a custom generator is the best approach.
     * See {@link Ext.data.IdGenerator} for details on creating custom id generators.
     *
     * @markdown
     */
    idgen: {
        isGenerator: true,
        type: 'default',

        generate: function () {
            return null;
        },
        getRecId: function (rec) {
            return rec.modelName + '-' + rec.internalId;
        }
    },

    /**
     * @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,

    /**
     * @cfg {String} persistenceProperty
     * The property on this Persistable object that its data is saved to. Defaults to 'data'
     * (e.g. all persistable data resides in this.data.)
     */
    persistenceProperty: 'data',

    evented: false,
    isModel: true,

    /**
     * @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,

    /**
     * @cfg {String} idProperty
     * The name of the field treated as this Model's unique id. Defaults to 'id'.
     */
    idProperty: 'id',

    /**
     * @cfg {String} defaultProxyType
     * The string type of the default Model Proxy. Defaults to 'ajax'.
     */
    defaultProxyType: 'ajax',

    // Fields config and property
    /**
     * @cfg {Object[]/String[]} fields
     * The fields for this model.
     */
    /**
     * @property {Ext.util.MixedCollection} fields
     * The fields defined on this model.
     */

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

    // Associations configs and properties
    /**
     * @cfg {Object[]} associations
     * An array of {@link Ext.data.Association associations} for this model.
     */
    /**
     * @cfg {String/Object/String[]/Object[]} hasMany
     * One or more {@link Ext.data.HasManyAssociation HasMany associations} for this model.
     */
    /**
     * @cfg {String/Object/String[]/Object[]} belongsTo
     * One or more {@link Ext.data.BelongsToAssociation BelongsTo associations} for this model.
     */
    /**
     * @property {Ext.util.MixedCollection} associations
     * {@link Ext.data.Association Associations} defined on this model.
     */

    /**
     * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
     * The {@link Ext.data.proxy.Proxy proxy} to use for this model.
     */

    // raw not documented intentionally, meant to be used internally.
    constructor: function(data, id, raw) {
        data = data || {};

        var me = this,
            fields,
            length,
            field,
            name,
            i,
            newId,
            isArray = Ext.isArray(data),
            newData = isArray ? {} : null; // to hold mapped array data if needed

        /**
         * An internal unique ID for each Model instance, used to identify Models that don't have an ID yet
         * @property internalId
         * @type String
         * @private
         */
        me.internalId = (id || id === 0) ? id : Ext.data.Model.id(me);

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

        Ext.applyIf(me, {
            data: {}
        });

        /**
         * @property {Object} modified Key: value pairs of all fields whose values have changed
         */
        me.modified = {};

        // Deal with spelling error in previous releases
        if (me.persistanceProperty) {
            //<debug>
            if (Ext.isDefined(Ext.global.console)) {
                Ext.global.console.warn('Ext.data.Model: persistanceProperty has been deprecated. Use persistenceProperty instead.');
            }
            //</debug>
            me.persistenceProperty = me.persistanceProperty;
        }
        me[me.persistenceProperty] = {};

        me.mixins.observable.constructor.call(me);

        //add default field values if present
        fields = me.fields.items;
        length = fields.length;

        for (i = 0; i < length; i++) {
            field = fields[i];
            name  = field.name;

            if (isArray){
                // Have to map array data so the values get assigned to the named fields
                // rather than getting set as the field names with undefined values.
                newData[name] = data[i];
            }
            else if (data[name] === undefined) {
                data[name] = field.defaultValue;
            }
        }

        me.set(newData || data);

        if (me.getId()) {
            me.phantom = false;
        } else if (me.phantom) {
            newId = me.idgen.generate();
            if (newId !== null) {
                me.setId(newId);
            }
        }

        // clear any dirty/modified since we're initializing
        me.dirty = false;
        me.modified = {};

        if (typeof me.init == 'function') {
            me.init();
        }

        me.id = me.idgen.getRecId(me);
    },

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

    /**
     * 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,
            fields = me.fields,
            modified = me.modified,
            convertFields = [],
            field, key, i, currentValue, notEditing, count, length;

        /*
         * 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 && Ext.isObject(fieldName)) {
            notEditing = !me.editing;
            count = 0;
            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 = fields.get(key);
                    if (field && field.convert !== field.type.convert) {
                        convertFields.push(key);
                        continue;
                    }

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

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

            if (notEditing && count) {
                me.endEdit();
            }
        } else {
            if (fields) {
                field = fields.get(fieldName);

                if (field && field.convert) {
                    value = field.convert(value, me);
                }
            }
            currentValue = me.get(fieldName);
            me[me.persistenceProperty][fieldName] = value;

            if (field && field.persist && !me.isEqual(currentValue, value)) {
                if (me.isModified(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;
                    modified[fieldName] = currentValue;
                }
            }

            if (!me.editing) {
                me.afterEdit();
            }
        }
    },

    /**
     * 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;
            me.dirtySave = me.dirty;
            me.dataSave = Ext.apply({}, me[me.persistenceProperty]);
            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[me.persistenceProperty] = me.dataSave;
            me.dirty = me.dirtySave;
            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
     */
    endEdit : function(silent){
        var me = this,
            didChange;
            
        if (me.editing) {
            me.editing = false;
            didChange = me.dirty || me.changedWhileEditing();
            delete me.modifiedSave;
            delete me.dataSave;
            delete me.dirtySave;
            if (silent !== true && didChange) {
                me.afterEdit();
            }
        }
    },
    
    /**
     * 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[me.persistenceProperty],
            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);
    },

    /**
     * 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.persist) {
                name = field.name;
                me.modified[name] = me.get(name);
            }
        }, me);
    },

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

    /**
     * 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[me.persistenceProperty][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;

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

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

    /**
     * Creates a copy (clone) of this Model instance.
     *
     * @param {String} [id] A new id, defaults to the id of the instance being copied.
     * See `{@link Ext.data.Model#id id}`. To generate a phantom instance with a new id use:
     *
     *     var rec = record.copy(); // clone the record
     *     Ext.data.Model.id(rec); // automatically generate a unique sequential id
     *
     * @return {Ext.data.Model}
     */
    copy : function(newId) {
        var me = this;

        return new me.self(Ext.apply({}, me[me.persistenceProperty]), newId || me.internalId);
    },

    /**
     * Sets the Proxy to use for this model. Accepts any options that can be accepted by
     * {@link Ext#createByAlias Ext.createByAlias}.
     *
     * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
     * @return {Ext.data.proxy.Proxy}
     */
    setProxy: function(proxy) {
        //make sure we have an Ext.data.proxy.Proxy object
        if (!proxy.isProxy) {
            if (typeof proxy === "string") {
                proxy = {
                    type: proxy
                };
            }
            proxy = Ext.createByAlias("proxy." + proxy.type, proxy);
        }
        proxy.setModel(this.self);
        this.proxy = proxy;

        return proxy;
    },

    /**
     * Returns the configured Proxy for this Model.
     * @return {Ext.data.proxy.Proxy} The proxy
     */
    getProxy: function() {
        return this.proxy;
    },

    /**
     * Validates the current data against all of its configured {@link #validations}.
     * @return {Ext.data.Errors} The errors object
     */
    validate: function() {
        var errors      = Ext.create('Ext.data.Errors'),
            validations = this.validations,
            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({
                        field  : field,
                        message: validation.message || validators[type + 'Message']
                    });
                }
            }
        }

        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();
    },

    /**
     * Saves the model instance using the configured proxy.
     * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
     * @return {Ext.data.Model} The Model instance
     */
    save: function(options) {
        options = Ext.apply({}, options);

        var me     = this,
            action = me.phantom ? 'create' : 'update',
            record = null,
            scope  = options.scope || me,
            operation,
            callback;

        Ext.apply(options, {
            records: [me],
            action : action
        });

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

        callback = function(operation) {
            if (operation.wasSuccessful()) {
                record = operation.getRecords()[0];
                //we need to make sure we've set the updated data here. Ideally this will be redundant once the
                //ModelCache is in place
                me.set(record.data);
                record.dirty = false;

                Ext.callback(options.success, scope, [record, operation]);
            } else {
                Ext.callback(options.failure, scope, [record, operation]);
            }

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

        me.getProxy()[action](operation, callback, me);

        return me;
    },

    /**
     * Destroys the model using the configured proxy.
     * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
     * @return {Ext.data.Model} The Model instance
     */
    destroy: function(options){
        options = Ext.apply({}, options);

        var me     = this,
            record = null,
            scope  = options.scope || me,
            operation,
            callback;

        Ext.apply(options, {
            records: [me],
            action : 'destroy'
        });

        operation = Ext.create('Ext.data.Operation', options);
        callback = function(operation) {
            if (operation.wasSuccessful()) {
                Ext.callback(options.success, scope, [record, operation]);
            } else {
                Ext.callback(options.failure, scope, [record, operation]);
            }
            Ext.callback(options.callback, scope, [record, operation]);
        };

        me.getProxy().destroy(operation, callback, me);
        return me;
    },

    /**
     * Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
     * @return {Number} The id
     */
    getId: function() {
        return this.get(this.idProperty);
    },

    /**
     * Sets the model instance's id field to the given id.
     * @param {Number} id The new id
     */
    setId: function(id) {
        this.set(this.idProperty, id);
    },

    /**
     * Tells this model instance that it has been added to a store.
     * @param {Ext.data.Store} store The store to which this model has been added.
     */
    join : function(store) {
        /**
         * @property {Ext.data.Store} store
         * The {@link Ext.data.Store Store} to which this Record belongs.
         */
        this.store = store;
    },

    /**
     * Tells this model instance that it has been removed from the store.
     * @param {Ext.data.Store} store The store from which this model has been removed.
     */
    unjoin: function(store) {
        delete this.store;
    },

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

    /**
     * @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.callStore("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() {
        this.callStore('afterCommit');
    },

    /**
     * @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
     */
    callStore: function(fn) {
        var store = this.store;

        if (store !== undefined && typeof store[fn] == "function") {
            store[fn](this);
        }
    },

    /**
     * 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, associatedName, associatedRecords, associatedRecord,
            associatedRecordCount, association, id, i, j, type, allow;

        for (i = 0; i < associationCount; i++) {
            association = associations[i];
            type = association.type;
            allow = true;
            if (associationType) {
                allow = type == associationType;
            }
            if (allow && type == 'hasMany') {

                //this is the hasMany store filled with the associated data
                associatedStore = record[association.storeName];

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

                //if it's loaded, put it into the association data
                if (associatedStore && associatedStore.data.length > 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[association.name][j] = associatedRecord.data;
                            Ext.apply(associationData[association.name][j], this.prepareAssociatedData(associatedRecord, ids, type));
                        }
                    }
                }
            } else if (allow && type == 'belongsTo') {
                associatedRecord = record[association.instanceName];
                if (associatedRecord !== undefined) {
                    id = associatedRecord.id;
                    if (Ext.Array.indexOf(ids, id) == -1) {
                        ids.push(id);
                        associationData[association.name] = associatedRecord.data;
                        Ext.apply(associationData[association.name], this.prepareAssociatedData(associatedRecord, ids, type));
                    }
                }
            }
        }

        return associationData;
    }
});

