/**
 * This relationship describes the case where any one entity of one type may relate to any
 * number of entities of another type, and also in the reverse.
 * 
 * This form of association cannot store id's in the related entities since that would
 * limit the number of related entities to one for the entity with the foreign key. Instead,
 * these relationships are typically implemented using a so-called "matrix" table. This
 * table typically has two columns to hold the id's of a pair of related entities. This
 * pair of id's is unique in the matrix table.
 * 
 * # Declaration Forms
 * 
 *      // Fully spelled out - all properties are their defaults:
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: {
 *              UserGroups: {
 *                  type: 'User',
 *                  role: 'users',
 *                  field: 'userId',
 *                  right: {
 *                      field: 'groupId',
 *                      role: 'groups'
 *                  }
 *              }
 *          }
 *      });
 *
 *      // Eliminate "right" object and use boolean to indicate Group is on the
 *      // right. By default, left/right is determined by alphabetic order.
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: {
 *              UserGroups: {
 *                  type: 'User',
 *                  role: 'users',
 *                  field: 'userId',
 *                  right: true
 *              }
 *          }
 *      });
 *
 *      // Eliminate object completely and rely on string to name the other type. Still
 *      // keep Group on the "right".
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: {
 *              UserGroups: 'User#'   // '#' is on the side (left or right) of Group
 *          }
 *      });
 *
 *      // Remove explicit matrix name and keep Group on the "right". Generated matrixName
 *      // remains "UserGroups".
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: [
 *              'User#'
 *          ]
 *      });
 *
 *      // Minimal definition but now Group is on the "left" since "Group" sorts before
 *      // "User". Generated matrixName is now "GroupUsers".
 *      
 *      Ext.define('App.models.Group', {
 *          extend: 'Ext.data.Model',
 *          
 *          manyToMany: [
 *              'User'
 *          ]
 *      });
 */
