/*
 * 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.jpa.spi;

import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.persistence.CacheRetrieveMode;
import javax.persistence.CacheStoreMode;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.TemporalType;

import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.QueryParameterException;
import org.hibernate.engine.query.spi.EntityGraphQueryHint;
import org.hibernate.jpa.AvailableSettings;
import org.hibernate.jpa.QueryHints;
import org.hibernate.jpa.TypedParameterValue;
import org.hibernate.jpa.graph.internal.EntityGraphImpl;
import org.hibernate.jpa.internal.EntityManagerMessageLogger;
import org.hibernate.jpa.internal.util.CacheModeHelper;
import org.hibernate.jpa.internal.util.ConfigurationHelper;
import org.hibernate.jpa.internal.util.LockModeTypeHelper;
import org.hibernate.jpa.internal.util.PessimisticNumberParser;
import org.hibernate.procedure.NoSuchParameterException;
import org.hibernate.procedure.ParameterStrategyException;

import org.jboss.logging.Logger;

import static org.hibernate.jpa.QueryHints.HINT_CACHEABLE;
import static org.hibernate.jpa.QueryHints.HINT_CACHE_MODE;
import static org.hibernate.jpa.QueryHints.HINT_CACHE_REGION;
import static org.hibernate.jpa.QueryHints.HINT_COMMENT;
import static org.hibernate.jpa.QueryHints.HINT_FETCHGRAPH;
import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE;
import static org.hibernate.jpa.QueryHints.HINT_FLUSH_MODE;
import static org.hibernate.jpa.QueryHints.HINT_LOADGRAPH;
import static org.hibernate.jpa.QueryHints.HINT_NATIVE_LOCKMODE;
import static org.hibernate.jpa.QueryHints.HINT_READONLY;
import static org.hibernate.jpa.QueryHints.HINT_TIMEOUT;
import static org.hibernate.jpa.QueryHints.SPEC_HINT_TIMEOUT;

/**
 * Intended as the base class for all {@link javax.persistence.Query} implementations, including
 * {@link javax.persistence.TypedQuery} and {@link javax.persistence.StoredProcedureQuery}.  Care should be taken
 * that all changes here fit with all those usages.
 *
 * @author Steve Ebersole
 */
public abstract class BaseQueryImpl implements Query {
	private static final EntityManagerMessageLogger LOG = Logger.getMessageLogger(
			EntityManagerMessageLogger.class,
			AbstractQueryImpl.class.getName()
	);

	private final HibernateEntityManagerImplementor entityManager;

	private int firstResult;
	private int maxResults = -1;
	private Map<String, Object> hints;

	private EntityGraphQueryHint entityGraphQueryHint;

	public BaseQueryImpl(HibernateEntityManagerImplementor entityManager) {
		this.entityManager = entityManager;
	}

	protected HibernateEntityManagerImplementor entityManager() {
		return entityManager;
	}

	protected void checkOpen(boolean markForRollbackIfClosed) {
		entityManager.checkOpen( markForRollbackIfClosed );
	}


	// Limits (first and max results) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	/**
	 * Apply the given first-result value.
	 *
	 * @param firstResult The specified first-result value.
	 */
	protected abstract void applyFirstResult(int firstResult);

	@Override
	public BaseQueryImpl setFirstResult(int firstResult) {
		checkOpen( true );

		if ( firstResult < 0 ) {
			throw new IllegalArgumentException(
					"Negative value (" + firstResult + ") passed to setFirstResult"
			);
		}
		this.firstResult = firstResult;
		applyFirstResult( firstResult );
		return this;
	}

	@Override
	public int getFirstResult() {
		checkOpen( false ); // technically should rollback
		return firstResult;
	}

	/**
	 * Apply the given max results value.
	 *
	 * @param maxResults The specified max results
	 */
	protected abstract void applyMaxResults(int maxResults);

	@Override
	public BaseQueryImpl setMaxResults(int maxResult) {
		checkOpen( true );
		if ( maxResult < 0 ) {
			throw new IllegalArgumentException(
					"Negative value (" + maxResult + ") passed to setMaxResults"
			);
		}
		this.maxResults = maxResult;
		applyMaxResults( maxResult );
		return this;
	}

