/*
 * Copyright (c) 2009, Red Hat Middleware LLC 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 Middleware LLC.
 *
 * 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.impl;

import java.io.Serializable;
import java.sql.Connection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.CacheMode;
import org.hibernate.ConnectionReleaseMode;
import org.hibernate.Criteria;
import org.hibernate.EmptyInterceptor;
import org.hibernate.EntityMode;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.LockMode;
import org.hibernate.MappingException;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.SessionException;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.cache.CacheKey;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.EntityKey;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.engine.QueryParameters;
import org.hibernate.engine.StatefulPersistenceContext;
import org.hibernate.engine.Versioning;
import org.hibernate.engine.LoadQueryInfluencers;
import org.hibernate.engine.NonFlushedChanges;
import org.hibernate.engine.query.HQLQueryPlan;
import org.hibernate.engine.query.NativeSQLQueryPlan;
import org.hibernate.engine.query.sql.NativeSQLQuerySpecification;
import org.hibernate.event.EventListeners;
import org.hibernate.id.IdentifierGeneratorHelper;
import org.hibernate.jdbc.Batcher;
import org.hibernate.jdbc.JDBCContext;
import org.hibernate.loader.criteria.CriteriaLoader;
import org.hibernate.loader.custom.CustomLoader;
import org.hibernate.loader.custom.CustomQuery;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.OuterJoinLoadable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.Type;
import org.hibernate.util.CollectionHelper;

/**
 * @author Gavin King
 */
