package eu.dnetlib.r2d2.neo4j.events;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.event.PropertyEntry;
import org.neo4j.graphdb.event.TransactionData;
import org.neo4j.graphdb.event.TransactionEventHandler;

import eu.dnetlib.r2d2.neo4j.Neo4jBean;
import eu.dnetlib.r2d2.neo4j.dao.Neo4JDao;
import eu.dnetlib.r2d2.neo4j.domain.Relationships;

public class EventGenerator implements TransactionEventHandler<TransactionEvents> {
	private static Logger logger = Logger.getLogger(EventGenerator.class);
	
	private List<Neo4jEventListener> listeners = new ArrayList<Neo4jEventListener>();
	
	@SuppressWarnings("unchecked")
	@Override
	public TransactionEvents beforeCommit(TransactionData tData) throws Exception {
		logger.debug("Before commit");
		
		try {
			TransactionEvents eventList = new TransactionEvents();
			
			for (Node n:tData.createdNodes()) {
				String beanId = (String) n.getProperty(Neo4jBean.ID);
				Class<? extends Neo4jBean> beanClass = this.getNodeClass(n);
				
				if (beanClass != null)
					eventList.newBean(beanId, beanClass, new Date());
			}
			
			for (Node n:tData.deletedNodes()) {
				String[] beanInfo = Neo4JDao.deletedNodes.get().get(n.getId());
				
				if (beanInfo != null) {
					String beanId = beanInfo[0];
					Class<? extends Neo4jBean> beanClass = (Class<? extends Neo4jBean>) Class.forName(beanInfo[1]);
					
					eventList.deletedBean(beanId, beanClass, new Date());
				}
			}
			
			for (PropertyEntry<Node> entry:tData.assignedNodeProperties()) {
				String beanId = (String) entry.entity().getProperty(Neo4jBean.ID);
				Class<? extends Neo4jBean> beanClass = this.getNodeClass(entry.entity());
				
				if (!eventList.isNewBean(beanId) && beanClass != null) {
					String propertyName = entry.key();
					Object previousValue = entry.previouslyCommitedValue();
					Object newValue = entry.value();
					
					BeanUpdateEvent event = eventList.getUpdateEvent(beanId);
					
					if (event == null) {
						event = new BeanUpdateEvent(beanClass, beanId, new Date());
						
						eventList.addUpdateEvent(event);
					}
					
					event.addUpdatedProperty(propertyName, previousValue, newValue);
				}
			}
			
			for (PropertyEntry<Node> entry:tData.removedNodeProperties()) {
				String[] beanInfo = Neo4JDao.deletedNodes.get().get(entry.entity().getId());
				
				if (beanInfo != null || eventList.isNewBean((String) entry.entity().getProperty(Neo4jBean.ID))) {
					// node is deleted. doing nothing
				} else {
					String beanId = (String) entry.entity().getProperty(Neo4jBean.ID);
					Class<? extends Neo4jBean> beanClass = (Class<? extends Neo4jBean>) Class.forName((String) entry.entity().getProperty(Neo4jBean.PREFIX));
					String propertyName = entry.key();
					Object previousValue = entry.previouslyCommitedValue();
					
					BeanUpdateEvent event = eventList.getUpdateEvent(beanId);
					
					if (event == null) {
						event = new BeanUpdateEvent(beanClass, beanId, new Date());
						
						eventList.addUpdateEvent(event);
					}
					
					event.addUpdatedProperty(propertyName, previousValue, null);
				}
			}

			for (Relationship rel:tData.deletedRelationships()) {
				if (!rel.getType().toString().startsWith("_")) {
					String sourceNodeId = null;
					String targetNodeId = null;
					
					String[] beanInfo = Neo4JDao.deletedNodes.get().get(rel.getStartNode().getId());
					
					if (beanInfo != null) {
						// node is deleted too
						sourceNodeId = beanInfo[0];
					} else {
						sourceNodeId = (String) rel.getStartNode().getProperty(Neo4jBean.ID);
					}
					
					beanInfo = Neo4JDao.deletedNodes.get().get(rel.getEndNode().getId());
					
					if (beanInfo != null) {
						// node is deleted too
						targetNodeId = beanInfo[0];
					} else {
						targetNodeId = (String) rel.getEndNode().getProperty(Neo4jBean.ID);
					}
					
					eventList.deletedRelation(sourceNodeId, targetNodeId, (Relationships) rel.getType(), new Date());
				}
			}
			
			for (Relationship rel:tData.createdRelationships()) {
				if (!rel.getType().toString().startsWith("_")) {
					String sourceNodeId = null;
					String targetNodeId = null;
					
					String[] beanInfo = Neo4JDao.deletedNodes.get().get(rel.getStartNode().getId());
					
					if (beanInfo != null) {
						// node is deleted too
						sourceNodeId = beanInfo[0];
					} else {
						sourceNodeId = (String) rel.getStartNode().getProperty(Neo4jBean.ID);
					}
					
					beanInfo = Neo4JDao.deletedNodes.get().get(rel.getEndNode().getId());
					
					if (beanInfo != null) {
						// node is deleted too
						targetNodeId = beanInfo[0];
					} else {
						targetNodeId = (String) rel.getEndNode().getProperty(Neo4jBean.ID);
					}
					
					eventList.newRelation(sourceNodeId, targetNodeId, (Relationships) rel.getType(), new Date());
				}
			}
			
			return eventList;
		} catch (Exception e) {
			logger.error("Error generating events", e);
		}
		
		return null;
	}

