/*
 * 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.entity.plan;

import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.LockOptions;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.loader.entity.UniqueEntityLoader;
import org.hibernate.loader.plan.build.internal.FetchGraphLoadPlanBuildingStrategy;
import org.hibernate.loader.plan.build.internal.FetchStyleLoadPlanBuildingAssociationVisitationStrategy;
import org.hibernate.loader.plan.build.internal.LoadGraphLoadPlanBuildingStrategy;
import org.hibernate.loader.plan.build.spi.LoadPlanBuildingAssociationVisitationStrategy;
import org.hibernate.loader.plan.build.spi.MetamodelDrivenLoadPlanBuilder;
import org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader;
import org.hibernate.loader.plan.exec.internal.BatchingLoadQueryDetailsFactory;
import org.hibernate.loader.plan.exec.query.spi.QueryBuildingParameters;
import org.hibernate.loader.plan.exec.spi.LoadQueryDetails;
import org.hibernate.loader.plan.spi.LoadPlan;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.OuterJoinLoadable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.type.Type;

/**
 * A UniqueEntityLoader implementation based on using LoadPlans
 *
 * @author Steve Ebersole
 */
public abstract class AbstractLoadPlanBasedEntityLoader extends AbstractLoadPlanBasedLoader implements UniqueEntityLoader {
	private static final CoreMessageLogger log = CoreLogging.messageLogger( AbstractLoadPlanBasedEntityLoader.class );

	private final OuterJoinLoadable entityPersister;
	private final Type uniqueKeyType;
	private final String entityName;

	private final LoadQueryDetails staticLoadQuery;

	public AbstractLoadPlanBasedEntityLoader(
			OuterJoinLoadable entityPersister,
			SessionFactoryImplementor factory,
			String[] uniqueKeyColumnNames,
			Type uniqueKeyType,
			QueryBuildingParameters buildingParameters) {
		super( factory );
		this.entityPersister = entityPersister;
		this.uniqueKeyType = uniqueKeyType;
		this.entityName = entityPersister.getEntityName();

		final LoadPlanBuildingAssociationVisitationStrategy strategy;
		if ( buildingParameters.getQueryInfluencers().getFetchGraph() != null ) {
			strategy = new FetchGraphLoadPlanBuildingStrategy(
					factory, buildingParameters.getQueryInfluencers(),buildingParameters.getLockMode()
			);
		}
		else if ( buildingParameters.getQueryInfluencers().getLoadGraph() != null ) {
			strategy = new LoadGraphLoadPlanBuildingStrategy(
					factory, buildingParameters.getQueryInfluencers(),buildingParameters.getLockMode()
			);
		}
		else {
			strategy = new FetchStyleLoadPlanBuildingAssociationVisitationStrategy(
					factory, buildingParameters.getQueryInfluencers(),buildingParameters.getLockMode()
			);
		}

		final LoadPlan plan = MetamodelDrivenLoadPlanBuilder.buildRootEntityLoadPlan( strategy, entityPersister );
		this.staticLoadQuery = BatchingLoadQueryDetailsFactory.INSTANCE.makeEntityLoadQueryDetails(
				plan,
				uniqueKeyColumnNames,
				buildingParameters,
				factory
		);
	}

	@Override
	protected LoadQueryDetails getStaticLoadQuery() {
		return staticLoadQuery;
	}

	protected String getEntityName() {
		return entityName;
	}

	/**
	 * Called by wrappers that batch load entities
	 * @param persister only needed for logging
	 * @param lockOptions
	 */
	public final List loadEntityBatch(
			final SessionImplementor session,
			final Serializable[] ids,
			final Type idType,
			final Object optionalObject,
			final String optionalEntityName,
			final Serializable optionalId,
			final EntityPersister persister,
			LockOptions lockOptions) throws HibernateException {

		if ( log.isDebugEnabled() ) {
			log.debugf( "Batch loading entity: %s", MessageHelper.infoString( persister, ids, getFactory() ) );
		}

		final Type[] types = new Type[ids.length];
		Arrays.fill( types, idType );
		List result;
		try {
			final QueryParameters qp = new QueryParameters();
			qp.setPositionalParameterTypes( types );
			qp.setPositionalParameterValues( ids );
			qp.setLockOptions( lockOptions );

			result = executeLoad(
					session,
					qp,
					staticLoadQuery,
					false,
					null
			);
		}
		catch ( SQLException sqle ) {
			throw getFactory().getSQLExceptionHelper().convert(
					sqle,
					"could not load an entity batch: " + MessageHelper.infoString( entityPersister, ids, getFactory() ),
					staticLoadQuery.getSqlStatement()
			);
		}

		log.debug( "Done entity batch load" );

		return result;

	}

	@Override
	public Object load(Serializable id, Object optionalObject, SessionImplementor session) throws HibernateException {
		return load( id, optionalObject, session, LockOptions.NONE );
	}

	@Override
	public Object load(Serializable id, Object optionalObject, SessionImplementor session, LockOptions lockOptions) {

		final Object result;
		try {
			final QueryParameters qp = new QueryParameters();
			qp.setPositionalParameterTypes( new Type[] { entityPersister.getIdentifierType() } );
			qp.setPositionalParameterValues( new Object[] { id } );
			qp.setOptionalObject( optionalObject );
			qp.setOptionalEntityName( entityPersister.getEntityName() );
			qp.setOptionalId( id );
			qp.setLockOptions( lockOptions );

			final List results = executeLoad(
					session,
					qp,
					staticLoadQuery,
					false,
					null
			);
			result = extractEntityResult( results );
		}
		catch ( SQLException sqle ) {
			throw getFactory().getSQLExceptionHelper().convert(
					sqle,
					"could not load an entity: " + MessageHelper.infoString(
							entityPersister,
							id,
							entityPersister.getIdentifierType(),
							getFactory()
					),
					staticLoadQuery.getSqlStatement()
			);
		}

		log.debugf( "Done entity load : %s#%s", getEntityName(), id );
		return result;
	}

	protected Object extractEntityResult(List results) {
		if ( results.size() == 0 ) {
			return null;
		}
		else if ( results.size() == 1 ) {
			return results.get( 0 );
		}
		else {
			final Object row = results.get( 0 );
			if ( row.getClass().isArray() ) {
				// the logical type of the result list is List<Object[]>.  See if the contained
				// array contains just one element, and return that if so
				final Object[] rowArray = (Object[]) row;
				if ( rowArray.length == 1 ) {
					return rowArray[0];
				}
			}
			else {
				return row;
			}
		}

		throw new HibernateException( "Unable to interpret given query results in terms of a load-entity query" );
	}

	protected int[] getNamedParameterLocs(String name) {
		throw new AssertionFailure("no named parameters");
	}

	protected void autoDiscoverTypes(ResultSet rs) {
		throw new AssertionFailure("Auto discover types not supported in this loader");
	}
}
