/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.collection.internal;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.hibernate.AssertionFailure;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.LazyInitializationException;
import org.hibernate.Session;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.internal.ForeignKeys;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.SessionFactoryRegistry;
import org.hibernate.internal.util.MarkerObject;
import org.hibernate.internal.util.collections.EmptyIterator;
import org.hibernate.internal.util.collections.IdentitySet;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.type.CompositeType;
import org.hibernate.type.IntegerType;
import org.hibernate.type.LongType;
import org.hibernate.type.PostgresUUIDType;
import org.hibernate.type.StringType;
import org.hibernate.type.Type;
import org.hibernate.type.UUIDBinaryType;
import org.hibernate.type.UUIDCharType;

/**
 * Base class implementing {@link org.hibernate.collection.spi.PersistentCollection}
 *
 * @author Gavin King
 */
public abstract class AbstractPersistentCollection implements Serializable, PersistentCollection {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( AbstractPersistentCollection.class );

	private transient SessionImplementor session;
	private boolean isTempSession = false;
	private boolean initialized;
	private transient List<DelayedOperation> operationQueue;
	private transient boolean directlyAccessible;
	private transient boolean initializing;
	private Object owner;
	private int cachedSize = -1;

	private String role;
	private Serializable key;
	// collections detect changes made via their public interface and mark
	// themselves as dirty as a performance optimization
	private boolean dirty;
	private Serializable storedSnapshot;

	private String sessionFactoryUuid;
	private boolean allowLoadOutsideTransaction;

	/**
	 * Not called by Hibernate, but used by non-JDK serialization,
	 * eg. SOAP libraries.
	 */
	public AbstractPersistentCollection() {
	}

	protected AbstractPersistentCollection(SessionImplementor session) {
		this.session = session;
	}

	@Override
	public final String getRole() {
		return role;
	}

	@Override
	public final Serializable getKey() {
		return key;
	}

	@Override
	public final boolean isUnreferenced() {
		return role == null;
	}

	@Override
	public final boolean isDirty() {
		return dirty;
	}

	@Override
	public final void clearDirty() {
		dirty = false;
	}

	@Override
	public final void dirty() {
		dirty = true;
	}

	@Override
	public final Serializable getStoredSnapshot() {
		return storedSnapshot;
	}

	//Careful: these methods do not initialize the collection.

	@Override
	public abstract boolean empty();

	/**
	 * Called by any read-only method of the collection interface
	 */
	protected final void read() {
		initialize( false );
	}

	/**
	 * Called by the {@link Collection#size} method
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean readSize() {
		if ( !initialized ) {
			if ( cachedSize != -1 && !hasQueuedOperations() ) {
				return true;
			}
			else {
				final boolean isExtraLazy = withTemporarySessionIfNeeded(
						new LazyInitializationWork<Boolean>() {
							@Override
							public Boolean doWork() {
								final CollectionEntry entry = session.getPersistenceContext().getCollectionEntry( AbstractPersistentCollection.this );

								if ( entry != null ) {
									final CollectionPersister persister = entry.getLoadedPersister();
									if ( persister.isExtraLazy() ) {
										if ( hasQueuedOperations() ) {
											session.flush();
										}
										cachedSize = persister.getSize( entry.getLoadedKey(), session );
										return true;
									}
									else {
										read();
									}
								}
								else{
									throwLazyInitializationExceptionIfNotConnected();
								}
								return false;
							}
						}
				);
				if ( isExtraLazy ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * TBH not sure why this is public
	 *
	 * @param <T> The java type of the return for this LazyInitializationWork
	 */
	public static interface LazyInitializationWork<T> {
		/**
		 * Do the represented work and return the result.
		 *
		 * @return The result
		 */
		public T doWork();
	}