	public int getSpecifiedMaxResults() {
		return maxResults;
	}

	@Override
	public int getMaxResults() {
		checkOpen( false ); // technically should rollback
		return maxResults == -1
				? Integer.MAX_VALUE // stupid spec... MAX_VALUE??
				: maxResults;
	}


	// Hints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	@SuppressWarnings( {"UnusedDeclaration"})
	public Set<String> getSupportedHints() {
		return QueryHints.getDefinedHints();
	}

	@Override
	public Map<String, Object> getHints() {
		checkOpen( false ); // technically should rollback
		return hints;
	}

	/**
	 * Apply the query timeout hint.
	 *
	 * @param timeout The timeout (in seconds!) specified as a hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyTimeoutHint(int timeout);

	/**
	 * Apply the lock timeout (in seconds!) hint
	 *
	 * @param timeout The timeout (in seconds!) specified as a hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyLockTimeoutHint(int timeout);

	/**
	 * Apply the comment hint.
	 *
	 * @param comment The comment specified as a hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyCommentHint(String comment);

	/**
	 * Apply the fetch size hint
	 *
	 * @param fetchSize The fetch size specified as a hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyFetchSizeHint(int fetchSize);

	/**
	 * Apply the cacheable (true/false) hint.
	 *
	 * @param isCacheable The value specified as hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyCacheableHint(boolean isCacheable);

	/**
	 * Apply the cache region hint
	 *
	 * @param regionName The name of the cache region specified as a hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyCacheRegionHint(String regionName);

	/**
	 * Apply the read-only (true/false) hint.
	 *
	 * @param isReadOnly The value specified as hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyReadOnlyHint(boolean isReadOnly);

	/**
	 * Apply the CacheMode hint.
	 *
	 * @param cacheMode The CacheMode value specified as a hint.
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyCacheModeHint(CacheMode cacheMode);

	/**
	 * Apply the FlushMode hint.
	 *
	 * @param flushMode The FlushMode value specified as hint
	 *
	 * @return {@code true} if the hint was "applied"
	 */
	protected abstract boolean applyFlushModeHint(FlushMode flushMode);

	/**
	 * Can alias-specific lock modes be applied?
	 *
	 * @return {@code true} indicates they can be applied, {@code false} otherwise.
	 */
	protected abstract boolean canApplyAliasSpecificLockModeHints();

	/**
	 * Apply the alias specific lock modes.  Assumes {@link #canApplyAliasSpecificLockModeHints()} has already been
	 * called and returned {@code true}.
	 *
	 * @param alias The alias to apply the 'lockMode' to.
	 * @param lockMode The LockMode to apply.
	 */
	protected abstract void applyAliasSpecificLockModeHint(String alias, LockMode lockMode);

