/**
 * A Model or Entity represents some object that your application manages. For example, one
 * might define a Model for Users, Products, Cars, or other real-world object that we want
 * to model in the system. Models are used by {@link Ext.data.Store stores}, which are in
 * turn used by many of the data-bound components in Ext.
 *
 * # Fields
 *
 * 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', convert: null},
 *             {name: 'phone', type: 'string'},
 *             {name: 'alive', type: 'boolean', defaultValue: true, convert: null}
 *         ],
 *
 *         changeName: function() {
 *             var oldName = this.get('name'),
 *                 newName = oldName + " The Barbarian";
 *
 *             this.set('name', newName);
 *         }
 *     });
 *
 * Now we can create instances of our User model and call any model logic we defined:
 *
 *     var user = Ext.create('User', {
 *         id   : 'ABCD12345',
 *         name : 'Conan',
 *         age  : 24,
 *         phone: '555-555-5555'
 *     });
 *
 *     user.changeName();
 *     user.get('name'); //returns "Conan The Barbarian"
 *
 * By default, the built in field types such as number and boolean coerce string values
 * in the raw data by virtue of their {@link Ext.data.field.Field#method-convert} method.
 * When the server can be relied upon to send data in a format that does not need to be
 * converted, disabling this can improve performance. The {@link Ext.data.reader.Json Json}
 * and {@link Ext.data.reader.Array Array} readers are likely candidates for this
 * optimization. To disable field conversions you simply specify `null` for the field's
 * {@link Ext.data.field.Field#cfg-convert convert config}.
 *
 * ## The "id" Field and `idProperty`
 *
 * A Model definition always has an *identifying field* which should yield a unique key
 * for each instance. By default, a field named "id" will be created with a
 * {@link Ext.data.Field#mapping mapping} of "id". This happens because of the default
 * {@link #idProperty} provided in Model definitions.
 *
 * To alter which field is the identifying field, use the {@link #idProperty} config.
 *
 * # Validators
 *
 * Models have built-in support for field validators. Validators are added to models as in
 * the follow example:
 *
 *     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 }
 *         ],
 *
 *         validators: {
 *             age: 'presence',
 *             name: { type: 'length', min: 2 },
 *             gender: { type: 'inclusion', list: ['Male', 'Female'] },
 *             username: [
 *                 { type: 'exclusion', list: ['Admin', 'Operator'] },
 *                 { type: 'format', matcher: /([a-z]+)[0-9]{2,3}/i }
 *             ]
 *         }
 *     });
 *
 * The derived type of `Ext.data.field.Field` can also provide validation. If `validators`
 * need to be duplicated on multiple fields, instead consider creating a custom field type.
 *
 * ## Validation
 *
 * The results of the validators can be retrieved via the "associated" validation record:
 *
 *     var instance = Ext.create('User', {
 *         name: 'Ed',
 *         gender: 'Male',
 *         username: 'edspencer'
 *     });
 *
 *     var validation = instance.getValidation();
 *
 * The returned object is an instance of `Ext.data.Validation` and has as its fields the
 * result of the field `validators`. The validation object is "dirty" if there are one or
 * more validation errors present.
 *
 * This record is also available when using data binding as a "pseudo-association" called
 * "validation". This pseudo-association can be hidden by an explicitly declared
 * association by the same name (for compatibility reasons), but doing so is not
 * recommended.
 *
 * The `{@link Ext.Component#modelValidation}` config can be used to enable automatic
 * binding from the "validation" of a record to the form fields that may be bound to its
 * values.
 *
 * # Associations
 * 
 * Models often have associations with other Models. These associations can be defined by
 * fields (often called "foreign keys") or by other data such as a many-to-many (or "matrix").
 * See {@link Ext.data.schema.Association} for information about configuring and using associations.
 *
 * # 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: '[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 accomplished with the static `load` method:
 *
 *     // 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!');
 *         }
 *     });
 * 
 * # HTTP Parameter names when using a {@link Ext.data.proxy.Ajax Ajax proxy}
 *
 * By default, the model ID is specified in an HTTP parameter named `id`. To change the
 * name of this parameter use the Proxy's {@link Ext.data.proxy.Ajax#idParam idParam}
 * configuration.
 *
 * Parameters for other commonly passed values such as
 * {@link Ext.data.proxy.Ajax#pageParam page number} or
 * {@link Ext.data.proxy.Ajax#startParam start row} may also be configured.
 *
 * # 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.
 */
