/*
 * 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.loader.plan.exec.process.internal;

import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.StaleObjectStateException;
import org.hibernate.WrongClassException;
import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor;
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.engine.internal.TwoPhaseLoad;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.EntityUniqueKey;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.CoreLogging;
import org.hibernate.loader.EntityAliases;
import org.hibernate.loader.plan.exec.process.spi.EntityReferenceInitializer;
import org.hibernate.loader.plan.exec.process.spi.ResultSetProcessingContext;
import org.hibernate.loader.plan.exec.spi.EntityReferenceAliases;
import org.hibernate.loader.plan.spi.EntityFetch;
import org.hibernate.loader.plan.spi.EntityReference;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.Loadable;
import org.hibernate.persister.entity.UniqueKeyLoadable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
import org.hibernate.type.VersionType;

import org.jboss.logging.Logger;

import static org.hibernate.loader.plan.exec.process.spi.ResultSetProcessingContext.EntityReferenceProcessingState;

/**
 * @author Steve Ebersole
 */
public class EntityReferenceInitializerImpl implements EntityReferenceInitializer {
	private static final Logger log = CoreLogging.logger( EntityReferenceInitializerImpl.class );

	private final EntityReference entityReference;
	private final EntityReferenceAliases entityReferenceAliases;
	private final boolean isReturn;

	public EntityReferenceInitializerImpl(
			EntityReference entityReference,
			EntityReferenceAliases entityReferenceAliases) {
		this( entityReference, entityReferenceAliases, false );
	}

	public EntityReferenceInitializerImpl(
			EntityReference entityReference,
			EntityReferenceAliases entityReferenceAliases,
			boolean isRoot) {
		this.entityReference = entityReference;
		this.entityReferenceAliases = entityReferenceAliases;
		isReturn = isRoot;
	}

	@Override
	public EntityReference getEntityReference() {
		return entityReference;
	}

	@Override
	public void hydrateIdentifier(ResultSet resultSet, ResultSetProcessingContextImpl context) throws SQLException {

		final EntityReferenceProcessingState processingState = context.getProcessingState( entityReference );

		// get any previously registered identifier hydrated-state
		Object identifierHydratedForm = processingState.getIdentifierHydratedForm();
		if ( identifierHydratedForm == null ) {
			// if there is none, read it from the result set
			identifierHydratedForm = readIdentifierHydratedState( resultSet, context );

			// broadcast the fact that a hydrated identifier value just became associated with
			// this entity reference
			processingState.registerIdentifierHydratedForm( identifierHydratedForm );
		}
	}

	/**
	 * Read the identifier state for the entity reference for the currently processing row in the ResultSet
	 *
	 * @param resultSet The ResultSet being processed
	 * @param context The processing context
	 *
	 * @return The hydrated state
	 *
	 * @throws java.sql.SQLException Indicates a problem accessing the ResultSet
	 */
	private Object readIdentifierHydratedState(ResultSet resultSet, ResultSetProcessingContext context)
			throws SQLException {
		try {
			return entityReference.getEntityPersister().getIdentifierType().hydrate(
					resultSet,
					entityReferenceAliases.getColumnAliases().getSuffixedKeyAliases(),
					context.getSession(),
					null
			);
		}
		catch (Exception e) {
			throw new HibernateException(
					"Encountered problem trying to hydrate identifier for entity ["
							+ entityReference.getEntityPersister() + "]",
					e
			);
		}
	}

	@Override
	public void resolveEntityKey(ResultSet resultSet, ResultSetProcessingContextImpl context) {

		final EntityReferenceProcessingState processingState = context.getProcessingState( entityReference );

		// see if we already have an EntityKey associated with this EntityReference in the processing state.
		// if we do, this should have come from the optional entity identifier...
		final EntityKey entityKey = processingState.getEntityKey();
		if ( entityKey != null ) {
			log.debugf(
					"On call to EntityIdentifierReaderImpl#resolve, EntityKey was already known; " +
							"should only happen on root returns with an optional identifier specified"
			);
			return;
		}

		// Look for the hydrated form
		final Object identifierHydratedForm = processingState.getIdentifierHydratedForm();
		if ( identifierHydratedForm == null ) {
			// we need to register the missing identifier, but that happens later after all readers have had a chance
			// to resolve its EntityKey
			return;
		}

		final Type identifierType = entityReference.getEntityPersister().getIdentifierType();
		final Serializable resolvedId = (Serializable) identifierType.resolve(
				identifierHydratedForm,
				context.getSession(),
				null
		);
		if ( resolvedId != null ) {
			processingState.registerEntityKey(
					context.getSession().generateEntityKey( resolvedId, entityReference.getEntityPersister() )
			);
		}
	}