	private <T> T withTemporarySessionIfNeeded(LazyInitializationWork<T> lazyInitializationWork) {
		SessionImplementor tempSession = null;

		if ( session == null ) {
			if ( allowLoadOutsideTransaction ) {
				tempSession = openTemporarySessionForLoading();
			}
			else {
				throwLazyInitializationException( "could not initialize proxy - no Session" );
			}
		}
		else if ( !session.isOpen() ) {
			if ( allowLoadOutsideTransaction ) {
				tempSession = openTemporarySessionForLoading();
			}
			else {
				throwLazyInitializationException( "could not initialize proxy - the owning Session was closed" );
			}
		}
		else if ( !session.isConnected() ) {
			if ( allowLoadOutsideTransaction ) {
				tempSession = openTemporarySessionForLoading();
			}
			else {
				throwLazyInitializationException( "could not initialize proxy - the owning Session is disconnected" );
			}
		}


		SessionImplementor originalSession = null;
		boolean isJTA = false;

		if ( tempSession != null ) {
			isTempSession = true;
			originalSession = session;
			session = tempSession;


			isJTA = session.getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta();
			
			if ( !isJTA ) {
				// Explicitly handle the transactions only if we're not in
				// a JTA environment.  A lazy loading temporary session can
				// be created even if a current session and transaction are
				// open (ex: session.clear() was used).  We must prevent
				// multiple transactions.
				( (Session) session ).beginTransaction();
			}

			session.getPersistenceContext().addUninitializedDetachedCollection(
					session.getFactory().getCollectionPersister( getRole() ),
					this
			);
		}

		try {
			return lazyInitializationWork.doWork();
		}
		finally {
			if ( tempSession != null ) {
				// make sure the just opened temp session gets closed!
				isTempSession = false;
				session = originalSession;

				try {
					if ( !isJTA ) {
						( (Session) tempSession ).getTransaction().commit();
					}
					( (Session) tempSession ).close();
				}
				catch (Exception e) {
					LOG.warn( "Unable to close temporary session used to load lazy collection associated to no session" );
				}
			}
		}
	}

	private SessionImplementor openTemporarySessionForLoading() {
		if ( sessionFactoryUuid == null ) {
			throwLazyInitializationException( "SessionFactory UUID not known to create temporary Session for loading" );
		}

		final SessionFactoryImplementor sf = (SessionFactoryImplementor)
				SessionFactoryRegistry.INSTANCE.getSessionFactory( sessionFactoryUuid );
		final SessionImplementor session = (SessionImplementor) sf.openSession();
		session.getPersistenceContext().setDefaultReadOnly( true );
		session.setFlushMode( FlushMode.MANUAL );
		return session;
	}

	protected Boolean readIndexExistence(final Object index) {
		if ( !initialized ) {
			final Boolean extraLazyExistenceCheck = withTemporarySessionIfNeeded(
					new LazyInitializationWork<Boolean>() {
						@Override
						public Boolean doWork() {
							final CollectionEntry entry = session.getPersistenceContext().getCollectionEntry( AbstractPersistentCollection.this );
							final CollectionPersister persister = entry.getLoadedPersister();
							if ( persister.isExtraLazy() ) {
								if ( hasQueuedOperations() ) {
									session.flush();
								}
								return persister.indexExists( entry.getLoadedKey(), index, session );
							}
							else {
								read();
							}
							return null;
						}
					}
			);
			if ( extraLazyExistenceCheck != null ) {
				return extraLazyExistenceCheck;
			}
		}
		return null;
	}

	protected Boolean readElementExistence(final Object element) {
		if ( !initialized ) {
			final Boolean extraLazyExistenceCheck = withTemporarySessionIfNeeded(
					new LazyInitializationWork<Boolean>() {
						@Override
						public Boolean doWork() {
							final CollectionEntry entry = session.getPersistenceContext().getCollectionEntry( AbstractPersistentCollection.this );
							final CollectionPersister persister = entry.getLoadedPersister();
							if ( persister.isExtraLazy() ) {
								if ( hasQueuedOperations() ) {
									session.flush();
								}
								return persister.elementExists( entry.getLoadedKey(), element, session );
							}
							else {
								read();
							}
							return null;
						}
					}
			);
			if ( extraLazyExistenceCheck != null ) {
				return extraLazyExistenceCheck;
			}
		}
		return null;
	}

