/**
 * A Schema is a collection of related {@link Ext.data.Model entities} and their respective
 * {@link Ext.data.schema.Association associations}.
 * 
 * # Schema Instances
 * 
 * By default a single instance of this class is created which serves as the schema for all
 * entities that do not have an explicit `{@link Ext.data.Model#cfg-schema schema}` config
 * either specified or inherited. This is sufficient in most cases.
 * 
 * When an entity does specify a `{@link Ext.data.Model#cfg-schema schema}`, however, that
 * looks up (or creates) an instance for that entity class which is then inherited.
 * 
 * **Important:** All related entities *must* belong to a single schema instance in order
 * to properly link up their associations.
 * 
 * ## Configuring Schemas
 * 
 * The best way to control the configuration of your `schema` is to define a base class for
 * all of your entities and use the `{@link Ext.data.Model#cfg-schema schema}` config like
 * this:
 * 
 *      Ext.define('MyApp.model.Base', {
 *          extend: 'Ext.data.Model',
 *
 *          // This configures the default schema because we don't assign an "id":
 *          schema: {
 *              // configs go here
 *          }
 *      });
 * 
 * **Note:** Only one explicit configuration can be applied to the default schema. In most
 * applications this will not be an issue.
 *
 * By using a base class for your entities you can ensure that the default schema is fully
 * configured before declaration of your classes proceeds. This is especially helpful if
 * you need to set the `namespace` for your schema (see below).
 *
 * ## Relative Naming
 * 
 * When describing associations between entities, it is desirable to use shorthand names
 * that do not contain the common namespace portion. This is called the `entityName` as
 * opposed to its class name. By default, the `entityName` is the full class name. However,
 * if a namespace is used, the common portion can be discarded and we can derive a shorter name.
 * In the following code, `"MyApp.model.Foo"` has an `entityName` of `"Foo"` and the schema has
 * a `namespace` of "MyApp.model".
 * 
 * If you use deeper nesting for entities, you may need to set the `namespace` config to
 * account for this. For example:
 * 
 *      Ext.define('MyApp.model.Base', {
 *          extend: 'Ext.data.Model',
 *
 *          schema: {
 *              namespace: 'MyApp.model'
 *          }
 *      });
 *
 * Your derived classes now will generate proper default `entityName` values even if they
 * have further namespaces. For example, "MyApp.model.foo.Thing" will produce "foo.Thing"
 * as the `entityName` given the above as a base class.
 *
 * # Association Naming
 * 
 * There are various terms involved when describing associations. Perhaps the simplest
 * example that will clarify these terms is that of the common many-to-many association
 * of User and Group.
 * 
 *   * `entityName` - The names "User" and "Group" are the `entityName` values associated
 *   with these two classes. These are derived from their full classnames (perhaps
 *   something like "App.model.User" and "App.model.Group").
 *   
 *   * `associationName` - When talking about associations, especially the many-to-many
 *   variety, it is important to give them names. Associations are not owned by either of
 *   the entities involved, so this name is similar to an `entityName`. In the case of
 *   "User" and "Group", the default `associationName` would be "GroupUsers".
 *   
 *   * `left` and `right` - Associations describe a relationship between two entities. To
 *   talk about specific associations we would use the `entityName` of the parties (such
 *   as "User" or "Group"). When discussing associations in the abstract, however, it is
 *   very helpful to be able to talk about the entities in an association in a general way.
 *   In the case of the "GroupUsers" association, "User" is said to be the `left` while
 *   "Group" is said to be the `right`. In a many-to-many association the selection of
 *   `left` and `right` is arbitrary. When a foreign-key is involved, the `left` entity
 *   is the one containing the foreign-key.
 *
 * ## Custom Naming Conventions
 * 
 * One of the jobs the the `Schema` is to manage name generation (such as `entityName`).
 * This job is delegated to a class called the `namer`. If you need to generate names in
 * other ways, you can provide a custom `namer` for your classes:
 *
 *      Ext.define('MyApp.model.Base', {
 *          extend: 'Ext.data.Model',
 *
 *          schema: {
 *              namespace: 'MyApp.model',
 *              namer: 'custom'
 *          }
 *      });
 *
 * This will create a class using the alias "namer.custom". For example:
 *
 *      Ext.define('MyApp.model.CustomNamer', {
 *          extend: 'Ext.data.schema.Namer',
 *
 *          alias: 'namer.custom',
 *          ...
 *      });
 *
 * For details see the documentation for {@link Ext.data.schema.Namer Namer}.
 */
