package eu.dnetlib.data.collective.manager.scheduling;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import eu.dnetlib.common.interfaces.nh.IBlackboardMessage.Action;
import eu.dnetlib.common.ws.subscription.AbstractSubscription;
import eu.dnetlib.data.collective.manager.IInstanceListener;
import eu.dnetlib.data.collective.manager.nh.AbstractServiceNotificationHandler;
import eu.dnetlib.data.collective.manager.nh.INotificationConsumer;
import eu.dnetlib.data.collective.manager.profile.AbstractInstance;

/**
 * @author jochen
 *
 * @param <T>
 */
public class EventRegistry<T extends AbstractInstance> implements IInstanceListener<T>{

	private static final Log log = LogFactory.getLog(EventRegistry.class);
	private AbstractServiceNotificationHandler<INotificationConsumer> eventNotificationHandler;
	private AbstractSubscription subscriber;
	private boolean isScheduling = true;
	
	private String storePath;
	
	private static final String subcriptionStoreName = "subscriptions";
	
	private Properties subscriptionProperties = new Properties();
	
	/**
	 * map of topics and subscriptionIds
	 */
	//private Map<String, String> subscriptionMap = new LinkedHashMap<String, String>();
	
	/**
	 * map of topics and sets of dataSourceIds
	 */
	private Map<String, Set<String>> registeredEvents = new LinkedHashMap<String, Set<String>>();
	
	/**
	 * map of dataSourceIds and instanceIds
	 */
	private Map<String, String> dataSourceInstanceMap = new LinkedHashMap<String, String>();

	public void init(){
		File f = new File(storePath + subcriptionStoreName);
		if (f.exists()){
			try {
				FileInputStream fis = new FileInputStream(f);
				subscriptionProperties.load(fis);
				fis.close();
			} catch (Exception e) {
				throw new IllegalStateException("unable to load properties from file", e);
			}
		}
	}
	
	/**
	 * adds a dataSource to a registered event. <p/>
	 * if the registered event is new, it will be created and added to the event notification handler. The registered event will be subscribed as a topic<p/>
	 * @param aRegisteredEvent the registeredEvent to add
	 * @param T the instance to add to the registeredEvent
	 */
	private void addRegisteredEvent(String aRegisteredEvent, T aInstance) {
		if (aInstance.getDataSourceIds().isEmpty()){
			log.warn("list of data sources is empty. cannot add for event based scheduling.");
			return;
		}
		if (!this.registeredEvents.containsKey(aRegisteredEvent)){
			log.debug("register new event: " + aRegisteredEvent);
			this.registeredEvents.put(aRegisteredEvent, new LinkedHashSet<String>());
			eventNotificationHandler.getTopicPrefixes().add(aRegisteredEvent.replace('/', '.'));
			subscriptionProperties.setProperty(aRegisteredEvent, subscriber.subscribeTopic(aRegisteredEvent));
			FileOutputStream outStream;
			try {
				outStream = new FileOutputStream(new File(storePath + subcriptionStoreName));
				subscriptionProperties.store(outStream, "");
				outStream.close();
			} catch (Exception e) {
				throw new IllegalStateException("unable to write subscriptionProperties file", e);
			}
			log.debug("subscribed topic with subscrId: " + subscriptionProperties.getProperty(aRegisteredEvent));
		}
		for (String dataSourceId: aInstance.getDataSourceIds()){
			this.dataSourceInstanceMap.put(dataSourceId, aInstance.getResourceId());
			this.registeredEvents.get(aRegisteredEvent).add(dataSourceId);			
		}
	}

	/**
	 * removes a dataSource from a registered event. <p/>
	 * If the registered event has no more dataSources, the registered event is removed and unsubscribed as well.
	 * @param aRegisteredEvent
	 * @param aInstance
	 */
	private void removeRegisteredEvent(String aRegisteredEvent, T aInstance){
		for (String dataSourceId: aInstance.getDataSourceIds()){
			log.debug("dataSourceId to be removed: " + dataSourceId);
			dataSourceInstanceMap.remove(dataSourceId);
			this.registeredEvents.get(aRegisteredEvent).remove(dataSourceId);
			if (this.registeredEvents.get(aRegisteredEvent).isEmpty()){
				log.debug("datasourceId set for event: " + aRegisteredEvent + " is empty.");
				if (subscriptionProperties.containsKey(aRegisteredEvent)){
					subscriber.unsubscribeTopic(subscriptionProperties.getProperty(aRegisteredEvent));
					eventNotificationHandler.getTopicPrefixes().remove(aRegisteredEvent.replace('/', '.'));
					subscriptionProperties.remove(aRegisteredEvent);
					FileOutputStream outStream;
					try {
						outStream = new FileOutputStream(new File(storePath + subcriptionStoreName));
						subscriptionProperties.store(outStream, "");
						outStream.close();
					} catch (Exception e) {
						throw new IllegalStateException("unable to write subscriptionProperties file", e);
					}
					log.debug("unsubscribed topic: " + aRegisteredEvent);
				}
				this.registeredEvents.remove(aRegisteredEvent);
			}
		}
	}
	
