/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat Inc. or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.engine;

import java.io.Serializable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.AssertionFailure;
import org.hibernate.CacheMode;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.cache.CacheKey;
import org.hibernate.cache.entry.CacheEntry;
import org.hibernate.event.PostLoadEvent;
import org.hibernate.event.PostLoadEventListener;
import org.hibernate.event.PreLoadEvent;
import org.hibernate.event.PreLoadEventListener;
import org.hibernate.intercept.LazyPropertyInitializer;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.property.BackrefPropertyAccessor;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;

/**
 * Functionality relating to Hibernate's two-phase loading process,
 * that may be reused by persisters that do not use the Loader
 * framework
 * 
 * @author Gavin King
 */
public final class TwoPhaseLoad {

	private static final Logger log = LoggerFactory.getLogger(TwoPhaseLoad.class);
	
	private TwoPhaseLoad() {}

	/**
	 * Register the "hydrated" state of an entity instance, after the first step of 2-phase loading.
	 * 
	 * Add the "hydrated state" (an array) of an uninitialized entity to the session. We don't try
	 * to resolve any associations yet, because there might be other entities waiting to be
	 * read from the JDBC result set we are currently processing
	 */
	public static void postHydrate(
		final EntityPersister persister, 
		final Serializable id, 
		final Object[] values, 
		final Object rowId,
		final Object object, 
		final LockMode lockMode,
		final boolean lazyPropertiesAreUnfetched, 
		final SessionImplementor session) 
	throws HibernateException {
		
		Object version = Versioning.getVersion(values, persister);
		session.getPersistenceContext().addEntry( 
				object, 
				Status.LOADING,
				values, 
				rowId, 
				id, 
				version, 
				lockMode, 
				true, 
				persister, 
				false, 
				lazyPropertiesAreUnfetched 
			);
	
		if ( log.isTraceEnabled() && version!=null ) {
			String versionStr = persister.isVersioned()
					? persister.getVersionType().toLoggableString( version, session.getFactory() )
			        : "null";
			log.trace( "Version: " + versionStr );
		}
	
	}