Ext.define('Ext.data.schema.Schema', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
 
    requires: [
        'Ext.util.ObjectTemplate',
 
        'Ext.data.schema.OneToOne',
        'Ext.data.schema.ManyToOne',
        'Ext.data.schema.ManyToMany',
        'Ext.data.schema.Namer'
    ],
 
    alias: 'schema.default', // also configures Factoryable
 
    aliasPrefix: 'schema.',
 
    isSchema: true,
 
    /**
     * @property {String} type
     * The name of the schema's type. This should be the suffix of the `alias` for this
     * class following the "schema." prefix. For example, if the `alias` for a schema is
     * "schema.foo" then `type` should "foo". If an `alias` is specified on the derived
     * class, this property is set automatically.
     * @readonly
     */
    type: 'default',
 
    statics: {
        /**
         * @property {Object} instances
         * A collection of `Schema` instances keyed by its `type`.
         * 
         *      var mySchema = Ext.data.schema.Schema.instances.mySchema;
         *
         * If the `Schema` may not have been created yet, use the {@link #get} method to
         * create the instance on first request:
         * 
         *      var mySchema = Ext.data.schema.Schema.get('mySchema');
         * 
         * @readonly
         * @private
         */
        instances: {},
 
        //<debug>
        // Method used for testing to clear cache for custom instances.
        clearInstance: function(id) {
            var schema = this.instances[id];
            
            delete this.instances[id];
            
            if (schema) {
                schema.clear(true);
                schema.destroy();
            }
        },
        //</debug>
 
        /**
         * Returns the `Schema` instance given its `id` or config object. If only the `id`
         * is specified, that `Schema` instance is looked up and returned. If there is no
         * instance already created, the `id` is assumed to be the `type`. For example:
         *
         *      schema: 'foo'
         *
         * Would be created from the alias `"schema.foo"` and assigned the `id` of "foo"
         * as well.
         *
         * @param {String/Object} config The id, type or config object of the schema.
         * @param {String} [config.type] The type alias of the schema. A "schema." prefix
         * is added to this string, if provided, to complete the alias. This should match
         * match the "alias" of some class derived from `Ext.data.schema.Schema`.
         * @return {Ext.data.schema.Schema} The previously existing or newly created
         * instance.
         */
        get: function(config) {
            var Schema = this,
                cache = Schema.instances,
                id = 'default',
                isString = config && Ext.isString(config),
                instance, newConfig;
 
            if (config) {
                if (config.isSchema) {
                    return config;
                }
                
                id = isString ? config : (config.id || id);
            }
 
            if (!(instance = cache[id])) {
                cache[id] = instance = Schema.create(config);
                instance.id = id;
            }
            else if (config && !isString) {
                //<debug>
                if (id !== 'default') {
                    Ext.raise('Only the default Schema instance can be reconfigured');
                }
                //</debug>
 
                // When a Model contains a "schema" config object it is allowed to set the
                // configuration of the default schema. This is the default behavior of
                // this config on a model unless there is an "id" specified on it. So
                // the trick is that we already have an instance so we want to merge the
                // incoming config with the initial config of the default schema and then
                // make that the effective initial config.
                newConfig = Ext.merge({}, instance.config);
                Ext.merge(newConfig, config);
                instance.setConfig(newConfig);
                instance.config = newConfig;
 
                //<debug>
                instance.setConfig = function() {
                    Ext.raise('The schema can only be reconfigured once');
                };
                //</debug>
            }
 
            return instance;
        },
 
        lookupEntity: function(entity) {
            var ret = null,
                instances = this.instances,
                match, name, schema;
 
            if (entity) {
                if (entity.isEntity) {
                    ret = entity.self; // a record
                }
                else if (Ext.isFunction(entity)) {
                    // A function (assume that a constructor is the Class).
                    ret = entity;
                }
                else if (Ext.isString(entity)) {
                    ret = Ext.ClassManager.get(entity);
 
                    // If we've found a singleton or non-Entity class by that name, ignore it.
                    if (ret && (!ret.prototype || !ret.prototype.isEntity)) {
                        ret = null;
                    }
                    
                    if (!ret) {
                        for (name in instances) {
                            schema = instances[name];
                            match = schema.getEntity(entity);
                            
                            if (match) {
                                if (ret) {
                                    Ext.raise('Ambiguous entity name "' + entity +
                                        '". Defined by schema "' + ret.schema.type +
                                        '" and "' + name + '"');
                                }
                                
                                ret = match;
                            }
                        }
                    }
 
                    if (!ret) {
                        Ext.raise('No such Entity "' + entity + '".');
                    }
                }
            }
 
            return ret;
        }
    },
 
    /**
     * @property {Number} assocCount The number of {@link Ext.data.schema.Association associations}
     * in this `schema`.
     * @readonly
     */
    assocCount: 0,
 
    /**
     * @property {Number} entityCount The number of {@link Ext.data.Model entities} in this
     * `schema`.
     * @readonly
     */
    entityCount: 0,
 
    config: {
        /**
         * @cfg {Object} defaultIdentifier
         * This config is used to initialize the `{@link Ext.data.Model#identifier}` config
         * for classes that do not define one.
         */
        defaultIdentifier: null,
 
        /**
        * @cfg {Number} keyCheckDelay
        * The time to wait (in ms) before checking for null foreign keys on records that
        * will cause them to be dropped. This is useful for allowing records to be moved to
        * a different source.
        * @private
        * @since 5.0.1
        */
        keyCheckDelay: 10,
 
        /**
         * @cfg {String/Object/Ext.data.schema.Namer} namer
         * Specifies or configures the name generator for the schema.
         */
        namer: 'default',
 
        /**
         * @cfg {String} namespace
         * The namespace for entity classes in this schema.
         */
        namespace: null,
 
        /**
         * @cfg {Object/Ext.util.ObjectTemplate} proxy
         * This is a template used to produce `Ext.data.proxy.Proxy` configurations for
         * Models that do not define an explicit `{@link Ext.data.Model#cfg-proxy proxy}`.
         *
         * This template is processed with the Model class as the data object which means
         * any static properties of the Model are available. The most useful of these are
         *
         *  * `prefix` - The `urlPrefix` property of this instance.
         *  * `entityName` - The {@link Ext.data.Model#entityName name} of the Model
         *      (for example, "User").
         *  * `schema` - This instance.
         */
        proxy: {
            type: 'ajax',
            url: '{prefix}/{entityName}'
        },
 
        /**
         * @cfg {String} [urlPrefix=""]
         * This is the URL prefix used for all requests to the server. It could be something
         * like "/~api". This value is included in the `proxy` template data as "prefix".
         */
        urlPrefix: ''
    },
 
    onClassExtended: function(cls, data) {
        var alias = data.alias;
 
        if (alias && !data.type) {
            if (!Ext.isString(alias)) {
                alias = alias[0];
            }
 
            cls.prototype.type = alias.substring(this.prototype.aliasPrefix.length);
        }
    },
 
    constructor: function(config) {
        this.initConfig(config);
        this.clear();
    },
 
    //-------------------------------------------------------------------------
    // Config
    // <editor-fold>
 
    applyDefaultIdentifier: function(identifier) {
        return identifier && Ext.Factory.dataIdentifier(identifier);
    },
 
    applyNamer: function(namer) {
        var ret = Ext.data.schema.Namer.create(namer);
        
        ret.schema = this;
        
        return ret;
    },
 
    applyNamespace: function(namespace) {
        var end;
        
        if (namespace) {
            end = namespace.length - 1;
            
            if (namespace.charAt(end) !== '.') {
                namespace += '.';
            }
        }
 
        return namespace;
    },
 
    applyProxy: function(proxy) {
        return Ext.util.ObjectTemplate.create(proxy);
    },
 
    // </editor-fold>
 
    //-------------------------------------------------------------------------
    // Public
 
    eachAssociation: function(fn, scope) {
        var associations = this.associations,
            name;
 
        for (name in associations) {
            if (associations.hasOwnProperty(name)) {
                if (fn.call(scope, name, associations[name]) === false) {
                    break;
                }
            }
        }
    },
 
    eachEntity: function(fn, scope) {
        var entities = this.entities,
            name;
 
        for (name in entities) {
            if (entities.hasOwnProperty(name)) {
                if (fn.call(scope, name, entities[name].cls) === false) {
                    break;
                }
            }
        }
    },
 
    /**
     * Returns an `Association` by name.
     * @param {String} name The name of the association.
     * @return {Ext.data.schema.Association} The association instance.
     */
    getAssociation: function(name) {
        var entry = this.associations[name];
        
        return entry || null;
    },
 
    /**
     * Returns an entity by name.
     * @param {String} name The name of the entity
     * @return {Ext.data.Model} The entity class.
     */
    getEntity: function(name) {
        var entry = this.entityClasses[name] || this.entities[name];
        
        return (entry && entry.cls) || null;
    },
    
    /**
     * Get the entity name taking into account the {@link #namespace}.
     * @param {String/Ext.data.Model} cls The model class or name of the class.
     * @return {String} The entity name
     */
    getEntityName: function(cls) {
        var ns = this.getNamespace(),
            index, name;
            
        if (typeof cls === 'string') {
            name = cls;
        }
        else {
            name = cls.$className || null;
        }
 
        if (name) { // if (not anonymous class)
            if (ns) {
                index = ns.length;
                
                if (name.substring(0, index) !== ns) {
                    return name;
                }
            }
 
            if (index) {
                name = name.substring(index);
            }
        }
 
        return name;
    },
    
    /**
     * Checks if the passed entity has attached associations that need to be read when
     * using nested loading.
     * 
     * @param {String/Ext.Class/Ext.data.Model} name The name, instance, or Model class.
     * @return {Boolean} `true` if there are associations attached to the entity.
     */
    hasAssociations: function(name) {
        name = name.entityName || name;
        
        return !!this.associationEntityMap[name];
    },
    
    /**
     * Checks if an entity is defined
     * @param {String/Ext.data.Model} entity The name or model
     * @return {Boolean} True if this entity is defined
     */
    hasEntity: function(entity) {
        var name = this.getEntityName(entity);
        
        return !!(this.entities[name] || this.entityClasses[name]);
    },
 
    //-------------------------------------------------------------------------
    // Protected
 
    /**
     * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
     * entity.
     * 
     * This is the ideal method to override in a derived class if the standard, default
     * naming conventions need to be adjusted. In the override, apply whatever logic is
     * appropriate to determine the missing values and pass along the proper results to
     * this method in the `callParent`.
     * 
     * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
     *
     * @param {String} matrixName The name of the matrix association.
     *
     * @param {String} [relation] A base name for the matrix. For information about the
     * meaning of this see {@link Ext.data.schema.Schema#ManyToMany}.
     * 
     * @param {Object} left The descriptor for the "left" of the matrix.
     * @param {String} left.type The type of the entity on the "left" of the matrix.
     * 
     * @param {String} [left.field] The name of the field in the matrix table for the "left"
     * side entity. If not provided, this defaults to the `left.type` name
     * {@link Ext.util.Inflector#singularize singularized} and uncapitalized followed by
     * "Id". For example, "userId" for a `left.type` of "Users".
     * 
     * @param {String} [left.role] The name of the relationship from the `left.type` to the
     * `right.type`. If not provided, this defaults to the `left.type` name
     * {@link Ext.util.Inflector#pluralize pluralized} and uncapitalized. For example,
     * "users" for a `left.type` of "User".
     * 
     * @param {Object} right The descriptor for the "right" of the matrix.
     * @param {String} right.type The type of the entity on the "right" of the matrix.
     * 
     * @param {String} [right.field] The name of the field in the matrix table for the
     * "right" side entity. If not provided, this defaults in the same way as `left.field`
     * except this is based on `right.type`.
     * 
     * @param {String} [right.role] The name of the relationship from the `right.type` to
     * the `left.type`. If not provided, this defaults in the same way as `left.role`
     * except this is based on `right.type`.
     * 
     * @protected
     */
    addMatrix: function(entityType, matrixName, relation, left, right) {
        var me = this,
            namer = me.getNamer(),
            associations = me.associations,
            entities = me.entities,
            leftType = left.type,
            rightType = right.type,
            leftField = left.field || namer.apply('idField', leftType),
            rightField = right.field || namer.apply('idField', rightType),
            leftRole = left.role || namer.matrixRole(relation, leftType),
            rightRole = right.role || namer.matrixRole(relation, rightType),
            matrix, leftEntry, rightEntry;
 
        leftEntry = entities[leftType] ||
                   (entities[leftType] = { cls: null, name: leftType, associations: {} });
 
        rightEntry = entities[rightType] ||
                    (entities[rightType] = { cls: null, name: rightType, associations: {} });
 
        ++me.assocCount;
        
        associations[matrixName] = matrix = new Ext.data.schema.ManyToMany({
            name: matrixName,
            schema: me,
            definedBy: entityType,
            left: {
                cls: leftEntry.cls,
                type: leftType,
                role: leftRole,
                field: leftField,
                associationKey: left.associationKey
            },
            right: {
                cls: rightEntry.cls,
                type: rightType,
                role: rightRole,
                field: rightField,
                associationKey: right.associationKey
            }
        });
 
        leftEntry.associations[matrix.right.role] = matrix.right;
        rightEntry.associations[matrix.left.role] = matrix.left;
 
        if (leftEntry.cls) {
            me.associationEntityMap[leftEntry.cls.entityName] = true;
        }
 
        if (rightEntry.cls) {
            me.associationEntityMap[rightEntry.cls.entityName] = true;
        }
 
        me.decorateModel(matrix);
    },
 
    /**
     * Adds a {@link Ext.data.Field#reference reference} field association for an entity
     * to this `schema`.
     * 
     * This is the ideal method to override in a derived class if the standard, default
     * naming conventions need to be adjusted. In the override, apply whatever logic is
     * appropriate to determine the missing values and pass along the proper results to
     * this method in the `callParent`.
     * 
     * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
     * 
     * @param {Ext.data.field.Field} referenceField The `field` with the `reference` config.
     * 
     * @param {Object} [descr] The `reference` descriptor from the `referenceField` if one
     * was given in the field definition.
     *
     * @param {String} [descr.association] The name of the association. If empty or null, this
     * will be derived from `entityType`, `role`, `inverse` and
     * `referenceField.unique`.
     *
     * @param {String} [descr.role] The name of the relationship from `entityType` to the target
     * `type`. If not specified, the default is the `referenceField.name` (minus any "Id"
     * suffix if present).
     *
     * @param {String} [descr.inverse] The name of the relationship from the target `type`
     * to the `entityType`. If not specified, this is derived from the
     * {@link Ext.data.Model#entityName entityName} of the `entityType`
     * ({@link Ext.util.Inflector#singularize singularized} or
     * {@link Ext.util.Inflector#pluralize pluralized} based on `referenceField.unique`).
     *
     * @param {String} descr.type The {@link Ext.data.Model#entityName entityName} of the
     * target of the reference.
     *
     * @param {Boolean} [unique=false] Indicates if the reference is one-to-one.
     * @param {Boolean} [dupeCheck] (private)
     * 
     * @protected
     */
    addReference: function(entityType, referenceField, descr, unique, dupeCheck) {
        var me = this,
            namer = me.getNamer(),
            entities = me.entities,
            associations = me.associations,
            entityName = entityType.entityName,
            association = descr.association,
            child = descr.child,
            parent = descr.parent,
            rightRole = descr.role,
            // Allow { child: 'OrderItem' } or the reverse (for one-to-one mostly):
            rightType = descr.type || parent || child,
            leftVal = descr.inverse,
            left = Ext.isString(leftVal) ? { role: leftVal } : leftVal,
            leftRole = left && left.role,
            entry, T;
 
        if (!rightRole) {
            // In a FK association, the left side has the key in a field named something
            // like "orderId". The default implementation of "fieldRole" namer is to drop
            // the id suffix which gives is the role of the right side.
            if (!referenceField || descr.legacy) {
                rightRole = namer.apply('uncapitalize', rightType);
            }
            else {
                rightRole = namer.apply('fieldRole', referenceField.name);
            }
        }
 
        if (!leftRole) {
            leftRole = namer.inverseFieldRole(entityName, unique, rightRole, rightType);
        }
 
        if (!association) {
            if (unique) {
                association = namer.oneToOne(entityType, leftRole, rightType, rightRole);
            }
            else {
                association = namer.manyToOne(entityType, leftRole, rightType, rightRole);
            }
        }
 
        if (dupeCheck && association in associations) {
            if (dupeCheck(associations[association], association, leftRole, rightRole) !== false) {
                return;
            }
        }
 
        //<debug>
        if (association in associations) {
            Ext.raise('Duplicate association: "' + association + '" declared by ' +
                    entityName + (referenceField ? ('.' + referenceField.name) : '') +
                    ' (collides with ' +
                    associations[association].definedBy.entityName + ')');
        }
        
        if (referenceField && referenceField.definedBy === entities[rightType]) {
            Ext.raise('ForeignKey reference should not be owned by the target model');
        }
        //</debug>
 
        // Lookup the entry for the target of the reference. Since it may not as yet be
        // defined, we may need to create the entry.
        entry = entities[rightType] ||
               (entities[rightType] = { cls: null, name: rightType, associations: {} });
 
        // as a field w/reference we are always "left":
        T = unique ? Ext.data.schema.OneToOne : Ext.data.schema.ManyToOne;
        
        association = new T({
            name: association,
            // Note: "parent" or "child" can be strings so don't assume otherwise
            owner: child ? 'left' : (parent ? 'right' : null),
            definedBy: entityType,
            schema: me,
            field: referenceField,
            nullable: referenceField ? !!referenceField.allowBlank : true,
            left: {
                cls: entityType,
                type: entityName,
                role: leftRole,
                extra: left
            },
            right: {
                cls: entry.cls,
                type: rightType,
                role: rightRole,
                extra: descr
            },
            meta: descr
        });
 
        // Add the left and right association "sides" to the appropriate collections, but
        // remember that the right-side entity class may not yet be declared (that's ok as
        // we store the associations in the entry):
        entityType.associations[rightRole] = association.right;
        entry.associations[leftRole] = association.left;
        
        if (referenceField) {
            // Store the role on the FK field. This "upgrades" legacy associations to the
            // new "field.reference" form.
            referenceField.reference = association.right;
            entityType.references.push(referenceField);
        }
 
        ++me.assocCount;
        me.associationEntityMap[entityName] = true;
        
        if (entry.cls) {
            me.associationEntityMap[entry.cls.entityName] = true;
        }
        
        associations[association.name] = association;
        
        if (association.right.cls) {
            me.decorateModel(association);
        }
    },
 
    //-------------------------------------------------------------------------
 
    privates: {
        /**
         * Adds an {@link Ext.data.Model entity} to this `schema`.
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model}.
         * @private
         */
        addEntity: function(entityType) {
            var me = this,
                entities = me.entities,
                entityName = entityType.entityName,
                entry = entities[entityName],
                fields = entityType.fields,
                associations, field, i, length, name;
 
            if (!entry) {
                entities[entityName] = entry = {
                    name: entityName,
                    associations: {}
                };
            }
            //<debug>
            else if (entry.cls) {
                Ext.raise('Duplicate entity name "' + entityName + '": ' +
                        entry.cls.$className + ' and ' + entityType.$className);
            }
            //</debug>
            else {
                associations = entry.associations;
                
                for (name in associations) {
                    // the associations collection describes the types to which this entity is
                    // related, but the inverse descriptors need this entityType:
                    associations[name].inverse.cls = entityType;
 
                    me.associationEntityMap[entityName] = true;
 
                    // We already have an entry, which means other associations have likely
                    // been added for us, so go ahead and do the inverse decoration
                    me.decorateModel(associations[name].association);
                }
            }
 
            entry.cls = entityType;
            entityType.prototype.associations = entityType.associations = entry.associations;
            me.entityClasses[entityType.$className] = entry;
 
            ++me.entityCount;
 
            for (= 0, length = fields.length; i < length; ++i) {
                field = fields[i];
                
                if (field.reference) {
                    me.addReferenceDescr(entityType, field);
                }
            }
        },
 
        /**
         * Adds the matrix associations of an {@link Ext.data.Model entity} to this `schema`.
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
         * @param {Object/String[]} matrices The manyToMany matrices for the class.
         * @private
         */
        addMatrices: function(entityType, matrices) {
            var me = this,
                i, length, matrixName;
 
            if (Ext.isString(matrices)) {
                me.addMatrixDescr(entityType, null, matrices);
            }
            else if (matrices[0]) { // if (isArray)
                for (= 0, length = matrices.length; i < length; ++i) {
                    me.addMatrixDescr(entityType, null, matrices[i]);
                }
            }
            else {
                for (matrixName in matrices) {
                    me.addMatrixDescr(entityType, matrixName, matrices[matrixName]);
                }
            }
        },
 
        /**
         * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
         * {@link Ext.data.Model entity}.
         *
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
         * @param {String} [matrixName] The name of the matrix association.
         * @param {String/Object} matrixDef A {@link Ext.data.schema.ManyToMany matrix config}
         * declared by an {@link Ext.data.Model entity}.
         * @private
         */
        addMatrixDescr: function(entityType, matrixName, matrixDef) {
            var me = this,
                entityName = entityType.entityName,
                associations = me.associations,
                namer = me.getNamer(),
                left = matrixDef.left,
                right = matrixDef.right,
                last, relation;
 
            if (Ext.isString(matrixDef)) {
                if (matrixDef.charAt(0) === '#') { // "#User" (entity is on the left)
                    /*
                     *  Ext.define('User', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: '#Group'
                     *  });
                     */
                    left = { type: entityName }; // User
                    right = { type: matrixDef.substring(1) }; // Group
                }
                else if (matrixDef.charAt(last = matrixDef.length - 1) === '#') { // "User#"
                    /*
                     *  Ext.define('Group', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: 'User#'
                     *  });
                     */
                    left = { type: matrixDef.substring(0, last) }; // User
                    right = { type: entityName }; // Group
                }
                else if (namer.apply('multiRole', entityName) <
                         namer.apply('multiRole', matrixDef)) {
                    /*
                     *  Ext.define('Group', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: 'User'
                     *  });
                     */
                    left = { type: entityName };  // Group
                    right = { type: matrixDef };  // User
                }
                else {
                    /*
                     *  Ext.define('User', {
                     *      extend: 'Ext.data.Model',
                     *      manyToMany: 'Group'
                     *  });
                     */
                    left = { type: matrixDef };  // Group
                    right = { type: entityName };  // User
                }
            }
            else {
                //<debug>
                Ext.Assert.isString(matrixDef.type, 'No "type" for manyToMany in ' + entityName);
                //</debug>
 
                relation = matrixDef.relation;
 
                /* eslint-disable-next-line max-len */
                if (left || (!right && namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef.type))) {
                    if (!left || left === true) {
                        /*
                         *  Ext.define('User', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'Group',
                         *          left: true
                         *      }
                         *  });
                         */
                        left = { type: entityName }; // User
                    }
                    else {
                        /*
                         *  Ext.define('User', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'Group',
                         *          left: {
                         *              role: 'useroids'
                         *          }
                         *      }
                         *  });
                         */
                        left = Ext.apply({ type: entityName }, left); // User
                    }
                    
                    right = matrixDef;  // Group
                }
                else {
                    if (!right || right === true) {
                        /*
                         *  Ext.define('Group', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'User',
                         *          right: true
                         *      }
                         *  });
                         */
                        right = { type: entityName }; // Group
                    }
                    else {
                        /*
                         *  Ext.define('Group', {
                         *      extend: 'Ext.data.Model',
                         *      manyToMany: {
                         *          type: 'User',
                         *          right: {
                         *              role: 'groupoids'
                         *          }
                         *      }
                         *  });
                         */
                        right = Ext.apply({ type: entityName }, right); // Group
                    }
                    
                    left = matrixDef; // User
                }
            }
 
            if (!matrixName) {
                matrixName = namer.manyToMany(relation, left.type, right.type);
            }
 
            if (!(matrixName in associations)) {
                me.addMatrix(entityType, matrixName, relation, left, right);
            }
            //<debug>
            //
            // In the case of a matrix association, both sides may need to declare it to allow
            // them to be used w/o the other present. In development mode, we want to check
            // that they declare the same thing!
            //
            else {
                /* eslint-disable-next-line vars-on-top, one-var */
                var entry = associations[matrixName],
                    before = [entry.kind, entry.left.type, entry.left.role, entry.left.field,
                              entry.right.type, entry.right.role, entry.right.field].join('|'),
                    after;
 
                // Call back in to bypass this check and realize the new association:
                delete associations[matrixName];
                me.addMatrix(entityType, matrixName, relation, left, right);
                after = associations[matrixName];
 
                // Restore the originals so we match production behavior (for testing)
                associations[matrixName] = entry;
                entry.left.cls.associations[entry.right.role] = entry.right;
                entry.right.cls.associations[entry.left.role] = entry.left;
                --me.assocCount;
 
                // Now we can compare the old and the new to see if they are the same.
                after = [after.kind, after.left.type, after.left.role, after.left.field,
                         after.right.type, after.right.role, after.right.field].join('|');
 
                if (before != after) { // eslint-disable-line eqeqeq
                    Ext.log.warn(matrixName + '(' + entry.definedBy.entityName + '): ' + before);
                    Ext.log.warn(matrixName + '(' + entityName + '): ' + after);
                    Ext.raise('Conflicting association: "' + matrixName + '" declared by ' +
                        entityName + ' was previously declared by ' + entry.definedBy.entityName);
                }
            }
            //</debug>
        },
 
        /**
         * Adds a {@link Ext.data.Field#reference reference} {@link Ext.data.Field field}
         * association for an entity to this `schema`. This method decodes the `reference`
         * config of the `referenceField` and calls {@link #addReference}.
         *
         * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Model}.
         * @param {Ext.data.Field} referenceField The `field` with the `reference` config.
         * @private
         */
        addReferenceDescr: function(entityType, referenceField) {
            var me = this,
                descr = referenceField.$reference;
 
            if (Ext.isString(descr)) {
                descr = {
                    type: descr
                };
            }
            else {
                descr = Ext.apply({}, descr);
            }
 
            me.addReference(entityType, referenceField, descr, referenceField.unique);
        },
 
        addBelongsTo: function(entityType, assoc) {
            this.addKeylessSingle(entityType, assoc, false);
        },
 
        addHasOne: function(entityType, assoc) {
            this.addKeylessSingle(entityType, assoc, true);
        },
 
        addKeylessSingle: function(entityType, assoc, unique) {
            var foreignKey, referenceField;
 
            assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
            assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
 
            foreignKey = assoc.foreignKey || (assoc.type.toLowerCase() + '_id');
            referenceField = entityType.getField(foreignKey);
            assoc.fromSingle = true;
            
            if (referenceField) {
                referenceField.$reference = assoc;
                referenceField.unique = true;
                assoc.legacy = true;
                //<debug>
                Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' +
                             'See Ext.data.field.Field.reference');
                //</debug>
            }
            
            this.addReference(entityType, referenceField, assoc, unique);
        },
 
        addHasMany: function(entityType, assoc) {
            var me = this,
                entities = me.entities,
                pending = me.pending,
                cls, name, referenceField, target,
                foreignKey, inverseOptions, child, declaredInverse;
 
            assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
 
            assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
 
            name = assoc.type;
            target = entities[name];
            cls = target && target.cls;
            
            if (cls) {
                name = entityType.entityName;
                foreignKey = assoc.foreignKey || (name.toLowerCase() + '_id');
                delete assoc.foreignKey;
 
                // The assoc is really the inverse, so we only set the minimum.
                // We copy the inverse from assoc and apply it over assoc!
                declaredInverse = Ext.apply({}, assoc.inverse);
                delete assoc.inverse;
                inverseOptions = Ext.apply({}, assoc);
                delete inverseOptions.type;
 
                assoc = Ext.apply({
                    type: name,
                    inverse: inverseOptions
                }, declaredInverse);
 
                child = inverseOptions.child;
                
                if (child) {
                    delete inverseOptions.child;
                    assoc.parent = name;
                }
 
                referenceField = cls.getField(foreignKey);
                
                if (referenceField) {
                    referenceField.$reference = assoc;
                    assoc.legacy = true;
                    //<debug>
                    Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' +
                                 'See Ext.data.field.Field.reference');
                    //</debug>
                }
 
                // We already have the entity, we can process it
                me.addReference(cls, referenceField, assoc, false
                //<debug>
                    /* eslint-disable-next-line comma-style */
                    , function(association, name, leftRole, rightRole) {
                    // Check to see if the user has used belongsTo/hasMany in conjunction.
                        var result = !!association.meta.fromSingle && cls === association.left.cls,
                            l, r;
 
                        if (result) {
                            l = cls.entityName;
                            r = entityType.entityName;
                            Ext.raise('hasMany ("' + r + '") and belongsTo ("' + l +
                                      '") should not be used in conjunction to declare ' +
                                      'a relationship. Use only one.');
                        }
 
                        return result;
                    }
                //</debug>
                );
            }
            else {
                // Pending, push it in the queue for when we load it
                if (!pending[name]) {
                    pending[name] = [];
                }
                
                pending[name].push([entityType, assoc]);
            }
        },
 
        checkLegacyAssociation: function(entityType, assoc) {
            var name;
            
            if (Ext.isString(assoc)) {
                assoc = {
                    type: assoc
                };
            }
            else {
                assoc = Ext.apply({}, assoc);
            }
 
            if (assoc.model) {
                assoc.type = assoc.model;
                // TODO: warn
                delete assoc.model;
            }
 
            name = assoc.associatedName || assoc.name;
            
            if (name) {
                // TODO: warn
                delete assoc.associatedName;
                delete assoc.name;
                assoc.role = name;
            }
 
            return assoc;
        },
 
        afterKeylessAssociations: function(cls) {
            var pending = this.pending,
                name = cls.entityName,
                mine = pending[name],
                i, len;
 
            if (mine) {
                for (= 0, len = mine.length; i < len; ++i) {
                    this.addHasMany.apply(this, mine[i]);
                }
                
                delete pending[name];
            }
        },
 
        clear: function(clearNamespace) {
            // for testing
            var me = this,
                timer = me.timer;
 
            delete me.setConfig;
 
            if (timer) {
                window.clearTimeout(timer);
                me.timer = null;
            }
 
            me.associations = {};
            me.associationEntityMap = {};
            me.entities = {};
            me.entityClasses = {};
            me.pending = {};
            me.assocCount = me.entityCount = 0;
            
            if (clearNamespace) {
                me.setNamespace(null);
            }
        },
 
        constructProxy: function(Model) {
            var me = this,
                data = Ext.Object.chain(Model),
                proxy = me.getProxy();
 
            data.schema = me;
            data.prefix = me.getUrlPrefix();
 
            return proxy.apply(data);
        },
 
        applyDecoration: function(role) {
            var me = this,
                // To decorate a role like "users" (of a User / Group matrix) we need to add
                // getter/setter methods to access the "users" collection ... to Group! All
                // other data about the "users" role and the User class belong to the given
                // "role" but the receiver class is the inverse.
                cls = role.inverse.cls,
                namer = me.getNamer(),
                getterName, setterName, proto;
 
            // The cls may not be loaded yet, so we need to check if it is before
            // we can decorate it.
            if (cls && !role.decorated) {
                role.decorated = true;
 
                proto = cls.prototype;
 
                if (!(getterName = role.getterName)) {
                    role.getterName = getterName = namer.getterName(role);
                }
                
                proto[getterName] = role.createGetter();
 
                // Not all associations will create setters
                if (role.createSetter) {
                    if (!(setterName = role.setterName)) {
                        role.setterName = setterName = namer.setterName(role);
                    }
                    
                    proto[setterName] = role.createSetter();
                }
            }
        },
 
        decorateModel: function(association) {
            this.applyDecoration(association.left);
            this.applyDecoration(association.right);
        },
 
        processKeyChecks: function(processAll) {
            var me = this,
                keyCheckQueue = me.keyCheckQueue,
                timer = me.timer,
                len, i, item;
 
            if (timer) {
                window.clearTimeout(timer);
                me.timer = null;
            }
 
            if (!keyCheckQueue) {
                return;
            }
 
            // It's possible that processing a drop may cause another drop
            // to occur. If we're trying to forcibly resolve the state, then
            // we need to trigger all the drops at once. With processAll: false,
            // the loop will jump out after the first iteration.
            do {
                keyCheckQueue = me.keyCheckQueue;
                me.keyCheckQueue = [];
 
                for (= 0, len = keyCheckQueue.length; i < len; ++i) {
                    item = keyCheckQueue[i];
                    item.role.checkKeyForDrop(item.record);
                }
            } while (processAll && me.keyCheckQueue.length);
        },
 
        queueKeyCheck: function(record, role) {
            var me = this,
                keyCheckQueue = me.keyCheckQueue,
                timer = me.timer;
 
            if (!keyCheckQueue) {
                me.keyCheckQueue = keyCheckQueue = [];
            }
            
            keyCheckQueue.push({
                record: record,
                role: role
            });
 
            if (!timer) {
                me.timer = timer = Ext.defer(me.processKeyChecks, me.getKeyCheckDelay(), me);
            }
        },
 
        rankEntities: function() {
            var me = this,
                entities = me.entities,
                entityNames = Ext.Object.getKeys(entities),
                length = entityNames.length,
                entityType, i;
 
            me.nextRank = 1;
 
            // We do an alpha sort to make the results more stable.
            entityNames.sort();
 
            for (= 0; i < length; ++i) {
                entityType = entities[entityNames[i]].cls;
 
                if (!entityType.rank) {
                    me.rankEntity(entityType);
                }
            }
 
            //<debug>
            me.topoStack = null; // cleanup diagnostic stack
            //</debug>
        },
 
        rankEntity: function(entityType) {
            var associations = entityType.associations,
                associatedType, role, roleName;
 
            //<debug>
            /* eslint-disable-next-line vars-on-top, one-var */
            var topoStack = this.topoStack || (this.topoStack = []),
                entityName = entityType.entityName;
 
            topoStack.push(entityName);
 
            if (entityType.rank === 0) {
                Ext.raise(entityName + " has circular foreign-key references: " +
                                topoStack.join(" --> "));
            }
 
            entityType.rank = 0; // mark as "adding" so we can detect cycles
            //</debug>
 
            for (roleName in associations) {
                role = associations[roleName];
                
                // The role describes the thing to which entityType is associated, so we
                // want to know about *this* type and whether it has a foreign-key to the
                // associated type. The left side is the FK owner so if the associated
                // type is !left then entityType is left.
                //
                if (!role.left && role.association.field) {
                    // This entityType has a foreign-key to the associated type, so add
                    // that type first.
                    associatedType = role.cls;
                    
                    if (!associatedType.rank) {
                        this.rankEntity(associatedType);
                    }
                }
            }
 
            entityType.rank = this.nextRank++;
 
            //<debug>
            topoStack.pop();
            //</debug>
        }
    } // private
});