/**
 * **This class is never created directly. It should be constructed through associations in `Ext.data.Model`.**
 *
 * Declares a relationship between a single entity type and multiple related entities. The relationship can
 * be declared as a keyed or keyless relationship.
 *
 *     // Keyed
 *     Ext.define('Customer', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name']
 *     });
 *
 *     Ext.define('Ticket', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'title', {
 *             name: 'customerId',
 *             reference: 'Customer'
 *         }]
 *     });
 *
 *     // Keyless
 *     Ext.define('Customer', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name'],
 *         hasMany: 'Ticket'
 *     });
 *
 *     Ext.define('Ticket', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'title']
 *     });
 *
 *     // Generated methods
 *     var customer = new Customer();
 *     customer.tickets();
 *
 *     var ticket = new Ticket();
 *     ticket.getCustomer();
 *     ticket.setCustomer();
 *
 * By declaring a keyed relationship, extra functionality is gained that maintains
 * the key field in the model as changes are made to the association. 
 * 
 * For available configuration options, see {@link Ext.data.schema.Reference}.
 * The "one" record type will have a generated {@link Ext.data.schema.Association#storeGetter}. The "many" record type
 * will have a {@link Ext.data.schema.Association#recordGetter getter} and {@link Ext.data.schema.Association#recordSetter setter}.
 */