	protected static final Object UNKNOWN = new MarkerObject( "UNKNOWN" );

	protected Object readElementByIndex(final Object index) {
		if ( !initialized ) {
			class ExtraLazyElementByIndexReader implements LazyInitializationWork {
				private boolean isExtraLazy;
				private Object element;

				@Override
				public Object doWork() {
					final CollectionEntry entry = session.getPersistenceContext().getCollectionEntry( AbstractPersistentCollection.this );
					final CollectionPersister persister = entry.getLoadedPersister();
					isExtraLazy = persister.isExtraLazy();
					if ( isExtraLazy ) {
						if ( hasQueuedOperations() ) {
							session.flush();
						}
						element = persister.getElementByIndex( entry.getLoadedKey(), index, session, owner );
					}
					else {
						read();
					}
					return null;
				}
			}

			final ExtraLazyElementByIndexReader reader = new ExtraLazyElementByIndexReader();
			//noinspection unchecked
			withTemporarySessionIfNeeded( reader );
			if ( reader.isExtraLazy ) {
				return reader.element;
			}
		}
		return UNKNOWN;

	}

	protected int getCachedSize() {
		return cachedSize;
	}

	protected boolean isConnectedToSession() {
		return session != null
				&& session.isOpen()
				&& session.getPersistenceContext().containsCollection( this );
	}

	protected boolean isInitialized() {
		return initialized;
	}

	/**
	 * Called by any writer method of the collection interface
	 */
	protected final void write() {
		initialize( true );
		dirty();
	}

	/**
	 * Is this collection in a state that would allow us to
	 * "queue" operations?
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean isOperationQueueEnabled() {
		return !initialized
				&& isConnectedToSession()
				&& isInverseCollection();
	}

	/**
	 * Is this collection in a state that would allow us to
	 * "queue" puts? This is a special case, because of orphan
	 * delete.
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean isPutQueueEnabled() {
		return !initialized
				&& isConnectedToSession()
				&& isInverseOneToManyOrNoOrphanDelete();
	}

	/**
	 * Is this collection in a state that would allow us to
	 * "queue" clear? This is a special case, because of orphan
	 * delete.
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean isClearQueueEnabled() {
		return !initialized
				&& isConnectedToSession()
				&& isInverseCollectionNoOrphanDelete();
	}

	/**
	 * Is this the "inverse" end of a bidirectional association?
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean isInverseCollection() {
		final CollectionEntry ce = session.getPersistenceContext().getCollectionEntry( this );
		return ce != null && ce.getLoadedPersister().isInverse();
	}

	/**
	 * Is this the "inverse" end of a bidirectional association with
	 * no orphan delete enabled?
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean isInverseCollectionNoOrphanDelete() {
		final CollectionEntry ce = session.getPersistenceContext().getCollectionEntry( this );
		return ce != null
				&&
				ce.getLoadedPersister().isInverse() &&
				!ce.getLoadedPersister().hasOrphanDelete();
	}

	/**
	 * Is this the "inverse" end of a bidirectional one-to-many, or
	 * of a collection with no orphan delete?
	 */
	@SuppressWarnings({"JavaDoc"})
	protected boolean isInverseOneToManyOrNoOrphanDelete() {
		final CollectionEntry ce = session.getPersistenceContext().getCollectionEntry( this );
		return ce != null
				&& ce.getLoadedPersister().isInverse()
				&& ( ce.getLoadedPersister().isOneToMany() || !ce.getLoadedPersister().hasOrphanDelete() );
	}