Ext.define('Ext.data.Model', {
    alternateClassName: 'Ext.data.Record',
 
    requires: [
        'Ext.data.ErrorCollection',
        'Ext.data.operation.*',
        'Ext.data.field.*',
        'Ext.data.validator.Validator',
        'Ext.data.schema.Schema',
        'Ext.data.identifier.Generator',
        'Ext.data.identifier.Sequential'
    ],
    uses: [
        'Ext.data.Validation'
    ],
 
    /**
     * @property {Boolean} isEntity
     * The value `true` to identify this class and its subclasses.
     * @readonly
     */
    isEntity: true,
 
    /**
     * @property {Boolean} isModel
     * The value `true` to identify this class and its subclasses.
     * @readonly
     */
    isModel: true,
 
    // Record ids are more flexible.
    validIdRe: null,
 
    erasing: false,
 
    loadOperation: null,
    loadCount: 0,
 
    observableType: 'record',
 
    /**
     * @property {"C"/"R"/"U"/"D"} crudState
     * This value is initially "R" or "C" indicating the initial CRUD state. As the
     * record changes and the various joined parties (stores, sessions, etc.) are notified
     * this value is updated prior to these calls. In other words, the joined parties
     * are notified after the `crudState` is updated. This means that the `crudState`
     * property may be briefly out of sync with underlying changes if this state is used
     * outside of these notifications.
     *
     * The possible states have these meanings:
     *
     *  * "R" - The record is in a cleanly retrieved (unmodified) state.
     *  * "C" - The record is in a newly created (`phantom`) state.
     *  * "U" - The record is in an updated, `modified` (`dirty`) state.
     *  * "D" - The record is in a `dropped` state.
     *
     * @readonly
     * @protected
     * @since 6.2.0
     */
    crudState: 'R',
 
    /**
     * @property {"C"/"R"/"U"/"D"} crudStateWas
     * This value is initially `null` indicating there is no previous CRUD state. As the
     * record changes and the various joined parties (stores, sessions, etc.) are notified
     * this value is updated for the *subsequent* calls. In other words, the joined parties
     * are notified and then `crudStateWas` is modified for the next update.
     *
     * The value of this property has the same meaning as `crudState`.
     *
     * @readonly
     * @protected
     * @since 6.2.0
     */
    crudStateWas: null,
 
    constructor: function(data, session, skipStoreAddition) {
        var me = this,
            cls = me.self,
            identifier = cls.identifier,
            Model = Ext.data.Model,
            modelIdentifier = Model.identifier,
            idProperty = me.idField.name,
            array, id, initializeFn, internalId, len, i, fields;
 
        // Yes, this is here on purpose. See EXTJS-16494. The second
        // assignment seems to work around a strange JIT issue that prevents
        // this.data being assigned in random scenarios, even though the data
        // is passed into the constructor. The issue occurs on 4th gen iPads and
        // lower, possibly other older iOS devices.
        // A similar issue can occur with the hasListeners property of Observable
        // (see the constructor of Ext.mixin.Observable)
        me.data = me.data = data || (data = {});
        me.internalId = internalId = modelIdentifier.generate();
 
        //<debug>
        var dataId = data[idProperty]; // eslint-disable-line vars-on-top, one-var
 
        if (session && !session.isSession) {
            Ext.raise('Bad Model constructor argument 2 - "session" is not a Session');
        }
        //</debug>
 
        if ((array = data) instanceof Array) {
            me.data = data = {};
            fields = me.getFields();
            len = Math.min(fields.length, array.length);
 
            for (= 0; i < len; ++i) {
                data[fields[i].name] = array[i];
            }
        }
 
        if (!(initializeFn = cls.initializeFn)) {
            cls.initializeFn = initializeFn = Model.makeInitializeFn(cls);
        }
 
        if (!initializeFn.$nullFn) {
            cls.initializeFn(me);
        }
 
        // Must do this after running the initializeFn due to converters on idField
        if (!me.isSummaryModel) {
            if (!(me.id = id = data[idProperty]) && id !== 0) {
                //<debug>
                if (dataId) {
                    Ext.raise('The model ID configured in data ("' + dataId +
                              '") has been rejected by the ' + me.fieldsMap[idProperty].type +
                              ' field converter for the ' + idProperty + ' field');
                }
                //</debug>
 
                if (session) {
                    identifier = session.getIdentifier(cls);
                    id = identifier.generate();
                }
                else if (modelIdentifier === identifier) {
                    id = internalId;
                }
                else {
                    id = identifier.generate();
                }
 
                data[idProperty] = me.id = id;
                me.phantom = true;
                me.crudState = 'C';
            }
 
            if (session && !skipStoreAddition) {
                session.add(me);
            }
 
            // Needs to be set after the add to the session
            if (me.phantom) {
                me.crudStateWas = 'C';
            }
        }
 
        if (me.init && Ext.isFunction(me.init)) {
            me.init();
        }
    },
 
    /**
     * @property {String} entityName
     * The short name of this entity class. This name is derived from the `namespace` of
     * the associated `schema` and this class name. By default, a class is not given a
     * shortened name.
     *
     * All entities in a given `schema` must have a unique `entityName`.
     * 
     * For more details see "Relative Naming" in {@link Ext.data.schema.Schema}.
     */
 
    /**
     * @property {Boolean} editing
     * Internal flag used to track whether or not the model instance is currently being edited.
     * @readonly
     */
    editing: false,
 
    /**
     * @property {Boolean} dirty
     * True if this record has been modified.
     * @readonly
     */
    dirty: false,
 
    /**
     * @property {Ext.data.Session} session
     * The {@link Ext.data.Session} for this record.
     * @readonly
     */
    session: null,
 
    /**
     * @property {Boolean} dropped
     * True if this record is pending delete on the server. This is set by the `drop`
     * method and transmitted to the server by the `save` method.
     * @readonly
     */
    dropped: false,
 
    /**
     * @property {Boolean} erased
     * True if this record has been erased on the server. This flag is set of the `erase`
     * method.
     * @readonly
     */
    erased: false,
 
    /**
     * @cfg {String} clientIdProperty
     * The name of the property a server will use to send back a client-generated id in a
     * `create` or `update` `{@link Ext.data.operation.Operation operation}`.
     *
     * If specified, this property cannot have the same name as any other field.
     *
     * For example:
     *
     *      Ext.define('Person', {
     *          idProperty: 'id',  // this is the default value (for clarity)
     *
     *          clientIdProperty: 'clientId',
     *
     *          identifier: 'negative', // to generate -1, -2 etc on the client
     *
     *          fields: [ 'name' ]
     *      });
     *
     *      var person = new Person({
     *          // no id provided, so -1 is generated
     *          name: 'Clark Kent'
     *      });
     *
     * The server is given this data during the `create`:
     *
     *      {
     *          id: -1,
     *          name: 'Clark Kent'
     *      }
     *
     * The server allocates a real id and responds like so:
     *
     *      {
     *          id: 427,
     *          clientId: -1
     *      }
     *
     * This property is most useful when creating multiple entities in a single call to
     * the server in a `{@link Ext.data.operation.Create create operation}`. Alternatively,
     * the server could respond with records that correspond one-to-one to those sent in
     * the `operation`.
     *
     * For example the client could send a `create` with this data:
     *
     *      [ { id: -1, name: 'Clark Kent' },
     *        { id: -2, name: 'Peter Parker' },
     *        { id: -3, name: 'Bruce Banner' } ]
     *
     * And the server could respond in the same order:
     *
     *      [ { id: 427 },      // updates id = -1
     *        { id: 428 },      // updates id = -2
     *        { id: 429 } ]     // updates id = -3
     *
     * Or using `clientIdProperty` the server could respond in arbitrary order:
     *
     *      [ { id: 427, clientId: -3 },
     *        { id: 428, clientId: -1 },
     *        { id: 429, clientId: -2 } ]
     *
     * **IMPORTANT:** When upgrading from previous versions be aware that this property
     * used to perform the role of `{@link Ext.data.writer.Writer#clientIdProperty}` as
     * well as that described above. To continue send a client-generated id as other than
     * the `idProperty`, set `clientIdProperty` on the `writer`. A better solution, however,
     * is most likely a properly configured `identifier` as that would work better with
     * associations.
     */
    clientIdProperty: null,
 
    evented: false,
 
    /**
     * @property {Boolean} phantom
     * True when the record does not yet exist in a server-side database. Any record which
     * has a real database identity set as its `idProperty` is NOT a phantom -- it's real.
     */
    phantom: false,
 
    /**
     * @cfg {String} idProperty
     * The name of the field treated as this Model's unique id.
     *
     * If changing the idProperty in a subclass, the generated id field will replace the
     * one generated by the superclass, for example;
     *
     *      Ext.define('Super', {
     *          extend: 'Ext.data.Model',
     *          fields: ['name']
     *      });
     *
     *      Ext.define('Sub', {
     *          extend: 'Super',
     *          idProperty: 'customId'
     *      });
     *
     *      var fields = Super.getFields();
     *      // Has 2 fields, "name" & "id"
     *      console.log(fields[0].name, fields[1].name, fields.length);
     *
     *      fields = Sub.getFields();
     *      // Has 2 fields, "name" & "customId", "id" is replaced
     *      console.log(fields[0].name, fields[1].name, fields.length);
     *
     * The data values for this field must be unique or there will be id value collisions
     * in the {@link Ext.data.Store Store}.
     */
    idProperty: 'id',
 
    /**
     * @cfg {Object} manyToMany
     * A config object for a {@link Ext.data.schema.ManyToMany ManyToMany} association.
     * See the class description for {@link Ext.data.schema.ManyToMany ManyToMany} for
     * configuration examples.
     */
    manyToMany: null,
 
    /**
     * @cfg {String/Object} identifier
     * The id generator to use for this model. The `identifier` generates values for the
     * {@link #idProperty} when no value is given. Records with client-side generated
     * values for {@link #idProperty} are called {@link #phantom} records since they are
     * not yet known to the server.
     *
     * 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.identifier.Sequential'],
     *          identifier: 'sequential',
     *          ...
     *      });
     *
     * The above would generate {@link Ext.data.identifier.Sequential sequential} id's
     * such as 1, 2, 3 etc..
     *
     * Another useful id generator is {@link Ext.data.identifier.Uuid}:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          requires: ['Ext.data.identifier.Uuid'],
     *          identifier: 'uuid',
     *          ...
     *      });
     *
     * An id generator can also be further configured:
     *
     *      Ext.define('MyApp.data.MyModel', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              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',
     *          identifier: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     *      Ext.define('MyApp.data.MyModelY', {
     *          extend: 'Ext.data.Model',
     *          identifier: {
     *              type: 'sequential',
     *              id: 'xy'
     *          }
     *      });
     *
     * For more complex, shared id generators, a custom generator is the best approach.
     * See {@link Ext.data.identifier.Generator} for details on creating custom id
     * generators.
     */
    identifier: null,
 
    // Fields config and property
    // @cmd-auto-dependency {aliasPrefix: "data.field."}
    /**
     * @cfg {Object[]/String[]} fields
     * An Array of `Ext.data.field.Field` config objects, simply the field 
     * {@link Ext.data.field.Field#name name}, or a mix of config objects and strings. 
     * If just a name is given, the field type defaults to `auto`.
     * 
     * In a {@link Ext.data.field.Field Field} config object you may pass the alias of 
     * the `Ext.data.field.*` type using the `type` config option.
     * 
     *     // two fields are set:
     *     // - an 'auto' field with a name of 'firstName'
     *     // - and an Ext.data.field.Integer field with a name of 'age'
     *     fields: ['firstName', {
     *         type: 'int',
     *         name: 'age'
     *     }]
     * 
     * Fields will automatically be created at read time for any for any keys in the 
     * data passed to the Model's {@link #proxy proxy's} 
     * {@link Ext.data.reader.Reader reader} whose name is not explicitly configured in 
     * the `fields` config.
     * 
     * Extending a Model class will inherit all the `fields` from the superclass / 
     * ancestor classes.
     */
    /**
     * @property {Ext.data.field.Field[]} fields
     * An array fields defined for this Model (including fields defined in superclasses)
     * in ordinal order; that is in declaration order.
     * @private
     * @readonly
     */
 
    /**
     * @property {Object} fieldOrdinals
     * This property is indexed by field name and contains the ordinal of that field. The
     * ordinal often has meaning to servers and is derived based on the position in the
     * `fields` array.
     * 
     * This can be used like so:
     * 
     *      Ext.define('MyApp.models.User', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: [
     *              { name: 'name' }
     *          ]
     *      });
     * 
     *      var nameOrdinal = MyApp.models.User.fieldOrdinals.name;
     *      
     *      // or, if you have an instance:
     *
     *      var user = new MyApp.models.User();
     *      var nameOrdinal = user.fieldOrdinals.name;
     *
     * @private
     * @readonly
     */
 
    /**
      * @property {Object} modified
      * A hash of field values which holds the initial values of fields before a set of
      * edits are {@link #commit committed}.
      */
 
    /**
     * @property {Object} previousValues
     * This object is similar to the `modified` object except it holds the data values as
     * they were prior to the most recent change.
     * @readonly
     * @private
     */
    previousValues: undefined, // Not "null" so getPrevious returns undefined first time
 
    // @cmd-auto-dependency { aliasPrefix : "proxy.", defaultPropertyName : "defaultProxyType"}
    /**
     * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
     * The {@link Ext.data.proxy.Proxy proxy} to use for this class.
     *
     * By default, the proxy is configured from the {@link Ext.data.schema.Schema schema}.
     * You can ignore the schema defaults by setting `schema: false` on the `proxy` config.
     *
     *      Ext.define('MyApp.data.CustomProxy', {
     *          extend: 'Ext.data.proxy.Ajax',
     *          alias: 'proxy.customproxy',
     *
     *          url: 'users.json'
     *      });
     *
     *      Ext.define('MyApp.models.CustomModel', {
     *          extend: 'Ext.data.Model',
     *
     *          fields: ['name'],
     *          proxy: {
     *              type: 'customproxy,
     *              schema: false
     *          }
     *      });
     *
     * With `schema: false`, the `url` of the proxy will be used instead of what has been defined
     * on the schema.
     */
    proxy: undefined,
 
    /**
     * @cfg {String/Object} [schema='default']
     * The name of the {@link Ext.data.schema.Schema schema} to which this entity and its
     * associations belong. For details on custom schemas see `Ext.data.schema.Schema`.
     */
    /**
     * @property {Ext.data.schema.Schema} schema
     * The `Ext.data.schema.Schema` to which this entity and its associations belong.
     * @readonly
     */
    schema: 'default',
 
    /**
     * @cfg {Object} summary
     * Summary fields are a special kind of field that is used to assist in creating an
     * aggregation for this model. A new model type that extends this model will be
     * created, accessible via {@link #method-getSummaryModel}. This summary model will
     * have these virtual aggregate fields in the fields collection like a normal model.
     * Each key in the object is the field name. The value for each field should mirror
     * the {@link #cfg-fields}, excluding the `name` option. The summary model generated
     * will have 2 fields, 'rate', which will aggregate using an average and maxRate,
     * which will aggregate using the maximum value.
     *
     * See {@link Ext.data.summary.Base} for more information.
     *
     *      Ext.define('User', {
     *          extend: 'Ext.data.Model',
     *          fields: [{
     *              name: 'rate',
     *              summary: 'avg'
     *          }],
     *
     *          summary: {
     *              maxRate: {
     *                  field: 'rate', // calculated from rate field
     *                  summary: 'max'
     *              }
     *          }
     *      });
     *
     * @since 6.5.0
     */
    summary: null,
 
    /**
     * @cfg {String} versionProperty
     * If specified, this is the name of the property that contains the entity "version".
     * The version property is used to manage a long-running transaction and allows the
     * detection of simultaneous modification.
     * 
     * The way a version property is used is that the client receives the version as it
     * would any other entity property. When saving an entity, this property is always
     * included in the request and the server uses the value in a "conditional update".
     * If the current version of the entity on the server matches the version property
     * sent by the client, the update is allowed. Otherwise, the update fails.
     * 
     * On successful update, both the client and server increment the version. This is
     * done on the server in the conditional update and on the client when it receives a
     * success on its update request.
     */
    versionProperty: null,
 
    /**
     * @property {Number} generation
     * This property is incremented on each modification of a record.
     * @readonly
     * @since 5.0.0
     */
    generation: 1,
 
    /**
     * @cfg {Object[]} validators
     * An array of {@link Ext.data.validator.Validator validators} for this model.
     */
 
    /**
     * @cfg {String} validationSeparator
     * If specified this property is used to concatenate multiple errors for each field
     * as reported by the `validators`.
     */
    validationSeparator: null,
 
    /**
     * @cfg {Boolean} convertOnSet
     * Set to `false` to prevent any converters from being called on fields specified in 
     * a {@link Ext.data.Model#set set} operation.
     * 
     * **Note:** Setting the config to `false` will only prevent the convert / calculate 
     * call when the set `fieldName` param matches the field's `{@link #name}`.  In the 
     * following example the calls to set `salary` will not execute the convert method 
     * on `set` while the calls to set `vested` will execute the convert method on the 
     * initial read as well as on `set`.
     * 
     * Example model definition:
     * 
     *     Ext.define('MyApp.model.Employee', {
     *         extend: 'Ext.data.Model',
     *         fields: ['yearsOfService', {
     *             name: 'salary',
     *             convert: function (val) {
     *                 var startingBonus = val * .1;
     *                 return val + startingBonus;
     *             }
     *         }, {
     *             name: 'vested',
     *             convert: function (val, record) {
     *                 return record.get('yearsOfService') >= 4;
     *             },
     *             depends: 'yearsOfService'
     *         }],
     *         convertOnSet: false
     *     });
     *     
     *     var tina = Ext.create('MyApp.model.Employee', {
     *         salary: 50000,
     *         yearsOfService: 3
     *     });
     *     
     *     console.log(tina.get('salary')); // logs 55000
     *     console.log(tina.get('vested')); // logs false
     *     
     *     tina.set({
     *         salary: 60000,
     *         yearsOfService: 4
     *     });
     *     console.log(tina.get('salary')); // logs 60000
     *     console.log(tina.get('vested')); // logs true
     */
    convertOnSet: true,
 
    // Associations configs and properties
    /**
     * @cfg {Object[]} associations
     * An array of {@link Ext.data.schema.Association associations} for this model.
     *
     * For further documentation, see {@link Ext.data.schema.Association}.
     *
     * @deprecated 6.2.0 Use `hasMany/hasOne/belongsTo`.
     */
 
    /**
     * @cfg {String/Object/String[]/Object[]} hasMany
     * One or more `Ext.data.schema.HasMany` associations for this model.
     */
 
    /**
     * @cfg {String/Object/String[]/Object[]} hasOne
     * One or more `Ext.data.schema.HasOne` associations for this model.
     */
 
    /**
     * @cfg {String/Object/String[]/Object[]} belongsTo
     * One or more `Ext.data.schema.BelongsTo` associations for this model.
     */
 
    /**
     * 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 `endEdit` or `cancelEdit`.
     */
    beginEdit: function() {
        var me = this,
            modified = me.modified,
            previousValues = me.previousValues;
 
        if (!me.editing) {
            me.editing = true;
 
            me.editMemento = {
                dirty: me.dirty,
                data: Ext.apply({}, me.data),
                generation: me.generation,
                modified: modified && Ext.apply({}, modified),
                previousValues: previousValues && Ext.apply({}, previousValues)
            };
        }
    },
 
    /**
     * Calculate all summary fields on this record.
     * @param {Ext.data.Model[]} records The records to use for calculation.
     *
     * @since 6.5.0
     */
    calculateSummary: function(records) {
        var fields = this.getFields(),
            len = fields.length,
            recLen = records.length,
            i, result, summary, prop, name, field;
 
        for (= 0; i < len; ++i) {
            field = fields[i];
            summary = field.getSummary();
 
            if (summary) {
                result = result || {};
                name = field.name;
                prop = field.summaryField || name;
                result[name] = summary.calculate(records, prop, 'data', 0, recLen);
            }
        }
 
        if (result) {
            this.set(result, this._commitOptions);
        }
    },
 
    /**
     * Cancels all changes made in the current edit operation.
     */
    cancelEdit: function() {
        var me = this,
            editMemento = me.editMemento,
            validation = me.validation;
 
        if (editMemento) {
            me.editing = false;
 
            // reset the modified state, nothing changed since the edit began
            Ext.apply(me, editMemento);
            me.editMemento = null;
 
            if (validation && validation.syncGeneration !== me.generation) {
                validation.syncGeneration = 0;
            }
        }
    },
 
    /**
     * 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 any stores of the change.
     * @param {String[]} [modifiedFieldNames] Array of field names changed during edit.
     */
    endEdit: function(silent, modifiedFieldNames) {
        var me = this,
            editMemento = me.editMemento;
 
        if (editMemento) {
            me.editing = false;
            me.editMemento = null;
 
            // Since these reflect changes we never notified others about, the real set
            // of "previousValues" is what we captured in the memento:
            me.previousValues = editMemento.previousValues;
 
            if (!silent) {
                if (!modifiedFieldNames) {
                    modifiedFieldNames = me.getModifiedFieldNames(editMemento.data);
                }
 
                if (me.dirty || (modifiedFieldNames && modifiedFieldNames.length)) {
                    me.callJoined('afterEdit', [modifiedFieldNames]);
                }
            }
        }
    },
 
    getField: function(name) {
        return this.self.getField(name);
    },
 
    /**
     * Get the fields array for this model.
     * @return {Ext.data.field.Field[]} The fields array
     */
    getFields: function() {
        return this.self.getFields();
    },
 
    getFieldsMap: function() {
        return this.fieldsMap;
    },
 
    /**
     * Get the idProperty for this model.
     * @return {String} The idProperty
     */
    getIdProperty: function() {
        return this.idProperty;
    },
 
    /**
     * Returns the unique ID allocated to this model instance as defined by `idProperty`.
     * @return {Number/String} The id
     */
    getId: function() {
        return this.id;
    },
 
    /**
     * Return a unique observable ID. Model is not observable but tree nodes
     * (`Ext.data.NodeInterface`) are, so they must be globally unique within the
     * {@link #observableType}.
     * @protected
     */
    getObservableId: function() {
        return this.internalId;
    },
 
    /**
     * Sets the model instance's id field to the given id.
     * @param {Number/String} id The new id.
     * @param {Object} [options] See {@link #set}.
     */
    setId: function(id, options) {
        this.set(this.idProperty, id, options);
    },
 
    /**
     * This method returns the value of a field given its name prior to its most recent
     * change.
     * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
     * @return {Object} The value of the given field prior to its current value. `undefined`
     * if there is no previous value;
     */
    getPrevious: function(fieldName) {
        var previousValues = this.previousValues;
 
        return previousValues && previousValues[fieldName];
    },
 
    /**
     * Returns true if the passed field name has been `{@link #modified}` since the load
     * or last commit.
     * @param {String} fieldName The field's {@link Ext.data.field.Field#name name}.
     * @return {Boolean} 
     */
    isModified: function(fieldName) {
        var modified = this.modified;
 
        return !!(modified && modified.hasOwnProperty(fieldName));
    },
 
    /**
     * Returns the original value of a modified field. If there is no modified value,
     * `undefined` will be return. Also see {@link #isModified}.
     * @param {String} fieldName The name of the field for which to return the original value.
     * @return {Object} modified
     */
    getModified: function(fieldName) {
        var out;
 
        if (this.isModified(fieldName)) {
            out = this.modified[fieldName];
        }
 
        return out;
    },
 
    /**
     * Returns the value of the given field.
     * @param {String} fieldName The name of the field.
     * @return {Object} The value of the specified field.
     */
    get: function(fieldName) {
        return this.data[fieldName];
    },
 
    // This object is used whenever the set() method is called and given a string as the
    // first argument. This approach saves memory (and GC costs) since we could be called
    // a lot.
    _singleProp: {},
 
    _rejectOptions: {
        convert: false,
        silent: true
    },
 
    /**
     * Sets the given field to the given value. For example:
     * 
     *      record.set('name', 'value');
     * 
     * This method can also be passed an object containing multiple values to set at once.
     * For example:
     * 
     *      record.set({
     *          name: 'value',
     *          age: 42
     *      });
     * 
     * The following store events are fired when the modified record belongs to a store:
     *
     *  - {@link Ext.data.Store#event-beginupdate beginupdate}
     *  - {@link Ext.data.Store#event-update update}
     *  - {@link Ext.data.Store#event-endupdate endupdate}
     * 
     * @param {String/Object} fieldName The field to set, or an object containing key/value 
     * pairs.
     * @param {Object} newValue The value for the field (if `fieldName` is a string).
     * @param {Object} [options] Options for governing this update.
     * @param {Boolean} [options.convert=true] Set to `false` to  prevent any converters from 
     * being called during the set operation. This may be useful when setting a large bunch of 
     * raw values.
     * @param {Boolean} [options.dirty=true] Pass `false` if the field values are to be
     * understood as non-dirty (fresh from the server). When `true`, this change will be
     * reflected in the `modified` collection.
     * @param {Boolean} [options.commit=false] Pass `true` to call the {@link #commit} method 
     * after setting fields. If this option is passed, the usual after change processing will 
     * be bypassed. {@link #commit Commit} will be called even if there are no field changes.
     * @param {Boolean} [options.silent=false] Pass `true` to suppress notification of any
     * changes made by this call. Use with caution.
     * @return {String[]} The array of modified field names or null if nothing was modified.
     */
    set: function(fieldName, newValue, options) {
        var me = this,
            cls = me.self,
            data = me.data,
            modified = me.modified,
            prevVals = me.previousValues,
            session = me.session,
            single = Ext.isString(fieldName),
            opt = (single ? options : newValue),
            convertOnSet = opt ? opt.convert !== false : me.convertOnSet,
            fieldsMap = me.fieldsMap,
            silent = opt && opt.silent,
            commit = opt && opt.commit,
            updateRefs = !(opt && opt.refs === false) && session,
            // Don't need to do dirty processing with commit, since we'll always
            // end up with nothing modified and not dirty
            dirty = !(opt && opt.dirty === false && !commit),
            modifiedFieldNames = null,
            dirtyRank = 0,
            associations = me.associations,
            currentValue, field, idChanged, key, name, oldId, comparator, dep, dependents,
            i, numFields, newId, rankedFields, reference, value, values, roleName;
 
        if (single) {
            values = me._singleProp;
            values[fieldName] = newValue;
        }
        else {
            values = fieldName;
        }
 
        if (!(rankedFields = cls.rankedFields)) {
            // On the first edit of a record of this type we need to ensure we have the
            // topo-sort done:
            rankedFields = cls.rankFields();
        }
 
        numFields = rankedFields.length;
 
        do {
            for (name in values) {
                value = values[name];
                currentValue = data[name];
                comparator = me;
                field = fieldsMap[name];
 
                if (field) {
                    if (convertOnSet && field.convert) {
                        value = field.convert(value, me);
                    }
 
                    comparator = field;
                    reference = field.reference;
                }
                else {
                    reference = null;
                }
 
                if (comparator.isEqual(currentValue, value)) {
                    continue; // new value is the same, so no change...
                }
 
                data[name] = value;
                (modifiedFieldNames || (modifiedFieldNames = [])).push(name);
                (prevVals || (me.previousValues = prevVals = {}))[name] = currentValue;
 
                // We need the cls to be present because it means the association class is loaded,
                // otherwise it could be pending.
                if (reference && reference.cls) {
                    if (updateRefs) {
                        session.updateReference(me, field, value, currentValue);
                    }
 
                    reference.onValueChange(me, session, value, currentValue);
                }
 
                i = (dependents = field && field.dependents) && dependents.length;
 
                while (i-- > 0) {
                    // we use the field instance to hold the dirty bit to avoid any
                    // extra allocations... we'll clear this before we depart. We do
                    // this so we can perform the fewest recalculations possible as
                    // each dependent field only needs to be recalculated once.
                    (dep = dependents[i]).dirty = true;
                    dirtyRank = dirtyRank ? Math.min(dirtyRank, dep.rank) : dep.rank;
                }
 
                if (!field || field.persist) {
                    if (modified && modified.hasOwnProperty(name)) {
                        if (!dirty || comparator.isEqual(modified[name], value)) {
                            // The original value in me.modified equals the new value, so
                            // the field is no longer modified:
                            delete modified[name];
                            me.dirty = -1; // fix me.dirty later (still truthy)
                        }
                    }
                    else if (dirty) {
                        if (!modified) {
                            me.modified = modified = {}; // create only when needed
                        }
 
                        me.dirty = true;
                        modified[name] = currentValue;
                    }
                }
 
                if (name === me.idField.name) {
                    idChanged = true;
                    oldId = currentValue;
                    newId = value;
                }
            }
 
            if (!dirtyRank) {
                // Unless there are dependent fields to process we can break now. This is
                // what will happen for all code pre-dating the depends or simply not
                // using it, so it will add very little overhead when not used.
                break;
            }
 
            // dirtyRank has the minimum rank (a 1-based value) of any dependent field
            // that needs recalculating due to changes above. The way we go about this
            // is to use our helper object for processing single argument invocations
            // to process just this one field. This is because the act of setting it
            // may cause another field to be invalidated, so while we cannot know at
            // this moment all the fields we need to recalculate, we know that only
            // those following this field in rankedFields can possibly be among them.
 
            field = rankedFields[dirtyRank - 1]; // dirtyRank is 1-based
            field.dirty = false; // clear just this field's dirty state
 
            if (single) {
                delete values[fieldName]; // cleanup last value
            }
            else {
                values = me._singleProp; // switch over
                single = true;
            }
 
            fieldName = field.name;
            values[fieldName] = data[fieldName];
            // We are now processing a dependent field, so we want to force a
            // convert to occur because it's the only way it will get a value
            convertOnSet = true;
 
            // Since dirtyRank is 1-based and refers to the field we need to handle
            // on this pass, we can treat it like an index for a minute and look at
            // the next field on towards the end to find the index of the next dirty
            // field.
            for (; dirtyRank < numFields; ++dirtyRank) {
                if (rankedFields[dirtyRank].dirty) {
                    break;
                }
            }
 
            if (dirtyRank < numFields) {
                // We found a field after this one marked as dirty so make the index
                // a proper 1-based rank:
                ++dirtyRank;
            }
            else {
                // We did not find any more dirty fields after this one, so clear the
                // dirtyRank and we will perhaps fall out after the next update
                dirtyRank = 0;
            }
        } while (1); // eslint-disable-line no-constant-condition
 
        if (me.dirty < 0) {
            // 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;
                }
            }
        }
 
        if (single) {
            // cleanup our reused object for next time... important to do this before
            // we fire any events or call anyone else (like afterEdit)!
            delete values[fieldName];
        }
 
        ++me.generation;
 
        if (idChanged) {
            me.id = newId;
            me.onIdChanged(newId, oldId);
            me.callJoined('onIdChanged', [oldId, newId]);
 
            if (associations) {
                for (roleName in associations) {
                    associations[roleName].onIdChanged(me, oldId, newId);
                }
            }
        }
 
        if (commit) {
            me.commit(silent, modifiedFieldNames);
        }
        else if (!silent && !me.editing && modifiedFieldNames) {
            me.callJoined('afterEdit', [modifiedFieldNames]);
        }
 
        return modifiedFieldNames;
    },
 
    /**
     * 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#event-update} event to have their
     * code notified of reject operations.
     *
     * @param {Boolean} [silent=false] `true` to skip notification of the owning store of the
     * change.
     */
    reject: function(silent) {
        var me = this,
            modified = me.modified;
 
        //<debug>
        if (me.erased) {
            Ext.raise('Cannot reject once a record has been erased.');
        }
        //</debug>
 
        if (modified) {
            me.set(modified, me._rejectOptions);
        }
 
        me.dropped = false;
        me.clearState();
 
        if (!silent) {
            me.callJoined('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#event-update} event to have their
     * code notified of commit operations.
     *
     * @param {Boolean} [silent=false] Pass `true` to skip notification of the owning store of the
     * change.
     * @param {String[]} [modifiedFieldNames] Array of field names changed during sync with server
     * if known. Omit or pass `null` if unknown. An empty array means that it is known that
     * no fields were modified by the server's response.
     * Defaults to false.
     */
    commit: function(silent, modifiedFieldNames) {
        var me = this,
            versionProperty = me.versionProperty,
            data = me.data,
            erased;
 
        me.clearState();
 
        if (versionProperty && !me.phantom && !isNaN(data[versionProperty])) {
            ++data[versionProperty];
        }
 
        me.phantom = false;
 
        if (me.dropped) {
            me.erased = erased = true;
        }
 
        if (!silent) {
            if (erased) {
                me.callJoined('afterErase');
            }
            else {
                me.callJoined('afterCommit', [modifiedFieldNames]);
            }
        }
    },
 
    clearState: function() {
        var me = this;
 
        me.dirty = me.editing = false;
        me.editMemento = me.modified = null;
    },
 
    /**
     * Marks this record as `dropped` and waiting to be deleted on the server. When a
     * record is dropped, it is automatically removed from all association stores and
     * any child records associated to this record are also dropped (a "cascade delete")
     * depending on the `cascade` parameter.
     *
     * @param {Boolean} [cascade=true] Pass `false` to disable the cascade to drop child
     * records.
     * @since 5.0.0
     */
    drop: function(cascade) {
        var me = this,
            associations = me.associations,
            session = me.session,
            roleName;
 
        if (me.erased || me.dropped) {
            return;
        }
 
        me.dropped = true;
 
        if (associations && cascade !== false) {
            for (roleName in associations) {
                associations[roleName].onDrop(me, session);
            }
        }
 
        me.callJoined('afterDrop');
 
        if (me.phantom) {
            me.setErased();
        }
    },
 
    /**
     * Tells this model instance that an observer is looking at it.
     * @param {Ext.data.Store} owner The store or other owner object to which this model
     * has been added.
     */
    join: function(owner) {
        var me = this,
            joined = me.joined;
 
        // Optimize this, gets called a lot
        if (!joined) {
            joined = me.joined = [owner];
        }
        else if (!joined.length) {
            joined[0] = owner;
        }
        else {
            // TODO: do we need joined here? Perhaps push will do.
            Ext.Array.include(joined, owner);
        }
 
        if (owner.isStore && !me.store) {
            /**
            * @property {Ext.data.Store} store
            * The {@link Ext.data.Store Store} to which this instance belongs.
            *
            * **Note:** If this instance is bound to multiple stores, this property
            * will reference only the first.
            */
            me.store = owner;
        }
    },
 
    /**
     * Tells this model instance that it has been removed from the store.
     * @param {Ext.data.Store} owner The store or other owner object from which this
     * model has been removed.
     */
    unjoin: function(owner) {
        var me = this,
            joined = me.joined,
 
            // TreeModels are never joined to their TreeStore.
            // But unjoin is called by the base class's onCollectionRemove, so joined may be
            // undefined.
            len = joined && joined.length,
            store = me.store,
            i;
 
        if (owner === me.session) {
            me.session = null;
        }
        else {
            if (len === 1 && joined[0] === owner) {
                joined.length = 0;
            }
            else if (len) {
                Ext.Array.remove(joined, owner);
            }
 
            if (store === owner) {
                store = null;
 
                if (joined) {
                    for (= 0, len = joined.length; i < len; ++i) {
                        owner = joined[i];
 
                        if (owner.isStore) {
                            store = owner;
 
                            break;
                        }
                    }
                }
 
                me.store = store;
            }
        }
    },
 
    /**
     * Creates a clone of this record. States like `dropped`, `phantom` and `dirty` are
     * all preserved in the cloned record.
     *
     * @param {Ext.data.Session} [session] The session to which the new record
     * belongs.
     * @return {Ext.data.Model} The cloned record.
     */
    clone: function(session) {
        var me = this,
            modified = me.modified,
            ret = me.copy(me.id, session);
 
        if (modified) {
            // Restore the modified fields state
            ret.modified = Ext.apply({}, modified);
        }
 
        ret.dirty = me.dirty;
        ret.dropped = me.dropped;
        ret.phantom = me.phantom;
 
        return ret;
    },
 
    /**
     * Creates a clean copy of this record. The returned record will not consider any its
     * fields as modified.
     *
     * To generate a phantom instance with a new id pass `null`:
     *
     *     var rec = record.copy(null); // clone the record but no id (one is generated)
     *
     * @param {String} [newId] A new id, defaults to the id of the instance being copied.
     * See `{@link Ext.data.Model#idProperty idProperty}`.
     * @param {Ext.data.Session} [session] The session to which the new record
     * belongs.
     *
     * @return {Ext.data.Model} 
     */
    copy: function(newId, session) {
        var me = this,
            data = Ext.apply({}, me.data),
            idProperty = me.idProperty,
            T = me.self;
 
        if (newId || newId === 0) {
            data[idProperty] = newId;
        }
        else if (newId === null) {
            delete data[idProperty];
        }
 
        return new T(data, session);
    },
 
    /**
     * Returns the configured Proxy for this Model.
     * @return {Ext.data.proxy.Proxy} The proxy
     */
    getProxy: function() {
        return this.self.getProxy();
    },
 
    /**
     * Returns the `Ext.data.Validation` record holding the results of this record's
     * `validators`. This record is lazily created on first request and is then kept on
     * this record to be updated later.
     *
     * See the class description for more about `validators`.
     *
     * @param {Boolean} [refresh] Pass `false` to not call the `refresh` method on the
     * validation instance prior to returning it. Pass `true` to force a `refresh` of the
     * validation instance. By default the returned record is only refreshed if changes
     * have been made to this record.
     * @return {Ext.data.Validation} The `Validation` record for this record.
     * @since 5.0.0
     */
    getValidation: function(refresh) {
        var me = this,
            ret = me.validation;
 
        if (!ret) {
            me.validation = ret = new Ext.data.Validation();
            ret.attach(me);
        }
 
        if (refresh === true || (refresh !== false && ret.syncGeneration !== me.generation)) {
            ret.refresh(refresh);
        }
 
        return ret;
    },
 
    /**
     * Validates the current data against all of its configured {@link #validators}. The
     * returned collection holds an object for each reported problem from a `validator`.
     *
     * @return {Ext.data.ErrorCollection} The errors collection.
     * @deprecated 5.0 Use `getValidation` instead.
     */
    validate: function() {
        return new Ext.data.ErrorCollection().init(this);
    },
 
    /**
     * Checks if the model is valid. See {@link #getValidation}.
     * @return {Boolean} True if the model is valid.
     */
    isValid: function() {
        return this.getValidation().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();
    },
 
    /**
     * @method erase
     * @localdoc Destroys the model using the configured proxy.  The erase action is
     * asynchronous.  Any processing of the erased record should be done in a callback.
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *
     *     // pass the phantom record data to the server to be saved
     *     user.save({
     *         success: function(record, operation) {
     *             // do something if the save succeeded
     *             // erase the created record
     *             record.erase({
     *                 failure: function(record, operation) {
     *                     // do something if the erase failed
     *                 },
     *                 success: function(record, operation) {
     *                     // do something if the erase succeeded
     *                 },
     *                 callback: function(record, operation, success) {
     *                     // do something if the erase succeeded or failed
     *                 }
     *             });
     *         }
     *     });
     *
     * **NOTE:** If a {@link #phantom} record is erased it will not be processed via the
     * proxy.  However, any passed `success` or `callback` functions will be called.
     *
     * The options param is an {@link Ext.data.operation.Destroy} config object
     * containing success, failure and callback functions, plus optional scope.
     *
     * @inheritdoc #method-load
     * @return {Ext.data.operation.Destroy} The destroy operation
     */
    erase: function(options) {
        var me = this;
 
        me.erasing = true;
 
        // Drop causes a removal from the backing Collection.
        // The store's onCollectionRemove will respond to this by adding the record to
        // its "to remove" stack and setting its needsSync
        // flag unless the above "erasing" flag is set.
        me.drop();
 
        me.erasing = false;
 
        return me.save(options);
    },
 
    setErased: function() {
        this.erased = true;
        this.callJoined('afterErase');
    },
 
    /**
     * Gets an object of only the fields that have been modified since this record was
     * created or committed. Only persistent fields are tracked in the `modified` set so
     * this method will only return changes to persistent fields.
     *
     * For more control over the returned data, see `{@link #getData}`.
     * @return {Object} 
     */
    getChanges: function() {
        return this.getData(this._getChangesOptions);
    },
 
    /**
     * Returns the array of fields that are declared as critical (must always send).
     * @return {Ext.data.field.Field[]} 
     */
    getCriticalFields: function() {
        var cls = this.self,
            ret = cls.criticalFields;
 
        if (!ret) {
            cls.rankFields();
            ret = cls.criticalFields;
        }
 
        return ret;
    },
 
    /**
     * This method is called by the {@link Ext.data.reader.Reader} after loading a model from
     * the server. This is after processing any inline associations that are available.
     * 
     * @method onLoad
     *
     * @protected
     * @template
     */
 
    /**
     * 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: [
     *                     ...
     *                 ]
     *             }
     *         ]
     *     }
     *
     * @param {Object} [result] The object on to which the associations will be added. If
     * no object is passed one is created. This object is then returned.
     * @param {Boolean/Object} [options] An object containing options describing the data
     * desired.
     * @param {Boolean} [options.associated=true] Pass `true` to include associated data from
     * other associated records.
     * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
     * have been modified. Note that field modifications are only tracked for fields that
     * are not declared with `persist` set to `false`. In other words, only persistent
     * fields have changes tracked so passing `true` for this means `options.persist` is
     * redundant.
     * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
     * This is only meaningful when `options.changes` is `true` since critical fields may
     * not have been modified.
     * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
     * This is implied when `options.changes` is set to `true`.
     * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
     * method on the returned fields.
     * @return {Object} The nested data set for the Model's loaded associations.
     */
    getAssociatedData: function(result, options) {
        var me = this,
            associations = me.associations,
            deep, i, item, items, itemData, length,
            record, role, roleName, opts, clear, associated;
 
        result = result || {};
 
        me.$gathering = 1;
 
        if (options) {
            options = Ext.apply({}, options);
        }
 
        for (roleName in associations) {
            role = associations[roleName];
            item = role.getAssociatedItem(me);
 
            if (!item || item.$gathering) {
                continue;
            }
 
            if (item.isStore) {
                item.$gathering = 1;
 
                items = item.getData().items; // get the records for the store
                length = items.length;
                itemData = [];
 
                for (= 0; i < length; ++i) {
                    // NOTE - we don't check whether the record is gathering here because
                    // we cannot remove it from the store (it would invalidate the index
                    // values and misrepresent the content). Instead we tell getData to
                    // only get the fields vs descend further.
                    record = items[i];
                    deep = !record.$gathering;
                    record.$gathering = 1;
 
                    if (options) {
                        associated = options.associated;
 
                        if (associated === undefined) {
                            options.associated = deep;
                            clear = true;
                        }
                        else if (!deep) {
                            options.associated = false;
                            clear = true;
                        }
 
                        opts = options;
                    }
                    else {
                        opts = deep ? me._getAssociatedOptions : me._getNotAssociatedOptions;
                    }
 
                    itemData.push(record.getData(opts));
 
                    if (clear) {
                        options.associated = associated;
                        clear = false;
                    }
 
                    delete record.$gathering;
                }
 
                delete item.$gathering;
            }
            else {
                opts = options || me._getAssociatedOptions;
 
                if (options && options.associated === undefined) {
                    opts.associated = true;
                }
 
                itemData = item.getData(opts);
            }
 
            result[roleName] = itemData;
        }
 
        delete me.$gathering;
 
        return result;
    },
 
    /**
     * Gets all values for each field in this model and returns an object containing the
     * current data. This can be tuned by passing an `options` object with various
     * properties describing the desired result. Passing `true` simply returns all fields
     * *and* all associated record data.
     *
     * To selectively gather some associated data, the `options` object can be used as
     * follows:
     *
     *      var data = order.getData({
     *          associated: {
     *              orderItems: true
     *          }
     *      });
     *
     * This will include all data fields as well as an "orderItems" array with the data
     * for each `OrderItem`. To include the associated `Item` for each `OrderItem`, the
     * call would look like:
     *
     *      var data = order.getData({
     *          associated: {
     *              orderItems: {
     *                  item: true
     *              }
     *          }
     *      });
     *
     * @param {Boolean/Object} [options] An object containing options describing the data
     * desired. If `true` is passed it is treated as an object with `associated` set to
     * `true`.
     * @param {Boolean/Object} [options.associated=false] Pass `true` to recursively
     * include all associated data. This is equivalent to pass `true` as the only argument.
     * See `getAssociatedData`. If `associated` is an object, it describes the specific
     * associations to gather.
     * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
     * have been modified. Note that field modifications are only tracked for fields that
     * are not declared with `persist` set to `false`. In other words, only persistent
     * fields have changes tracked so passing `true` for this means `options.persist` is
     * redundant.
     * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
     * This is only meaningful when `options.changes` is `true` since critical fields may
     * not have been modified.
     * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
     * This is implied when `options.changes` is set to `true`.
     * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
     * method on the returned fields.
     * @return {Object} An object containing all the values in this model.
     */
    getData: function(options) {
        var me = this,
            ret = {},
            opts = (options === true) ? me._getAssociatedOptions : (options || ret), // cheat
            data = me.data,
            associated = opts.associated,
            changes = opts.changes,
            critical = changes && opts.critical,
            content = changes ? me.modified : data,
            fieldsMap = me.fieldsMap,
            persist = opts.persist,
            serialize = opts.serialize,
            criticalFields, field, n, name, value;
 
        // DON'T use "opts" from here on...
 
        // Keep in mind the two legacy use cases:
        //  - getData() ==> Ext.apply({}, me.data)
        //  - getData(true) ==> Ext.apply(Ext.apply({}, me.data), me.getAssociatedData())
 
        if (content) { // when processing only changes, me.modified could be null
            for (name in content) {
                value = data[name];
 
                field = fieldsMap[name];
 
                if (field) {
                    if (persist && !field.persist) {
                        continue;
                    }
 
                    if (serialize && field.serialize) {
                        value = field.serialize(value, me);
                    }
                }
 
                ret[name] = value;
            }
        }
 
        if (critical) {
            criticalFields = me.self.criticalFields || me.getCriticalFields();
 
            for (= criticalFields.length; n-- > 0;) {
                name = (field = criticalFields[n]).name;
 
                if (!(name in ret)) {
                    value = data[name];
 
                    if (serialize && field.serialize) {
                        value = field.serialize(value, me);
                    }
 
                    ret[name] = value;
                }
            }
        }
 
        if (associated) {
            if (typeof associated === 'object') {
                me.getNestedData(opts, ret);
            }
            else {
                me.getAssociatedData(ret, opts);
            }
        }
 
        return ret;
    },
 
    getNestedData: function(options, result) {
        var me = this,
            associations = me.associations,
            graph = options.associated,
            i, item, items, itemData, length, record, role, roleName, opts;
 
        result = result || {};
 
        // For example:
        //
        //      associated: {
        //          orderItems: true
        //      }
        //
        //      associated: {
        //          orderItems: {
        //              item: true
        //          }
        //      }
        //
        for (roleName in graph) {
            role = associations[roleName];
            opts = graph[roleName];
 
            if (opts === true) {
                delete options.associated;
            }
            else {
                options.associated = opts;
            }
 
            item = role.getAssociatedItem(me);
 
            // If the current associated item is null, move to the next item
            if (!item) {
                continue;
            }
 
            if (item.isStore) {
                items = item.getData().items; // get the records for the store
                length = items.length;
                itemData = [];
 
                for (= 0; i < length; ++i) {
                    record = items[i];
                    itemData.push(record.getData(options));
                }
            }
            else {
                itemData = item.getData(options);
            }
 
            result[roleName] = itemData;
        }
 
        options.associated = graph; // restore the original value
 
        return result;
    },
 
    /**
     * Returns the array of fields that are declared as non-persist or "transient".
     * @return {Ext.data.field.Field[]} 
     * @since 5.0.0
     */
    getTransientFields: function() {
        var cls = this.self,
            ret = cls.transientFields;
 
        if (!ret) {
            cls.rankFields(); // populates transientFields as well as rank
            ret = cls.transientFields;
        }
 
        return ret;
    },
 
    /**
     * Checks whether this model is loading data from the {@link #proxy}.
     * @return {Boolean} `true` if in a loading state.
     */
    isLoading: function() {
        return !!this.loadOperation;
    },
 
    /**
     * Aborts a pending {@link #method!load} operation. If the record is not loading, this does
     * nothing.
     */
    abort: function() {
        var operation = this.loadOperation;
 
        if (operation) {
            operation.abort();
        }
    },
 
    /**
     * @localdoc Loads the model instance using the configured proxy.  The load action
     * is asynchronous.  Any processing of the loaded record should be done in a
     * callback.
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User();
     *     user.load({
     *         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, success) {
     *             // do something whether the load succeeded or failed
     *         }
     *     });
     *
     * The options param is an {@link Ext.data.operation.Read} config object containing
     * success, failure and callback functions, plus optional scope.
     *
     * @param {Object} [options] Options to pass to the proxy.
     * @param {Function} options.success A function to be called when the
     * model is processed by the proxy successfully.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.success.record The record.
     * @param {Ext.data.operation.Operation} options.success.operation The operation.
     * 
     * @param {Function} options.failure A function to be called when the
     * model is unable to be processed by the server.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.failure.record The record.
     * @param {Ext.data.operation.Operation} options.failure.operation The operation.
     * 
     * @param {Function} options.callback A function to be called whether the proxy
     * transaction was successful or not.
     * The callback is passed the following parameters:
     * @param {Ext.data.Model} options.callback.record The record.
     * @param {Ext.data.operation.Operation} options.callback.operation The operation.
     * @param {Boolean} options.callback.success `true` if the operation was successful.
     * 
     * @param {Object} options.scope The scope in which to execute the callback
     * functions.  Defaults to the model instance.
     *
     * @return {Ext.data.operation.Read} The read operation.
     */
    load: function(options) {
        options = Ext.apply({}, options);
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            scope = options.scope || me,
            proxy = me.getProxy(),
            callback = options.callback,
            operation = me.loadOperation,
            id = me.getId(),
            extras;
 
        if (operation) {
            // Already loading, push any callbacks on and jump out
            extras = operation.extraCalls;
 
            if (!extras) {
                extras = operation.extraCalls = [];
            }
 
            extras.push(options);
 
            return operation;
        }
 
        //<debug>
        var doIdCheck = true; // eslint-disable-line vars-on-top, one-var
 
        if (me.phantom) {
            doIdCheck = false;
        }
        //</debug>
 
        options.id = id;
 
        // Always set the recordCreator. If we have a session, we're already
        // part of said session, so we don't need to handle that.
        options.recordCreator = function(data, type, readOptions) {
            // Important to change this here, because we might be loading associations,
            // so we do not want this to propagate down. If we have a session, use that
            // so that we end up getting the same record. Otherwise, just remove it.
            var session = me.session;
 
            if (readOptions) {
                readOptions.recordCreator = session ? session.recordCreator : null;
            }
 
            me.set(data, me._commitOptions);
 
            //<debug>
            // Do the id check after set since converters may have run
            if (doIdCheck && me.getId() !== id) {
                Ext.raise('Invalid record id returned for ' + id + '@' + me.entityName);
            }
            //</debug>
 
            return me;
        };
 
        options.internalCallback = function(operation) {
            var success = operation.wasSuccessful() && operation.getRecords().length > 0,
                op = me.loadOperation,
                extras = op.extraCalls,
                successFailArgs = [me, operation],
                callbackArgs = [me, operation, success],
                i, len;
 
            me.loadOperation = null;
            ++me.loadCount;
 
            if (success) {
                Ext.callback(options.success, scope, successFailArgs);
            }
            else {
                Ext.callback(options.failure, scope, successFailArgs);
            }
 
            Ext.callback(callback, scope, callbackArgs);
 
            // Some code repetition here, however in a vast majority of cases
            // we'll only have a single callback, so optimize for that case rather
            // than setup arrays for all the callback options
            if (extras) {
                for (= 0, len = extras.length; i < len; ++i) {
                    options = extras[i];
 
                    if (success) {
                        Ext.callback(options.success, scope, successFailArgs);
                    }
                    else {
                        Ext.callback(options.failure, scope, successFailArgs);
                    }
 
                    Ext.callback(options.callback, scope, callbackArgs);
                }
            }
 
            me.callJoined('afterLoad');
        };
 
        delete options.callback;
 
        me.loadOperation = operation = proxy.createOperation('read', options);
        operation.execute();
 
        return operation;
    },
 
    /**
     * Merge incoming data from the server when this record exists
     * in an active session. This method is not called if this record is
     * loaded directly via {@link #method!load}. The default behaviour is to use incoming
     * data if the record is not {@link #dirty}, otherwise the data is
     * discarded. This method should be overridden in subclasses to
     * provide a different behavior.
     * @param {Object} data The model data retrieved from the server.
     *
     * @protected
     *
     * @since 6.5.0
     */
    mergeData: function(data) {
        if (!this.dirty) {
            this.set(data, this._commitOptions);
        }
    },
 
    /**
     * @method save
     * @localdoc Saves the model instance using the configured proxy.  The save action
     * is asynchronous.  Any processing of the saved record should be done in a callback.
     *
     * Create example:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *
     *     // pass the phantom record data to the server to be saved
     *     user.save({
     *         failure: function(record, operation) {
     *             // do something if the save failed
     *         },
     *         success: function(record, operation) {
     *             // do something if the save succeeded
     *         },
     *         callback: function(record, operation, success) {
     *             // do something whether the save succeeded or failed
     *         }
     *     });
     *
     * The response from a create operation should include the ID for the newly created
     * record:
     *
     *     // sample response
     *     {
     *         success: true,
     *         id: 1
     *     }
     *
     *     // the id may be nested if the proxy's reader has a rootProperty config
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url',
     *             reader: {
     *                 type: 'ajax',
     *                 rootProperty: 'data'
     *             }
     *         }
     *     });
     *
     *     // sample nested response
     *     {
     *         success: true,
     *         data: {
     *             id: 1
     *         }
     *     }
     *
     * (Create + ) Update example:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *     user.save({
     *         success: function(record, operation) {
     *             record.set('name', 'Bar');
     *             // updates the remote record via the proxy
     *             record.save();
     *         }
     *     });
     *
     * (Create + ) Destroy example - see also {@link #erase}:
     *
     *     Ext.define('MyApp.model.User', {
     *         extend: 'Ext.data.Model',
     *         fields: [
     *             {name: 'id', type: 'int'},
     *             {name: 'name', type: 'string'}
     *         ],
     *         proxy: {
     *             type: 'ajax',
     *             url: 'server.url'
     *         }
     *     });
     *
     *     var user = new MyApp.model.User({
     *         name: 'Foo'
     *     });
     *     user.save({
     *         success: function(record, operation) {
     *             record.drop();
     *             // destroys the remote record via the proxy
     *             record.save();
     *         }
     *     });
     *
     * **NOTE:** If a {@link #phantom} record is {@link #drop dropped} and subsequently
     * saved it will not be processed via the proxy.  However, any passed `success`
     * or `callback` functions will be called.
     *
     * The options param is an Operation config object containing success, failure and
     * callback functions, plus optional scope.  The type of Operation depends on the
     * state of the model being saved.
     *
     *  - {@link #phantom} model - {@link Ext.data.operation.Create}
     *  - {@link #isModified modified} model - {@link Ext.data.operation.Update}
     *  - {@link #dropped} model - {@link Ext.data.operation.Destroy}
     *
     * @inheritdoc #method-load
     * @return {Ext.data.operation.Create/Ext.data.operation.Update/Ext.data.operation.Destroy}
     * The operation instance for saving this model.  The type of operation returned
     * depends on the model state at the time of the action.
     *
     *  - {@link #phantom} model - {@link Ext.data.operation.Create}
     *  - {@link #isModified modified} model - {@link Ext.data.operation.Update}
     *  - {@link #dropped} model - {@link Ext.data.operation.Destroy}
     */
    save: function(options) {
        options = Ext.apply({}, options);
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            phantom = me.phantom,
            dropped = me.dropped,
            action = dropped ? 'destroy' : (phantom ? 'create' : 'update'),
            scope = options.scope || me,
            callback = options.callback,
            proxy = me.getProxy(),
            operation;
 
        options.records = [me];
 
        options.internalCallback = function(operation) {
            var args = [me, operation],
                success = operation.wasSuccessful();
 
            if (success) {
                Ext.callback(options.success, scope, args);
            }
            else {
                Ext.callback(options.failure, scope, args);
            }
 
            args.push(success);
            Ext.callback(callback, scope, args);
        };
 
        delete options.callback;
 
        operation = proxy.createOperation(action, options);
 
        // Not a phantom, then we must perform this operation on the remote datasource.
        // Record will be removed from the store in the callback upon a success response
        if (dropped && phantom) {
            // If it's a phantom, then call the callback directly with a dummy successful ResultSet
            operation.setResultSet(Ext.data.reader.Reader.prototype.nullResultSet);
            me.setErased();
            operation.setSuccessful(true);
        }
        else {
            operation.execute();
        }
 
        return operation;
    },
 
    //-------------------------------------------------------------------------
    // Statics
 
    statics: {
        /**
         * @property {String/Object}
         * The default proxy to use for instances of this Model when no proxy is configured
         * on the instance.  When specified, the model will use this proxy instead of
         * requesting one from the {@link Ext.data.Session Session}.
         *
         * Can be a string "type", or a {@link Ext.data.proxy.Proxy Proxy} config object.
         *
         * This proxy is not inherited by subclasses.
         * @static
         * @protected
         */
        defaultProxy: 'memory'
    },
 
    inheritableStatics: {
        /**
         * @property {Object} _associatedReadOptions
         * The options for the proxy reader for loadData.
         *
         * @private
         */
        _associatedReadOptions: {
            recordsOnly: true,
            asRoot: true
        },
 
        /**
         * Create a model while also parsing any data for associations.
         * @param {Object} data The model data, including any associated data if required.
         * The type of data should correspond to what the configured data reader would expect.
         * @param {Ext.data.Session} [session] The session.
         * @return {Ext.data.Model} The model.
         *
         * @static
         * @inheritable
         * @since 6.5.0
         */
        loadData: function(data, session) {
            var rec;
 
            if (data) {
                /* eslint-disable-next-line max-len, newline-per-chained-call */
                rec = this.getProxy().getReader().readRecords([data], session ? { recordCreator: session.recordCreator } : undefined, this._associatedReadOptions)[0];
            }
            else {
                rec = new this(data, session);
            }
 
            return rec;
        },
 
        /**
         * Get the summary model type. If {@link #summary} is specified, it is
         * a new type that extends from this type. If not, then it is the same
         * model type.
         * @return {Ext.Class} The summary model type.
         *
         * @static
         * @inheritable
         * @since 6.5.0
         */
        getSummaryModel: function() {
            var me = this,
                proto = me.prototype,
                summaryModel = me.summaryModel;
 
            if (!summaryModel) {
                summaryModel = Ext.define(null, {
                    extend: me,
                    fields: proto.summaryFields || [],
                    isSummaryModel: true
                });
 
                summaryModel.isSummaryModel = true;
                me.summaryModel = proto.summaryModel = summaryModel;
            }
 
            return summaryModel || null;
        },
 
        /**
         * This method adds the given set of fields to this model class.
         *
         * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
         * of a field this may replace a previous field definition.
         *
         * @protected
         * @static
         * @inheritable
         * @since 5.0.0
         */
        addFields: function(newFields) {
            this.replaceFields(newFields);
        },
 
        /**
         * This method replaces the specified set of fields with a given set of new fields.
         * Fields should normally be considered immutable, but if the timing is right (that
         * is, before derived classes are declared), it is permissible to change the fields
         * collection.
         *
         * @param {String[]/Object[]} newFields The new fields to add. Based on the `name`
         * of a field this may replace a previous field definition.
         * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
         * to remove all existing fields. Removes are processed first followed by adds so
         * if a field name appears in `newFields` as well that field will effectively be
         * added (however, in that case there is no need to include the field in this
         * array).
         *
         * @protected
         * @static
         * @inheritable
         * @since 5.0.0
         */
        replaceFields: function(newFields, removeFields) {
            var me = this,
                proto = me.prototype,
                Field = Ext.data.field.Field,
                fields = me.fields,
                fieldsMap = me.fieldsMap,
                ordinals = me.fieldOrdinals,
                field, i, idField, len, name, ordinal, cleared;
 
            if (removeFields === true) {
                fields.length = 0;
                me.fieldsMap = fieldsMap = {};
                me.fieldOrdinals = ordinals = {};
                cleared = true;
            }
            else if (removeFields) {
                for (= removeFields.length; i-- > 0;) {
                    name = removeFields[i];
 
                    if (name in ordinals) {
                        delete ordinals[name];
                        delete fieldsMap[name];
                    }
                }
 
                for (= 0, len = fields.length; i < len; ++i) {
                    name = (field = fields[i]).name;
 
                    if (name in ordinals) {
                        ordinals[name] = i;
                    }
                    else {
                        // This field is being removed (it is no longer in ordinals).
                        fields.splice(i, 1);
                        --i;
                        --len;
                        // we need to do this forwards so that ordinals don't become
                        // invalid due to a splice
                    }
                }
            }
 
            for (= 0, len = newFields ? newFields.length : 0; i < len; i++) {
                name = (field = newFields[i]).name;
 
                if (!(name in ordinals)) {
                    ordinals[name] = ordinal = fields.length; // 0-based
                    fields.push(field = Field.create(field));
 
                    fieldsMap[name] = field;
                    field.ordinal = ordinal;
                    field.definedBy = field.owner = this; // Ext.data.NodeInterface
                }
            }
 
            // Reset all ranks if we didn't get cleared, since this could
            // alter the dependencies
            if (!cleared) {
                for (= 0, len = fields.length; i < len; ++i) {
                    fields[i].rank = null;
                }
            }
 
            // The idField could have been replaced, so reacquire it.
            me.idField = proto.idField = idField = fieldsMap[proto.idProperty];
 
            if (idField) {
                idField.allowNull = idField.critical = idField.identifier = true;
                idField.defaultValue = null;
            }
 
            // In case we've created the initializer we need to zap it so we recreate it
            // next time. Likewise with field ranking.
            me.initializeFn = me.rankedFields = me.transientFields = me.criticalFields = null;
        },
 
        /**
         * Removes the given set of fields from this model.
         *
         * @param {Boolean/String[]} removeFields The names of fields to remove or `true`
         * to remove all existing fields. Removes are processed first followed by adds so
         * if a field name appears in `newFields` as well that field will effectively be
         * added (however, in that case there is no need to include the field in this
         * array).
         *
         * @protected
         * @static
         * @inheritable
         * @since 5.0.0
         */
        removeFields: function(removeFields) {
            this.replaceFields(null, removeFields);
        },
 
        /**
         * @private
         * @static
         * @inheritable
         */
        getIdFromData: function(data) {
            var T = this,
                idField = T.idField,
                id = idField.calculated ? (new T(data)).id : data[idField.name];
 
            return id;
        },
 
        /**
         * @private
         * @static
         * @inheritable
         */
        createWithId: function(id, data, session) {
            var d = data,
                T = this;
 
            if (id || id === 0) {
                d = {};
 
                if (data) {
                    Ext.apply(d, data);
                }
 
                d[T.idField.name] = id;
            }
 
            return new T(d, session);
        },
 
        /**
         * @private
         * @static
         * @inheritable
         */
        getFields: function() {
            return this.fields;
        },
 
        /**
         * @private
         * @static
         * @inheritable
         */
        getFieldsMap: function() {
            return this.fieldsMap;
        },
 
        /**
         * @private
         * @static
         * @inheritable
         */
        getField: function(name) {
            return this.fieldsMap[name] || null;
        },
 
        /**
         * Returns the configured Proxy for this Model.
         * @return {Ext.data.proxy.Proxy} The proxy
         * @static
         * @inheritable
         */
        getProxy: function() {
            var me = this,
                proxy = me.proxy,
                defaultProxy = me.defaultProxy,
                defaults;
 
            if (!proxy) {
                // Check what was defined by the class (via onClassExtended):
                proxy = me.proxyConfig;
 
                if (!proxy && defaultProxy) {
                    proxy = defaultProxy;
                }
 
                if (!proxy || !proxy.isProxy) {
                    if (typeof proxy === 'string') {
                        proxy = {
                            type: proxy
                        };
                    }
 
                    // We have nothing or a config for the proxy. Get some defaults from
                    // the Schema and smash anything we've provided over the top.
                    defaults = Ext.merge(me.schema.constructProxy(me), proxy);
 
                    if (proxy && proxy.type) {
                        proxy = proxy.schema === false ? proxy : defaults;
                    }
                    else {
                        proxy = defaults;
                    }
                }
 
                proxy = me.setProxy(proxy);
            }
 
            return proxy;
        },
 
        /**
         * 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) {
            var me = this,
                model;
 
            if (proxy) {
                if (!proxy.isProxy) {
                    proxy = Ext.Factory.proxy(proxy);
                }
                else {
                    model = proxy.getModel();
 
                    if (model && model !== me) {
                        proxy = proxy.clone();
                    }
                }
 
                proxy.setModel(me);
            }
 
            return (me.prototype.proxy = me.proxy = proxy);
        },
 
        /**
         * Asynchronously loads a model instance by id. Any processing of the loaded
         * record should be done in a callback.
         *
         * Sample usage:
         *
         *     Ext.define('MyApp.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, success) {
         *             //do something whether the load succeeded or failed
         *         }
         *     });
         *
         * @param {Number/String} id The ID of the model to load.
         * **NOTE:** The model returned must have an ID matching the param in the load
         * request.
         *
         * @param {Object} [options] The options param is an
         * {@link Ext.data.operation.Read} config object containing success, failure and
         * callback functions, plus optional scope.
         *
         * @param {Function} options.success A function to be called when the
         * model is processed by the proxy successfully.
         * The callback is passed the following parameters:
         * @param {Ext.data.Model} options.success.record The record.
         * @param {Ext.data.operation.Operation} options.success.operation The operation.
         * 
         * @param {Function} options.failure A function to be called when the
         * model is unable to be processed by the server.
         * The callback is passed the following parameters:
         * @param {Ext.data.Model} options.failure.record The record.
         * @param {Ext.data.operation.Operation} options.failure.operation The operation.
         * 
         * @param {Function} options.callback A function to be called whether the proxy
         * transaction was successful or not.
         * The callback is passed the following parameters:
         * @param {Ext.data.Model} options.callback.record The record.
         * @param {Ext.data.operation.Operation} options.callback.operation The
         * operation.
         * @param {Boolean} options.callback.success `true` if the operation was
         * successful.
         * 
         * @param {Object} options.scope The scope in which to execute the callback
         * functions.  Defaults to the model instance.
         *
         * @param {Ext.data.Session} [session] The session for this record.
         *
         * @return {Ext.data.Model} The newly created model. Note that the model will
         * (probably) still be loading once it is returned from this method. To do any
         * post-processing on the data, the appropriate place to do see is in the
         * callback.
         * 
         * @static
         * @inheritable
         */
        load: function(id, options, session) {
            var data = {},
                rec;
 
            if (session) {
                rec = session.peekRecord(this, id);
            }
 
            if (!rec) {
                data[this.prototype.idProperty] = id;
                rec = new this(data, session);
            }
 
            rec.load(options);
 
            return rec;
        }
    },
 
    deprecated: {
        5: {
            methods: {
                hasId: null,
                markDirty: null,
                setDirty: null,
                eachStore: function(callback, scope) {
                    var me = this,
                        stores = me.stores,
                        len = stores.length,
                        i;
 
                    for (= 0; i < len; ++i) {
                        callback.call(scope, stores[i]);
                    }
                },
 
                join: function(item) {
                    var me = this,
                        stores = me.stores,
                        joined = me.joined;
 
                    if (!joined) {
                        joined = me.joined = [item];
                    }
                    else {
                        joined.push(item);
                    }
 
                    if (item.isStore) {
                        me.store = me.store || item;
 
                        if (!stores) {
                            stores = me.stores = [];
                        }
 
                        stores.push(item);
                    }
                },
 
                unjoin: function(item) {
                    var me = this,
                        stores = me.stores,
                        joined = me.joined;
 
                    if (joined.length === 1) {
                        joined.length = 0;
                    }
                    else {
                        Ext.Array.remove(joined, item);
                    }
 
                    if (item.isStore) {
                        Ext.Array.remove(stores, item);
                        me.store = stores[0] || null;
                    }
                }
            },
            properties: {
                persistenceProperty: null
            },
            inheritableStatics: {
                methods: {
                    setFields: null
                }
            }
        }
    },
 
    //-------------------------------------------------------------------------
    privates: {
        _commitOptions: {
            commit: true
        },
        _getChangesOptions: {
            changes: true
        },
        _getAssociatedOptions: {
            associated: true
        },
        _getNotAssociatedOptions: {
            associated: false
        },
 
        _metaProperties: {
            dirty: 'isDirty',
            phantom: 'isPhantom',
            valid: 'isValid'
        },
 
        /**
         * Copies data from the passed record into this record. If the passed record is undefined,
         * does nothing.
         *
         * If this is a phantom record (represented only in the client, with no corresponding
         * database entry), and the source record is not a phantom, then this record acquires
         * the id of the source record.
         *
         * @param {Ext.data.Model} sourceRecord The record to copy data from.
         * @return {String[]} The names of the fields which changed value.
         * @private
         */
        copyFrom: function(sourceRecord) {
            var me = this,
                fields = me.fields,
                fieldCount = fields.length,
                modifiedFieldNames = [],
                idProperty = me.idProperty,
                i = 0,
                field, myData, sourceData, name, value;
 
            if (sourceRecord) {
                myData = me.data;
                sourceData = sourceRecord.data;
 
                for (; i < fieldCount; i++) {
                    field = fields[i];
                    name = field.name;
 
                    // Do not use setters.
                    // Copy returned values in directly from the data object.
                    // Converters have already been called because new Records
                    // have been created to copy from.
                    // This is a direct record-to-record value copy operation.
                    // don't copy the id, we'll do it at the end
                    if (name !== idProperty) {
                        value = sourceData[name];
 
                        // If source property is specified, and value is different
                        // copy field value in and build updatedFields
                        if (value !== undefined && !me.isEqual(myData[name], value)) {
                            myData[name] = value;
                            modifiedFieldNames.push(name);
                        }
                    }
                }
 
                // If this is a phantom record being updated from a concrete record, copy the ID in.
                if (me.phantom && !sourceRecord.phantom) {
                    // beginEdit to prevent events firing
                    // commit at the end to prevent dirty being set
                    me.beginEdit();
                    me.setId(sourceRecord.getId());
                    me.endEdit(true);
                    me.commit(true);
                }
            }
 
            return modifiedFieldNames;
        },
 
        /**
         * Helper function used by afterEdit, afterReject and afterCommit. Calls the given
         * method on the `Ext.data.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. If this model is joined to a Ext.data.NodeStore, then this method calls
         * the given method on the NodeStore and the associated Ext.data.TreeStore.
         * @param {String} funcName The name function to call on each store.
         * @param {Array} [args] The arguments to pass to the method. This instance is
         * always inserted as the first argument.
         * @private
         */
        callJoined: function(funcName, args) {
            var me = this,
                joined = me.joined,
                session = me.session,
                state = me.dropped ? 'D' : (me.phantom ? 'C' : (me.dirty ? 'U' : 'R')),
                i, len, fn, item;
 
            me.crudState = state;
 
            if (joined || session) {
                if (args) {
                    args.unshift(me);
                }
                else {
                    args = [me];
                }
 
                fn = session && session[funcName];
 
                if (fn) {
                    fn.apply(session, args);
                }
 
                if (joined) {
                    for (= 0, len = joined.length; i < len; ++i) {
                        item = joined[i];
 
                        if (item && (fn = item[funcName])) {
                            fn.apply(item, args);
                        }
                    }
                }
            }
 
            me.crudStateWas = state;
        },
 
        /**
         * Currently this only checks the loading state, this method exists for API
         * parity with stores.
         * @return {Boolean} `true` if the model is loading or has a pending load.
         *
         * @private
         */
        hasPendingLoad: function() {
            return this.isLoading();
        },
 
        interpret: function(name) {
            var me = this,
                accessor = me._metaProperties[name];
 
            if (!accessor) {
                accessor = me.associations;
                // e.g. "orderItems"
                accessor = accessor && accessor[name] && accessor[name].getterName;
            }
 
            if (accessor) {
                return me[accessor](); // e.g., me.isPhantom()
            }
 
            return me.data[name];
        },
 
        /**
         * Gets the dirty state of this record.
         * @return {Boolean} The dirty state.
         *
         * @private
         */
        isDirty: function() {
            // Added as a method to be used by data binding
            return this.dirty;
        },
 
        /**
         * Gets the phantom state of this record.
         * @return {Boolean} The phantom state.
         *
         * @private
         */
        isPhantom: function() {
            // Added as a method to be used by data binding
            return this.phantom;
        },
 
        /**
         * Called when an associated record instance has been set.
         * @param {Ext.data.Model} record The record.
         * @param {Ext.data.schema.Role} role The role.
         *
         * @private
         */
        onAssociatedRecordSet: function(record, role) {
            this.callJoined('afterAssociatedRecordSet', [record, role]);
        },
 
        /**
         * @method
         * Called when the model id is changed.
         * @param {Object} id The new id.
         * @param {Object} oldId The old id.
         */
        onIdChanged: Ext.privateFn,
 
        /**
         * Set the session for this record.
         * @param {Ext.data.Session} session The session
         */
        setSession: function(session) {
            //<debug>
            if (session) {
                if (this.session) {
                    Ext.raise('This model already belongs to a session.');
                }
 
                if (!this.id) {
                    Ext.raise('The model must have an id to participate in a session.');
                }
            }
            //</debug>
 
            this.session = session;
 
            if (session) {
                session.add(this);
            }
        },
 
        /**
         * Gets the names of all the fields that were modified during an edit.
         * @param {Object} [old] The saved data from `beginEdit`.
         * @return {String[]} The array of modified field names.
         * @private
         */
        getModifiedFieldNames: function(old) {
            var me = this,
                data = me.data,
                modified = [],
                oldData = old || me.editMemento.data,
                key;
 
            for (key in data) {
                if (data.hasOwnProperty(key)) {
                    if (!me.isEqual(data[key], oldData[key], key)) {
                        modified.push(key);
                    }
                }
            }
 
            return modified;
        },
 
        /**
         * Checks if two values are equal, taking into account certain special factors, for
         * example dates.
         * @param {Object} lhs The first value.
         * @param {Object} rhs The second value.
         * @param {String/Ext.data.Field} [field] The field name or instance.
         * @return {Boolean} True if the values are equal.
         * @private
         */
        isEqual: function(lhs, rhs, field) {
            var f;
 
            if (field) {
                f = field.isField ? field : this.fieldsMap[field];
 
                if (f) {
                    return f.isEqual(lhs, rhs);
                }
            }
 
            // instanceof is ~10 times faster then Ext.isDate. Values here will not be
            // cross-document objects
            if (lhs instanceof Date && rhs instanceof Date) {
                return lhs.getTime() === rhs.getTime();
            }
 
            return lhs === rhs;
        },
 
        statics: {
            /**
             * @property
             * @static
             * @private
             * @readonly
             * @deprecated 5.0 Use the string `"edit"` directly.
             * The update operation of type 'edit'. Used by the
             * {@link Ext.data.Store#event-update Store.update} event.
             */
            EDIT: 'edit',
 
            /**
             * @property
             * @static
             * @private
             * @readonly
             * @deprecated 5.0 Use the string `"reject"` directly.
             * The update operation of type 'reject'. Used by the
             * {@link Ext.data.Store#event-update Store.update} event.
             */
            REJECT: 'reject',
 
            /**
             * @property
             * @static
             * @private
             * @readonly
             * @deprecated 5.0 Use the string `"commit"` directly.
             * The update operation of type 'commit'. Used by the
             * {@link Ext.data.Store#event-update Store.update} event.
             */
            COMMIT: 'commit',
 
            rankFields: function() {
                var cls = this,
                    prototype = cls.prototype,
                    fields = cls.fields,
                    length = fields.length,
                    rankedFields = [],
                    criticalFields = [],
                    transientFields = [],
                    evilFields, field, i;
 
                cls.rankedFields = prototype.rankedFields = rankedFields;
                cls.criticalFields = prototype.criticalFields = criticalFields;
                cls.transientFields = prototype.transientFields = transientFields;
 
                // This first pass brings over any fields that have no dependencies at all
                // and gathers the evil fields to the side (the fields that could depend on
                // anything). This avoids the call to topoAdd that we must perform on all of
                // the fields that do have depends (which is good since most fields will be
                // handled here).
                for (= 0; i < length; ++i) {
                    field = fields[i];
 
                    if (field.critical) {
                        criticalFields.push(field);
                    }
 
                    if (!field.persist) {
                        transientFields.push(field);
                    }
 
                    if (field.evil) {
                        (evilFields || (evilFields = [])).push(field);
                    }
                    else if (!field.depends) {
                        rankedFields.push(field);
                        field.rank = rankedFields.length; // 1-based
                    }
                }
 
                for (= 0; i < length; ++i) {
                    if (!(field = fields[i]).rank && !field.evil) {
                        cls.topoAdd(field);
                    }
                }
 
                if (evilFields) {
                    for (= 0, length = evilFields.length; i < length; ++i) {
                        rankedFields.push(field = evilFields[i]);
                        field.rank = rankedFields.length; // 1-based
                    }
                }
 
                //<debug>
                cls.topoStack = null; // cleanup diagnostic stack
                //</debug>
 
                return rankedFields;
            },
 
            topoAdd: function(field) {
                var cls = this,
                    dep = field.depends,
                    dependsLength = dep ? dep.length : 0,
                    rankedFields = cls.rankedFields,
                    i, targetField;
 
                //<debug>
                /* eslint-disable-next-line vars-on-top, one-var */
                var topoStack = cls.topoStack || (cls.topoStack = []);
 
                topoStack.push(field.name);
 
                if (field.rank === 0) { // if (adding)
                    Ext.raise(cls.$className + " has circular field dependencies: " +
                            topoStack.join(" --> "));
                }
 
                if (topoStack.length && field.evil) {
                    Ext.raise(cls.$className + ": Field " +
                            topoStack[topoStack.length - 1] +
                            " cannot depend on depends-less field " + field.name);
                }
 
                field.rank = 0; // adding (falsey but we can still detect cycles)
                //</debug>
 
                for (= 0; i < dependsLength; ++i) {
                    // Get the targetField on which we depend and add this field to the
                    // targetField.dependents[]
                    targetField = cls.fieldsMap[dep[i]];
 
                    //<debug>
                    if (!targetField) {
                        Ext.raise(cls.$className + ": Field " + field.name +
                                  " depends on undefined field " + dep[i]);
                    }
                    //</debug>
 
                    (targetField.dependents || (targetField.dependents = [])).push(field);
 
                    if (!targetField.rank) { // if (!added)
                        cls.topoAdd(targetField);
                    }
                }
 
                rankedFields.push(field);
                field.rank = rankedFields.length; // 1-based (truthy to track "added" state)
 
                //<debug>
                topoStack.pop();
                //</debug>
            },
 
            initFields: function(data, cls, proto) {
                var Field = Ext.data.field.Field,
                    fieldDefs = data.fields,
                    // allocate fields [] and ordinals {} for the new class:
                    fields = [],
                    fieldOrdinals = {},
                    fieldsMap = {},
                    references = [],
                    superFields = proto.fields,
                    versionProperty = data.versionProperty || proto.versionProperty,
                    idProperty = cls.idProperty,
                    idField, field, i, length, name, ordinal,
                    reference, superIdField, superIdFieldName,
                    superIdDeclared, idDeclared;
 
                // Process any inherited fields to produce a fields [] and ordinals {} for
                // this class:
                cls.fields = proto.fields = fields;
                cls.fieldOrdinals = proto.fieldOrdinals = fieldOrdinals;
                cls.fieldsMap = proto.fieldsMap = fieldsMap;
                cls.references = proto.references = references;
 
                if (superFields) {
                    // We chain the super field so we can write to it
                    for (= 0, length = superFields.length; i < length; ++i) {
                        fields[i] = field = Ext.Object.chain(superFields[i]);
 
                        field.dependents = null; // we need to recalculate these
                        field.owner = cls;
                        fieldOrdinals[name = field.name] = i;
                        fieldsMap[name] = field;
                        // Clear the rank because it needs to be set on the first pass through
                        // the fields in the subclass, don't inherit it from the parent
                        field.rank = null;
 
                        if (field.generated) {
                            superIdField = field;
                            superIdFieldName = field.name;
                        }
                    }
                }
 
                // Merge in any fields from this class:
                delete data.fields;
 
                if (fieldDefs) {
                    for (= 0, length = fieldDefs.length; i < length; ++i) {
                        field = fieldDefs[i];
                        reference = field.reference;
 
                        // Create a copy of the reference since we'll modify
                        // the reference on the field. Needed for subclasses
                        if (reference && typeof reference !== 'string') {
                            // Can have child objects, so merge it deeply
                            reference = Ext.merge({}, reference);
                        }
 
                        field.$reference = reference;
                        field = Field.create(fieldDefs[i]);
                        name = field.name;
                        ordinal = fieldOrdinals[name];
 
                        if (ordinal === undefined) {
                            // If the field is new, add it to the end of the fields[]
                            fieldOrdinals[name] = ordinal = fields.length;
                        }
                        // else, overwrite the field at the established ordinal
 
                        fieldsMap[name] = field;
                        fields[ordinal] = field;
                        field.definedBy = field.owner = cls;
                        field.ordinal = ordinal;
 
                        if (name === idProperty) {
                            idDeclared = field;
                        }
 
                        if (name === superIdFieldName) {
                            superIdDeclared = true;
                        }
                    }
                }
 
                // Lookup the idProperty in the ordinals map and create a synthetic field if
                // we don't have one.
                idField = fieldsMap[idProperty];
 
                if (!idField) {
                    if (superIdField && superIdField.generated) {
                        ordinal = superIdField.ordinal;
                    }
                    else {
                        ordinal = fields.length;
                    }
 
                    delete fieldsMap[superIdFieldName];
                    delete fieldOrdinals[superIdFieldName];
 
                    idField = new Field(idProperty);
                    fields[ordinal] = idField;
                    fieldOrdinals[idProperty] = ordinal;
                    fieldsMap[idProperty] = idField;
                    idField.definedBy = cls;
                    idField.ordinal = ordinal;
                    idField.generated = true;
                }
                else if (idDeclared && !superIdDeclared && superIdField && superIdField.generated) {
                    // If we're declaring the id as a field in our fields array and it's different
                    // to the super id field that has been generated, pull it out and fix up
                    // the ordinals. This likely won't happen often, to do it earlier we would need
                    // to know the contents of the fields which would mean iterating over them
                    // twice.
                    Ext.Array.remove(fields, superIdField);
 
                    delete fieldsMap[superIdFieldName];
                    delete fieldOrdinals[superIdFieldName];
 
                    fieldsMap[idProperty] = idDeclared;
 
                    for (= 0, length = fields.length; i < length; ++i) {
                        field = fields[i];
                        fields.ordinal = i;
                        fieldOrdinals[field.name] = i;
                    }
                }
 
                idField.allowNull = idField.critical = idField.identifier = true;
                idField.defaultValue = null;
 
                cls.idField = proto.idField = idField;
 
                if (versionProperty) {
                    field = fieldsMap[versionProperty];
 
                    if (!field) {
                        ordinal = fields.length;
 
                        field = new Field({
                            name: versionProperty,
                            type: 'int'
                        });
 
                        fields[ordinal] = field;
                        fieldOrdinals[versionProperty] = ordinal;
                        fieldsMap[versionProperty] = field;
                        field.definedBy = cls;
                        field.ordinal = ordinal;
                        field.generated = true;
                    }
 
                    field.defaultValue = 1;
                    field.critical = true;
                }
 
                // NOTE: Be aware that the one fellow that manipulates these after this
                // point is Ext.data.NodeInterface.
            },
 
            initSummaries: function(data, cls, proto) {
                var summaryDefs = data.summary,
                    fields = proto.fields,
                    superSummaryFields = proto.summaryFields,
                    field, fieldName, i, index, inlineDefs, len, name, summaryFields,
                    summaryFieldMap, summaryField, type;
 
                if (superSummaryFields) {
                    summaryFields = [];
                    summaryFieldMap = {};
 
                    for (= 0, len = superSummaryFields.length; i < len; ++i) {
                        summaryField = superSummaryFields[i];
                        summaryFields.push(summaryField);
                        summaryFieldMap[summaryField.name] = i;
                    }
                }
 
                // We take any "summary: foo" configs on the model fields (but not ones
                // that were inherited since those come in above) and reform them into an
                // equivalent "summary: {}" as one would define on the class body.
                for (= 0, len = fields.length; i < len; ++i) {
                    field = fields[i];
 
                    if (field.summary && field.definedBy === cls) {
                        inlineDefs = inlineDefs || {};
                        inlineDefs[field.name] = field.summary;
                    }
                }
 
                // If they've got inline summary configs on the fields, merge them with
                // whatever is on the class body. The "summary" on the class body wins
                // when there is a collision. We don't worry about collisions because it
                // is a common need to use code generation to define model fields, so we
                // allow the class summary definition to override whatever is on them.
                if (inlineDefs) {
                    summaryDefs = Ext.apply(inlineDefs, summaryDefs);
                }
 
                if (summaryDefs) {
                    delete data.summary;
 
                    summaryFields = summaryFields || [];
                    summaryFieldMap = summaryFieldMap || {};
 
                    for (name in summaryDefs) {
                        type = typeof(summaryField = summaryDefs[name]);
 
                        if (type === 'function' || type === 'string') {
                            summaryField = {
                                summary: summaryField
                            };
                        }
 
                        // If it's not in the summaries, it's new here. We've already
                        // applied when copying down so this is safe to do
                        index = summaryFieldMap[name];
 
                        summaryField = Ext.apply({
                            name: name
                        }, summaryField);
 
                        field = summaryField.field;
 
                        if (field) {
                            delete summaryField.field;
                            summaryField.summaryField = field;
                        }
 
                        fieldName = summaryField.summaryField;
 
                        // Tempting to do the following, but we do not require that all
                        // fields be defined:
                        //
                        // if (fieldName && !proto.fieldsMap[fieldName]) {
                        //     Ext.raise('No field named "' + fieldName + '" on ' + (
                        //         proto.hasOwnProperty('$className')
                        //             ? '"' + proto.$className + '"'
                        //             : 'anonymous model'
                        //     ));
                        // }
 
                        field = proto.fieldsMap[fieldName || name];
 
                        if (field && !summaryField.type) {
                            summaryField.type = field.type;
                        }
 
                        if (index === undefined) {
                            index = summaryFields.length;
                            summaryFieldMap[name] = index;
                        }
 
                        summaryFields[index] = summaryField;
                    }
                }
 
                // Store these in an array so we have a predictable order when subclassing
                if (summaryFields) {
                    proto.summaryFields = summaryFields;
                }
            },
 
            initValidators: function(data, cls, proto) {
                var superValidators = proto.validators,
                    validators, field, copy, validatorDefs,
                    i, length, fieldValidator, name, validator, item;
 
                if (superValidators) {
                    validators = {};
 
                    for (field in superValidators) {
                        validators[field] = Ext.Array.clone(superValidators[field]);
                    }
                }
 
                validatorDefs = data.validators || data.validations;
 
                //<debug>
                if (data.validations) {
                    delete data.validations;
                    Ext.log.warn((cls.$className || 'Ext.data.Model') +
                                 ': validations has been deprecated. Please use validators ' +
                                 'instead.');
                }
                //</debug>
 
                if (validatorDefs) {
                    delete data.validators;
 
                    validators = validators || {};
 
                    // Support older array syntax
                    if (Ext.isArray(validatorDefs)) {
                        copy = {};
 
                        for (= 0, length = validatorDefs.length; i < length; ++i) {
                            item = validatorDefs[i];
                            name = item.field;
 
                            if (!copy[name]) {
                                copy[name] = [];
                            }
 
                            // Check for function form
                            item = item.fn || item;
                            copy[name].push(item);
                        }
 
                        validatorDefs = copy;
                    }
 
                    for (name in validatorDefs) {
                        fieldValidator = validatorDefs[name];
 
                        if (!Ext.isArray(fieldValidator)) {
                            fieldValidator = [fieldValidator];
                        }
 
                        validator = validators[name];
 
                        if (validator) {
                            // Declared in super
                            Ext.Array.push(validator, fieldValidator);
                        }
                        else {
                            validators[name] = fieldValidator;
                        }
                    }
                }
 
                if (validators) {
                    for (name in validators) {
                        field = cls.getField(name);
 
                        if (field) {
                            field.setModelValidators(validators[name]);
                        }
                    }
                }
 
                cls.validators = proto.validators = validators;
            },
 
            initAssociations: function(schema, data, cls) {
                // Handle keyless associations
                var associations = data.associations,
                    belongsTo = data.belongsTo,
                    hasMany = data.hasMany,
                    hasOne = data.hasOne,
                    // manyToMany can't be declared via reference
                    matrices = data.manyToMany,
                    i, length, assoc, o;
 
                delete data.associations;
                delete data.belongsTo;
                delete data.hasMany;
                delete data.hasOne;
                delete data.manyToMany;
 
                if (matrices) {
                    schema.addMatrices(cls, matrices);
                }
 
                if (associations) {
                    associations = Ext.isArray(associations) ? associations : [ associations ];
 
                    for (= 0, length = associations.length; i < length; ++i) {
                        assoc = associations[i];
                        o = Ext.apply({}, assoc);
                        delete o.type;
 
                        switch (assoc.type) {
                            case 'belongsTo':
                                schema.addBelongsTo(cls, o);
                                break;
 
                            case 'hasMany':
                                schema.addHasMany(cls, o);
                                break;
 
                            case 'hasOne':
                                schema.addHasOne(cls, o);
                                break;
 
                            //<debug>
                            default:
                                Ext.raise('Invalid association type: "' + assoc.type + '"');
                            //</debug>
                        }
                    }
                }
 
                if (belongsTo) {
                    belongsTo = Ext.isArray(belongsTo) ? belongsTo : [ belongsTo ];
 
                    for (= 0, length = belongsTo.length; i < length; ++i) {
                        schema.addBelongsTo(cls, belongsTo[i]);
                    }
                }
 
                if (hasMany) {
                    hasMany = Ext.isArray(hasMany) ? hasMany : [ hasMany ];
 
                    for (= 0, length = hasMany.length; i < length; ++i) {
                        schema.addHasMany(cls, hasMany[i]);
                    }
                }
 
                if (hasOne) {
                    hasOne = Ext.isArray(hasOne) ? hasOne : [ hasOne ];
 
                    for (= 0, length = hasOne.length; i < length; ++i) {
                        schema.addHasOne(cls, hasOne[i]);
                    }
                }
 
                schema.afterKeylessAssociations(cls);
            },
 
            initIdentifier: function(data, cls, proto) {
                var identifier = data.identifier || data.idgen,
                    superIdent = proto.identifier || cls.schema._defaultIdentifier,
                    generatorPrefix;
 
                //<debug>
                if (data.idgen) {
                    Ext.log.warn('Ext.data.Model: idgen has been deprecated. Please use ' +
                                 'identifier instead.');
                }
                //</debug>
 
                if (identifier) {
                    delete data.identifier;
                    delete data.idgen;
 
                    // An idgen was specified on the definition, use it explicitly.
                    identifier = Ext.Factory.dataIdentifier(identifier);
                }
                else if (superIdent) {
                    // If we have a cloneable instance, and we don't have an id
                    // clone it. If we have an id, then we should use the same
                    // instance since it's the same as looking it up via id.
                    if (superIdent.clone && !superIdent.getId()) {
                        identifier = superIdent.clone();
                    }
                    else if (superIdent.isGenerator) {
                        identifier = superIdent;
                    }
                    else {
                        identifier = Ext.Factory.dataIdentifier(superIdent);
                    }
                }
 
                cls.identifier = proto.identifier = identifier;
 
                if (!identifier) {
                    // If we didn't find one, create it and push it onto the class.
                    // Don't put it on the prototype, so a subclass will create
                    // it's own generator. If we have an anonymous model, go ahead and
                    // generate a unique prefix for it.
                    generatorPrefix = cls.entityName;
 
                    if (!generatorPrefix) {
                        generatorPrefix = Ext.id(null, 'extModel');
                    }
 
                    cls.identifier = Ext.Factory.dataIdentifier({
                        type: 'sequential',
                        prefix: generatorPrefix + '-'
                    });
                }
            },
 
            findValidator: function(validators, name, cfg) {
                var type = cfg.type || cfg,
                    field = validators[name],
                    len, i, item;
 
                if (field) {
                    for (= 0, len = field.length; i < len; ++i) {
                        item = field[i];
 
                        if (item.type === type) {
                            return item;
                        }
                    }
                }
 
                return null;
            },
 
            /**
             * This method produces the `initializeFn` for this class. If there are no fields
             * requiring {@link Ext.data.field.Field#cfg-convert conversion} and no fields requiring
             * a {@link Ext.data.field.Field#defaultValue default value} then this method will
             * return `null`.
             * @return {Function} The `initializeFn` for this class (or null).
             * @private
             */
            makeInitializeFn: function(cls) {
                var code = ['var '],
                    body = ['\nreturn function (e) {\n    var data = e.data, v;\n'],
                    work = 0,
                    bc, ec, // == beginClone, endClone
                    convert, expr, factory, field, fields, fs, hasDefValue, i, length;
 
                if (!(fields = cls.rankedFields)) {
                    // On the first edit of a record of this type we need to ensure we have the
                    // topo-sort done:
                    fields = cls.rankFields();
                }
 
                for (= 0, length = fields.length; i < length; ++i) {
                    // The generated method declares vars for each field using "f0".."fN' as the
                    // name. These are used to access properties of the field (e.g., the convert
                    // method or defaultValue).
                    field = fields[i];
                    fs = 'f' + i;
                    convert = field.convert;
 
                    if (i) {
                        code.push(',  \n    ');
                    }
 
                    code.push(fs, ' = $fields[' + i + ']');
 
                    //<debug>
                    // this can be helpful when debugging (at least in Chrome):
                    code.push('  /*  ', field.name, '  */');
                    //</debug>
 
                    // NOTE: added string literals are "folded" by the compiler so we
                    // are better off doing an "'foo' + 'bar'" then "'foo', 'bar'". But
                    // for variables we are better off pushing them into the array for
                    // the final join.
 
                    if ((hasDefValue = (field.defaultValue !== undefined)) || convert) {
                        // For non-calculated fields that have some work required (a convert method
                        // and/or defaultValue), generate a chunk of logic appropriate for the
                        // field.
                        // expr = data["fieldName"];
                        expr = 'data["' + field.name + '"]';
                        ++work;
 
                        bc = ec = '';
 
                        if (field.cloneDefaultValue) {
                            bc = 'Ext.clone(';
                            ec = ')';
                        }
 
                        body.push('\n');
 
                        if (convert && hasDefValue) {
                            // v = data.fieldName;
                            // if (v !== undefined) {
                            //     v = f2.convert(v, e);
                            // }
                            // if (v === undefined) {
                            //     v = f2.defaultValue;
                            //      // or
                            //     v = Ext.clone(f2.defaultValue);
                            // }
                            // data.fieldName = v;
                            //
                            body.push('    v = ', expr, ';\n' +
                                      '    if (v !== undefined) {\n' +
                                      '        v = ', fs, '.convert(v, e);\n' +
                                      '    }\n' +
                                      '    if (v === undefined) {\n' +
                                      '        v = ', bc, fs, '.defaultValue', ec, ';\n' +
                                      '    }\n' +
                                      '    ', expr, ' = v;');
                        }
                        else if (convert) { // no defaultValue
                            // v = f2.convert(data.fieldName,e);
                            // if (v !== undefined) {
                            //     data.fieldName = v;
                            // }
                            //
                            body.push('    v = ', fs, '.convert(', expr, ',e);\n' +
                                      '    if (v !== undefined) {\n' +
                                      '        ', expr, ' = v;\n' +
                                      '    }\n');
                        }
                        else if (hasDefValue) { // no convert
                            // if (data.fieldName === undefined) {
                            //     data.fieldName = f2.defaultValue;
                            //          // or
                            //     data.fieldName = Ext.clone(f2.defaultValue);
                            // }
                            //
                            body.push('    if (', expr, ' === undefined) {\n' +
                                      '        ', expr, ' = ', bc, fs, '.defaultValue', ec, ';\n' +
                                      '    }\n');
                        }
                    }
                }
 
                if (!work) {
                    // There are no fields that need special processing
                    return Ext.emptyFn;
                }
 
                code.push(';\n');
                code.push.apply(code, body);
                code.push('}');
                code = code.join('');
 
                // Ensure that Ext in the function code refers to the same Ext that we are
                // using here. If we are in a sandbox, global.Ext might be different.
                factory = new Function('$fields', 'Ext', code);
 
                return factory(fields, Ext);
            }
        } // static
    } // privates
},
/* eslint-disable indent */
function() {
    var Model = this,
        proto = Model.prototype,
        Schema = Ext.data.schema.Schema,
        defaultSchema;
 
    Model.proxyConfig = proto.proxy;
    delete proto.proxy;
 
    // Base Model class may be used. It needs an empty fields array.
    Model.fields = [];
 
    // Base Model class may be used. It needs an empty fieldsMap hash.
    Model.fieldsMap = proto.fieldsMap = {};
 
    Model.schema = proto.schema = Schema.get(proto.schema);
    proto.idField = new Ext.data.field.Field(proto.idProperty);
    Model.identifier = new Ext.data.identifier.Sequential();
 
    Model.onExtended(function(cls, data) {
        var proto = cls.prototype,
            schemaName = data.schema,
            superCls = proto.superclass.self,
            schema, entityName, proxy;
 
        cls.idProperty = data.idProperty || proto.idProperty;
 
        if (schemaName) {
            delete data.schema;
            schema = Schema.get(schemaName);
        }
        else if (!(schema = proto.schema)) {
            schema = defaultSchema || (defaultSchema = Schema.get('default'));
        }
 
        // These are in "privates" so we manually make them inherited:
        cls.rankFields = Model.rankFields;
        cls.topoAdd = Model.topoAdd;
 
        // if we picked up a schema from cls.prototype.schema, it is because it was found
        // in the prototype chain on a base class.
        proto.schema = cls.schema = schema;
 
        // Unless specified on the declaration data, we need to provide the entityName of
        // the new Entity-derived class. Store it on the prototype and the class.
        if (!(entityName = data.entityName)) {
            proto.entityName = entityName = schema.getEntityName(cls);
 
            //<debug>
            if (!entityName) {
                if (data.associations) {
                    Ext.raise('Anonymous entities cannot specify "associations"');
                }
 
                if (data.belongsTo) {
                    Ext.raise('Anonymous entities cannot specify "belongsTo"');
                }
 
                if (data.hasMany) {
                    Ext.raise('Anonymous entities cannot specify "hasMany"');
                }
 
                if (data.hasOne) {
                    Ext.raise('Anonymous entities cannot specify "hasOne"');
                }
 
                if (data.matrices) {
                    Ext.raise('Anonymous entities cannot specify "manyToMany"');
                }
            }
            //</debug>
        }
 
        cls.entityName = entityName;
        cls.fieldExtractors = {};
 
        Model.initIdentifier(data, cls, proto);
        Model.initFields(data, cls, proto);
        Model.initValidators(data, cls, proto);
 
        if (!data.isSummaryModel) {
            Model.initSummaries(data, cls, proto);
        }
 
        // This is a compat hack to allow "rec.fields.items" to work as it used to when
        // fields was a MixedCollection
        cls.fields.items = cls.fields;
 
        if (entityName) {
            schema.addEntity(cls);
            Model.initAssociations(schema, data, cls);
        }
 
        proxy = data.proxy;
 
        if (proxy) {
            delete data.proxy;
        }
        else if (superCls !== Model) {
            proxy = superCls.proxyConfig || superCls.proxy;
        }
 
        cls.proxyConfig = proxy;
    });
});