package eu.dnetlib.dlms.benchmark;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

import org.apache.commons.io.DirectoryWalker;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

/**
 * A DirectoryWalker implementation that walks a given directory taking every file and every subdirectory.
 * 
 * @author lexis
 */
public class RecursiveLoader extends DirectoryWalker {
	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(RecursiveLoader.class);
	/**
	 * Transaction Manager: commit every time the method handleDirectoryEnd is called and when the number of files to be
	 * committed exceed the threshold defined by numOfFilesPerCommit.
	 */
	private PlatformTransactionManager transactionManager;
	/** Current opened transaction. */
	private TransactionStatus status;
	/**
	 * Sax reader for xml documents.
	 */
	private SAXReader reader;
	/** Instance that implements the method to process the xml documents. */
	private ProcessXMLInterface worker;
	/** Time when the handleStart and handleEnd methods are called. */
	private long startTime, endTime;
	/** Number of the files (non-directory) walked . */
	private int totalFiles;

	/**
	 * Number of files to process before committing. Please tune this property since processing too many files can cause
	 * OutOfMemoryException due to memroy leak in the hibernate session.
	 */
	private int numOfFilesPerCommit;
	/**
	 * Optional logger to log operation in synch, since CustomLogger class flushes its stream each time a message is
	 * written.
	 */
	private CustomLogger synchLogger;

	/**
	 * Starts the walking phase.
	 * 
	 * @param startDirPath
	 *            directory to walk
	 * @throws IOException
	 *             accessing files
	 */
	public void startWalking(final String startDirPath) throws IOException {
		//log.debug(this + " Start walking...");
		this.synchLogger.write(this + " Start walking...");
		ArrayList<WalkerResult> results = new ArrayList<WalkerResult>();
		this.walk(new File(startDirPath), results);
		//log.debug("Walk ended");
		this.synchLogger.write("Walk ended");
		this.synchLogger.close();
	}

	/**
	 * {@inheritDoc}. This method is called only once just before the walking phase starts; the given implementation
	 * calls the initialization method of the worker instance.
	 * 
	 * @see org.apache.commons.io.DirectoryWalker#handleStart(java.io.File, java.util.Collection)
	 */
	@SuppressWarnings("unchecked")
	@Override
	protected void handleStart(final File startDirectory, final Collection results) throws IOException {
		//log.debug("handleStart from directory " + startDirectory.getPath());
		this.synchLogger.write("handleStart from directory " + startDirectory.getPath());
		DefaultTransactionDefinition def = new DefaultTransactionDefinition();
		// explicitly setting the transaction name is something that can only be done programmatically
		def.setName("Tx-initPhase");
		def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
		this.status = this.transactionManager.getTransaction(def);
		//log.debug("HandleStart: Created transaction with name :" + def.getName() + " status: " + this.status);
		this.synchLogger.write("HandleStart: Created transaction with name :" + def.getName() + " status: " + this.status);
		//la uso qui per creare il tipo delle structure e il set: DMFType e DMFSet
		this.totalFiles = 0;
		results.add(new WalkerResult());
		this.worker.init();
		//log.debug("HandleStart: Transaction " + this.status + " committed");
		//this.synchLogger.write("HandleStart: Transaction " + this.status + " committed");
		this.transactionManager.commit(this.status);
		this.startTime = System.currentTimeMillis();
	}

	/**
	 * Checks if the current directory has at least one regular file. Transaction are not started otherwise.
	 * 
	 * @param directory
	 *            File dentoing a directory
	 * @return true if the directory has at least one regular file to process.
	 * @throws IOException
	 */
	private boolean hasFilesToProcess(final File directory) throws IOException {
		File[] files = directory.listFiles(new FileFilter() {
			public boolean accept(final File pathName) {
				return (pathName != null && pathName.isFile());
			}
		});
		log.debug("hasFileToProcess(directory)");
		//this.synchLogger.write("hasFileToProcess(directory)");
		return (files != null && files.length > 0);
	}