Ext.define('Ext.data.schema.ManyToOne', {
    extend: 'Ext.data.schema.Association',
 
    isManyToOne: true,
 
    isToOne: true,
 
    kind: 'many-to-one',
 
    Left: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
 
        isMany: true,
 
        onDrop: function(rightRecord, session) {
            var me = this,
                store = me.getAssociatedItem(rightRecord),
                leftRecords, len, i, refs, id;
 
            if (store) {
                // Removing will cause the foreign key to be set to null.
                leftRecords = store.removeAll();
                if (leftRecords && me.inverse.owner) {
                    // If we're a child, we need to destroy all the "tickets"
                    for (= 0, len = leftRecords.length; i < len; ++i) {
                        leftRecords[i].drop();
                    }
                }
 
                store.destroy();
                rightRecord[me.getStoreName()] = null;
            } else if (session) {
                leftRecords = session.getRefs(rightRecord, me);
                if (leftRecords) {
                    for (id in leftRecords) {
                        leftRecords[id].drop();
                    }
                }
            }
        },
 
        onIdChanged: function(rightRecord, oldId, newId) {
            var fieldName = this.association.getFieldName(),
                store = this.getAssociatedItem(rightRecord),
                leftRecords, i, len, filter;
 
            if (store) {
                filter = store.getFilters().get(this.$roleFilterId);
                if (filter) {
                    filter.setValue(newId);
                }
                // A session will automatically handle this updating. If we don't have a field
                // then there's nothing to do here.
                if (!rightRecord.session && fieldName) {
                    leftRecords = store.getDataSource().items;
                    for (= 0, len = leftRecords.length; i < len; ++i) {
                        leftRecords[i].set(fieldName, newId);
                    }
                }
            }
        },
 
        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);
                    }
                }
            }
        },
 
        findRecords: function(session, rightRecord, leftRecords, allowInfer) {
            var ret = leftRecords,
                refs = session.getRefs(rightRecord, this, true),
                field = this.association.field,
                fieldName, leftRecord, id, i, len, seen;
 
            if (field && (refs || allowInfer)) {
                fieldName = field.name;
                ret = [];
 
                if (leftRecords) {
                    seen = {};
                    // Loop over the records returned by the server and
                    // check they all still belong. If the session doesn't have any prior knowledge
                    // and we're allowed to infer the parent id (via nested loading), only do so if
                    // we explicitly have an id specified
                    for (= 0, len = leftRecords.length; i < len; ++i) {
                        leftRecord = leftRecords[i];
                        id = leftRecord.id;
                        if (refs && refs[id]) {
                            ret.push(leftRecord);
                        } else if (allowInfer && leftRecord.data[fieldName] === undefined) {
                            ret.push(leftRecord);
                            leftRecord.data[fieldName] = rightRecord.id;
                            session.updateReference(leftRecord, field, rightRecord.id, undefined);
                        }
                        seen[id] = true;
                    }
                }
 
                // Loop over the expected set and include any missing records.
                if (refs) {
                    for (id in refs) {
                        if (!seen || !seen[id]) {
                            ret.push(refs[id]);
                        }
                    }
                }
            }
 
            return ret;
        },
 
        processLoad: function(store, rightRecord, leftRecords, session) {
            var ret = leftRecords;
 
            if (session) {
                // Allow infer here, we only get called when loading an associated store
                ret = this.findRecords(session, rightRecord, leftRecords, true);
            }
            this.onLoadMany(rightRecord, ret, session);
            return ret;
        },
 
        adoptAssociated: function(rightRecord, session) {
            var store = this.getAssociatedItem(rightRecord),
                leftRecords, i, len;
            if (store) {
                store.setSession(session);
                leftRecords = store.getData().items;
                for (= 0, len = leftRecords.length; i < len; ++i) {
                    session.adopt(leftRecords[i]);
                }
            }
        },
 
        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, true);
            };
        },
 
        createSetter: null, // no setter for an isMany side
 
        onAddToMany: function (store, leftRecords) {
            this.syncFK(leftRecords, store.getAssociatedEntity(), false);
        },
 
        onLoadMany: function(rightRecord, leftRecords, session) {
            var instanceName = this.inverse.getInstanceName(),
                id = rightRecord.getId(),
                field = this.association.field,
                i, len, leftRecord, oldId, data, name;
 
            if (field) {
                for (= 0, len = leftRecords.length; i < len; ++i) {
                    leftRecord = leftRecords[i];
                    leftRecord[instanceName] = rightRecord;
                    if (field) {
                        name = field.name;
                        data = leftRecord.data;
                        oldId = data[name];
                        if (oldId !== id) {
                            data[name] = id;
                            if (session) {
                                session.updateReference(leftRecord, field, id, oldId);
                            }
                        }
                    }
                }
            }
        },
 
        onRemoveFromMany: function (store, leftRecords) {
            this.syncFK(leftRecords, store.getAssociatedEntity(), true);
        },
 
        read: function(rightRecord, node, fromReader, readOptions) {
            var me = this,
                // We use the inverse role here since we're setting ourselves
                // on the other record
                instanceName = me.inverse.getInstanceName(),
                leftRecords = me.callParent([rightRecord, node, fromReader, readOptions]),
                store, len, i;
            
            if (leftRecords) {
                // Create the store and dump the data
                store = rightRecord[me.getterName](null, null, leftRecords);
                // Inline associations should *not* arrive on the "data" object:
                delete rightRecord.data[me.role];
 
                leftRecords = store.getData().items;
 
                for (= 0, len = leftRecords.length; i < len; ++i) {
                    leftRecords[i][instanceName] = rightRecord;
                }
            }
        },
 
        syncFK: function (leftRecords, rightRecord, clearing) {
            // We are called to set things like the FK (ticketId) of an array of Comment
            // entities. The best way to do that is call the setter on the Comment to set
            // the Ticket. Since we are setting the Ticket, the name of that setter is on
            // our inverse role.
 
            var foreignKeyName = this.association.getFieldName(),
                inverse = this.inverse,
                setter = inverse.setterName, // setTicket
                instanceName = inverse.getInstanceName(),
                i = leftRecords.length,
                id = rightRecord.getId(),
                different, leftRecord, val;
 
            while (i-- > 0) {
                leftRecord = leftRecords[i];
                different = !leftRecord.isEqual(id, leftRecord.get(foreignKeyName));
 
                val = clearing ? null : rightRecord;
                if (different !== clearing) {
                    // clearing === true
                    //      different === true  :: leave alone (not associated anymore)
                    //   ** different === false :: null the value (no longer associated)
                    //
                    // clearing === false
                    //   ** different === true  :: set the value (now associated)
                    //      different === false :: leave alone (already associated)
                    //
                    leftRecord.changingKey = true;
                    leftRecord[setter](val);
                    leftRecord.changingKey = false;
                } else {
                    // Ensure we set the instance, we may only have the key
                    leftRecord[instanceName] = val;
                }
            }
        }
    }),
 
    Right: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
 
        left: false,
        side: 'right',
 
        onDrop: function(leftRecord, session) {
            // By virtue of being dropped, this record will be removed
            // from any stores it belonged to. The only case we have
            // to worry about is if we have a session but were not yet
            // part of any stores, so we need to clear the foreign key.
            var field = this.association.field;
            if (field) {
                leftRecord.set(field.name, null);
            }
            leftRecord[this.getInstanceName()] = null;
        },
 
        createGetter: function() {
            // As the target of the FK (say "ticket" for the Comment entity) this
            // getter is responsible for getting the entity referenced by the FK value.
            var me = this;
 
            return function (options, scope) {
                // 'this' refers to the Comment instance inside this function
                return me.doGetFK(this, options, scope);
            };
        },
        
        createSetter: function() {
            var me = this;
 
            return function (rightRecord, options, scope) {
                // 'this' refers to the Comment instance inside this function
                return me.doSetFK(this, rightRecord, options, scope);
            };
        },
 
        checkMembership: function(session, leftRecord) {
            var field = this.association.field,
                store;
 
            if (field) {
                store = this.getSessionStore(session, leftRecord.get(field.name));
                // Check we're not in the middle of an add to the store.
                if (store && !store.contains(leftRecord)) {
                    store.add(leftRecord);
                }
            }
        },
 
        onValueChange: function(leftRecord, session, newValue, oldValue) {
            // If we have a session, we may be able to find the new store this belongs to
            // If not, the best we can do is to remove the record from the associated store/s.
            var me = this,
                instanceName = me.getInstanceName(),
                cls = me.cls,
                hasNewValue,
                joined, store, i, associated, rightRecord;
 
            if (!leftRecord.changingKey) {
                hasNewValue = newValue || newValue === 0;
                if (!hasNewValue) {
                    leftRecord[instanceName] = null;
                }
                if (session) {
                    // Find the store that holds this record and remove it if possible.
                    store = me.getSessionStore(session, oldValue);
                    if (store) {
                        store.remove(leftRecord);
                    }
                    // If we have a new value, try and find it and push it into the new store.
                    if (hasNewValue) {
                        store = me.getSessionStore(session, newValue);
                        if (store && !store.isLoading()) {
                            store.add(leftRecord);
                        }
                        if (cls) {
                            rightRecord = session.peekRecord(cls, newValue);
                        }
                        // Setting to undefined is important so that we can load the record later.
                        leftRecord[instanceName] = rightRecord || undefined;
                    }
                } else {
                    joined = leftRecord.joined;
                    if (joined) {
                        // Loop backwards because the store remove may cause unjoining, which means 
                        // removal from the joined array.
                        for (= joined.length - 1; i >= 0; i--) {
                            store = joined[i];
                            if (store.isStore) {
                                associated = store.getAssociatedEntity();
                                if (associated && associated.self === me.cls && associated.getId() === oldValue) {
                                    store.remove(leftRecord);
                                }
                            }
                        }
                    }
                }
            }
 
            if (me.owner && newValue === null) {
                me.association.schema.queueKeyCheck(leftRecord, me);
            }
        },
 
        checkKeyForDrop: function(leftRecord) {
            var field = this.association.field;
            if (leftRecord.get(field.name) === null) {
                leftRecord.drop();
            }
        },
 
        getSessionStore: function(session, value) {
            // May not have the cls loaded yet
            var cls = this.cls,
                rec;
 
            if (cls) {
                rec = session.peekRecord(cls, value);
 
                if (rec) {
                    return this.inverse.getAssociatedItem(rec);
                }
            }
        },
        
        read: function(leftRecord, node, fromReader, readOptions) {
            var rightRecords = this.callParent([leftRecord, node, fromReader, readOptions]),
                rightRecord;
 
            if (rightRecords) {
                rightRecord = rightRecords[0];
                if (rightRecord) {
                    leftRecord[this.getInstanceName()] = rightRecord;
                    delete leftRecord.data[this.role];
                }
            }
        }
    })
});