package com.txmq.exo.transactionrouter;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;

import com.txmq.exo.messaging.ExoMessage;
import com.txmq.exo.core.ExoState;

/**
 * ExoTransactionRouter implements an annotation-based transaction routing 
 * scheme.  To implement, you annotate a transaction processing method with 
 * the ExoTransactionType value that the method handles.
 * 
 * ExoTransactionRouter is a singleton, and is managed by ExoPlatformLocator.
 * Application code doesn't need to instantiate ExoTransactionRouter.  
 * Application code can access the router through ExoPlatformLocator.
 * 
 * During initialization, Exo applications should call addPackage() for each
 * package that contains annotated processing methods.  ExoTransactionRouter
 * will scan the package for @ExoTransaction annotations and catalog those
 * methods by the transactiont type they process.
 * 
 * States that inherit from ExoState will automatically route transactions
 * that come into the handleTransaction() method with no additional code 
 * (assuming you remembered to call super.handleTransaction()).
 * 
 * Methods that implement transactions must use the following signature:
 * 
 * @ExoTransaction("sometransaction")
 * public void myTransactionHandler(ExoMessage message, ExoState state, boolean consensus)
 * 
 * TODO:  Add a means for transaction processors to return data which will
 * later be made available through an API to client applications.
 */
public class ExoTransactionRouter {
	
	/**
	 * Map of transaction type values to the methods that handle them.
	 */
	protected Map<String, Method> transactionMap;

	/**
	 * Methods have to be invoked on an instance of an object (unless
	 * we use static transaction handlers and that makes me feel dirty).
	 * 
	 * This map holds instances of each transaction processor class.
	 * An instance is automatically created if it doesn't exist.
	 * Transaction processor classes should be written as if they will
	 * only be instantiated once, and should be careful about any
	 * state they maintain.  Realize that Exo will probably only ever
	 * create one instance.
	 */
	protected Map<Class<?>, Object> transactionProcessors;
	
	/**
	 * No-op constructor.  ExoTransactionRouter will be instantiated by 
	 * ExoPlatformLocator and TransactionServer, and managed by the platform.
	 * 
	 * Applications should not create instances of ExoTransactionRouter.
	 * 
	 * @see com.txmq.exo.core.ExoPlatformLocator
	 */
	public ExoTransactionRouter() {
		this.transactionMap = new HashMap<String, Method>();
		this.transactionProcessors = new HashMap<Class<?>, Object>(); 
	}
	
	/**
	 * Scans a package, e.g. "com.txmq.exo.messaging.rest" for 
	 * @ExoTransaction annotations using reflection and sets up the
	 * internal mapping of transaction type to processing method.
	 */
	public ExoTransactionRouter addPackage(String transactionPackage) {
		/*
		Reflections reflections = new Reflections(new ConfigurationBuilder()
		     .setUrls(ClasspathHelper.forPackage(transactionPackage))
		     .setScanners(new MethodAnnotationsScanner())
		);*/
		Reflections reflections = new Reflections(transactionPackage, new MethodAnnotationsScanner());			
		
		Set<Method> methods = reflections.getMethodsAnnotatedWith(ExoTransaction.class);
		for (Method method : methods) {
			ExoTransaction annotation = method.getAnnotation(ExoTransaction.class);
			this.transactionMap.put(annotation.value(), method);
		}
		
		return this;
	}
	
	/**
	 * Routes an incoming transaction to its processor.  Application code 
	 * should not need to call this method directly.  It is invoked by 
	 * ExoState.handleTransaction() when transactions are received by the 
	 * Hashgraph state.
	 * 
	 * Internally, it looks for a method that handles the type of transaction
	 * in the message and an instance of the class that encloses that method.
	 * It will create the instance if it needs to.  Assuming it finds/creates
	 * what it needs, it invokes the method passing in the message and state.
	 */
	public Object routeTransaction(ExoMessage message, ExoState state, boolean consensus) throws ReflectiveOperationException { 
		if (this.transactionMap.containsKey(message.transactionType.getValue())) {
			Method method = this.transactionMap.get(message.transactionType.getValue());
			Class<?> processorClass = method.getDeclaringClass();			
			if (!this.transactionProcessors.containsKey(processorClass)) {
				Constructor<?> processorConstructor = processorClass.getConstructor();
				this.transactionProcessors.put(processorClass, processorConstructor.newInstance());				
			}
			
			Object transactionProcessor = this.transactionProcessors.get(processorClass);
			return method.invoke(transactionProcessor, message, state, consensus);
		} else {
			throw new IllegalArgumentException(
					"A handler for transaction type " + message.transactionType.getValue() + 
					" was not registered with the transaction router"
			);
		}
	}
}