	/**
	 * Queue an addition
	 */
	@SuppressWarnings({"JavaDoc"})
	protected final void queueOperation(DelayedOperation operation) {
		if ( operationQueue == null ) {
			operationQueue = new ArrayList<DelayedOperation>( 10 );
		}
		operationQueue.add( operation );
		//needed so that we remove this collection from the second-level cache
		dirty = true;
	}

	/**
	 * Replace entity instances with copy in {@code copyCache}/.
	 *
	 * @param copyCache - mapping from entity in the process of being
	 *                    merged to managed copy.
	 */
	public final void replaceQueuedOperationValues(CollectionPersister persister, Map copyCache) {
		for ( DelayedOperation operation : operationQueue ) {
			if ( ValueDelayedOperation.class.isInstance( operation ) ) {
				( (ValueDelayedOperation) operation ).replace( persister, copyCache );
			}
		}
	}

	/**
	 * After reading all existing elements from the database,
	 * add the queued elements to the underlying collection.
	 */
	protected final void performQueuedOperations() {
		for ( DelayedOperation operation : operationQueue ) {
			operation.operate();
		}
	}

	@Override
	public void setSnapshot(Serializable key, String role, Serializable snapshot) {
		this.key = key;
		this.role = role;
		this.storedSnapshot = snapshot;
	}

	@Override
	public void postAction() {
		operationQueue = null;
		cachedSize = -1;
		clearDirty();
	}

	@Override
	public Object getValue() {
		return this;
	}

	@Override
	public void beginRead() {
		// override on some subclasses
		initializing = true;
	}

	@Override
	public boolean endRead() {
		//override on some subclasses
		return afterInitialize();
	}

	@Override
	public boolean afterInitialize() {
		setInitialized();
		//do this bit after setting initialized to true or it will recurse
		if ( operationQueue != null ) {
			performQueuedOperations();
			operationQueue = null;
			cachedSize = -1;
			return false;
		}
		else {
			return true;
		}
	}

	/**
	 * Initialize the collection, if possible, wrapping any exceptions
	 * in a runtime exception
	 *
	 * @param writing currently obsolete
	 *
	 * @throws LazyInitializationException if we cannot initialize
	 */
	protected final void initialize(final boolean writing) {
		if ( initialized ) {
			return;
		}

		withTemporarySessionIfNeeded(
				new LazyInitializationWork<Object>() {
					@Override
					public Object doWork() {
						session.initializeCollection( AbstractPersistentCollection.this, writing );
						return null;
					}
				}
		);
	}

	private void throwLazyInitializationExceptionIfNotConnected() {
		if ( !isConnectedToSession() ) {
			throwLazyInitializationException( "no session or session was closed" );
		}
		if ( !session.isConnected() ) {
			throwLazyInitializationException( "session is disconnected" );
		}
	}

	private void throwLazyInitializationException(String message) {
		throw new LazyInitializationException(
				"failed to lazily initialize a collection" +
						(role == null ? "" : " of role: " + role) +
						", " + message
		);
	}

	protected final void setInitialized() {
		this.initializing = false;
		this.initialized = true;
	}

	protected final void setDirectlyAccessible(boolean directlyAccessible) {
		this.directlyAccessible = directlyAccessible;
	}

	@Override
	public boolean isDirectlyAccessible() {
		return directlyAccessible;
	}

	@Override
	public final boolean unsetSession(SessionImplementor currentSession) {
		prepareForPossibleLoadingOutsideTransaction();
		if ( currentSession == this.session ) {
			if ( !isTempSession ) {
				this.session = null;
			}
			return true;
		}
		else {
			if ( this.session != null ) {
				LOG.logCannotUnsetUnexpectedSessionInCollection( generateUnexpectedSessionStateMessage( currentSession ) );
			}
			return false;
		}
	}

	protected void prepareForPossibleLoadingOutsideTransaction() {
		if ( session != null ) {
			allowLoadOutsideTransaction = session.getFactory().getSessionFactoryOptions().isInitializeLazyStateOutsideTransactionsEnabled();

			if ( allowLoadOutsideTransaction && sessionFactoryUuid == null ) {
				sessionFactoryUuid = session.getFactory().getUuid();
			}
		}
	}

