package org.neo4j.tutor.search;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.StringTokenizer;

import org.neo4j.api.core.Direction;
import org.neo4j.api.core.EmbeddedNeo;
import org.neo4j.api.core.Node;
import org.neo4j.api.core.Relationship;
import org.neo4j.api.core.ReturnableEvaluator;
import org.neo4j.api.core.StopEvaluator;
import org.neo4j.api.core.TraversalPosition;
import org.neo4j.api.core.Traverser.Order;

public abstract class AbstractSearchEngine
{
	static final String LANGUAGE_ID = "lang_id";
	static final String KEYWORD_NAME = "name";
	
	private EmbeddedNeo neo;
	
	// language/Map<Keyword string,Keyword Node>
	private Map<String,Map<String,Node>> languages = 
		new HashMap<String,Map<String,Node>>();
	// language/language node
	private Map<String,Node> languageNodes = new HashMap<String,Node>();
	
	public abstract Iterable<Node> searchDocuments( String language, 
		String keyword );
	
	AbstractSearchEngine( EmbeddedNeo neo )
	{
		this.neo = neo;
		Node referenceNode = neo.getReferenceNode();
		for ( Relationship language : referenceNode.getRelationships( 
			MyRelationshipTypes.KEYWORD_LANGUAGE, Direction.OUTGOING ) )
		{
			addLanguage( language.getEndNode() );
		}
	}
	
	private void addLanguage( Node languageNode )
	{
		String language = (String ) languageNode.getProperty( LANGUAGE_ID );
		languageNodes.put( language, languageNode );
		Map<String,Node> keywords = languages.get( language ); 
		if ( keywords == null )
		{
			keywords = new HashMap<String,Node>();
			languages.put( language, keywords );
		}
		for ( Relationship keywordRelationship : languageNode.getRelationships( 
			MyRelationshipTypes.KEYWORD, Direction.OUTGOING ) )
		{
			Node keywordNode = keywordRelationship.getEndNode();
			keywords.put( (String) keywordNode.getProperty( KEYWORD_NAME ), 
				keywordNode );
		}
	}
	
	private boolean hasTranslation( Node keyword1, Node keyword2 )
	{
		for ( Relationship translation : keyword1.getRelationships( 
			MyRelationshipTypes.TRANSLATION ) )
		{
			if ( translation.getOtherNode( keyword1 ).equals(  keyword2 ) )
			{
				return true;
			}
		}
		return false;
	}
	
	public Node getKeywordNode( String language, String keyword )
	{
		Map<String,Node> keywords = languages.get( language );
		if ( keywords != null )
		{
			return keywords.get( keyword );
		}
		return null;
	}

	public Node createKeywordNode( String language, String keyword )
	{
		Node keywordNode = getKeywordNode( language, keyword );
		if ( keywordNode != null )
		{
			return keywordNode;
		}
		Node languageNode = createLanguageNodeIfNeeded( language );
		keywordNode = neo.createNode();
		keywordNode.setProperty( KEYWORD_NAME, keyword );
		keywordNode.setProperty( LANGUAGE_ID, language );
		languageNode.createRelationshipTo( keywordNode, 
			MyRelationshipTypes.KEYWORD );
		languages.get( language ).put( keyword, keywordNode );
		return keywordNode;
	}
	
	private Node createLanguageNodeIfNeeded( String language )
	{
		Node languageNode = languageNodes.get( language );
		if ( languageNode != null )
		{
			return languageNode;
		}
		languageNode = neo.createNode();
		languageNode.setProperty( LANGUAGE_ID, language );
		neo.getReferenceNode().createRelationshipTo( languageNode, 
			MyRelationshipTypes.KEYWORD_LANGUAGE );
		languageNodes.put(  language, languageNode );
		languages.put( language, new HashMap<String,Node>() );
		return languageNode;
	}
	
	public void injectTranslations( Properties translations )
	{
		for ( Object objKey : translations.keySet() )
		{
			StringTokenizer tok = new StringTokenizer( (String) objKey, "," );
			String firstLanguage = tok.nextToken().toLowerCase();
			String firstKeyword = tok.nextToken().toLowerCase();
			tok = new StringTokenizer( translations.getProperty( 
				(String) objKey ), "," );
			String secondLanguage = tok.nextToken().toLowerCase();
			String secondKeyword = tok.nextToken().toLowerCase();
			if ( firstLanguage.equals( secondLanguage ) )
			{
				throw new IllegalArgumentException( "Cannot translate " + 
					firstKeyword + " to " + secondKeyword + 
					", they both have language " + firstLanguage );
			}
			Node keyword1 = createKeywordNode( firstLanguage, firstKeyword );
			Node keyword2 = createKeywordNode( secondLanguage, secondKeyword );
			if ( !hasTranslation( keyword1, keyword2 ) )
			{
				keyword1.createRelationshipTo( keyword2, 
					MyRelationshipTypes.TRANSLATION );
			}
		}
	}
	
	public void clean()
	{
		Iterable<Node> nodes = neo.getReferenceNode().traverse( 
			Order.DEPTH_FIRST, 
			StopEvaluator.END_OF_NETWORK, new ReturnableEvaluator()
			{
				public boolean isReturnableNode( 
					TraversalPosition currentPosition )
				{
					Relationship last = 
						currentPosition.lastRelationshipTraversed();
					if ( last == null )
					{
						return false;
					}
					Node current = currentPosition.currentNode();
					for ( Relationship rel : current.getRelationships( 
						MyRelationshipTypes.HAS_KEYWORD ) )
					{
						rel.delete();
					}
					for ( Relationship rel : current.getRelationships( 
						MyRelationshipTypes.TRANSLATION ) )
					{
						rel.delete();
					}
					last.delete();
					return true;
				}
			
			}, MyRelationshipTypes.KEYWORD_LANGUAGE, Direction.OUTGOING, 
			MyRelationshipTypes.KEYWORD, Direction.OUTGOING ).getAllNodes();
		for ( Node node : nodes )
		{
			node.delete();
		}
	}
	
	public Node getRandomKeyword()
	{
		Node[] langs = getNodeArray( neo.getReferenceNode().traverse( 
			Order.BREADTH_FIRST, StopEvaluator.DEPTH_ONE, 
			ReturnableEvaluator.ALL_BUT_START_NODE, 
			MyRelationshipTypes.KEYWORD_LANGUAGE, Direction.OUTGOING 
			).getAllNodes() );
		if ( langs.length > 0 )
		{
			Random r = new Random( System.currentTimeMillis() );
			Node language = langs[r.nextInt( langs.length )];
			Node[] keywords = getNodeArray( language.traverse( 
				Order.BREADTH_FIRST, StopEvaluator.DEPTH_ONE, 
				ReturnableEvaluator.ALL_BUT_START_NODE, 
				MyRelationshipTypes.KEYWORD, Direction.OUTGOING 
				).getAllNodes() );
			if ( keywords.length > 0 )
			{
				return keywords[ r.nextInt( keywords.length ) ];
			}
		}
		return null;
	}

	private Node[] getNodeArray( Collection<Node> allNodes )
	{
		return allNodes.toArray( new Node[ allNodes.size() ] );
	}
}