	@Override
	@SuppressWarnings( {"deprecation"})
	public BaseQueryImpl setHint(String hintName, Object value) {
		checkOpen( true );
		boolean applied = false;
		try {
			if ( HINT_TIMEOUT.equals( hintName ) ) {
				applied = applyTimeoutHint( ConfigurationHelper.getInteger( value ) );
			}
			else if ( SPEC_HINT_TIMEOUT.equals( hintName ) ) {
				// convert milliseconds to seconds
				int timeout = (int)Math.round(ConfigurationHelper.getInteger( value ).doubleValue() / 1000.0 );
				applied = applyTimeoutHint( timeout );
			}
			else if ( AvailableSettings.LOCK_TIMEOUT.equals( hintName ) ) {
				applied = applyLockTimeoutHint( ConfigurationHelper.getInteger( value ) );
			}
			else if ( HINT_COMMENT.equals( hintName ) ) {
				applied = applyCommentHint( (String) value );
			}
			else if ( HINT_FETCH_SIZE.equals( hintName ) ) {
				applied = applyFetchSizeHint( ConfigurationHelper.getInteger( value ) );
			}
			else if ( HINT_CACHEABLE.equals( hintName ) ) {
				applied = applyCacheableHint( ConfigurationHelper.getBoolean( value ) );
			}
			else if ( HINT_CACHE_REGION.equals( hintName ) ) {
				applied = applyCacheRegionHint( (String) value );
			}
			else if ( HINT_READONLY.equals( hintName ) ) {
				applied = applyReadOnlyHint( ConfigurationHelper.getBoolean( value ) );
			}
			else if ( HINT_CACHE_MODE.equals( hintName ) ) {
				applied = applyCacheModeHint( ConfigurationHelper.getCacheMode( value ) );
			}
			else if ( HINT_FLUSH_MODE.equals( hintName ) ) {
				applied = applyFlushModeHint( ConfigurationHelper.getFlushMode( value ) );
			}
			else if ( AvailableSettings.SHARED_CACHE_RETRIEVE_MODE.equals( hintName ) ) {
				final CacheRetrieveMode retrieveMode = value != null ? CacheRetrieveMode.valueOf( value.toString() ) : null;
				final CacheStoreMode storeMode = getHint( AvailableSettings.SHARED_CACHE_STORE_MODE, CacheStoreMode.class );
				applied = applyCacheModeHint( CacheModeHelper.interpretCacheMode( storeMode, retrieveMode ) );
			}
			else if ( AvailableSettings.SHARED_CACHE_STORE_MODE.equals( hintName ) ) {
				final CacheStoreMode storeMode = value != null ? CacheStoreMode.valueOf( value.toString() ) : null;
				final CacheRetrieveMode retrieveMode = getHint( AvailableSettings.SHARED_CACHE_RETRIEVE_MODE, CacheRetrieveMode.class );
				applied = applyCacheModeHint( CacheModeHelper.interpretCacheMode( storeMode, retrieveMode ) );
			}
			else if ( QueryHints.HINT_NATIVE_LOCKMODE.equals( hintName ) ) {
				if ( !isNativeSqlQuery() ) {
					throw new IllegalStateException(
							"Illegal attempt to set lock mode on non-native query via hint; use Query#setLockMode instead"
					);
				}
				if ( LockMode.class.isInstance( value ) ) {
					internalApplyLockMode( LockModeTypeHelper.getLockModeType( (LockMode) value ) );
				}
				else if ( LockModeType.class.isInstance( value ) ) {
					internalApplyLockMode( (LockModeType) value );
				}
				else {
					throw new IllegalArgumentException(
							String.format(
									"Native lock-mode hint [%s] must specify %s or %s.  Encountered type : %s",
									HINT_NATIVE_LOCKMODE,
									LockMode.class.getName(),
									LockModeType.class.getName(),
									value.getClass().getName()
							)
					);
				}
				applied = true;
			}
			else if ( hintName.startsWith( AvailableSettings.ALIAS_SPECIFIC_LOCK_MODE ) ) {
				if ( canApplyAliasSpecificLockModeHints() ) {
					// extract the alias
					final String alias = hintName.substring( AvailableSettings.ALIAS_SPECIFIC_LOCK_MODE.length() + 1 );
					// determine the LockMode
					try {
						final LockMode lockMode = LockModeTypeHelper.interpretLockMode( value );
						applyAliasSpecificLockModeHint( alias, lockMode );
					}
					catch ( Exception e ) {
						LOG.unableToDetermineLockModeValue( hintName, value );
						applied = false;
					}
				}
				else {
					applied = false;
				}
			}
			else if ( HINT_FETCHGRAPH.equals( hintName ) || HINT_LOADGRAPH.equals( hintName ) ) {
				if (value instanceof EntityGraphImpl) {
					entityGraphQueryHint = new EntityGraphQueryHint( (EntityGraphImpl) value );
				}
				else {
					LOG.warnf( "The %s hint was set, but the value was not an EntityGraph!", hintName );
				}
				applied = true;
			}
			else {
				LOG.ignoringUnrecognizedQueryHint( hintName );
			}
		}
		catch ( ClassCastException e ) {
			throw new IllegalArgumentException( "Value for hint" );
		}

		if ( applied ) {
			if ( hints == null ) {
				hints = new HashMap<String,Object>();
			}
			hints.put( hintName, value );
		}
		else {
			LOG.debugf( "Skipping unsupported query hint [%s]", hintName );
		}

		return this;
	}