Ext.define('Ext.data.schema.ManyToMany', {
    extend: 'Ext.data.schema.Association',
 
    isManyToMany: true,
 
    isToMany: true,
 
    kind: 'many-to-many',
 
    Left: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
 
        isMany: true,
 
        digitRe: /^\d+$/,
 
        findRecords: function(session, rightRecord, leftRecords) {
            var slice = session.getMatrixSlice(this.inverse, rightRecord.id),
                members = slice.members,
                ret = [],
                cls = this.cls,
                seen, i, len, id, member, leftRecord;
 
            if (leftRecords) {
                seen = {};
 
                // Loop over the records returned by the server and
                // check they all still belong
                for (= 0, len = leftRecords.length; i < len; ++i) {
                    leftRecord = leftRecords[i];
                    id = leftRecord.id;
                    member = members[id];
 
                    if (!(member && member[2] === -1)) {
                        ret.push(leftRecord);
                    }
 
                    seen[id] = true;
                }
            }
 
            // Loop over the expected set and include any missing records.
            for (id in members) {
                member = members[id];
 
                if (!seen || !seen[id] && (member && member[2] !== -1)) {
                    leftRecord = session.peekRecord(cls, id);
 
                    if (leftRecord) {
                        ret.push(leftRecord);
                    }
                }
            }
 
            return ret;
        },
 
        onIdChanged: function(rightRecord, oldId, newId) {
            var store = this.getAssociatedItem(rightRecord);
 
            if (store) {
                store.getFilters().get(this.$roleFilterId).setValue(newId);
            }
        },
 
        processLoad: function(store, rightRecord, leftRecords, session) {
            var ret = leftRecords;
 
            if (session) {
                ret = this.findRecords(session, rightRecord, leftRecords);
                this.onAddToMany(store, ret, true);
            }
 
            return ret;
        },
 
        processUpdate: function(session, associationData) {
            var me = this,
                entityType = me.inverse.cls,
                items = associationData.R,
                id, rightRecord, store, leftRecords;
 
            if (items) {
                for (id in items) {
                    rightRecord = session.peekRecord(entityType, id);
 
                    if (rightRecord) {
                        leftRecords = session.getEntityList(me.cls, items[id]);
                        store = me.getAssociatedItem(rightRecord);
 
                        if (store) {
                            store.loadData(leftRecords);
                            store.complete = true;
                        }
                        else {
                            // We don't have a store. Create it and add the records.
                            rightRecord[me.getterName](null, null, leftRecords);
                        }
                    }
                    else {
                        session.onInvalidAssociationEntity(entityType, id);
                    }
                }
            }
 
            me.processMatrixBlock(session, associationData.C, 1);
            me.processMatrixBlock(session, associationData.D, -1);
        },
 
        checkMembership: function(session, rightRecord) {
            var matrix = session.getMatrix(this.association, true),
                side, entityType, inverse, slice, slices,
                id, members, member, leftRecord, store;
 
            if (!matrix) {
                return;
            }
 
            side = this.left ? matrix.right : matrix.left;
            entityType = side.inverse.role.cls;
            inverse = this.inverse;
            slices = side.slices;
 
            if (slices) {
                slice = slices[rightRecord.id];
 
                if (slice) {
                    members = slice.members;
 
                    for (id in members) {
                        member = members[id];
 
                        if (member[2] !== -1) {
                            // Do we have the record in the session?
                            // If so, do we also have the store?
                            leftRecord = session.peekRecord(entityType, id);
 
                            if (leftRecord) {
                                store = inverse.getAssociatedItem(leftRecord);
 
                                if (store) {
                                    store.matrixUpdate = 1;
                                    store.add(rightRecord);
                                    store.matrixUpdate = 0;
                                }
                            }
                        }
                    }
                }
            }
        },
 
        onStoreCreate: function(store, session, id) {
            var me = this,
                matrix;
 
            if (session) {
                // If we are creating a store of say Groups in a UserGroups matrix, we want
                // to traverse the inverse side of the matrix (Users) because the id we have
                // is that of the User to which these Groups are associated.
                matrix = session.getMatrixSlice(me.inverse, id);
 
                matrix.attach(store);
                matrix.notify = me.onMatrixUpdate;
                matrix.scope = me;
            }
        },
 
        processMatrixBlock: function(session, leftKeys, state) {
            var inverse = this.inverse,
                digitRe = this.digitRe,
                slice, id;
 
            if (leftKeys) {
                for (id in leftKeys) {
                    // We may not have the record available to pull out the id, so the best we can
                    // do here is try to detect a number id.
                    if (digitRe.test(id)) {
                        id = parseInt(id, 10);
                    }
 
                    slice = session.getMatrixSlice(inverse, id);
                    slice.update(leftKeys[id], state);
                }
            }
        },
 
        createGetter: function() {
            var me = this;
 
            return function(options, scope, leftRecords) {
                // 'this' refers to the Model instance inside this function
                return me.getAssociatedStore(this, options, scope, leftRecords, false);
            };
        },
 
        /*
         * This method is called when records are added to the association store. If this
         * is happening as a side-effect of the underlying matrix update, we skip telling
         * the matrix what it already knows. Otherwise we need to tell the matrix of the
         * changes on this side so that they can be reflected on the other side.
         */
        onAddToMany: function(store, leftRecords, load) {
            if (!store.matrixUpdate) {
                store.matrixUpdate = 1;
                // By default the "load" param is really the index, but we call this manually
                // in a few spots to indicate it's a default load
                store.matrix.update(leftRecords, load === true ? 0 : 1);
                store.matrixUpdate = 0;
            }
        },
 
        /*
         * This method is called when records are removed from the association store. The
         * same logic applies here as in onAddToMany with respect to the update that may
         * or may not be taking place on the underlying matrix.
         */
        onRemoveFromMany: function(store, records) {
            if (!store.matrixUpdate) {
                store.matrixUpdate = 1;
                store.matrix.update(records, -1);
                store.matrixUpdate = 0;
            }
        },
 
        read: function(rightRecord, node, fromReader, readOptions) {
            var me = this,
                leftRecords = me.callParent([rightRecord, node, fromReader, readOptions]);
 
            if (leftRecords) {
                // Create the store and dump the data
                rightRecord[me.getterName](null, null, leftRecords);
                // Inline associations should *not* arrive on the "data" object:
                delete rightRecord.data[me.role];
            }
 
        },
 
        onMatrixUpdate: function(matrixSlice, id, state) {
            var store = matrixSlice.store,
                index, leftRecord, entry;
 
            if (store && !store.loading && !store.matrixUpdate) {
                store.matrixUpdate = 1;
 
                index = store.indexOfId(id);
 
                if (state < 0) {
                    if (index >= 0) {
                        store.remove([ index ]);
                    }
                }
                else if (index < 0) {
                    entry = store.getSession().getEntry(this.type, id);
                    leftRecord = entry && entry.record;
 
                    if (leftRecord) {
                        store.add(leftRecord);
                    }
                }
 
                store.matrixUpdate = 0;
            }
        },
 
        adoptAssociated: function(record, session) {
            var store = this.getAssociatedItem(record),
                records, i, len;
 
            if (store) {
                store.setSession(session);
                this.onStoreCreate(store, session, record.getId());
                records = store.getData().items;
 
                for (= 0, len = records.length; i < len; ++i) {
                    session.adopt(records[i]);
                }
            }
        }
    }, function() {
        var Left = this; // Left is created but ManyToMany may not yet be created
 
        Ext.ClassManager.onCreated(function() {
            Ext.data.schema.ManyToMany.prototype.Right = Ext.define(null, {
                extend: Left,
                left: false,
                side: 'right'
            });
        }, null, 'Ext.data.schema.ManyToMany');
    })
});