public class StatelessSessionImpl extends AbstractSessionImpl
		implements JDBCContext.Context, StatelessSession {

	private static final Logger log = LoggerFactory.getLogger( StatelessSessionImpl.class );

	private JDBCContext jdbcContext;
	private PersistenceContext temporaryPersistenceContext = new StatefulPersistenceContext( this );

	StatelessSessionImpl(Connection connection, SessionFactoryImpl factory) {
		super( factory );
		this.jdbcContext = new JDBCContext( this, connection, EmptyInterceptor.INSTANCE );
	}


	// inserts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	public Serializable insert(Object entity) {
		errorIfClosed();
		return insert(null, entity);
	}

	public Serializable insert(String entityName, Object entity) {
		errorIfClosed();
		EntityPersister persister = getEntityPersister(entityName, entity);
		Serializable id = persister.getIdentifierGenerator().generate(this, entity);
		Object[] state = persister.getPropertyValues(entity, EntityMode.POJO);
		if ( persister.isVersioned() ) {
			boolean substitute = Versioning.seedVersion(state, persister.getVersionProperty(), persister.getVersionType(), this);
			if ( substitute ) {
				persister.setPropertyValues( entity, state, EntityMode.POJO );
			}
		}
		if ( id == IdentifierGeneratorHelper.POST_INSERT_INDICATOR ) {
			id = persister.insert(state, entity, this);
		}
		else {
			persister.insert(id, state, entity, this);
		}
		persister.setIdentifier( entity, id, this );
		return id;
	}


	// deletes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	public void delete(Object entity) {
		errorIfClosed();
		delete(null, entity);
	}

	public void delete(String entityName, Object entity) {
		errorIfClosed();
		EntityPersister persister = getEntityPersister(entityName, entity);
		Serializable id = persister.getIdentifier( entity, this );
		Object version = persister.getVersion(entity, EntityMode.POJO);
		persister.delete(id, version, entity, this);
	}


	// updates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	public void update(Object entity) {
		errorIfClosed();
		update(null, entity);
	}

	public void update(String entityName, Object entity) {
		errorIfClosed();
		EntityPersister persister = getEntityPersister(entityName, entity);
		Serializable id = persister.getIdentifier( entity, this );
		Object[] state = persister.getPropertyValues(entity, EntityMode.POJO);
		Object oldVersion;
		if ( persister.isVersioned() ) {
			oldVersion = persister.getVersion(entity, EntityMode.POJO);
			Object newVersion = Versioning.increment( oldVersion, persister.getVersionType(), this );
			Versioning.setVersion(state, newVersion, persister);
			persister.setPropertyValues(entity, state, EntityMode.POJO);
		}
		else {
			oldVersion = null;
		}
		persister.update(id, state, null, false, null, oldVersion, entity, null, this);
	}


	// loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	public Object get(Class entityClass, Serializable id) {
		return get( entityClass.getName(), id );
	}

	public Object get(Class entityClass, Serializable id, LockMode lockMode) {
		return get( entityClass.getName(), id, lockMode );
	}

	public Object get(String entityName, Serializable id) {
		return get(entityName, id, LockMode.NONE);
	}

	public Object get(String entityName, Serializable id, LockMode lockMode) {
		errorIfClosed();
		Object result = getFactory().getEntityPersister(entityName)
				.load(id, null, lockMode, this);
		if ( temporaryPersistenceContext.isLoadFinished() ) {
			temporaryPersistenceContext.clear();
		}
		return result;
	}

	public void refresh(Object entity) {
		refresh( bestGuessEntityName( entity ), entity, LockMode.NONE );
	}

	public void refresh(String entityName, Object entity) {
		refresh( entityName, entity, LockMode.NONE );
	}

	public void refresh(Object entity, LockMode lockMode) {
		refresh( bestGuessEntityName( entity ), entity, lockMode );
	}

	public void refresh(String entityName, Object entity, LockMode lockMode) {
		final EntityPersister persister = this.getEntityPersister( entityName, entity );
		final Serializable id = persister.getIdentifier( entity, this );
		if ( log.isTraceEnabled() ) {
			log.trace(
					"refreshing transient " +
					MessageHelper.infoString( persister, id, this.getFactory() )
			);
		}
		// TODO : can this ever happen???
//		EntityKey key = new EntityKey( id, persister, source.getEntityMode() );
//		if ( source.getPersistenceContext().getEntry( key ) != null ) {
//			throw new PersistentObjectException(
//					"attempted to refresh transient instance when persistent " +
//					"instance was already associated with the Session: " +
//					MessageHelper.infoString( persister, id, source.getFactory() )
//			);
//		}

		if ( persister.hasCache() ) {
			final CacheKey ck = new CacheKey(
					id,
			        persister.getIdentifierType(),
			        persister.getRootEntityName(),
			        this.getEntityMode(),
			        this.getFactory()
			);
			persister.getCacheAccessStrategy().evict( ck );
		}

		String previousFetchProfile = this.getFetchProfile();
		Object result = null;
		try {
			this.setFetchProfile( "refresh" );
			result = persister.load( id, entity, lockMode, this );
		}
		finally {
			this.setFetchProfile( previousFetchProfile );
		}
		UnresolvableObjectException.throwIfNull( result, id, persister.getEntityName() );
	}

	public Object immediateLoad(String entityName, Serializable id)
			throws HibernateException {
		throw new SessionException("proxies cannot be fetched by a stateless session");
	}

	public void initializeCollection(
			PersistentCollection collection,
	        boolean writing) throws HibernateException {
		throw new SessionException("collections cannot be fetched by a stateless session");
	}

	public Object instantiate(
			String entityName,
	        Serializable id) throws HibernateException {
		errorIfClosed();
		return getFactory().getEntityPersister( entityName )
				.instantiate( id, this );
	}

	public Object internalLoad(
			String entityName,
	        Serializable id,
	        boolean eager,
	        boolean nullable) throws HibernateException {
		errorIfClosed();
		EntityPersister persister = getFactory().getEntityPersister( entityName );
		// first, try to load it from the temp PC associated to this SS
		Object loaded = temporaryPersistenceContext.getEntity( new EntityKey( id, persister, getEntityMode() ) );
		if ( loaded != null ) {
			// we found it in the temp PC.  Should indicate we are in the midst of processing a result set
			// containing eager fetches via join fetch
			return loaded;
		}
		if ( !eager && persister.hasProxy() ) {
			// if the metadata allowed proxy creation and caller did not request forceful eager loading,
			// generate a proxy
			return persister.createProxy( id, this );
		}
		// otherwise immediately materialize it
		return get( entityName, id );
	}

	public Iterator iterate(String query, QueryParameters queryParameters) throws HibernateException {
		throw new UnsupportedOperationException();
	}

	public Iterator iterateFilter(Object collection, String filter, QueryParameters queryParameters)
	throws HibernateException {
		throw new UnsupportedOperationException();
	}

	public List listFilter(Object collection, String filter, QueryParameters queryParameters)
	throws HibernateException {
		throw new UnsupportedOperationException();
	}


	public boolean isOpen() {
		return !isClosed();
	}

	public void close() {
		managedClose();
	}

	public ConnectionReleaseMode getConnectionReleaseMode() {
		return factory.getSettings().getConnectionReleaseMode();
	}

	public boolean isAutoCloseSessionEnabled() {
		return factory.getSettings().isAutoCloseSessionEnabled();
	}

	public boolean isFlushBeforeCompletionEnabled() {
		return true;
	}

	public boolean isFlushModeNever() {
		return false;
	}

	public void managedClose() {
		if ( isClosed() ) {
			throw new SessionException( "Session was already closed!" );
		}
		jdbcContext.getConnectionManager().close();
		setClosed();
	}

	public void managedFlush() {
		errorIfClosed();
		getBatcher().executeBatch();
	}

	public boolean shouldAutoClose() {
		return isAutoCloseSessionEnabled() && !isClosed();
	}

	public void afterTransactionCompletion(boolean successful, Transaction tx) {}

	public void beforeTransactionCompletion(Transaction tx) {}

	public String bestGuessEntityName(Object object) {
		if (object instanceof HibernateProxy) {
			object = ( (HibernateProxy) object ).getHibernateLazyInitializer().getImplementation();
		}
		return guessEntityName(object);
	}

	public Connection connection() {
		errorIfClosed();
		return jdbcContext.borrowConnection();
	}

	public int executeUpdate(String query, QueryParameters queryParameters)
			throws HibernateException {
		errorIfClosed();
		queryParameters.validateParameters();
		HQLQueryPlan plan = getHQLQueryPlan( query, false );
		boolean success = false;
		int result = 0;
		try {
			result = plan.performExecuteUpdate( queryParameters, this );
			success = true;
		}
		finally {
			afterOperation(success);
		}
		temporaryPersistenceContext.clear();
		return result;
	}

	public Batcher getBatcher() {
		errorIfClosed();
		return jdbcContext.getConnectionManager()
				.getBatcher();
	}

	public CacheMode getCacheMode() {
		return CacheMode.IGNORE;
	}

	public int getDontFlushFromFind() {
		return 0;
	}

	public Map getEnabledFilters() {
		return CollectionHelper.EMPTY_MAP;
	}

	public Serializable getContextEntityIdentifier(Object object) {
		errorIfClosed();
		return null;
	}

	public EntityMode getEntityMode() {
		return EntityMode.POJO;
	}

	public EntityPersister getEntityPersister(String entityName, Object object)
			throws HibernateException {
		errorIfClosed();
		if ( entityName==null ) {
			return factory.getEntityPersister( guessEntityName( object ) );
		}
		else {
			return factory.getEntityPersister( entityName )
					.getSubclassEntityPersister( object, getFactory(), EntityMode.POJO );
		}
	}

	public Object getEntityUsingInterceptor(EntityKey key) throws HibernateException {
		errorIfClosed();
		return null;
	}

	public Type getFilterParameterType(String filterParameterName) {
		throw new UnsupportedOperationException();
	}

	public Object getFilterParameterValue(String filterParameterName) {
		throw new UnsupportedOperationException();
	}

	public FlushMode getFlushMode() {
		return FlushMode.COMMIT;
	}

	public Interceptor getInterceptor() {
		return EmptyInterceptor.INSTANCE;
	}

	public EventListeners getListeners() {
		throw new UnsupportedOperationException();
	}

	public PersistenceContext getPersistenceContext() {
		return temporaryPersistenceContext;
	}

	public long getTimestamp() {
		throw new UnsupportedOperationException();
	}

	public String guessEntityName(Object entity) throws HibernateException {
		errorIfClosed();
		return entity.getClass().getName();
	}


	public boolean isConnected() {
		return jdbcContext.getConnectionManager().isCurrentlyConnected();
	}

	public boolean isTransactionInProgress() {
		return jdbcContext.isTransactionInProgress();
	}

	public void setAutoClear(boolean enabled) {
		throw new UnsupportedOperationException();
	}

	public void setCacheMode(CacheMode cm) {
		throw new UnsupportedOperationException();
	}

	public void setFlushMode(FlushMode fm) {
		throw new UnsupportedOperationException();
	}

	public Transaction getTransaction() throws HibernateException {
		errorIfClosed();
		return jdbcContext.getTransaction();
	}

	public Transaction beginTransaction() throws HibernateException {
		errorIfClosed();
		Transaction result = getTransaction();
		result.begin();
		return result;
	}

	public boolean isEventSource() {
		return false;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isDefaultReadOnly() {
		return false;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setDefaultReadOnly(boolean readOnly) throws HibernateException {
		if ( readOnly == true ) {
			throw new UnsupportedOperationException();
		}
	}

/////////////////////////////////////////////////////////////////////////////////////////////////////

	//TODO: COPY/PASTE FROM SessionImpl, pull up!

	public List list(String query, QueryParameters queryParameters) throws HibernateException {
		errorIfClosed();
		queryParameters.validateParameters();
		HQLQueryPlan plan = getHQLQueryPlan( query, false );
		boolean success = false;
		List results = CollectionHelper.EMPTY_LIST;
		try {
			results = plan.performList( queryParameters, this );
			success = true;
		}
		finally {
			afterOperation(success);
		}
		temporaryPersistenceContext.clear();
		return results;
	}

	public void afterOperation(boolean success) {
		if ( !jdbcContext.isTransactionInProgress() ) {
			jdbcContext.afterNontransactionalQuery(success);
		}
	}

	public Criteria createCriteria(Class persistentClass, String alias) {
		errorIfClosed();
		return new CriteriaImpl( persistentClass.getName(), alias, this );
	}

	public Criteria createCriteria(String entityName, String alias) {
		errorIfClosed();
		return new CriteriaImpl(entityName, alias, this);
	}

	public Criteria createCriteria(Class persistentClass) {
		errorIfClosed();
		return new CriteriaImpl( persistentClass.getName(), this );
	}

	public Criteria createCriteria(String entityName) {
		errorIfClosed();
		return new CriteriaImpl(entityName, this);
	}

	public ScrollableResults scroll(CriteriaImpl criteria, ScrollMode scrollMode) {
		errorIfClosed();
		String entityName = criteria.getEntityOrClassName();
		CriteriaLoader loader = new CriteriaLoader(
				getOuterJoinLoadable( entityName ),
		        factory,
		        criteria,
		        entityName,
		        getLoadQueryInfluencers()
		);
		return loader.scroll(this, scrollMode);
	}

	public List list(CriteriaImpl criteria) throws HibernateException {
		errorIfClosed();
		String[] implementors = factory.getImplementors( criteria.getEntityOrClassName() );
		int size = implementors.length;

		CriteriaLoader[] loaders = new CriteriaLoader[size];
		for( int i=0; i <size; i++ ) {
			loaders[i] = new CriteriaLoader(
					getOuterJoinLoadable( implementors[i] ),
			        factory,
			        criteria,
			        implementors[i],
			        getLoadQueryInfluencers()
			);
		}


		List results = Collections.EMPTY_LIST;
		boolean success = false;
		try {
			for( int i=0; i<size; i++ ) {
				final List currentResults = loaders[i].list(this);
				currentResults.addAll(results);
				results = currentResults;
			}
			success = true;
		}
		finally {
			afterOperation(success);
		}
		temporaryPersistenceContext.clear();
		return results;
	}

	private OuterJoinLoadable getOuterJoinLoadable(String entityName) throws MappingException {
		EntityPersister persister = factory.getEntityPersister(entityName);
		if ( !(persister instanceof OuterJoinLoadable) ) {
			throw new MappingException( "class persister is not OuterJoinLoadable: " + entityName );
		}
		return ( OuterJoinLoadable ) persister;
	}

	public List listCustomQuery(CustomQuery customQuery, QueryParameters queryParameters)
	throws HibernateException {
		errorIfClosed();
		CustomLoader loader = new CustomLoader( customQuery, getFactory() );

		boolean success = false;
		List results;
		try {
			results = loader.list(this, queryParameters);
			success = true;
		}
		finally {
			afterOperation(success);
		}
		temporaryPersistenceContext.clear();
		return results;
	}

	public ScrollableResults scrollCustomQuery(CustomQuery customQuery, QueryParameters queryParameters)
	throws HibernateException {
		errorIfClosed();
		CustomLoader loader = new CustomLoader( customQuery, getFactory() );
		return loader.scroll(queryParameters, this);
	}

	public ScrollableResults scroll(String query, QueryParameters queryParameters) throws HibernateException {
		errorIfClosed();
		HQLQueryPlan plan = getHQLQueryPlan( query, false );
		return plan.performScroll( queryParameters, this );
	}

	public void afterScrollOperation() {
		temporaryPersistenceContext.clear();
	}

	public void flush() {}

	public NonFlushedChanges getNonFlushedChanges() {
		throw new UnsupportedOperationException();
	}

	public void applyNonFlushedChanges(NonFlushedChanges nonFlushedChanges) {
		throw new UnsupportedOperationException();
	}

	public String getFetchProfile() {
		return null;
	}

	public JDBCContext getJDBCContext() {
		return jdbcContext;
	}

	public LoadQueryInfluencers getLoadQueryInfluencers() {
		return LoadQueryInfluencers.NONE;
	}

	public void registerInsertedKey(EntityPersister persister, Serializable id) {
		errorIfClosed();
		// nothing to do
	}

	public boolean wasInsertedDuringTransaction(EntityPersister persister, Serializable id) {
		errorIfClosed();
		// not in any meaning we need to worry about here.
		return false;
	}

	public void setFetchProfile(String name) {}

	public void afterTransactionBegin(Transaction tx) {}

	protected boolean autoFlushIfRequired(Set querySpaces) throws HibernateException {
		// no auto-flushing to support in stateless session
		return false;
	}
	
	public int executeNativeUpdate(NativeSQLQuerySpecification nativeSQLQuerySpecification,
			QueryParameters queryParameters) throws HibernateException {
		errorIfClosed();
		queryParameters.validateParameters();
		NativeSQLQueryPlan plan = getNativeSQLQueryPlan(nativeSQLQuerySpecification);

		boolean success = false;
		int result = 0;
		try {
			result = plan.performExecuteUpdate(queryParameters, this);
			success = true;
		} finally {
			afterOperation(success);
		}
		temporaryPersistenceContext.clear();
		return result;
	}

}