	private <T extends Enum<T>> T getHint(String key, Class<T> hintClass) {
		Object hint = hints != null ? hints.get( key ) : null;

		if ( hint == null ) {
			hint = entityManager.getProperties().get( key );
		}

		return hint != null ? Enum.valueOf( hintClass, hint.toString() ) : null;
	}

	/**
	 * Is the query represented here a native SQL query?
	 *
	 * @return {@code true} if it is a native SQL query; {@code false} otherwise
	 */
	protected abstract boolean isNativeSqlQuery();

	/**
	 * Is the query represented here a SELECT query?
	 *
	 * @return {@code true} if the query is a SELECT; {@code false} otherwise.
	 */
	protected abstract boolean isSelectQuery();

	protected abstract void internalApplyLockMode(javax.persistence.LockModeType lockModeType);

	// FlushMode ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	private FlushModeType jpaFlushMode;

	@Override
	public BaseQueryImpl setFlushMode(FlushModeType jpaFlushMode) {
		checkOpen( true );
		this.jpaFlushMode = jpaFlushMode;
		// TODO : treat as hint?
		if ( jpaFlushMode == FlushModeType.AUTO ) {
			applyFlushModeHint( FlushMode.AUTO );
		}
		else if ( jpaFlushMode == FlushModeType.COMMIT ) {
			applyFlushModeHint( FlushMode.COMMIT );
		}
		return this;
	}

	@SuppressWarnings( {"UnusedDeclaration"})
	protected FlushModeType getSpecifiedFlushMode() {
		return jpaFlushMode;
	}

	@Override
	public FlushModeType getFlushMode() {
		checkOpen( false );
		return jpaFlushMode != null
				? jpaFlushMode
				: entityManager.getFlushMode();
	}


	// Parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	private Set<ParameterRegistration<?>> parameterRegistrations;

	protected <X> ParameterRegistration<X> findParameterRegistration(Parameter<X> parameter) {
		if ( ParameterRegistration.class.isInstance( parameter ) ) {
			final ParameterRegistration<X> reg = (ParameterRegistration<X>) parameter;
			// validate the parameter source
			if ( reg.getQuery() != this ) {
				throw new IllegalArgumentException( "Passed Parameter was from different Query" );
			}
			return reg;
		}
		else {
			if ( parameter.getName() != null ) {
				return findParameterRegistration( parameter.getName() );
			}
			else if ( parameter.getPosition() != null ) {
				return findParameterRegistration( parameter.getPosition() );
			}
		}

		throw new IllegalArgumentException( "Unable to resolve incoming parameter [" + parameter + "] to registration" );
	}

	@SuppressWarnings("unchecked")
	protected <X> ParameterRegistration<X> findParameterRegistration(String parameterName) {
		if ( parameterRegistrations != null ) {
			for ( ParameterRegistration<?> param : parameterRegistrations ) {
				if ( parameterName.equals( param.getName() ) ) {
					return (ParameterRegistration<X>) param;
				}
			}

			// legacy allowance of the application to access the parameter using the position as a String
			final Integer jpaPositionalParameter = PessimisticNumberParser.toNumberOrNull( parameterName );
			if ( jpaPositionalParameter != null ) {
				for ( ParameterRegistration<?> param : parameterRegistrations ) {
					if ( param.isJpaPositionalParameter() && jpaPositionalParameter.equals( param.getPosition() ) ) {
						LOG.deprecatedJpaPositionalParameterAccess( jpaPositionalParameter );
						return (ParameterRegistration<X>) param;
					}
				}
			}
		}
		throw new IllegalArgumentException( "Parameter with that name [" + parameterName + "] did not exist" );
	}