	@Override
	public final boolean setCurrentSession(SessionImplementor session) throws HibernateException {
		if ( session == this.session ) {
			return false;
		}
		else {
			if ( this.session != null ) {
				final String msg = generateUnexpectedSessionStateMessage( session );
				if ( isConnectedToSession() ) {
					throw new HibernateException(
							"Illegal attempt to associate a collection with two open sessions. " + msg
					);
				}
				else {
					LOG.logUnexpectedSessionInCollectionNotConnected( msg );
					this.session = session;
					return true;
				}
			}
			else {
				this.session = session;
				return true;
			}
		}
	}

	private String generateUnexpectedSessionStateMessage(SessionImplementor session) {
		// NOTE: If this.session != null, this.session may be operating on this collection
		// (e.g., by changing this.role, this.key, or even this.session) in a different thread.

		// Grab the current role and key (it can still get changed by this.session...)
		// If this collection is connected to this.session, then this.role and this.key should
		// be consistent with the CollectionEntry in this.session (as long as this.session doesn't
		// change it). Don't access the CollectionEntry in this.session because that could result
		// in multi-threaded access to this.session.
		final String roleCurrent = role;
		final Serializable keyCurrent = key;

		final StringBuilder sb = new StringBuilder( "Collection : " );
		if ( roleCurrent != null ) {
			sb.append( MessageHelper.collectionInfoString( roleCurrent, keyCurrent ) );
		}
		else {
			final CollectionEntry ce = session.getPersistenceContext().getCollectionEntry( this );
			if ( ce != null ) {
				sb.append(
						MessageHelper.collectionInfoString(
								ce.getLoadedPersister(),
								this,
								ce.getLoadedKey(),
								session
						)
				);
			}
			else {
				sb.append( "<unknown>" );
			}
		}
		// only include the collection contents if debug logging
		if ( LOG.isDebugEnabled() ) {
			final String collectionContents = wasInitialized() ? toString() : "<uninitialized>";
			sb.append( "\nCollection contents: [" ).append( collectionContents ).append( "]" );
		}
		return sb.toString();
	}

	@Override
	public boolean needsRecreate(CollectionPersister persister) {
		// Workaround for situations like HHH-7072.  If the collection element is a component that consists entirely
		// of nullable properties, we currently have to forcefully recreate the entire collection.  See the use
		// of hasNotNullableColumns in the AbstractCollectionPersister constructor for more info.  In order to delete
		// row-by-row, that would require SQL like "WHERE ( COL = ? OR ( COL is null AND ? is null ) )", rather than
		// the current "WHERE COL = ?" (fails for null for most DBs).  Note that
		// the param would have to be bound twice.  Until we eventually add "parameter bind points" concepts to the
		// AST in ORM 5+, handling this type of condition is either extremely difficult or impossible.  Forcing
		// recreation isn't ideal, but not really any other option in ORM 4.
		// Selecting a type used in where part of update statement
		// (must match condidion in org.hibernate.persister.collection.BasicCollectionPersister.doUpdateRows).
		// See HHH-9474
		Type whereType;
		if ( persister.hasIndex() ) {
			whereType = persister.getIndexType();
		}
		else {
			whereType = persister.getElementType();
		}
		if ( whereType instanceof CompositeType ) {
			CompositeType componentIndexType = (CompositeType) whereType;
			return !componentIndexType.hasNotNullProperty();
		}
		return false;
	}

	@Override
	public final void forceInitialization() throws HibernateException {
		if ( !initialized ) {
			if ( initializing ) {
				throw new AssertionFailure( "force initialize loading collection" );
			}
			initialize( false );
		}
	}


