package eu.dnetlib.enabling.is.registry;

import javax.xml.transform.dom.DOMResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.springframework.beans.factory.annotation.Required;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpService;
import eu.dnetlib.enabling.is.registry.rmi.ISRegistryService;
import eu.dnetlib.miscutils.jaxb.JaxbFactory;
import eu.dnetlib.enabling.tools.OpaqueResource;
import eu.dnetlib.enabling.tools.ServiceLocator;
import eu.dnetlib.enabling.tools.StringOpaqueResource;
import eu.dnetlib.enabling.tools.blackboard.BlackboardMessage;
import eu.dnetlib.miscutils.datetime.DateUtils;

/**
 * Simple registry blackboard manager implementation.
 *
 * @author marko
 *
 */
public class RegistryBlackboardManagerImpl implements RegistryBlackboardManager { // NOPMD

	/**
	 * timestamp padding.
	 */
	private static final String PADDING = "000";

	/**
	 * milliseconds in a second.
	 */
	private static final int MILLIS = 1000;

	/**
	 * Provides access to the lookup service.
	 */
	private ServiceLocator<ISLookUpService> lookupLocator;

	/**
	 * the registry which is bound with this registry blackboard manager implementation.
	 */
	private ISRegistryService registryService;

	/**
	 * blackboard message factory.
	 */
	private JaxbFactory<BlackboardMessage> messageFactory;

	/**
	 * provides the current date. Testers can override.
	 */
	private MessageDater messageDater = new MessageDater();

	/**
	 * Testers can override.
	 *
	 * @author marko
	 *
	 */
	public static class MessageDater {
		public String getCurrentDate() {
			return DateUtils.now_ISO8601();
		}

		public String getNumericStamp() {
			return Long.toString(System.currentTimeMillis() / MILLIS) + "." + System.currentTimeMillis() % MILLIS + PADDING;
		}
	}

	/**
	 * LAST_REQUEST or LAST_RESPONSE blackboard time stamp holders.
	 *
	 * @author marko
	 *
	 */
	public enum LastStamp {
		/**
		 * LAST_REQUEST, used when the blackboard message flows from the client to the server.
		 */
		REQUEST,

		/**
		 * LAST_RESPONSE, used when the blackboard message flows from the server to the client, signaling the
		 * completion/progress of the action..
		 */
		RESPONSE
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.is.registry.RegistryBlackboardManager#addMessage(java.lang.String, java.lang.String,
	 *      java.lang.String)
	 */
	@Override
	public void addMessage(final String profId, final String messageId, final String message) { // NOPMD
		try {
			final BlackboardMessage bbm = messageFactory.parse(message);
			bbm.setDate(messageDater.getCurrentDate()); // preserve compatibility.

			if (bbm.getId() == null || !bbm.getId().equals(messageId))
				throw new IllegalArgumentException("invalid blackboard message id");

			synchronized (this) {
				final OpaqueResource serviceProfile = new StringOpaqueResource(lookupLocator.getService().getResourceProfile(profId));

				final Document doc = serviceProfile.asDom();
				final Node bboard = (Node) XPathFactory.newInstance().newXPath().evaluate("//BLACKBOARD", doc, XPathConstants.NODE);

				// delete current element if exists
				final Node messageElement = (Node) XPathFactory.newInstance().newXPath().evaluate("//BLACKBOARD/MESSAGE[@id='" + messageId + "']", doc,
						XPathConstants.NODE);

				if (messageElement != null)
					bboard.removeChild(messageElement);

				// append the serialized node to the blackboard node
				messageFactory.serialize(bbm, new DOMResult(bboard));

				updateLastStamps(doc, LastStamp.REQUEST, messageId);

				registryService.updateProfile(profId, serviceProfile.asString(), serviceProfile.getResourceType());
			}
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Helper method which updates the LAST_* stamps in the blackboard header.
	 *
	 * @param doc
	 *            profile DOM document
	 * @param stamp
	 *            which stamp to modify
	 * @param messageId
	 *            message id to point the stamp to
	 * @throws XPathExpressionException
	 *             could happen
	 */
	protected void updateLastStamps(final Document doc, final LastStamp stamp, final String messageId) throws XPathExpressionException {
		final Element lastRequest = (Element) XPathFactory.newInstance().newXPath().evaluate("//BLACKBOARD/LAST_" + stamp, doc, XPathConstants.NODE);
		lastRequest.setTextContent(messageId);
		lastRequest.setAttribute("date", messageDater.getNumericStamp());
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.is.registry.RegistryBlackboardManager#deleteMessage(java.lang.String, java.lang.String)
	 */
	@Override
	public void deleteMessage(final String profId, final String messageId) {
		try {
			synchronized (this) {
				final OpaqueResource serviceProfile = new StringOpaqueResource(lookupLocator.getService().getResourceProfile(profId));
				final Document doc = serviceProfile.asDom();

				final Node message = (Node) XPathFactory.newInstance().newXPath().evaluate("//BLACKBOARD/MESSAGE[@id='" + messageId + "']", doc,
						XPathConstants.NODE);

				message.getParentNode().removeChild(message);

				registryService.updateProfile(profId, serviceProfile.asString(), serviceProfile.getResourceType());
			}
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.enabling.is.registry.RegistryBlackboardManager#replyMessage(java.lang.String, java.lang.String)
	 */
	@Override
	public void replyMessage(final String profId, final String message) {
		try {
			final BlackboardMessage bbm = messageFactory.parse(message);
			bbm.setDate(messageDater.getCurrentDate()); // preserve compatibility.

			final String messageId = bbm.getId();

			synchronized (this) {
				final OpaqueResource serviceProfile = new StringOpaqueResource(lookupLocator.getService().getResourceProfile(profId));
				final Document doc = serviceProfile.asDom();

				final Node messageElement = (Node) XPathFactory.newInstance().newXPath().evaluate("//BLACKBOARD/MESSAGE[@id='" + messageId + "']", doc,
						XPathConstants.NODE);

				if (messageElement == null)
					throw new IllegalArgumentException("no such blackboard message " + messageId + ". Unably to reply");

				final Node bboard = messageElement.getParentNode();
				bboard.removeChild(messageElement);

				// append the serialized node to the blackboard node
				messageFactory.serialize(bbm, new DOMResult(bboard));

				updateLastStamps(doc, LastStamp.RESPONSE, messageId);

				registryService.updateProfile(profId, serviceProfile.asString(), serviceProfile.getResourceType());
			}
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	public ServiceLocator<ISLookUpService> getLookupLocator() {
		return lookupLocator;
	}

	@Required
	public void setLookupLocator(final ServiceLocator<ISLookUpService> lookupLocator) {
		this.lookupLocator = lookupLocator;
	}

	public ISRegistryService getRegistryService() {
		return registryService;
	}

	@Required
	public void setRegistryService(final ISRegistryService registryService) {
		this.registryService = registryService;
	}

	public JaxbFactory<BlackboardMessage> getMessageFactory() {
		return messageFactory;
	}

	@Required
	public void setMessageFactory(final JaxbFactory<BlackboardMessage> messageFactory) {
		this.messageFactory = messageFactory;
	}

	public MessageDater getMessageDater() {
		return messageDater;
	}

	public void setMessageDater(final MessageDater messageDater) {
		this.messageDater = messageDater;
	}

}