	@SuppressWarnings("unchecked")
	protected <X> ParameterRegistration<X> findParameterRegistration(int parameterPosition) {
		if ( parameterRegistrations != null ) {
			for ( ParameterRegistration<?> param : parameterRegistrations ) {
				if ( param.getPosition() == null ) {
					continue;
				}
				if ( parameterPosition == param.getPosition() ) {
					return (ParameterRegistration<X>) param;
				}
			}
		}
		throw new IllegalArgumentException( "Parameter with that position [" + parameterPosition + "] did not exist" );
	}

	protected static class ParameterBindImpl<T> implements ParameterBind<T> {
		private final T value;
		private final TemporalType specifiedTemporalType;

		public ParameterBindImpl(T value, TemporalType specifiedTemporalType) {
			this.value = value;
			this.specifiedTemporalType = specifiedTemporalType;
		}

		public T getValue() {
			return value;
		}

		public TemporalType getSpecifiedTemporalType() {
			return specifiedTemporalType;
		}
	}

	private Set<ParameterRegistration<?>> parameterRegistrations() {
		if ( parameterRegistrations == null ) {
			// todo : could se use an identity set here?
			parameterRegistrations = new HashSet<ParameterRegistration<?>>();
		}
		return parameterRegistrations;
	}

	protected void registerParameter(ParameterRegistration parameter) {
		if ( parameter == null ) {
			throw new IllegalArgumentException( "parameter cannot be null" );
		}

		if ( parameterRegistrations().contains( parameter ) ) {
			LOG.debug( "Parameter registered multiple times : " + parameter );
			return;
		}

		parameterRegistrations().add( parameter );
	}