	/**
	 * Get the current snapshot from the session
	 */
	@SuppressWarnings({"JavaDoc"})
	protected final Serializable getSnapshot() {
		return session.getPersistenceContext().getSnapshot( this );
	}

	@Override
	public final boolean wasInitialized() {
		return initialized;
	}

	@Override
	public boolean isRowUpdatePossible() {
		return true;
	}

	@Override
	public final boolean hasQueuedOperations() {
		return operationQueue != null;
	}

	@Override
	public final Iterator queuedAdditionIterator() {
		if ( hasQueuedOperations() ) {
			return new Iterator() {
				private int index;

				@Override
				public Object next() {
					return operationQueue.get( index++ ).getAddedInstance();
				}

				@Override
				public boolean hasNext() {
					return index < operationQueue.size();
				}

				@Override
				public void remove() {
					throw new UnsupportedOperationException();
				}
			};
		}
		else {
			return EmptyIterator.INSTANCE;
		}
	}

	@Override
	@SuppressWarnings({"unchecked"})
	public final Collection getQueuedOrphans(String entityName) {
		if ( hasQueuedOperations() ) {
			final Collection additions = new ArrayList( operationQueue.size() );
			final Collection removals = new ArrayList( operationQueue.size() );
			for ( DelayedOperation operation : operationQueue ) {
				additions.add( operation.getAddedInstance() );
				removals.add( operation.getOrphan() );
			}
			return getOrphans( removals, additions, entityName, session );
		}
		else {
			return Collections.EMPTY_LIST;
		}
	}

	@Override
	public void preInsert(CollectionPersister persister) throws HibernateException {
	}

	@Override
	public void afterRowInsert(CollectionPersister persister, Object entry, int i) throws HibernateException {
	}

	@Override
	public abstract Collection getOrphans(Serializable snapshot, String entityName) throws HibernateException;

	/**
	 * Get the session currently associated with this collection.
	 *
	 * @return The session
	 */
	public final SessionImplementor getSession() {
		return session;
	}

	protected final class IteratorProxy implements Iterator {
		protected final Iterator itr;

		public IteratorProxy(Iterator itr) {
			this.itr = itr;
		}

		@Override
		public boolean hasNext() {
			return itr.hasNext();
		}

		@Override
		public Object next() {
			return itr.next();
		}

		@Override
		public void remove() {
			write();
			itr.remove();
		}
	}

	protected final class ListIteratorProxy implements ListIterator {
		protected final ListIterator itr;

