package eu.dnetlib.r2d2.neo4j.util;

import javax.annotation.Resource;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Transaction;
import org.neo4j.index.Isolation;
import org.neo4j.index.lucene.LuceneFulltextIndexService;
import org.neo4j.index.lucene.LuceneIndexService;

import eu.dnetlib.r2d2.neo4j.dao.Neo4JDao;

public class TransactionInterceptor implements MethodInterceptor {

	private static Logger logger = Logger.getLogger(TransactionInterceptor.class);

	@Resource
	private GraphDatabaseService graphDb = null;
	
	@Resource
	private LuceneIndexService index = null;
	
	@Resource
	private LuceneFulltextIndexService fulltextIndex = null;
	
	// the same interceptor may be called concurrently in more than one threads.
	// Keeping the transaction in a thread local object.
	private ThreadLocal<Transaction> currentTx = new ThreadLocal<Transaction>() {
		protected Transaction initialValue() {
			return graphDb.beginTx();
		}
	};
	
	// The interceptor may be called twice during the same transcation. Making
	// sure that commit will be called only once.
	private ThreadLocal<Integer> nestLevel = new ThreadLocal<Integer>() {
		protected Integer initialValue() {
			return 0;
		}
	};

	/**
	 * Set to true if you don't want to save the transaction. Useful if your "aspected" method is read only.
	 * 
	 * (Antonis) Deprecated since the beans and daos "know" when a change has been made.
	 */
	@Deprecated
	private boolean readonly = false;

	@Override
	public Object invoke(MethodInvocation mi) throws Throwable {
		Object result = null;
		Throwable throwable = null;

		logger.debug("Starting neo4j transaction (nest level: " + this.nestLevel.get() + ")");
		Transaction tx = currentTx.get();

		try {
			nestLevel.set(nestLevel.get() + 1);
			
			// setting the id index isolation level to same transaction.
			index.setIsolation(Isolation.SAME_TX);
			
			// full text index updated on a different thread
			fulltextIndex.setIsolation(Isolation.ASYNC_OTHER_TX);

			result = mi.proceed();
		} catch (Throwable t) {
			logger.warn("Exception thrown. Rolling back transaction", t);
			tx.failure();
			throwable = t;
		} finally {
			if (nestLevel.get() == 1) {
				logger.debug("Transaction finished and commiting (nest level " + nestLevel.get() + ")");
				
				if (TransactionStatusHolder.getWriteStatus()) {
					logger.debug("Transaction is read/write. Calling success");
					tx.success();
					TransactionStatusHolder.reset();
				} else {
					logger.debug("Transaction seems to be read only.");
				}

				logger.debug("Finishing transaction.");
				try {
					tx.finish();
				} catch (Throwable t) {
					logger.error("Error finishing transaction", t);
					throwable = t;
				} finally {
					logger.debug("cleaning up transaction objects");
					currentTx.remove();

					Neo4JDao.createdNodes.remove();
					Neo4JDao.deletedNodes.remove();
					Neo4JDao.deletedRelations.remove();
				}
			} else {
				logger.debug("Transaction finished but not commiting yet (nest level " + nestLevel.get() + ")");
			}
			
			nestLevel.set(nestLevel.get() - 1);
			
			if (throwable != null)
				throw throwable;
		}

		return result;
	}

	public GraphDatabaseService getGraphDb() {
		return graphDb;
	}

	public void setGraphDb(GraphDatabaseService graphDb) {
		this.graphDb = graphDb;
	}

	public boolean isReadonly() {
		return readonly;
	}

	public void setReadonly(boolean readonly) {
		this.readonly = readonly;
	}
}