	@Override
	public <T> BaseQueryImpl setParameter(Parameter<T> param, T value) {
		checkOpen( true );

		try {
			final ParameterRegistration<T> parameterRegistrationExisting = findParameterRegistration( param );
			if ( value == null
					&& param.getParameterType() != null
					&& parameterRegistrationExisting.getParameterType() == null &&
					NullTypeBindableParameterRegistration.class.isInstance( parameterRegistrationExisting ) ) {
				// we have:
				// 1) a null value to bind;
				// 2) parameterRegistrationExisting has no information about the Java type for that null value;
				// 3) parameter.getParameterType() supplies a non-null parameter type;
				//    NOTE: According to Javadoc for javax.persistenceParameter#getParameterType:
				//          "Applications that use this method for Java Persistence query language
				//           queries and native queries will not be portable."
				// and 4) parameterRegistrationExisting allows for overriding the null parameter type when
				// binding a null value;
				( (NullTypeBindableParameterRegistration) parameterRegistrationExisting ).bindNullValue( param.getParameterType() );
			}
			else {
				// NOTE: The Javadoc for javax.persistence.Query#setParameter(Parameter<T> param, T value)
				// does not say anything about throwing IllegalArgumentException if
				// javax.persistenceParameter#getParameterType is not assignable to the type, so we simply log
				// a message if this is the case.
				if ( param.getParameterType() != null &&
						parameterRegistrationExisting.getParameterType() != null &&
						!parameterRegistrationExisting.getParameterType().isAssignableFrom( param.getParameterType() ) ) {
					LOG.warnf(
							"Parameter type [%s] is not assignment compatible with requested type [%s] for parameter %s",
							parameterRegistrationExisting.getParameterType().getName(),
							param.getParameterType().getName(),
							parameterRegistrationExisting.getName() == null ?
									"at position [" + param.getPosition() + "]" :
									"named [" + parameterRegistrationExisting.getName() + "]"
					);
				}
				parameterRegistrationExisting.bindValue( value );
			}
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(Parameter<Calendar> param, Calendar value, TemporalType temporalType) {
		checkOpen( true );

		try {
			findParameterRegistration( param ).bindValue( value, temporalType );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(Parameter<Date> param, Date value, TemporalType temporalType) {
		checkOpen( true );

		try {
			findParameterRegistration( param ).bindValue( value, temporalType );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	@SuppressWarnings("unchecked")
	public BaseQueryImpl setParameter(String name, Object value) {
		checkOpen( true );

		try {
			findParameterRegistration( name ).bindValue( value );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(String name, Calendar value, TemporalType temporalType) {
		checkOpen( true );

		try {
			findParameterRegistration( name ).bindValue( value, temporalType );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(String name, Date value, TemporalType temporalType) {
		checkOpen( true );

		try {
			findParameterRegistration( name ).bindValue( value, temporalType );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(int position, Object value) {
		checkOpen( true );

		try {
			findParameterRegistration( position ).bindValue( value );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(int position, Calendar value, TemporalType temporalType) {
		checkOpen( true );

		try {
			findParameterRegistration( position ).bindValue( value, temporalType );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	public BaseQueryImpl setParameter(int position, Date value, TemporalType temporalType) {
		checkOpen( true );

		try {
			findParameterRegistration( position ).bindValue( value, temporalType );
		}
		catch (ParameterStrategyException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( "Invalid mix of named and positional parameters", e );
		}
		catch (NoSuchParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (QueryParameterException e) {
			entityManager().markForRollbackOnly();
			throw new IllegalArgumentException( e.getMessage(), e );
		}
		catch (HibernateException he) {
			throw entityManager.convert( he );
		}

		return this;
	}

	@Override
	@SuppressWarnings("unchecked")
	public Set getParameters() {
		checkOpen( false );
		return parameterRegistrations();
	}

	@Override
	public Parameter<?> getParameter(String name) {
		checkOpen( false );
		return findParameterRegistration( name );
	}

	@Override
	@SuppressWarnings("unchecked")
	public <T> Parameter<T> getParameter(String name, Class<T> type) {
		checkOpen( false );
		Parameter param = findParameterRegistration( name );

		if ( param.getParameterType() != null ) {
			// we were able to determine the expected type during analysis, so validate it here
			if ( ! param.getParameterType().isAssignableFrom( type ) ) {
				throw new IllegalArgumentException(
						String.format(
								"Parameter type [%s] is not assignment compatible with requested type [%s] for parameter named [%s]",
								param.getParameterType().getName(),
								type.getName(),
								name
						)
				);
			}
		}
		return (Parameter<T>) param;
	}

	@Override
	public Parameter<?> getParameter(int position) {
		checkOpen( false );
		return findParameterRegistration( position );
	}

	@Override
	@SuppressWarnings("unchecked")
	public <T> Parameter<T> getParameter(int position, Class<T> type) {
		checkOpen( false );

		Parameter param = findParameterRegistration( position );

		if ( param.getParameterType() != null ) {
			// we were able to determine the expected type during analysis, so validate it here
			if ( ! param.getParameterType().isAssignableFrom( type ) ) {
				throw new IllegalArgumentException(
						String.format(
								"Parameter type [%s] is not assignment compatible with requested type [%s] for parameter at position [%s]",
								param.getParameterType().getName(),
								type.getName(),
								position
						)
				);
			}
		}
		return (Parameter<T>) param;
	}

	@Override
	public boolean isBound(Parameter<?> param) {
		checkOpen( false );
		final ParameterRegistration registration = findParameterRegistration( param );
		return registration != null && registration.isBindable() && registration.getBind() != null;
	}

	@Override
	@SuppressWarnings("unchecked")
	public <T> T getParameterValue(Parameter<T> param) {
		checkOpen( false );

		final ParameterRegistration<T> registration = findParameterRegistration( param );
		if ( registration == null ) {
			throw new IllegalArgumentException( "Passed parameter [" + param + "] is not a (registered) parameter of this query" );
		}

		if ( ! registration.isBindable() ) {
			throw new IllegalStateException( "Passed parameter [" + param + "] is not bindable" );
		}

		final ParameterBind<T> bind = registration.getBind();
		if ( bind == null ) {
			throw new IllegalStateException( "Parameter [" + param + "] has not yet been bound" );
		}

		return bind.getValue();
	}

	@Override
	public Object getParameterValue(String name) {
		checkOpen( false );
		return getParameterValue( getParameter( name ) );
	}

	@Override
	public Object getParameterValue(int position) {
		checkOpen( false );
		return getParameterValue( getParameter( position ) );
	}
	
	protected EntityGraphQueryHint getEntityGraphQueryHint() {
		return entityGraphQueryHint;
	}

















	protected static void validateBinding(Class parameterType, Object bind, TemporalType temporalType) {
		if ( bind == null || parameterType == null ) {
			// nothing we can check
			return;
		}

		if ( bind instanceof TypedParameterValue  ) {
			bind = ( (TypedParameterValue ) bind ).getValue();
		}

		if ( Collection.class.isInstance( bind ) && ! Collection.class.isAssignableFrom( parameterType ) ) {
			// we have a collection passed in where we are expecting a non-collection.
			// 		NOTE : this can happen in Hibernate's notion of "parameter list" binding
			// 		NOTE2 : the case of a collection value and an expected collection (if that can even happen)
			//			will fall through to the main check.
			validateCollectionValuedParameterBinding( parameterType, (Collection) bind, temporalType );
		}
		else if ( bind.getClass().isArray() ) {
			validateArrayValuedParameterBinding( parameterType, bind, temporalType );
		}
		else {
			if ( ! isValidBindValue( parameterType, bind, temporalType ) ) {
				throw new IllegalArgumentException(
						String.format(
								"Parameter value [%s] did not match expected type [%s (%s)]",
								bind,
								parameterType.getName(),
								extractName( temporalType )
						)
				);
			}
		}
	}

	private static String extractName(TemporalType temporalType) {
		return temporalType == null ? "n/a" : temporalType.name();
	}

	private static void validateCollectionValuedParameterBinding(
			Class parameterType,
			Collection value,
			TemporalType temporalType) {
		// validate the elements...
		for ( Object element : value ) {
			if ( ! isValidBindValue( parameterType, element, temporalType ) ) {
				throw new IllegalArgumentException(
						String.format(
								"Parameter value element [%s] did not match expected type [%s (%s)]",
								element,
								parameterType.getName(),
								extractName( temporalType )
						)
				);
			}
		}
	}

	private static void validateArrayValuedParameterBinding(
			Class parameterType,
			Object value,
			TemporalType temporalType) {
		if ( ! parameterType.isArray() ) {
			throw new IllegalArgumentException(
					String.format(
							"Encountered array-valued parameter binding, but was expecting [%s (%s)]",
							parameterType.getName(),
							extractName( temporalType )
					)
			);
		}

		if ( value.getClass().getComponentType().isPrimitive() ) {
			// we have a primitive array.  we validate that the actual array has the component type (type of elements)
			// we expect based on the component type of the parameter specification
			if ( ! parameterType.getComponentType().isAssignableFrom( value.getClass().getComponentType() ) ) {
				throw new IllegalArgumentException(
						String.format(
								"Primitive array-valued parameter bind value type [%s] did not match expected type [%s (%s)]",
								value.getClass().getComponentType().getName(),
								parameterType.getName(),
								extractName( temporalType )
						)
				);
			}
		}
		else {
			// we have an object array.  Here we loop over the array and physically check each element against
			// the type we expect based on the component type of the parameter specification
			final Object[] array = (Object[]) value;
			for ( Object element : array ) {
				if ( ! isValidBindValue( parameterType.getComponentType(), element, temporalType ) ) {
					throw new IllegalArgumentException(
							String.format(
									"Array-valued parameter value element [%s] did not match expected type [%s (%s)]",
									element,
									parameterType.getName(),
									extractName( temporalType )
							)
					);
				}
			}
		}
	}


	private static boolean isValidBindValue(Class expectedType, Object value, TemporalType temporalType) {
		if ( expectedType.isInstance( value ) ) {
			return true;
		}

		if ( temporalType != null ) {
			final boolean parameterDeclarationIsTemporal = Date.class.isAssignableFrom( expectedType )
					|| Calendar.class.isAssignableFrom( expectedType );
			final boolean bindIsTemporal = Date.class.isInstance( value )
					|| Calendar.class.isInstance( value );

			if ( parameterDeclarationIsTemporal && bindIsTemporal ) {
				return true;
			}
		}

		return false;
	}




}