	/**
	 * Starts a new Transaction.
	 * 
	 * @param transactionName
	 *            name of the newly created transaction
	 * @throws IOException
	 *             writing on sync log
	 */
	private void startNewTransaction(final String transactionName) throws IOException {
		if (this.status != null && !this.status.isCompleted()) {
			//log.debug("No new transaction: another one is still to be completed: " + this.status);
			this.synchLogger.write("No new transaction: another one is still to be completed: " + this.status);
		} else {
			DefaultTransactionDefinition def = new DefaultTransactionDefinition();
			// explicitly setting the transaction name is something that can only be done programmatically
			def.setName("Tx-" + transactionName);
			def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
			this.status = this.transactionManager.getTransaction(def);
			//log.debug("Started transaction with name :" + def.getName() + ", status: " + this.status);
			this.synchLogger.write("Started transaction with name :" + def.getName() + ", status: " + this.status);
			//TODO: should also empty the hibernate session cache

		}
	}

	/**
	 * {@inheritDoc} Start a transaction when we enter in a directory.
	 * 
	 * @see org.apache.commons.io.DirectoryWalker#handleDirectoryStart(java.io.File, int, java.util.Collection)
	 */
	@SuppressWarnings("unchecked")
	@Override
	protected void handleDirectoryStart(final File directory, final int depth, final Collection results) throws IOException {
		if (this.hasFilesToProcess(directory)) {
			this.startNewTransaction(directory.getAbsolutePath());
		}
	}

	/**
	 * {@inheritDoc} This method is called every time a normal file is walked; this implementation calls the worker's
	 * processing method.
	 * 
	 * @see org.apache.commons.io.DirectoryWalker#handleFile(java.io.File, int, java.util.Collection)
	 */
	@SuppressWarnings("unchecked")
	@Override
	protected void handleFile(final File file, final int depth, final Collection results) throws IOException {
		this.synchLogger.write("Handling file: " + file);
		log.debug("Handling file: " + file);
		try {
			ArrayList<WalkerResult> resList = (ArrayList<WalkerResult>) results;
			WalkerResult wr = resList.get(0);
			if (wr.filesToCommit >= this.numOfFilesPerCommit) {
				// save the set and commit!
				this.worker.end();
				this.transactionManager.commit(this.status);
				wr.addToFilesProcessed(wr.getFilesToCommit());
				wr.setFilesToCommit(0);
				//	log.debug("handleFile: committed existing transaction");
				this.synchLogger.write("handleFile: committed existing transaction");
				//then start a new Transaction
				this.startNewTransaction(file.getParent());
			}
			//process the current document, it will be committed at the next commit.
			Document doc = this.reader.read(file);
			this.worker.process(doc);
			this.totalFiles++;
			wr.incrFilesToCommit();
			this.synchLogger.write("File handled: " + file);
		} catch (DocumentException docExce) {
			//this exception occurs if an xml document is not well formed: log the exception, do not process the file and return
			//log.info("Document " + file.getName() + " cannot be processed because of " + docExce.getMessage());
			this.synchLogger.write("Document " + file.getName() + " cannot be processed because of " + docExce.getMessage());
			return;
		} catch (Exception ex) {
			//For all other exceptions if the transaction is not already completed, I rollback.
			if (!this.status.isCompleted()) {
				this.transactionManager.rollback(this.status);
			}
			//log.debug(ex.getCause());
			this.synchLogger.write(ex.getCause().toString());
			throw new IOException(ex);
		}

	}