		public ListIteratorProxy(ListIterator itr) {
			this.itr = itr;
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public void add(Object o) {
			write();
			itr.add( o );
		}

		@Override
		public boolean hasNext() {
			return itr.hasNext();
		}

		@Override
		public boolean hasPrevious() {
			return itr.hasPrevious();
		}

		@Override
		public Object next() {
			return itr.next();
		}

		@Override
		public int nextIndex() {
			return itr.nextIndex();
		}

		@Override
		public Object previous() {
			return itr.previous();
		}

		@Override
		public int previousIndex() {
			return itr.previousIndex();
		}

		@Override
		public void remove() {
			write();
			itr.remove();
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public void set(Object o) {
			write();
			itr.set( o );
		}
	}

	protected class SetProxy implements java.util.Set {
		protected final Collection set;

		public SetProxy(Collection set) {
			this.set = set;
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public boolean add(Object o) {
			write();
			return set.add( o );
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public boolean addAll(Collection c) {
			write();
			return set.addAll( c );
		}

		@Override
		public void clear() {
			write();
			set.clear();
		}

		@Override
		public boolean contains(Object o) {
			return set.contains( o );
		}

		@Override
		@SuppressWarnings("unchecked")
		public boolean containsAll(Collection c) {
			return set.containsAll( c );
		}

		@Override
		public boolean isEmpty() {
			return set.isEmpty();
		}

		@Override
		public Iterator iterator() {
			return new IteratorProxy( set.iterator() );
		}

		@Override
		public boolean remove(Object o) {
			write();
			return set.remove( o );
		}

		@Override
		@SuppressWarnings("unchecked")
		public boolean removeAll(Collection c) {
			write();
			return set.removeAll( c );
		}

		@Override
		@SuppressWarnings("unchecked")
		public boolean retainAll(Collection c) {
			write();
			return set.retainAll( c );
		}

		@Override
		public int size() {
			return set.size();
		}

		@Override
		public Object[] toArray() {
			return set.toArray();
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public Object[] toArray(Object[] array) {
			return set.toArray( array );
		}
	}

	protected final class ListProxy implements java.util.List {
		protected final List list;

		public ListProxy(List list) {
			this.list = list;
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public void add(int index, Object value) {
			write();
			list.add( index, value );
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public boolean add(Object o) {
			write();
			return list.add( o );
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public boolean addAll(Collection c) {
			write();
			return list.addAll( c );
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public boolean addAll(int i, Collection c) {
			write();
			return list.addAll( i, c );
		}

		@Override
		public void clear() {
			write();
			list.clear();
		}

		@Override
		public boolean contains(Object o) {
			return list.contains( o );
		}

		@Override
		@SuppressWarnings("unchecked")
		public boolean containsAll(Collection c) {
			return list.containsAll( c );
		}

		@Override
		public Object get(int i) {
			return list.get( i );
		}

		@Override
		public int indexOf(Object o) {
			return list.indexOf( o );
		}

		@Override
		public boolean isEmpty() {
			return list.isEmpty();
		}

		@Override
		public Iterator iterator() {
			return new IteratorProxy( list.iterator() );
		}

		@Override
		public int lastIndexOf(Object o) {
			return list.lastIndexOf( o );
		}

		@Override
		public ListIterator listIterator() {
			return new ListIteratorProxy( list.listIterator() );
		}

		@Override
		public ListIterator listIterator(int i) {
			return new ListIteratorProxy( list.listIterator( i ) );
		}

		@Override
		public Object remove(int i) {
			write();
			return list.remove( i );
		}

		@Override
		public boolean remove(Object o) {
			write();
			return list.remove( o );
		}

		@Override
		@SuppressWarnings("unchecked")
		public boolean removeAll(Collection c) {
			write();
			return list.removeAll( c );
		}

		@Override
		@SuppressWarnings("unchecked")
		public boolean retainAll(Collection c) {
			write();
			return list.retainAll( c );
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public Object set(int i, Object o) {
			write();
			return list.set( i, o );
		}

		@Override
		public int size() {
			return list.size();
		}

		@Override
		public List subList(int i, int j) {
			return list.subList( i, j );
		}

		@Override
		public Object[] toArray() {
			return list.toArray();
		}

		@Override
		@SuppressWarnings({"unchecked"})
		public Object[] toArray(Object[] array) {
			return list.toArray( array );
		}

	}

	/**
	 * Contract for operations which are part of a collection's operation queue.
	 */
	protected interface DelayedOperation {
		public void operate();

		public Object getAddedInstance();

		public Object getOrphan();
	}

	protected interface ValueDelayedOperation extends DelayedOperation {
		void replace(CollectionPersister collectionPersister, Map copyCache);
	}

	protected abstract class AbstractValueDelayedOperation implements ValueDelayedOperation {
		private Object addedValue;
		private Object orphan;

		protected AbstractValueDelayedOperation(Object addedValue, Object orphan) {
			this.addedValue = addedValue;
			this.orphan = orphan;
		}

		public void replace(CollectionPersister persister, Map copyCache) {
			if ( addedValue != null ) {
				addedValue = getReplacement( persister.getElementType(), addedValue, copyCache );
			}
		}

		protected final Object getReplacement(Type type, Object current, Map copyCache) {
			return type.replace( current, null, session, owner, copyCache );
		}

		@Override
		public final Object getAddedInstance() {
			return addedValue;
		}

		@Override
		public final Object getOrphan() {
			return orphan;
		}
	}

	/**
	 * Given a collection of entity instances that used to
	 * belong to the collection, and a collection of instances
	 * that currently belong, return a collection of orphans
	 */
	@SuppressWarnings({"JavaDoc", "unchecked"})
	protected static Collection getOrphans(
			Collection oldElements,
			Collection currentElements,
			String entityName,
			SessionImplementor session) throws HibernateException {

		// short-circuit(s)
		if ( currentElements.size() == 0 ) {
			// no new elements, the old list contains only Orphans
			return oldElements;
		}
		if ( oldElements.size() == 0 ) {
			// no old elements, so no Orphans neither
			return oldElements;
		}

		final EntityPersister entityPersister = session.getFactory().getEntityPersister( entityName );
		final Type idType = entityPersister.getIdentifierType();
		final boolean useIdDirect = mayUseIdDirect( idType );

		// create the collection holding the Orphans
		final Collection res = new ArrayList();

		// collect EntityIdentifier(s) of the *current* elements - add them into a HashSet for fast access
		final java.util.Set currentIds = new HashSet();
		final java.util.Set currentSaving = new IdentitySet();
		for ( Object current : currentElements ) {
			if ( current != null && ForeignKeys.isNotTransient( entityName, current, null, session ) ) {
				final EntityEntry ee = session.getPersistenceContext().getEntry( current );
				if ( ee != null && ee.getStatus() == Status.SAVING ) {
					currentSaving.add( current );
				}
				else {
					final Serializable currentId = ForeignKeys.getEntityIdentifierIfNotUnsaved(
							entityName,
							current,
							session
					);
					currentIds.add( useIdDirect ? currentId : new TypedValue( idType, currentId ) );
				}
			}
		}

		// iterate over the *old* list
		for ( Object old : oldElements ) {
			if ( !currentSaving.contains( old ) ) {
				final Serializable oldId = ForeignKeys.getEntityIdentifierIfNotUnsaved( entityName, old, session );
				if ( !currentIds.contains( useIdDirect ? oldId : new TypedValue( idType, oldId ) ) ) {
					res.add( old );
				}
			}
		}

		return res;
	}

	private static boolean mayUseIdDirect(Type idType) {
		return idType == StringType.INSTANCE
			|| idType == IntegerType.INSTANCE
			|| idType == LongType.INSTANCE
			|| idType == UUIDBinaryType.INSTANCE
			|| idType == UUIDCharType.INSTANCE
			|| idType == PostgresUUIDType.INSTANCE;
	}

	/**
	 * Removes entity entries that have an equal identifier with the incoming entity instance
	 *
	 * @param list The list containing the entity instances
	 * @param entityInstance The entity instance to match elements.
	 * @param entityName The entity name
	 * @param session The session
	 */
	public static void identityRemove(
			Collection list,
			Object entityInstance,
			String entityName,
			SessionImplementor session) {

		if ( entityInstance != null && ForeignKeys.isNotTransient( entityName, entityInstance, null, session ) ) {
			final EntityPersister entityPersister = session.getFactory().getEntityPersister( entityName );
			final Type idType = entityPersister.getIdentifierType();

			final Serializable idOfCurrent = ForeignKeys.getEntityIdentifierIfNotUnsaved( entityName, entityInstance, session );
			final Iterator itr = list.iterator();
			while ( itr.hasNext() ) {
				final Serializable idOfOld = ForeignKeys.getEntityIdentifierIfNotUnsaved( entityName, itr.next(), session );
				if ( idType.isEqual( idOfCurrent, idOfOld, session.getFactory() ) ) {
					itr.remove();
					break;
				}
			}

		}
	}

	@Override
	public Object getIdentifier(Object entry, int i) {
		throw new UnsupportedOperationException();
	}

	@Override
	public Object getOwner() {
		return owner;
	}

	@Override
	public void setOwner(Object owner) {
		this.owner = owner;
	}

}
