/**
 * **This class is never created directly. It should be constructed through associations
 * in `Ext.data.Model`.**
 *
 * This is a specialized version of `Ext.data.schema.ManyToOne` that declares a relationship between
 * a single  entity type and a single related entities. The relationship can be declared as a keyed
 * or keyless relationship.
 *
 *     // Keyed
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name', {
 *             name: 'userInfoId',
 *             reference: {
 *                 type: 'UserInfo',
 *                 unique: true
 *             }
 *         }]
 *     });
 *
 *     Ext.define('UserInfo', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'secretKey']
 *     });
 *
 *     // Keyless
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'name'],
 *         hasOne: 'UserInfo'
 *     });
 *
 *     Ext.define('Ticket', {
 *         extend: 'Ext.data.Model',
 *         fields: ['id', 'secretKey']
 *     });
 *
 *     // Generated methods
 *     var user = new User();
 *     user.getUserInfo();
 *     user.setUserInfo();
 *     
 *     var info = new UserInfo();
 *     info.getUser();
 *     info.setUser();
 *     
 *
 *     var ticket = new Ticket();
 *     ticket.setCustomer(customer);
 *     console.log(ticket.getCustomer()); // The customer object
 *
 * 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}.
 * Each 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.OneToOne', {
    extend: 'Ext.data.schema.Association',
 
    isOneToOne: true,
 
    isToOne: true,
 
    kind: 'one-to-one',
 
    Left: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
 
        onDrop: function(rightRecord, session) {
            var leftRecord = this.getAssociatedItem(rightRecord);
            
            rightRecord[this.getInstanceName()] = null;
            
            if (leftRecord) {
                leftRecord[this.inverse.getInstanceName()] = null;
            }
        },
 
        onIdChanged: function(rightRecord, oldId, newId) {
            var leftRecord = this.getAssociatedItem(rightRecord),
                fieldName = this.association.getFieldName();
 
            if (!rightRecord.session && leftRecord && fieldName) {
                leftRecord.set(fieldName, newId);
            }
        },
 
        createGetter: function() {
            var me = this;
            
            return function() {
                // 'this' refers to the Model instance inside this function
                return me.doGet(this);
            };
        },
 
        createSetter: function() {
            var me = this;
            
            return function(value) {
                // 'this' refers to the Model instance inside this function
                return me.doSet(this, value);
            };
        },
 
        doGet: function(rightRecord) {
            // Consider the Department entity with a managerId to a User entity. The
            // Department is on the left (the FK holder's side) so we are implementing the
            // guts of the getManagerDepartment method we place on the User entity. Since
            // we represent the "managerDepartment" role and as such our goal is to get a
            // Department instance, we start that from the User (rightRecord). Sadly that
            // record has no FK back to us.
            var instanceName = this.getInstanceName(), // ex "managerDepartment"
                ret = rightRecord[instanceName],
                session = rightRecord.session;
 
            if (!ret && session) {
                // @TODO: session - we'll cache the result on the record as always
                // but to get it we must ask the session
            }
 
            return ret || null;
        },
 
        doSet: function(rightRecord, leftRecord) {
            // We are the guts of the setManagerDepartment method we place on the User
            // entity. Our goal here is to establish the relationship between the new
            // Department (leftRecord) and the User (rightRecord).
            var instanceName = this.getInstanceName(), // ex "managerDepartment"
                ret = rightRecord[instanceName],
                inverseSetter = this.inverse.setterName; // setManager for Department
 
            if (ret !== leftRecord) {
                rightRecord[instanceName] = leftRecord;
 
                if (inverseSetter) {
                    // Because the FK is owned by the inverse record, we delegate the
                    // majority of work to its setter. We've already locked in the only
                    // thing we keep on this side so we won't recurse back-and-forth.
                    leftRecord[inverseSetter](rightRecord);
                }
                
                rightRecord.onAssociatedRecordSet(leftRecord, this);
            }
 
            return ret;
        },
 
        read: function(rightRecord, node, fromReader, readOptions) {
            var me = this,
                leftRecords = me.callParent([rightRecord, node, fromReader, readOptions]),
                leftRecord;
 
            if (leftRecords) {
                leftRecord = leftRecords[0];
                
                if (leftRecord) {
                    leftRecord[me.inverse.getInstanceName()] = rightRecord;
 
                    rightRecord[me.getInstanceName()] = leftRecord;
                    // Inline associations should *not* arrive on the "data" object:
                    delete rightRecord.data[me.role];
                }
            }
        }
    }),
 
    Right: Ext.define(null, {
        extend: 'Ext.data.schema.Role',
 
        left: false,
        side: 'right',
        
        createGetter: function() {
            // As the target of the FK (say "manager" for the Department 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 Model instance inside this function
                return me.doGetFK(this, options, scope);
            };
        },
        
        createSetter: function() {
            var me = this;
 
            return function(value, options, scope) {
                // 'this' refers to the Model instance inside this function
                return me.doSetFK(this, value, options, scope);
            };
        },
 
        onDrop: function(leftRecord, session) {
            var me = this,
                field = me.association.field,
                rightRecord = me.getAssociatedItem(leftRecord),
                id;
 
            if (me.inverse.owner) {
                if (session && field) {
                    id = leftRecord.get(field.name);
                    
                    if (id || id === 0) {
                        rightRecord = session.getEntry(me.cls, id).record;
                        
                        if (rightRecord) {
                            rightRecord.drop();
                        }
                    }
                }
                else {
                    if (rightRecord) {
                        rightRecord.drop();
                    }
                }
            }
             
            if (field) {
                leftRecord.set(field.name, null);
            }
            
            leftRecord[me.getInstanceName()] = null;
            
            if (rightRecord) {
                rightRecord[me.inverse.getInstanceName()] = null;
            }
        },
 
        onValueChange: function(leftRecord, session, newValue) {
            // Important to get the record before changing the key.
            var me = this,
                rightRecord = leftRecord[me.getOldInstanceName()] || me.getAssociatedItem(leftRecord), // eslint-disable-line max-len
                hasNewValue = newValue || newValue === 0,
                instanceName = me.getInstanceName(),
                cls = me.cls;
 
            leftRecord.changingKey = true;
            me.doSetFK(leftRecord, newValue);
            
            if (!hasNewValue) {
                leftRecord[instanceName] = null;
            }
            else if (session && cls) {
                // Setting to undefined is important so that we can load the record later.
                leftRecord[instanceName] = session.peekRecord(cls, newValue) || undefined;
            }
            
            if (me.inverse.owner && rightRecord) {
                me.association.schema.queueKeyCheck(rightRecord, me);
            }
            
            leftRecord.changingKey = false;
        },
 
        checkKeyForDrop: function(rightRecord) {
            var leftRecord = this.inverse.getAssociatedItem(rightRecord);
            
            if (!leftRecord) {
                // Not reassigned to another parent
                rightRecord.drop();
            }
        },
        
        read: function(leftRecord, node, fromReader, readOptions) {
            var me = this,
                rightRecords = me.callParent([leftRecord, node, fromReader, readOptions]),
                rightRecord, field, fieldName, session,
                refs, id, oldId, setKey, data;
 
            if (rightRecords) {
                rightRecord = rightRecords[0];
                field = me.association.field;
                
                if (field) {
                    fieldName = field.name;
                }
                
                session = leftRecord.session;
                data = leftRecord.data;
                
                if (rightRecord) {
                    if (session) {
                        refs = session.getRefs(rightRecord, this.inverse, true);
                        // If we have an existing reference in the session, or we don't and the data
                        // is undefined, allow the nested load to go ahead
                        setKey = (refs && refs[leftRecord.id]) || (data[fieldName] === undefined);
                    }
                    else {
                        setKey = true;
                    }
                    
                    if (setKey) {
                        // We want to poke the inferred key onto record if it exists, but we don't
                        // want to mess with the dirty or modified state of the record.
                        if (field) {
                            oldId = data[fieldName];
                            id = rightRecord.id;
                            
                            if (oldId !== id) {
                                data[fieldName] = id;
                                
                                if (session) {
                                    session.updateReference(leftRecord, field, id, oldId);
                                }
                            }
                        }
                        
                        rightRecord[me.inverse.getInstanceName()] = leftRecord;
                        leftRecord[me.getInstanceName()] = rightRecord;
                    }
                    
                    // Inline associations should *not* arrive on the "data" object:
                    delete data[me.role];
                }
            }
        }
    })
});