package eu.dnetlib.r2d2.neo4j.dao;

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

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.ReturnableEvaluator;
import org.neo4j.graphdb.StopEvaluator;
import org.neo4j.graphdb.TraversalPosition;
import org.neo4j.graphdb.Traverser;
import org.neo4j.graphdb.Traverser.Order;
import org.neo4j.index.lucene.LuceneFulltextIndexService;
import org.neo4j.index.lucene.LuceneIndexService;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Iterables;

import eu.dnetlib.miscutils.collections.MappedCollection;
import eu.dnetlib.miscutils.functional.UnaryFunction;
import eu.dnetlib.miscutils.iterators.IterableIterator;
import eu.dnetlib.r2d2.neo4j.BeanDao;
import eu.dnetlib.r2d2.neo4j.Neo4jBean;
import eu.dnetlib.r2d2.neo4j.SearchResults;
import eu.dnetlib.r2d2.neo4j.util.ChainIterable;
import eu.dnetlib.r2d2.neo4j.util.TransactionStatusHolder;
import eu.dnetlib.r2d2.neo4j.util.UUIDIdBroker;

/**
 * 
 * @author antleb
 *
 * @param <B>
 */

@Transactional
public abstract class Neo4JDao<B extends Neo4jBean> implements BeanDao<B> {
	private Logger logger = Logger.getLogger(this.getClass());
	
	protected GraphDatabaseService graphDb = null;
	private LuceneIndexService index = null;
	private LuceneFulltextIndexService fulltextIndex = null;
	
	private IdBroker idBroker = null;

	private Node subNode = null;
	
	private RelationshipType rootToSubNodeRelation = null;
	private RelationshipType subNodeToNodesRelation = null;
	
	private String propertyNamePrefix = null;
	
	public static final ThreadLocal<Map<String, Node>> createdNodes = new ThreadLocal<Map<String, Node>>() {
		protected Map<String, Node> initialValue() {
			return new HashMap<String, Node>();
		}
	};
	
	public Neo4JDao(RelationshipType rootToSubNodeRelation, RelationshipType subNodeToNodesRelation, String propertyNamePrefix) {
		this.rootToSubNodeRelation = rootToSubNodeRelation;
		this.subNodeToNodesRelation = subNodeToNodesRelation;
		this.propertyNamePrefix = propertyNamePrefix;
	}
	
	protected abstract B createBean();
	
	@Transactional
	public void init() {
		logger.debug("initializing");

		if (this.idBroker == null) {
			logger.warn("id broker is null, using default one");
			this.idBroker = new UUIDIdBroker();
		}
		
		Relationship rel = graphDb.getReferenceNode().getSingleRelationship(rootToSubNodeRelation, Direction.OUTGOING);

		if (rel == null) {
			subNode = graphDb.createNode();
			
			// the following properties are not used, but are set to avoid
			// adding complex if/then clauses in the transaction event handler.
			subNode.setProperty(Neo4jBean.ID, idBroker.generateId());
			subNode.setProperty(Neo4jBean.PREFIX, rootToSubNodeRelation.toString());
			
			graphDb.getReferenceNode().createRelationshipTo(subNode, rootToSubNodeRelation);
			
			TransactionStatusHolder.setWriteStatus();
		} else {
			subNode = rel.getEndNode();
		}
	}

	@Override
	public B newBean() {
		return newBean(idBroker.generateId());
	}

	@Override
	@SuppressWarnings("unchecked")
	public Class<? extends Neo4jBean> getNodeType(String beanId) {
		Node node = this.getNode(beanId);

		if (node == null)
			return null;
		
		try {
			return (Class<? extends Neo4jBean>) Class.forName((String) node.getProperty(Neo4jBean.PREFIX));
		} catch (ClassNotFoundException e) {
			throw new RuntimeException("Unknown type (" + node.getProperty(Neo4jBean.PREFIX) + ") of node " + beanId, e);
		}
	}

	@Override
	public boolean beanExists(String beanId) {
		return this.getNode(beanId) != null;
	}
	
	@Override
	public B newBean(String id) {
		B b = this.createBean();
		
		b.setNode(this.newNode());
		b.setId(id);
		b.setValue(Neo4jBean.PREFIX, this.propertyNamePrefix);
		
		createdNodes.get().put(b.getId(), b.getNode());
		
		TransactionStatusHolder.setWriteStatus();
		
		return b;
	}
	
	@Override
	public void deleteBean(String beanId) {
		logger.debug("deleting bean " + beanId);
		
		Node node = this.getNode(beanId);
		
		// delete all incoming and outgoing relation first
		for (Relationship rel:node.getRelationships()) {
			rel.delete();
		}

		// then the node.
		node.delete();
		
		TransactionStatusHolder.setWriteStatus();
	}
	
	@Override
	public B getBean(String beanId) {
		logger.debug("Getting profile with id " + beanId);
		
		Node node = this.getNode(beanId);
		
		if (node != null)
			return newBean(node);
		else
			return null;
	}
	