	@Override
	public void afterCommit(TransactionData tData, TransactionEvents events) {
		for (Neo4jEventListener listener:listeners) {
			for (BeanCreateEvent event:events.createEvents())
				listener.beanCreated(event);
			for (BeanUpdateEvent event:events.updateEvents())
				listener.beanUpdated(event);
			for (BeanDeleteEvent event:events.deleteEvents())
				listener.beanDeleted(event);
			for (RelationDeleteEvent event:events.deletedRelations())
				listener.relationDeleted(event);
			for (RelationCreateEvent event:events.newRelations())
				listener.relationCreated(event);
		}
	}

	@Override
	public void afterRollback(TransactionData arg0, TransactionEvents arg1) {
		// transaction failed. no events sent
	}
	
	@SuppressWarnings("unchecked")
	private Class<? extends Neo4jBean> getNodeClass(Node node) throws ClassNotFoundException {
		String className = (String) node.getProperty(Neo4jBean.PREFIX, null);

		if (!className.startsWith("_"))
			return (Class<? extends Neo4jBean>) Class.forName(className);
		
		return null;
	}
	
	public Neo4jEventListener addListener(Neo4jEventListener listener) {
		this.listeners.add(listener);
		
		return listener;
	}
}

class TransactionEvents {
	private Map<String, BeanCreateEvent> createEvents = new HashMap<String, BeanCreateEvent>();
	private Map<String, BeanUpdateEvent> updateEvents = new HashMap<String, BeanUpdateEvent>();
	private Map<String, BeanDeleteEvent> deleteEvents = new HashMap<String, BeanDeleteEvent>();
	private List<RelationCreateEvent> newRelations = new ArrayList<RelationCreateEvent>();
	private List<RelationDeleteEvent> deletedRelations = new ArrayList<RelationDeleteEvent>();
	
	public Iterable<BeanCreateEvent> createEvents() {
		return createEvents.values();
	}
	
	public Iterable<BeanUpdateEvent> updateEvents() {
		return updateEvents.values();
	}
	
	public Iterable<BeanDeleteEvent> deleteEvents() {
		return deleteEvents.values();
	}

	public Iterable<RelationCreateEvent> newRelations() {
		return newRelations;
	}
	
	public Iterable<RelationDeleteEvent> deletedRelations() {
		return deletedRelations;
	}
	
	public void newBean(String beanId, Class<? extends Neo4jBean> beanClass, Date date) {
		this.createEvents.put(beanId, new BeanCreateEvent(beanClass, beanId, date));
	}
	
	public void deletedBean(String beanId, Class<? extends Neo4jBean> beanClass, Date date) {
		this.deleteEvents.put(beanId, new BeanDeleteEvent(beanClass, beanId, date));
	}
	
	public void newRelation(String sourceNodeId, String targetNodeId,
			Relationships relationship, Date eventdate) {
		this.newRelations.add(new RelationCreateEvent(sourceNodeId, targetNodeId, relationship, eventdate));
	}
	
	public void deletedRelation(String sourceNodeId, String targetNodeId,
			Relationships relationship, Date eventdate) {
		this.deletedRelations.add(new RelationDeleteEvent(sourceNodeId, targetNodeId, relationship, eventdate));
	}
	
	public boolean isNewBean(String beanId) {
		return this.createEvents.containsKey(beanId);
	}
	
	public boolean isDeletedBean(String beanId) {
		return this.deleteEvents.containsKey(beanId);
	}
	
	public BeanUpdateEvent getUpdateEvent(String beanId) {
		return updateEvents.get(beanId);
	}
	
	public void addUpdateEvent(BeanUpdateEvent event) {
		updateEvents.put(event.getBeanId(), event);
	}
	
	
}