	@Override
	public void hydrateEntityState(ResultSet resultSet, ResultSetProcessingContextImpl context) {
		final EntityReferenceProcessingState processingState = context.getProcessingState( entityReference );

		// If there is no identifier for this entity reference for this row, nothing to do
		if ( processingState.isMissingIdentifier() ) {
			handleMissingIdentifier( context );
			return;
		}

		// make sure we have the EntityKey
		final EntityKey entityKey = processingState.getEntityKey();
		if ( entityKey == null ) {
			handleMissingIdentifier( context );
			return;
		}

		// Have we already hydrated this entity's state?
		if ( processingState.getEntityInstance() != null ) {
			return;
		}


		// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// In getting here, we know that:
		// 		1) We need to hydrate the entity state
		//		2) We have a valid EntityKey for the entity

		// see if we have an existing entry in the session for this EntityKey
		final Object existing = context.getSession().getEntityUsingInterceptor( entityKey );
		if ( existing != null ) {
			// It is previously associated with the Session, perform some checks
			if ( ! entityReference.getEntityPersister().isInstance( existing ) ) {
				throw new WrongClassException(
						"loaded object was of wrong class " + existing.getClass(),
						entityKey.getIdentifier(),
						entityReference.getEntityPersister().getEntityName()
				);
			}
			checkVersion( resultSet, context, entityKey, existing );

			// use the existing association as the hydrated state
			processingState.registerEntityInstance( existing );
			//context.registerHydratedEntity( entityReference, entityKey, existing );

			// see if the entity is enhanced and is being used as a "proxy" (is fully uninitialized)
			final BytecodeEnhancementMetadata enhancementMetadata = entityReference.getEntityPersister()
					.getEntityMetamodel()
					.getBytecodeEnhancementMetadata();

			if ( enhancementMetadata.isEnhancedForLazyLoading() ) {
				final BytecodeLazyAttributeInterceptor interceptor = enhancementMetadata.extractLazyInterceptor( existing );
				if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) {
					final LockMode requestedLockMode = context.resolveLockMode( entityReference );
					final LockMode lockModeToAcquire = requestedLockMode == LockMode.NONE
							? LockMode.READ
							: requestedLockMode;

					loadFromResultSet(
							resultSet,
							context,
							existing,
							getConcreteEntityTypeName( resultSet, context, entityKey ),
							entityKey,
							lockModeToAcquire
					);
				}
			}

			return;
		}

		// Otherwise, we need to load it from the ResultSet...

		// determine which entity instance to use.  Either the supplied one, or instantiate one
		Object entityInstance = null;
		if ( isReturn &&
				context.shouldUseOptionalEntityInformation() &&
				context.getQueryParameters().getOptionalObject() != null ) {
			final EntityKey optionalEntityKey = ResultSetProcessorHelper.getOptionalObjectKey(
					context.getQueryParameters(),
					context.getSession()
			);
			if ( optionalEntityKey != null && optionalEntityKey.equals( entityKey ) ) {
				entityInstance = context.getQueryParameters().getOptionalObject();
			}
		}

		final String concreteEntityTypeName = getConcreteEntityTypeName( resultSet, context, entityKey );
		if ( entityInstance == null ) {
			entityInstance = context.getSession().instantiate( concreteEntityTypeName, entityKey.getIdentifier() );
		}
		processingState.registerEntityInstance( entityInstance );

		// need to hydrate it.
		// grab its state from the ResultSet and keep it in the Session
		// (but don't yet initialize the object itself)
		// note that we acquire LockMode.READ even if it was not requested
		log.trace( "hydrating entity state" );
		final LockMode requestedLockMode = context.resolveLockMode( entityReference );
		final LockMode lockModeToAcquire = requestedLockMode == LockMode.NONE
				? LockMode.READ
				: requestedLockMode;