	/**
	 * {@inheritDoc} This method is called when a directory walk has ended. For our purpose, every time a directory walk
	 * ends we call the worker.end(). In case of ProcessDriverXML instances, thus leads to a set save operation. Then
	 * the transaction started at directoryStart will be committed
	 * 
	 * @see org.apache.commons.io.DirectoryWalker#handleDirectoryEnd(java.io.File, int, java.util.Collection)
	 */
	@SuppressWarnings("unchecked")
	@Override
	protected void handleDirectoryEnd(final File directory, final int depth, final Collection results) throws IOException {
		if (this.hasFilesToProcess(directory)) {
			//log.debug("handle directory end --> call worker.end() if there's smt to commit.");
			//this.synchLogger.write("handle directory end --> call worker.end() if there's smt to commit.");
			ArrayList<WalkerResult> resList = (ArrayList<WalkerResult>) results;
			WalkerResult wr = resList.get(0);
			if (wr.getFilesToCommit() > 0) {
				this.worker.end();
				wr.addToFilesProcessed(wr.getFilesToCommit());
				wr.setFilesToCommit(0);
			}
			this.transactionManager.commit(this.status);
			//log.debug("HandleDirectoryEnd: Transaction " + this.status + " committed");
			this.synchLogger.write("HandleDirectoryEnd: Transaction " + this.status + " committed");
		}
	}

	/**
	 * {@inheritDoc} This method is called just after the walking phase has ended; the given implementation calls the
	 * post-processing method of the worker.
	 * 
	 * @see org.apache.commons.io.DirectoryWalker#handleEnd(java.util.Collection)
	 */
	@SuppressWarnings("unchecked")
	@Override
	protected void handleEnd(final Collection results) throws IOException {
		this.endTime = System.currentTimeMillis();
		//log.info(this.totalFiles + " have been processed. Driver DMF ingestion completed in " + (this.endTime - this.startTime) + " ms!");
		this.synchLogger.write(this.totalFiles + " have been processed. Driver DMF ingestion completed in " + (this.endTime - this.startTime) + " ms!");
	}

	/**
	 * Class used to represent the results of the walk process.
	 * 
	 * @author lexis
	 */
	class WalkerResult {
		/** How many files have been processed but not yet committed. */
		private int filesToCommit;
		/** How many files have been processed and committed sofar. */
		private int filesProcessed;

		public int getFilesToCommit() {
			return this.filesToCommit;
		}

		public void setFilesToCommit(final int filesToCommit) {
			this.filesToCommit = filesToCommit;
		}

		public int getFilesProcessed() {
			return this.filesProcessed;
		}

		public void setFilesProcessed(final int filesProcessed) {
			this.filesProcessed = filesProcessed;
		}

		/** Increments the number of files processed but not yet committed by 1. */
		public void incrFilesToCommit() {
			this.filesToCommit++;
		}

		/**
		 * Increments the number of files processed and commit by the number specified in the i parameter.
		 * 
		 * @param i
		 *            number to add to the current value of filesProcessed
		 */
		public void addToFilesProcessed(final int i) {
			this.filesProcessed += i;
		}

	}

	public int getNumOfFilesPerCommit() {
		return this.numOfFilesPerCommit;
	}

	@Required
	public void setNumOfFilesPerCommit(final int numOfFilesPerCommit) {
		this.numOfFilesPerCommit = numOfFilesPerCommit;
	}

	public void setTransactionManager(final PlatformTransactionManager transactionManager) {
		this.transactionManager = transactionManager;
	}

	public PlatformTransactionManager getTransactionManager() {
		return this.transactionManager;
	}

	public void setTotalFiles(final int totalFiles) {
		this.totalFiles = totalFiles;
	}

	public int getTotalFiles() {
		return this.totalFiles;
	}

	public void setReader(final SAXReader reader) {
		this.reader = reader;
	}

	public SAXReader getReader() {
		return this.reader;
	}

	public ProcessXMLInterface getWorker() {
		return this.worker;
	}

	public void setWorker(final ProcessXMLInterface worker) {
		this.worker = worker;
	}

	@Required
	public void setSynchLogger(final CustomLogger synchLogger) {
		this.synchLogger = synchLogger;
	}

	public CustomLogger getSynchLogger() {
		return this.synchLogger;
	}
}