	@Override
	public String saveBean(B bean) {
		String id = bean.getId();
		
		return id;
	}
	
	@Override
	public Iterable<B> getAll() {
		logger.debug("Returning all users");
		
		Traverser trav = subNode.traverse(
				Order.DEPTH_FIRST, 
				StopEvaluator.DEPTH_ONE,
				ReturnableEvaluator.ALL_BUT_START_NODE, 
				subNodeToNodesRelation,
				Direction.OUTGOING);
		
		return new BeanIterable(trav);
	}
	
	@Override
	public Iterable<B> search(String term, String... fields) {
		if (term == null || term.trim().equals(""))
			return this.getAll();
		
		ChainIterable<B> iter = new ChainIterable<B>();
		
		for (String field:fields) {
			String propertyName = propertyNamePrefix + "." + field;
			logger.debug("Searching for " + term + " in " + propertyName);
			
			iter.addIterable(new BeanIterable(fulltextIndex.getNodes(propertyName, term)));
		}

		return iter;
	}
	
	@Override
	public Iterable<B> search(String term) {
		if (term == null || term.trim().equals(""))
			return this.getAll();
		
		String propertyName = propertyNamePrefix + ".*";
		
		logger.debug("Searching for " + term + " in " + propertyName);
		
		return new BeanIterable(fulltextIndex.getNodes(propertyName, term));
	}
	
	public SearchResults<B> search(int from, int size, String term) {
		if (term == null || term.trim().equals(""))
			return this.getAll(from, size);
		
		Iterable<B> iter = this.search(term);

		return consumeIterator(from, size, iter.iterator());
	}
	
	public SearchResults<B> search(int from, int size, String term, String... fields) {
		if (term == null || term.trim().equals(""))
			return this.getAll(from, size);
		
		Iterable<B> iter = this.search(term, fields);
		return consumeIterator(from, size, iter.iterator());
	}
	
	public SearchResults<B> getAll(int from, int count) {
		logger.debug("Returning " + count + " beans " + " starting from" + from);
		
		RangedEvaluator re = new RangedEvaluator(from, count);
		Traverser trav = subNode.traverse(
				Order.DEPTH_FIRST, 
				StopEvaluator.DEPTH_ONE,
				re, 
				subNodeToNodesRelation,
				Direction.OUTGOING);

		List<B> res = new ArrayList<B>();
		
		for (Node n:trav)
			res.add(this.newBean(n));
		
		return new SearchResults<B>(res, re.getCount());
	}
	
	private SearchResults<B> consumeIterator(int from, int size,
			Iterator<B> iter) {
		List<B> res = new ArrayList<B>();
		int count = 0;
		
		while (iter.hasNext()) {
			B bean = iter.next();
			
			if (count >= from && count < size + from)
				res.add(bean);
			
			count++;
		}
		
		return new SearchResults<B>(res, count);
	}
	
	private Node newNode() {
		Node node = graphDb.createNode();

		subNode.createRelationshipTo(node, subNodeToNodesRelation);
		
		TransactionStatusHolder.setWriteStatus();
		
		return node;
	}
	
	public Iterable<B> findRelatedNodes(String targetId, RelationshipType rel, Direction direction) {
		Node node = this.getNode(targetId);
		if(node == null)
			return Iterables.emptyIterable();
		
		Traverser trav = node.traverse(
				Order.DEPTH_FIRST, 
				StopEvaluator.DEPTH_ONE,
				ReturnableEvaluator.ALL_BUT_START_NODE, 
				rel,
				direction);
		
		return new BeanIterable(trav);
	}
	
	/**
	 * For backwards compat.
	 * 
	 * @param targetId
	 * @param rel
	 * @return
	 */
	public Iterable<B> findRelatedNodes(String targetId, RelationshipType rel) {
		return findRelatedNodes(targetId, rel, Direction.INCOMING);
	}
	
	/**
	 * For symmetry with findRelatedNodesOutgoing.
	 * 
	 * @param targetId
	 * @param rel
	 * @return
	 */
	public Iterable<B> findRelatedNodesIncoming(String targetId, RelationshipType rel) {
		return findRelatedNodes(targetId, rel, Direction.INCOMING);
	}
	
	/**
	 * FIX: ugly name, I don't know if you intended it this way, but I needed it to get all members of a group, since
	 * findRelatedNodes counts only INCOMING arcs.
	 * 
	 * @param targetId
	 * @param rel
	 * @return
	 */
	public Iterable<B> findRelatedNodesOutgoing(String targetId, RelationshipType rel) {
		return findRelatedNodes(targetId, rel, Direction.OUTGOING);
	}
	
