/*
 * 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.envers.internal.synchronization;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator;
import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit;
import org.hibernate.envers.tools.Pair;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.jboss.logging.Logger;

/**
 * @author Adam Warski (adam at warski dot org)
 * @author Chris Cranford
 */
public class AuditProcess implements BeforeTransactionCompletionProcess {
	private static final Logger log = Logger.getLogger( AuditProcess.class );

	private final RevisionInfoGenerator revisionInfoGenerator;
	private final SessionImplementor session;

	private final LinkedList<AuditWorkUnit> workUnits;
	private final Queue<AuditWorkUnit> undoQueue;
	private final Map<Pair<String, Object>, AuditWorkUnit> usedIds;
	private final Map<Pair<String, Object>, Object[]> entityStateCache;
	private final EntityChangeNotifier entityChangeNotifier;
	private Object revisionData;

	public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) {
		this.revisionInfoGenerator = revisionInfoGenerator;
		this.session = session;

		workUnits = new LinkedList<>();
		undoQueue = new LinkedList<>();
		usedIds = new HashMap<>();
		entityStateCache = new HashMap<>();
		entityChangeNotifier = new EntityChangeNotifier( revisionInfoGenerator, session );
	}

	public void cacheEntityState(Object id, String entityName, Object[] snapshot) {
		final Pair<String, Object> key = new Pair<>( entityName, id );
		if ( entityStateCache.containsKey( key ) ) {
			throw new AuditException( "The entity [" + entityName + "] with id [" + id + "] is already cached." );
		}
		entityStateCache.put( key, snapshot );
	}

	public Object[] getCachedEntityState(Object id, String entityName) {
		final Pair<String, Object> key = new Pair<>( entityName, id );
		final Object[] entityState = entityStateCache.get( key );
		if ( entityState != null ) {
			entityStateCache.remove( key );
		}
		return entityState;
	}

	private void removeWorkUnit(AuditWorkUnit vwu) {
		workUnits.remove( vwu );
		if ( vwu.isPerformed() ) {
			// If this work unit has already been performed, it must be deleted (undone) first.
			undoQueue.offer( vwu );
		}
	}

	public void addWorkUnit(AuditWorkUnit vwu) {
		if ( vwu.containsWork() ) {
			final Object entityId = vwu.getEntityId();

			if ( entityId == null ) {
				// Just adding the work unit - it's not associated with any persistent entity.
				workUnits.offer( vwu );
			}
			else {
				final String entityName = vwu.getEntityName();
				final Pair<String, Object> usedIdsKey = Pair.make( entityName, entityId );

				if ( usedIds.containsKey( usedIdsKey ) ) {
					final AuditWorkUnit other = usedIds.get( usedIdsKey );
					final AuditWorkUnit result = vwu.dispatch( other );

					if ( result != other ) {
						removeWorkUnit( other );

						if ( result != null ) {
							usedIds.put( usedIdsKey, result );
							workUnits.offer( result );
						}
						// else: a null result means that no work unit should be kept
					}
					// else: the result is the same as the work unit already added. No need to do anything.
				}
				else {
					usedIds.put( usedIdsKey, vwu );
					workUnits.offer( vwu );
				}
			}
		}
	}

	private void executeInSession(Session session) {
		// Making sure the revision data is persisted.
		final Object currentRevisionData = getCurrentRevisionData( session, true );

		AuditWorkUnit vwu;

		// First undoing any performed work units
		while ( (vwu = undoQueue.poll()) != null ) {
			vwu.undo( session );
		}

		while ( (vwu = workUnits.poll()) != null ) {
			vwu.perform( session, revisionData );
			entityChangeNotifier.entityChanged( session, currentRevisionData, vwu );
		}
	}

	public Object getCurrentRevisionData(Session session, boolean persist) {
		// Generating the revision data if not yet generated
		if ( revisionData == null ) {
			revisionData = revisionInfoGenerator.generate();
		}

		// Saving the revision data, if not yet saved and persist is true
		if ( !session.contains( revisionData ) && persist ) {
			revisionInfoGenerator.saveRevisionData( session, revisionData );
		}

		return revisionData;
	}

	@Override
	public void doBeforeTransactionCompletion(SessionImplementor session) {
		if ( workUnits.size() == 0 && undoQueue.size() == 0 ) {
			return;
		}

		if ( !session.getTransactionCoordinator().isActive() ) {
			log.debug( "Skipping envers transaction hook due to non-active (most likely marked-rollback-only) transaction" );
			return;
		}

		// see: http://www.jboss.com/index.html?module=bb&op=viewtopic&p=4178431
		if ( FlushMode.MANUAL.equals( session.getHibernateFlushMode() ) || session.isClosed() ) {
			Session temporarySession = null;
			try {
				temporarySession = session.sessionWithOptions()
						.connection()
						.autoClose( false )
						.connectionHandlingMode( PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION )
						.openSession();
				executeInSession( temporarySession );
				temporarySession.flush();
			}
			finally {
				if ( temporarySession != null ) {
					temporarySession.close();
				}
			}
		}
		else {
			executeInSession( session );

			// Explicitly flushing the session, as the auto-flush may have already happened.
			session.flush();
		}
	}
}