		loadFromResultSet(
				resultSet,
				context,
				entityInstance,
				concreteEntityTypeName,
				entityKey,
				lockModeToAcquire
		);
	}

	private void handleMissingIdentifier(ResultSetProcessingContext context) {
		if ( EntityFetch.class.isInstance( entityReference ) ) {
			final EntityFetch fetch = (EntityFetch) entityReference;
			final EntityType fetchedType = fetch.getFetchedType();
			if ( ! fetchedType.isOneToOne() ) {
				return;
			}

			final EntityReferenceProcessingState fetchOwnerState = context.getOwnerProcessingState( fetch );
			if ( fetchOwnerState == null ) {
				throw new IllegalStateException( "Could not locate fetch owner state" );
			}

			final EntityKey ownerEntityKey = fetchOwnerState.getEntityKey();
			if ( ownerEntityKey != null ) {
				context.getSession().getPersistenceContextInternal().addNullProperty(
						ownerEntityKey,
						fetchedType.getPropertyName()
				);
			}
		}
	}

	private void loadFromResultSet(
			ResultSet resultSet,
			ResultSetProcessingContext context,
			Object entityInstance,
			String concreteEntityTypeName,
			EntityKey entityKey,
			LockMode lockModeToAcquire) {
		final Serializable id = entityKey.getIdentifier();

		// Get the persister for the _subclass_
		final SharedSessionContractImplementor session = context.getSession();
		final Loadable concreteEntityPersister = (Loadable) session.getFactory().getMetamodel().entityPersister( concreteEntityTypeName );

		if ( log.isTraceEnabled() ) {
			log.tracev(
					"Initializing object from ResultSet: {0}",
					MessageHelper.infoString(
							concreteEntityPersister,
							id,
							session.getFactory()
					)
			);
		}

		// add temp entry so that the next step is circular-reference
		// safe - only needed because some types don't take proper
		// advantage of two-phase-load (esp. components)
		TwoPhaseLoad.addUninitializedEntity(
				entityKey,
				entityInstance,
				concreteEntityPersister,
				lockModeToAcquire,
				session
		);

		final EntityPersister rootEntityPersister = session.getFactory().getMetamodel().entityPersister(
				concreteEntityPersister.getRootEntityName()
		);
		final Object[] values;
		try {
			values = concreteEntityPersister.hydrate(
					resultSet,
					id,
					entityInstance,
					(Loadable) entityReference.getEntityPersister(),
					concreteEntityPersister == rootEntityPersister
							? entityReferenceAliases.getColumnAliases().getSuffixedPropertyAliases()
							: entityReferenceAliases.getColumnAliases().getSuffixedPropertyAliases( concreteEntityPersister ),
					context.getLoadPlan().areLazyAttributesForceFetched(),
					session
			);

			context.getProcessingState( entityReference ).registerHydratedState( values );
		}
		catch (SQLException e) {
			throw session.getFactory().getServiceRegistry().getService( JdbcServices.class ).getSqlExceptionHelper().convert(
					e,
					"Could not read entity state from ResultSet : " + entityKey
			);
		}

		final Object rowId;
		try {
			rowId = concreteEntityPersister.hasRowId()
					? resultSet.getObject( entityReferenceAliases.getColumnAliases().getRowIdAlias() )
					: null;

			if ( rowId != null && log.isTraceEnabled() ) {
				log.tracev(
						"extracted ROWID value: {0}",
						rowId
				);
			}
		}
		catch (SQLException e) {
			throw session.getFactory().getServiceRegistry().getService( JdbcServices.class ).getSqlExceptionHelper().convert(
					e,
					"Could not read entity row-id from ResultSet : " + entityKey
			);
		}

		final EntityType entityType = EntityFetch.class.isInstance( entityReference )
				? ( (EntityFetch) entityReference ).getFetchedType()
				: entityReference.getEntityPersister().getEntityMetamodel().getEntityType();

		if ( entityType != null ) {
			String ukName = entityType.getRHSUniqueKeyPropertyName();
			if ( ukName != null ) {
				final int index = ( (UniqueKeyLoadable) concreteEntityPersister ).getPropertyIndex( ukName );
				final Type type = concreteEntityPersister.getPropertyTypes()[index];

				// polymorphism not really handled completely correctly,
				// perhaps...well, actually its ok, assuming that the
				// entity name used in the lookup is the same as the
				// the one used here, which it will be

				EntityUniqueKey euk = new EntityUniqueKey(
						entityReference.getEntityPersister().getEntityName(),
						ukName,
						type.semiResolve( values[index], session, entityInstance ),
						type,
						concreteEntityPersister.getEntityMode(),
						session.getFactory()
				);
				session.getPersistenceContextInternal().addEntity( euk, entityInstance );
			}
		}

		TwoPhaseLoad.postHydrate(
				concreteEntityPersister,
				id,
				values,
				rowId,
				entityInstance,
				lockModeToAcquire,
				session
		);

		context.registerHydratedEntity( entityReference, entityKey, entityInstance );
	}

	private String getConcreteEntityTypeName(
			ResultSet resultSet,
			ResultSetProcessingContext context,
			EntityKey entityKey) {
		final Loadable loadable = (Loadable) entityReference.getEntityPersister();
		if ( ! loadable.hasSubclasses() ) {
			return entityReference.getEntityPersister().getEntityName();
		}

		final Object discriminatorValue;
		try {
			discriminatorValue = loadable.getDiscriminatorType().nullSafeGet(
					resultSet,
					entityReferenceAliases.getColumnAliases().getSuffixedDiscriminatorAlias(),
					context.getSession(),
					null
			);
		}
		catch (SQLException e) {
			throw context.getSession().getFactory().getServiceRegistry().getService( JdbcServices.class ).getSqlExceptionHelper().convert(
					e,
					"Could not read discriminator value from ResultSet"
			);
		}

		final String result = loadable.getSubclassForDiscriminatorValue( discriminatorValue );

		if ( result == null ) {
			// whoops! we got an instance of another class hierarchy branch
			throw new WrongClassException(
					"Discriminator: " + discriminatorValue,
					entityKey.getIdentifier(),
					entityReference.getEntityPersister().getEntityName()
			);
		}

		return result;
	}

	private void checkVersion(
			ResultSet resultSet,
			ResultSetProcessingContext context,
			EntityKey entityKey,
			Object existing) {
		final LockMode requestedLockMode = context.resolveLockMode( entityReference );
		if ( requestedLockMode != LockMode.NONE ) {
			final SharedSessionContractImplementor session = context.getSession();
			final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
			final LockMode currentLockMode = persistenceContext.getEntry( existing ).getLockMode();
			final boolean isVersionCheckNeeded = entityReference.getEntityPersister().isVersioned()
					&& currentLockMode.lessThan( requestedLockMode );

			// we don't need to worry about existing version being uninitialized because this block isn't called
			// by a re-entrant load (re-entrant loads *always* have lock mode NONE)
			if ( isVersionCheckNeeded ) {
				//we only check the version when *upgrading* lock modes
				checkVersion(
						session,
						resultSet,
						entityReference.getEntityPersister(),
						entityReferenceAliases.getColumnAliases(),
						entityKey,
						existing
				);
				//we need to upgrade the lock mode to the mode requested
				persistenceContext.getEntry( existing ).setLockMode( requestedLockMode );
			}
		}
	}

	private void checkVersion(
			SharedSessionContractImplementor session,
			ResultSet resultSet,
			EntityPersister persister,
			EntityAliases entityAliases,
			EntityKey entityKey,
			Object entityInstance) {
		final Object version = session.getPersistenceContextInternal().getEntry( entityInstance ).getVersion();

		if ( version != null ) {
			//null version means the object is in the process of being loaded somewhere else in the ResultSet
			VersionType versionType = persister.getVersionType();
			final Object currentVersion;
			try {
				currentVersion = versionType.nullSafeGet(
						resultSet,
						entityAliases.getSuffixedVersionAliases(),
						session,
						null
				);
			}
			catch (SQLException e) {
				throw session.getFactory().getServiceRegistry().getService( JdbcServices.class ).getSqlExceptionHelper().convert(
						e,
						"Could not read version value from result set"
				);
			}

			if ( !versionType.isEqual( version, currentVersion ) ) {
				final StatisticsImplementor statistics = session.getFactory().getStatistics();
				if ( statistics.isStatisticsEnabled() ) {
					statistics.optimisticFailure( persister.getEntityName() );
				}
				throw new StaleObjectStateException( persister.getEntityName(), entityKey.getIdentifier() );
			}
		}
	}

	@Override
	public void finishUpRow(ResultSet resultSet, ResultSetProcessingContextImpl context) {
		// cant remember exactly what I was thinking here.  Maybe managing the row value caching stuff that is currently
		// done in ResultSetProcessingContextImpl.finishUpRow()
		//
		// anything else?
	}
}