	protected void createRelationship(String sourceId, String targetId, RelationshipType relation) {
		logger.debug("Creating relation of type " + relation + " from " + sourceId + " to " + targetId);
		
		Node sourceNode = this.getNode(sourceId);
		Node targetNode = this.getNode(targetId);
		
		if (sourceNode == null)
			throw new IllegalStateException("no such source node: " + sourceId);
		
		if (targetNode == null)
			throw new IllegalStateException("no such target node: " + targetId);
		
		sourceNode.createRelationshipTo(targetNode, relation);
		
		TransactionStatusHolder.setWriteStatus();
	}
	
	protected void removeRelationship(String sourceId, String targetId, RelationshipType relation) {
		logger.debug("Removing relation of type " + relation + " from " + sourceId + " to " + targetId);
		
		Node sourceNode = this.getNode(sourceId);
		
		// search for relations from group node to user nodes
		for (Relationship rel:sourceNode.getRelationships(relation, Direction.OUTGOING)) {
			if (rel.getEndNode().getProperty(Neo4jBean.ID).equals(targetId)) {
				logger.debug(relation + " found. Deleting it");
				
				rel.delete();
				
				TransactionStatusHolder.setWriteStatus();
				
				break;
			}
		}
	}
	
	protected Node getNode(String nodeId) {
		Node n = createdNodes.get().get(nodeId);
		
		if (n == null)
			n = index.getSingleNode(Neo4jBean.ID, nodeId);
		
		return n;
	}
	
	private final B newBean(Node node) {
		B b = this.createBean();
		
		b.setNode(node);
		
		return b;
	}
	
	@Resource
	public void setGraphDb(GraphDatabaseService graphDb) {
		this.graphDb = graphDb;
	}
	
	@Resource
	public void setIdBroker(IdBroker idBroker) {
		this.idBroker = idBroker;
	}
	
	@Resource
	public void setIndex(LuceneIndexService index) {
		this.index = index;
	}
	
	@Resource
	public void setFulltextIndex(LuceneFulltextIndexService fulltextIndex) {
		this.fulltextIndex = fulltextIndex;
	}

	/**
	 * Like BeanIterator but implements the UnaryFunction interface so that it can be used with MappedCollection
	 * to return an Iterable instead of a Iterator, which in turn can be used for java 5 foreach syntax.
	 * 
	 * <p>I have the feeling the the neo4j api is built on top of iterator (with reason), so this stuff perhaps is not
	 * needed, but let's keep it here it can be useful.</p>
	 * 
	 * @author marko
	 *
	 */
	class BeanTransformer implements UnaryFunction<B, Node> {
		@Override
		public B evaluate(Node arg) {
			return newBean(arg);
		}
	}
	
	/**
	 * Returns an iterable of beans from an iterable of nodes. The beans are constructed lazyly when consumed.
	 * 
	 * @param nodes
	 * @return
	 */
	public Iterable<B> asBeans(Iterable<Node> nodes) {
		return new MappedCollection<B, Node>(nodes, new BeanTransformer());
	}
	
	/**
	 * Returns an iterator which transforms lazyly nodes to beans.
	 * 
	 * @param nodes
	 * @return
	 */
	public Iterator<B> asBeans(Iterator<Node> nodes) {
		return new MappedCollection<B, Node>(new IterableIterator<Node>(nodes), new BeanTransformer()).iterator();
	}
	
	/**
	 * Wraps the Iterable<Node> returned by neo4j and converts it to Iterable<B>.
	 * 
	 * The IterableIterator<B> has the disadvantage that the iterator it returns
	 * can only be used once and will fail if you call iterable.iterator() a 
	 * second time.
	 * 
	 * @author antleb
	 *
	 */
	class BeanIterable implements Iterable<B> {
		
		private Iterable<Node> iterable = null;
		
		public BeanIterable(Iterable<Node> iterable) {
			this.iterable = iterable;
		}

		@Override
		public Iterator<B> iterator() {
			return new BeanIterator(iterable.iterator());
		}
	}
	
	/**
	 * IMHO we should use "asBeans" helper. Feel free to remove the deprecated annotations if you really don't like it.
	 * 
	 * @author marko
	 *
	 */
	class BeanIterator implements Iterator<B> {
		private Iterator<Node> nodes = null;
		
		public BeanIterator(Iterator<Node> iterator) {
			this.nodes = iterator;
		}

		@Override
		public boolean hasNext() {
			return nodes.hasNext();
		}

		@Override
		public B next() {
			return newBean(nodes.next());
		}

		@Override
		public void remove() {
			nodes.remove();
		}
	}
	
	class RangedEvaluator implements ReturnableEvaluator {
		private int from = 0;
		private int size = 0;
		private int count = 0;

		public RangedEvaluator(int from, int size) {
			this.from = from;
			this.size = size;
		}

		public boolean isReturnableNode(TraversalPosition arg0) {
			boolean ret = false;
			
			if (count >= from && count < size + from && arg0.depth() == 1)
				ret = true;
			
			if (arg0.depth() == 1)
				count++;
			
			return ret;
		}

		public int getCount() {
			return count;
		}
	}
}