	/**
	 * Perform the second step of 2-phase load. Fully initialize the entity 
	 * instance.
	 *
	 * After processing a JDBC result set, we "resolve" all the associations
	 * between the entities which were instantiated and had their state
	 * "hydrated" into an array
	 */
	public static void initializeEntity(
			final Object entity, 
			final boolean readOnly,
			final SessionImplementor session,
			final PreLoadEvent preLoadEvent,
			final PostLoadEvent postLoadEvent) throws HibernateException {
		
		//TODO: Should this be an InitializeEntityEventListener??? (watch out for performance!)
	
		final PersistenceContext persistenceContext = session.getPersistenceContext();
		EntityEntry entityEntry = persistenceContext.getEntry(entity);
		if ( entityEntry == null ) {
			throw new AssertionFailure( "possible non-threadsafe access to the session" );
		}
		EntityPersister persister = entityEntry.getPersister();
		Serializable id = entityEntry.getId();
		Object[] hydratedState = entityEntry.getLoadedState();
	
		if ( log.isDebugEnabled() )
			log.debug(
					"resolving associations for " +
					MessageHelper.infoString(persister, id, session.getFactory())
				);
	
		Type[] types = persister.getPropertyTypes();
		for ( int i = 0; i < hydratedState.length; i++ ) {
			final Object value = hydratedState[i];
			if ( value!=LazyPropertyInitializer.UNFETCHED_PROPERTY && value!=BackrefPropertyAccessor.UNKNOWN ) {
				hydratedState[i] = types[i].resolve( value, session, entity );
			}
		}
	
		//Must occur after resolving identifiers!
		if ( session.isEventSource() ) {
			preLoadEvent.setEntity(entity).setState(hydratedState).setId(id).setPersister(persister);
			PreLoadEventListener[] listeners = session.getListeners().getPreLoadEventListeners();
			for ( int i = 0; i < listeners.length; i++ ) {
				listeners[i].onPreLoad(preLoadEvent);
			}
		}
	
		persister.setPropertyValues( entity, hydratedState, session.getEntityMode() );
	
		final SessionFactoryImplementor factory = session.getFactory();
		if ( persister.hasCache() && session.getCacheMode().isPutEnabled() ) {
			
			if ( log.isDebugEnabled() )
				log.debug(
						"adding entity to second-level cache: " +
						MessageHelper.infoString( persister, id, session.getFactory() )
					);

			Object version = Versioning.getVersion(hydratedState, persister);
			CacheEntry entry = new CacheEntry(
					hydratedState, 
					persister, 
					entityEntry.isLoadedWithLazyPropertiesUnfetched(), 
					version, 
					session, 
					entity
			);
			CacheKey cacheKey = new CacheKey( 
					id, 
					persister.getIdentifierType(), 
					persister.getRootEntityName(), 
					session.getEntityMode(), 
					session.getFactory() 
			);

			// explicit handling of caching for rows just inserted and then somehow forced to be read
			// from the database *within the same transaction*.  usually this is done by
			// 		1) Session#refresh, or
			// 		2) Session#clear + some form of load
			//
			// we need to be careful not to clobber the lock here in the cache so that it can be rolled back if need be
			if ( session.getPersistenceContext().wasInsertedDuringTransaction( persister, id ) ) {
				persister.getCacheAccessStrategy().update(
						cacheKey,
						persister.getCacheEntryStructure().structure( entry ),
						version,
						version
				);
			}
			else {
				boolean put = persister.getCacheAccessStrategy().putFromLoad(
						cacheKey,
						persister.getCacheEntryStructure().structure( entry ),
						session.getTimestamp(),
						version,
						useMinimalPuts( session, entityEntry )
				);

				if ( put && factory.getStatistics().isStatisticsEnabled() ) {
					factory.getStatisticsImplementor().secondLevelCachePut( persister.getCacheAccessStrategy().getRegion().getName() );
				}
			}
		}

		boolean isReallyReadOnly = readOnly;
		if ( !persister.isMutable() ) {
			isReallyReadOnly = true;
		}
		else {
			Object proxy = persistenceContext.getProxy( entityEntry.getEntityKey() );
			if ( proxy != null ) {
				// there is already a proxy for this impl
				// only set the status to read-only if the proxy is read-only
				isReallyReadOnly = ( ( HibernateProxy ) proxy ).getHibernateLazyInitializer().isReadOnly();
			}
		}
		if ( isReallyReadOnly ) {
			//no need to take a snapshot - this is a 
			//performance optimization, but not really
			//important, except for entities with huge 
			//mutable property values
			persistenceContext.setEntryStatus(entityEntry, Status.READ_ONLY);
		}
		else {
			//take a snapshot
			TypeHelper.deepCopy(
					hydratedState, 
					persister.getPropertyTypes(), 
					persister.getPropertyUpdateability(), 
					hydratedState,  //after setting values to object, entityMode
					session
			);
			persistenceContext.setEntryStatus(entityEntry, Status.MANAGED);
		}
		
		persister.afterInitialize(
				entity, 
				entityEntry.isLoadedWithLazyPropertiesUnfetched(), 
				session
			);
		
		if ( session.isEventSource() ) {
			postLoadEvent.setEntity(entity).setId(id).setPersister(persister);
			PostLoadEventListener[] listeners = session.getListeners().getPostLoadEventListeners();
			for ( int i = 0; i < listeners.length; i++ ) {
				listeners[i].onPostLoad(postLoadEvent);
			}
		}
		
		if ( log.isDebugEnabled() )
			log.debug(
					"done materializing entity " +
					MessageHelper.infoString( persister, id, session.getFactory() )
				);
		
		if ( factory.getStatistics().isStatisticsEnabled() ) {
			factory.getStatisticsImplementor().loadEntity( persister.getEntityName() );
		}
	
	}

	private static boolean useMinimalPuts(SessionImplementor session, EntityEntry entityEntry) {
		return ( session.getFactory().getSettings().isMinimalPutsEnabled() && 
						session.getCacheMode()!=CacheMode.REFRESH ) ||
				( entityEntry.getPersister().hasLazyProperties() && 
						entityEntry.isLoadedWithLazyPropertiesUnfetched() && 
						entityEntry.getPersister().isLazyPropertiesCacheable() );
	}

	/**
	 * Add an uninitialized instance of an entity class, as a placeholder to ensure object 
	 * identity. Must be called before <tt>postHydrate()</tt>.
	 *
	 * Create a "temporary" entry for a newly instantiated entity. The entity is uninitialized,
	 * but we need the mapping from id to instance in order to guarantee uniqueness.
	 */
	public static void addUninitializedEntity(
			final EntityKey key, 
			final Object object, 
			final EntityPersister persister, 
			final LockMode lockMode,
			final boolean lazyPropertiesAreUnfetched, 
			final SessionImplementor session
	) {
		session.getPersistenceContext().addEntity(
				object, 
				Status.LOADING, 
				null, 
				key, 
				null, 
				lockMode, 
				true, 
				persister, 
				false, 
				lazyPropertiesAreUnfetched
			);
	}

	public static void addUninitializedCachedEntity(
			final EntityKey key, 
			final Object object, 
			final EntityPersister persister, 
			final LockMode lockMode,
			final boolean lazyPropertiesAreUnfetched,
			final Object version,
			final SessionImplementor session
	) {
		session.getPersistenceContext().addEntity(
				object, 
				Status.LOADING, 
				null, 
				key, 
				version, 
				lockMode, 
				true, 
				persister, 
				false, 
				lazyPropertiesAreUnfetched
			);
	}
}