	/**
	 * checks, if dataSource is registered for an event
	 * @param aDataSourceId
	 * @return true, if data source is registered, false else
	 */
	protected boolean isRegisteredEvent(String aDataSourceId){
		for (String event: this.registeredEvents.keySet()){
			if (this.registeredEvents.get(event).contains(aDataSourceId) && isScheduling){
				return true;
			}
		}
		log.debug("datasource id " + aDataSourceId + " is not registered for event");
		return false;
	}
	
	/**
	 * get the instanceId of an instance which has a registered event with a data source
	 * @param aDataSourceId
	 * @return a instanceId
	 */
	protected String getInstanceId(String aDataSourceId){
		return this.dataSourceInstanceMap.get(aDataSourceId);
	}

	/**
	 * @param eventNotificationHandler the eventNotificationHandler to set
	 */
	public void setEventNotificationHandler(AbstractServiceNotificationHandler<INotificationConsumer> eventNotificationHandler) {
		this.eventNotificationHandler = eventNotificationHandler;
	}

	/**
	 * @return the eventNotificationHandler
	 */
	public AbstractServiceNotificationHandler<INotificationConsumer> getEventNotificationHandler() {
		return eventNotificationHandler;
	}

	/**
	 * @param subscriber the subscriber to set
	 */
	public void setSubscriber(AbstractSubscription subscriber) {
		this.subscriber = subscriber;
	}

	/**
	 * @return the subscriber
	 */
	public AbstractSubscription getSubscriber() {
		return subscriber;
	}

	/**
	 * add instance for event scheduling, if instance is configured for event based scheduling
	 */
	@Override
	public boolean addInstance(T aInstance) {
		if (aInstance.getScheduling().isEventSchedulingConfigured()){
			addRegisteredEvent(aInstance.getScheduling().getEventScheduling().getTopic(), aInstance);
			return true;
		}
		return false;
	}

	@Override
	public boolean cancelOnce(T aInstance, Action aAction) {
		// unsupported
		return false;
	}

	@Override
	public boolean fireOnce(T aInstance, Action aAction) {
		// unsupported
		return false;
	}

	/**
	 * remove instance from event scheduling
	 */
	@Override
	public boolean removeInstance(T aInstance) {
		// TODO Auto-generated method stub
		if (aInstance.getScheduling().isEventSchedulingConfigured()){
			removeRegisteredEvent(aInstance.getScheduling().getEventScheduling().getTopic(), aInstance);
			return true;
		}
		return false;
	}

	/**
	 * update instance for event scheduling
	 */
	@Override
	public boolean updateInstance(T aInstance) {
		log.debug("updateInstance - " + aInstance.getResourceId());
		if (aInstance.getScheduling().isEventSchedulingConfigured()){
			log.debug("add event based scheduling");
			addRegisteredEvent(aInstance.getScheduling().getEventScheduling().getTopic(), aInstance);
		}else{
			log.debug("remove from event based scheduling");
			if (this.dataSourceInstanceMap.containsValue(aInstance.getResourceId())){
				// search the dataSourceId
				for (String dataSourceId: this.dataSourceInstanceMap.keySet()){
					if (this.dataSourceInstanceMap.get(dataSourceId).equals(aInstance.getResourceId())){
						// search the event
						for (String event: this.registeredEvents.keySet()){
							if (this.registeredEvents.get(event).contains(dataSourceId)){
								removeRegisteredEvent(event, aInstance);
								return true;
							}
						}						
					}
				}
			}
		}
		return false;
	}

	@Override
	public void enableScheduling(boolean aIsScheduling) {
		this.isScheduling = aIsScheduling;
	}

	@Override
	public void setEnableSchedulingOnInit(boolean aIsScheduling) {
		this.isScheduling = aIsScheduling;
	}

	@Override
	public boolean isSchedulingEnabled() {
		return this.isScheduling;
	}

	/**
	 * @param storePath the storePath to set
	 */
	public void setStorePath(String storePath) {
		this.storePath = storePath;
	}

	/**
	 * @return the storePath
	 */
	public String getStorePath() {
		return storePath;
	}

}
