context: using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Linq;
  using NHibernate.Classic;
  using NHibernate.Engine;
  using NHibernate.Intercept;
  using NHibernate.Persister.Entity;
  using NHibernate.Proxy;
  using NHibernate.Type;
  
  namespace NHibernate.Event.Default
  {
  	/// <summary>
  	/// Defines the default event listener for handling of merge events generated from a session.
  	/// </summary>
  	[Serializable]
  	public partial class DefaultMergeEventListener : AbstractSaveEventListener, IMergeEventListener
  	{
  		private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(DefaultMergeEventListener));
  
  		protected override CascadingAction CascadeAction
  		{
  			get { return CascadingAction.Merge; }
  		}
  
  		protected override bool? AssumedUnsaved
  		{
  			get { return false; }
  		}
  
  		protected override IDictionary GetMergeMap(object anything)
  		{
  			return ((EventCache)anything).InvertMap();
  		}
  
  		public virtual void OnMerge(MergeEvent @event)
  		{
  			EventCache copyCache = new EventCache();
  			
  			OnMerge(@event, copyCache);
  
  			// transientCopyCache may contain parent and child entities in random order.
  			// Child entities occurring ahead of their respective transient parents may fail 
  			// to get merged in one iteration.
  			// Retries are necessary as more and more children may be able to merge on subsequent iterations.
  			// Iteratively get transient entities and retry merge until one of the following conditions is true:
  			//   1) transientCopyCache.size() == 0
  			//   2) transientCopyCache.size() is not decreasing
  			
  			// TODO: find out if retrying can add entities to copyCache (don't think it can...)
  			// For now, just retry once; throw TransientObjectException if there are still any transient entities
  			
  			IDictionary transientCopyCache = this.GetTransientCopyCache(@event, copyCache);
  			
  			while (transientCopyCache.Count > 0)
  			{
  				var initialTransientCount = transientCopyCache.Count;
  
  				RetryMergeTransientEntities(@event, transientCopyCache, copyCache);
  				
  				// find any entities that are still transient after retry
  				transientCopyCache = this.GetTransientCopyCache(@event, copyCache);
  
  				// if a retry did nothing, the remaining transient entities 
  				// cannot be merged due to references to other transient entities 
  				// that are not part of the merge
  				if (transientCopyCache.Count == initialTransientCount)
  				{
  					ISet<string> transientEntityNames = new HashSet<string>();
  					
  					foreach (object transientEntity in transientCopyCache.Keys)
  					{
  						string transientEntityName = @event.Session.GuessEntityName(transientEntity);
  						
  						transientEntityNames.Add(transientEntityName);
  						
  						log.Info(
  							"transient instance could not be processed by merge: {0} [{1}]",
  							transientEntityName,
  							transientEntity.ToString());
  					}
  
  					throw new TransientObjectException("one or more objects is an unsaved transient instance - save transient instance(s) before merging: " + String.Join(",",  transientEntityNames.ToArray()));
  				}
  			}
  
  			copyCache.Clear();
  		}
  		
  		public virtual void OnMerge(MergeEvent @event, IDictionary copiedAlready)
  		{
  			EventCache copyCache = (EventCache)copiedAlready;
  			IEventSource source = @event.Session;
  			object original = @event.Original;
  
  			if (original != null)
  			{
  				object entity;
  				if (original.IsProxy())
  				{
  					ILazyInitializer li = ((INHibernateProxy)original).HibernateLazyInitializer;
  					if (li.IsUninitialized)
  					{
  						log.Debug("ignoring uninitialized proxy");
  						@event.Result = source.Load(li.EntityName, li.Identifier);
  						return; //EARLY EXIT!
  					}
  					else
  					{
  						entity = li.GetImplementation();
  					}
  				}
  				else
  				{
  					entity = original;
  				}
  				
  				if (copyCache.Contains(entity) && copyCache.IsOperatedOn(entity))
  				{
  					log.Debug("already in merge process");
  					@event.Result = entity;
  				}
  				else
  				{
  					if (copyCache.Contains(entity))
  					{
  						log.Info("already in copyCache; setting in merge process");
  						copyCache.SetOperatedOn(entity, true);
  					}
  					
  					@event.Entity = entity;
  					EntityState entityState = EntityState.Undefined;
  					if (ReferenceEquals(null, @event.EntityName))
  					{
  						@event.EntityName = source.BestGuessEntityName(entity);
  					}
  
  					// Check the persistence context for an entry relating to this
  					// entity to be merged...
  					EntityEntry entry = source.PersistenceContext.GetEntry(entity);
  					if (entry == null)
  					{
  						IEntityPersister persister = source.GetEntityPersister(@event.EntityName, entity);
  						object id = persister.GetIdentifier(entity);
  						if (id != null)
  						{
  							EntityKey key = source.GenerateEntityKey(id, persister);
  							object managedEntity = source.PersistenceContext.GetEntity(key);
  							entry = source.PersistenceContext.GetEntry(managedEntity);
  							if (entry != null)
  							{
  								// we have specialized case of a detached entity from the
  								// perspective of the merge operation.  Specifically, we
  								// have an incoming entity instance which has a corresponding
  								// entry in the current persistence context, but registered
  								// under a different entity instance
  								entityState = EntityState.Detached;
  							}
  						}
  					}
  
  					if (entityState == EntityState.Undefined)
  					{
  						entityState = GetEntityState(entity, @event.EntityName, entry, source);
  					}
  
  					switch (entityState)
  					{
  						case EntityState.Persistent:
  							EntityIsPersistent(@event, copyCache);
  							break;
  						case EntityState.Transient:
  							EntityIsTransient(@event, copyCache);
  							break;
  						case EntityState.Detached:
  							EntityIsDetached(@event, copyCache);
  							break;
  						default:
  							throw new ObjectDeletedException("deleted instance passed to merge", null, GetLoggableName(@event.EntityName, entity));
  					}
  				}
  			}
  		}
  
  		protected virtual void EntityIsPersistent(MergeEvent @event, IDictionary copyCache)
  		{
  			log.Debug("ignoring persistent instance");
  
  			//TODO: check that entry.getIdentifier().equals(requestedId)
  			
  			object entity = @event.Entity;
  			IEventSource source = @event.Session;
  			IEntityPersister persister = source.GetEntityPersister(@event.EntityName, entity);
  
  			((EventCache)copyCache).Add(entity, entity, true); //before cascade!
  
  			CascadeOnMerge(source, persister, entity, copyCache);
  			CopyValues(persister, entity, entity, source, copyCache);
  
  			@event.Result = entity;
  		}
  
  		protected virtual void EntityIsTransient(MergeEvent @event, IDictionary copyCache)
  		{
  			log.Info("merging transient instance");
  
  			object entity = @event.Entity;
  			IEventSource source = @event.Session;
  
  			IEntityPersister persister = source.GetEntityPersister(@event.EntityName, entity);
  			string entityName = persister.EntityName;
  			
  			@event.Result = this.MergeTransientEntity(entity, entityName, @event.RequestedId, source, copyCache);
  		}
  	
  		private object MergeTransientEntity(object entity, string entityName, object requestedId, IEventSource source, IDictionary copyCache)
  		{
  			IEntityPersister persister = source.GetEntityPersister(entityName, entity);
  
  			object id = persister.HasIdentifierProperty ? persister.GetIdentifier(entity) : null;
  			object copy = null;
  			
  			if (copyCache.Contains(entity))
  			{
  				copy = copyCache[entity];
  				persister.SetIdentifier(copy, id);
  			}
  			else
  			{
  				copy = source.Instantiate(persister, id);
  				((EventCache)copyCache).Add(entity, copy, true); // before cascade!
  			}
  
  			// cascade first, so that all unsaved objects get their
  			// copy created before we actually copy
  			//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);
  			base.CascadeBeforeSave(source, persister, entity, copyCache);
  			CopyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.ForeignKeyFromParent);
  
  			try
  			{
  				// try saving; check for non-nullable properties that are null or transient entities before saving
  				this.SaveTransientEntity(copy, entityName, requestedId, source, copyCache);
  			}
  			catch (PropertyValueException ex)
  			{
  				string propertyName = ex.PropertyName;
  				object propertyFromCopy = persister.GetPropertyValue(copy, propertyName);
  				object propertyFromEntity = persister.GetPropertyValue(entity, propertyName);
  				IType propertyType = persister.GetPropertyType(propertyName);
  				EntityEntry copyEntry = source.PersistenceContext.GetEntry(copy);
  
  				if (propertyFromCopy == null || !propertyType.IsEntityType)
  				{
  					log.Info("property '{0}.{1}' is null or not an entity; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromCopy);
  					throw;
  				}
  
  				if (!copyCache.Contains(propertyFromEntity))
  				{
  					log.Info("property '{0}.{1}' from original entity is not in copyCache; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromEntity);
  					throw;
  				}
  				
  				if (((EventCache)copyCache).IsOperatedOn(propertyFromEntity))
  				{
  					log.Info(ex, "property '{0}.{1}' from original entity is in copyCache and is in the process of being merged; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromEntity);
  				}
  				else
  				{
  					log.Info(ex, "property '{0}.{1}' from original entity is in copyCache and is not in the process of being merged; {1} =[{2}]", copyEntry.EntityName, propertyName, propertyFromEntity);
  				}
  				
  				// continue...; we'll find out if it ends up not getting saved later
  			}
  			
  			// cascade first, so that all unsaved objects get their
  			// copy created before we actually copy
  			base.CascadeAfterSave(source, persister, entity, copyCache);
  			CopyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.ForeignKeyToParent);
  
  			return copy;
  		}
  	
  		private void SaveTransientEntity(object entity, string entityName, object requestedId, IEventSource source, IDictionary copyCache)
  		{
  			// this bit is only *really* absolutely necessary for handling
  			// requestedId, but is also good if we merge multiple object
  			// graphs, since it helps ensure uniqueness
  			if (requestedId == null)
  			{
  				SaveWithGeneratedId(entity, entityName, copyCache, source, false);
  			}
  			else
  			{
  				SaveWithRequestedId(entity, requestedId, entityName, copyCache, source);
  			}
  		}
  
  		protected virtual void EntityIsDetached(MergeEvent @event, IDictionary copyCache)
  		{
  			log.Debug("merging detached instance");
  
  			object entity = @event.Entity;
  			IEventSource source = @event.Session;
  
  			IEntityPersister persister = source.GetEntityPersister(@event.EntityName, entity);
  			string entityName = persister.EntityName;
  
  			object id = @event.RequestedId;
  			if (id == null)
  			{
  
gt: 				id = persister.GetIdentifier(entity);