package eu.dnetlib.xml.database.exist;

import java.io.IOException;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exist.collections.triggers.DocumentTrigger;
import org.exist.collections.triggers.FilteringTrigger;
import org.exist.collections.triggers.Trigger;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.DocumentImpl;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.DBBroker;
import org.exist.storage.lock.Lock;
import org.exist.storage.txn.Txn;
import org.exist.xmldb.XmldbURI;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * test eXist trigger.
 * 
 * @author marko
 * 
 */
public abstract class AbstractDiffTrigger extends FilteringTrigger implements DocumentTrigger { // NOPMD
	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(AbstractDiffTrigger.class); // NOPMD by marko on 11/24/08 5:02 PM

	/**
	 * DOM converter.
	 */
	private final transient ExistDOMConverter domConverter = new ExistDOMConverter();

	/**
	 * thread local storage.
	 * 
	 * TODO: remove thread local
	 * 
	 * rationale: looking at the eXist sources it seems that a new trigger instance is created for each new document
	 * oriented operation. Probably even if the same trigger is reused for more than one document, for example for bulk
	 * operations, calls will be originated from the same thread
	 */
	private final transient ThreadLocal<Document> local = new ThreadLocal<Document>();

	/**
	 * {@inheritDoc}
	 * 
	 * @see org.exist.collections.triggers.DocumentTrigger#prepare(int, org.exist.storage.DBBroker,
	 *      org.exist.storage.txn.Txn, org.exist.xmldb.XmldbURI, org.exist.dom.DocumentImpl)
	 */
	public void prepare(final int eventType, final DBBroker broker, final Txn arg2, final XmldbURI uri, final DocumentImpl doc) throws TriggerException {
		log.debug("preparing trigger: " + eventType);
		if (doc != null) {
			try {
				local.set(domConverter.asDocument(doc));
			} catch (IOException e) {
				log.fatal("cannot convert", e);
				throw new TriggerException("cannot convert eXist DOM to normal DOM", e);
			} catch (ParserConfigurationException e) {
				log.fatal("cannot convert", e);
				throw new TriggerException("cannot convert eXist DOM to normal DOM", e);
			} catch (SAXException e) {
				log.fatal("cannot convert", e);
				throw new TriggerException("cannot convert eXist DOM to normal DOM", e);
			}
		}
	}

	/**
	 * helper.
	 * 
	 * @author marko
	 * 
	 */
	abstract class AbstractTriggerFunctor {
		/**
		 * requires current version.
		 */
		private final transient boolean requiresCurrent;

		/**
		 * default constructor.
		 */
		AbstractTriggerFunctor() {
			this(true);
		}

		/**
		 * constructor.
		 * 
		 * @param requiresCurrent
		 *            false if you don't need to fetch the current
		 */
		AbstractTriggerFunctor(final boolean requiresCurrent) {
			this.requiresCurrent = requiresCurrent;
		}

		/**
		 * run a trigger.
		 * 
		 * @param broker
		 *            broker
		 * @param relative
		 *            file name
		 */
		void runTrigger(final DBBroker broker, final XmldbURI relative) {
			if (local.get() == null) {
				log.fatal("thread local is empty");
			} else {
				try {
					Document newDoc = null; // NOPMD
					if (doesRequireCurrent()) {
						DocumentImpl resource = null; // NOPMD
						try {
							final XmldbURI uri = collection.getURI().append(relative);
							resource = broker.getXMLResource(uri, Lock.READ_LOCK);
							newDoc = resource;

							if (newDoc == null) {
								log.fatal("cannot fetch current document version: " + relative);
								return;
							}
							newDoc = domConverter.asDocument(newDoc);
						} finally {
							if (resource != null)
								resource.getUpdateLock().release(Lock.READ_LOCK);
						}
					}
					callTrigger(local.get(), newDoc);
				} catch (IOException e) {
					log.warn("trigger convert doc", e);
				} catch (ParserConfigurationException e) {
					log.warn("trigger convert doc ", e);
				} catch (SAXException e) {
					log.warn("trigger convert doc", e);
				} catch (PermissionDeniedException e) {
					log.warn("trigger convert doc", e);
				}
			}
		}

		/**
		 * override to false if you don't want to fetch the current document version.
		 * 
		 * @return true if the current document is required.
		 */
		boolean doesRequireCurrent() {
			return requiresCurrent;
		}

		/**
		 * forward the call to the correct trigger type.
		 * 
		 * @param oldDoc
		 *            old document version, if applicable.
		 * @param newDoc
		 *            new document version, if applicable.
		 */
		abstract void callTrigger(Document oldDoc, Document newDoc);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see org.exist.collections.triggers.FilteringTrigger#finish(int, org.exist.storage.DBBroker,
	 *      org.exist.storage.txn.Txn, org.exist.xmldb.XmldbURI, org.exist.dom.DocumentImpl)
	 */
	@Override
	public void finish(final int eventType, final DBBroker broker, final Txn arg2, final XmldbURI uri, final DocumentImpl doc) {
		super.finish(eventType, broker, arg2, uri, doc);

		log.debug("trigger finished. Event: " + eventType);

		AbstractTriggerFunctor functor;
		if (eventType == Trigger.UPDATE_DOCUMENT_EVENT) {
			functor = new AbstractTriggerFunctor() {
				@Override
				void callTrigger(final Document oldDoc, final Document newDoc) {
					triggerUpdate(uri, oldDoc, newDoc);
				}
			};
		} else if (eventType == Trigger.STORE_DOCUMENT_EVENT) {
			local.set(doc); // TODO: ugly
			functor = new AbstractTriggerFunctor() {
				@Override
				void callTrigger(final Document oldDoc, final Document newDoc) {
					triggerCreate(uri, newDoc);
				}
			};
		} else if (eventType == Trigger.REMOVE_DOCUMENT_EVENT) {
			functor = new AbstractTriggerFunctor(false) {
				@Override
				void callTrigger(final Document oldDoc, final Document newDoc) {
					triggerDelete(uri, oldDoc);
				}
			};
		} else {
			functor = new AbstractTriggerFunctor() {
				@Override
				void callTrigger(final Document oldDoc, final Document newDoc) {
					log.fatal("unhandled trigger event: " + eventType);
				}
			};
		}

		functor.runTrigger(broker, uri.lastSegment());
	}

	/**
	 * this method has to be implemented to intercept 'create' trigger events.
	 * 
	 * @param uri
	 *            document uri
	 * @param newDoc
	 *            created document
	 */
	protected abstract void triggerCreate(XmldbURI uri, Document newDoc);

	/**
	 * this method has to be implemented to intercept 'update' trigger events.
	 * 
	 * @param uri
	 *            document uri
	 * @param oldDoc
	 *            old version
	 * @param newDoc
	 *            new version
	 */
	protected abstract void triggerUpdate(XmldbURI uri, Document oldDoc, Document newDoc);

	/**
	 * this method has to be implemented to intercept 'create' trigger events.
	 * 
	 * @param uri
	 *            document uri
	 * @param oldDoc
	 *            removed document.
	 */
	protected abstract void triggerDelete(XmldbURI uri, Document oldDoc);

}
