package eu.dnetlib.enabling.is.sn;

import java.io.StringWriter;
import java.util.Collection;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Node;

import eu.dnetlib.enabling.is.sn.resourcestate.ResourceStateNotificationDetector;
import eu.dnetlib.enabling.is.sn.resourcestate.ResourceStateSubscription;
import eu.dnetlib.enabling.is.sn.resourcestate.ResourceStateSubscriptionRegistry;
import eu.dnetlib.enabling.tools.OpaqueResource;

/**
 * This notification detector uses a local xmldb trigger as a source of events.
 *
 * @author marko
 *
 */
public class NotificationDetectorImpl extends AbstractNotificationDetector implements ResourceStateNotificationDetector<OpaqueResource> {

	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(NotificationDetectorImpl.class); // NOPMD by marko on 11/24/08 5:02 PM

	/**
	 * subscription registries to lookup.
	 */
	private SubscriptionRegistry registry;

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.is.sn.resourcestate.ResourceStateNotificationDetector#resourceCreated(java.lang.Object)
	 */
	@Override
	public void resourceCreated(final OpaqueResource newResource) {
		log.debug("resource created: " + getRegistry());

		final Collection<ResourceStateSubscription> subs =
				getRegistry().listMatchingSubscriptions(ResourceStateSubscription.PREFIX_CREATE, newResource.getResourceType(), newResource.getResourceId());
		for (ResourceStateSubscription sub : subs) {
			if (matchPath(newResource, sub.getXpath())) {
				send(sub, newResource, ResourceStateSubscription.PREFIX_CREATE);
			}
		}
	}

	/**
	 * helper method. sends a notification for a given prefix.
	 *
	 * @param sub
	 *            subscription
	 * @param resource
	 *            resource
	 * @param prefix
	 *            prefix
	 */
	private void send(final ResourceStateSubscription sub, final OpaqueResource resource, final String prefix) {
		log.debug("RESOURCE " + resource);
		log.debug("id: " + resource.getResourceId());
		log.debug("dom: " + resource.asDom());

		final StringBuffer topicBuffer = new StringBuffer();

		if (sub.getPrefix() == null && "*".equals(sub.getPrefix()))
			topicBuffer.append(prefix);
		else
			topicBuffer.append(sub.getPrefix());

		topicBuffer.append('.');
		topicBuffer.append(sub.getType());
		topicBuffer.append('.');
		topicBuffer.append(sub.getResourceId());
		if (sub.getXpath() != null && !sub.getXpath().isEmpty()) {
			topicBuffer.append(sub.getXpath().replace('/', '.'));
		}

		getSender().send(sub.getSubscriberAsEpr(), new NotificationMessage(sub.getSubscriptionId(), topicBuffer.toString(), resource.getResourceId(), // NOPMD
				resource.asString()));
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.is.sn.resourcestate.ResourceStateNotificationDetector#resourceDeleted(java.lang.Object)
	 */
	@Override
	public void resourceDeleted(final OpaqueResource oldResource) {
		log.debug("resource deleted: " + getRegistry());

		final Collection<ResourceStateSubscription> subs =
				registry.listMatchingSubscriptions(ResourceStateSubscription.PREFIX_DELETE, oldResource.getResourceType(), oldResource.getResourceId());
		for (ResourceStateSubscription sub : subs) {
			if (matchPath(oldResource, sub.getXpath())) {
				send(sub, oldResource, ResourceStateSubscription.PREFIX_DELETE);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.is.sn.resourcestate.ResourceStateNotificationDetector#resourceUpdated(java.lang.Object,
	 *      java.lang.Object)
	 */
	@Override
	public void resourceUpdated(final OpaqueResource oldResource, final OpaqueResource newResource) {
		log.debug("resource updated: " + getRegistry());

		final Collection<ResourceStateSubscription> subs =
				registry.listMatchingSubscriptions(ResourceStateSubscription.PREFIX_UPDATE, oldResource.getResourceType(), oldResource.getResourceId());
		for (ResourceStateSubscription sub : subs) {
			if (comparePath(oldResource, newResource, sub.getXpath())) {
				log.debug("updated, sending: " + newResource.asString());
				send(sub, newResource, ResourceStateSubscription.PREFIX_UPDATE);
			}
		}
	}

	/**
	 * check if an xpath matches a given resource.
	 *
	 * @param resource
	 *            resource
	 * @param xpath
	 *            xpath
	 * @return true if the resource has some value for the given path
	 */
	private boolean matchPath(final OpaqueResource resource, final String xpath) {
		// by convention empty xpath matches any document
		if (xpath == null || xpath.isEmpty())
			return true;

		final XPath xpa = XPathFactory.newInstance().newXPath();
		try {
			return !xpa.evaluate(xpath, resource.asDom()).isEmpty();
		} catch (XPathExpressionException e) {
			log.warn("wrong xpath expression, notification possibly missed", e);
		}
		return false;
	}

	/**
	 * compare the content of two profiles for the same xpath. Return true if some change has been made so that the
	 * notification can be delivered.
	 *
	 * @param oldResource
	 *            old version
	 * @param newResource
	 *            new version
	 * @param xpath
	 *            XPath
	 * @return true if the two documents differ under a given path
	 */
	private boolean comparePath(final OpaqueResource oldResource, final OpaqueResource newResource, final String xpath) {
		// by convention empty xpath matches any document
		if (oldResource == null || newResource == null || xpath == null || xpath.isEmpty())
			return true;

		final XPath xpa = XPathFactory.newInstance().newXPath();
		try {
			final Transformer transformer = TransformerFactory.newInstance().newTransformer();

			final Node left = (Node) xpa.evaluate(xpath, oldResource.asDom(), XPathConstants.NODE);
			final Node right = (Node) xpa.evaluate(xpath, newResource.asDom(), XPathConstants.NODE);
			
			if (left==null || right==null) {
				if (left != null) {
					return true;
				}
				else {
					if (right != null) {
						return true;
					}
					else {
						return false;
					}
				}
			}
			
			final StringWriter leftWriter = new StringWriter();
			final StringWriter rightWriter = new StringWriter();

			transformer.transform(new DOMSource(left), new StreamResult(leftWriter));
			transformer.transform(new DOMSource(right), new StreamResult(rightWriter));
			
			return !leftWriter.toString().equals(rightWriter.toString());
		} catch (XPathExpressionException e) {
			log.warn("wrong xpath expression, notification possibly missed", e);
		} catch (TransformerException e) {
			log.warn("serialization problem", e);
		}

		return false;
	}

	public SubscriptionRegistry getRegistry() {
		return registry;
	}

	public void setRegistry(final SubscriptionRegistry registry) {
		this.registry = registry;
	}
}
