package omq.client.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;

import omq.Remote;
import omq.client.annotation.AsyncMethod;
import omq.client.annotation.RemoteInterface;
import omq.client.annotation.SyncMethod;
import omq.client.remote.response.ResponseListener;
import omq.common.event.EventDispatcher;
import omq.common.event.EventListener;
import omq.common.message.request.AsyncRequest;
import omq.common.message.request.Request;
import omq.common.message.request.SyncRequest;
import omq.common.message.response.Response;
import omq.common.util.ParameterQueue;
import omq.exception.NoContainsInstanceException;


import com.rabbitmq.client.Channel;

/**
 * EvoProxy class. This class inherits from InvocationHandler and gives you a
 * proxy with a server using an environment
 * 
 * @author Sergi Toda <sergi.toda@estudiants.urv.cat>
 * 
 */
public class EvoProxy implements InvocationHandler, Remote {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private static Map<String, Object> proxies = new Hashtable<String, Object>();

	private String objUid;
	private transient ResponseListener rListener;
	private transient EventDispatcher dispatcher;
	private transient Channel channel;
	private transient Properties env;
	private transient Map<String, Response> results;
	private transient Map<Method, CallType> methodTypes;
	private transient Map<String, EventListener> listeners;

	/**
	 * EvoProxy Constructor.
	 * 
	 * This constructor uses an objUid to know which object will call. It also
	 * uses Properties to set where to send the messages
	 * 
	 * @param objUid
	 *            The objUid represents the unique identifier of a remote object
	 * @param env
	 *            The environment is used to know where to send the messages
	 * @throws Exception
	 */
	public EvoProxy(String objUid, Properties env) throws Exception {
		this.objUid = objUid;
		this.rListener = ResponseListener.getRequestListener();
		this.dispatcher = EventDispatcher.getDispatcher();
		this.channel = rListener.getChannel();
		this.env = env;

		listeners = new HashMap<String, EventListener>();

		// Without the remote object's class, we cannot know which methods have
		// annotations. For this reason this map will be void.
		methodTypes = new HashMap<Method, CallType>();

		// Create a new hashmap and registry it in rListener.
		results = new HashMap<String, Response>();
		rListener.registerProxy(this);
	}

	/**
	 * EvoProxy Constructor.
	 * 
	 * This constructor uses an objUid to know which object will call. It also
	 * uses Properties to set where to send the messages
	 * 
	 * @param objUid
	 *            The objUid represents the unique identifier of a remote object
	 * @param clazz
	 *            It represents the real class of the remote object. With this
	 *            class the system can know the remoteInterface used and it can
	 *            also see which annotations are used
	 * @param env
	 *            The environment is used to know where to send the messages
	 * @throws Exception
	 */
	public EvoProxy(String objUid, Class<?> clazz, Properties env) throws Exception {
		this.objUid = objUid;
		this.rListener = ResponseListener.getRequestListener();
		this.dispatcher = EventDispatcher.getDispatcher();
		this.channel = rListener.getChannel();
		this.env = env;

		listeners = new HashMap<String, EventListener>();

		// Get annotated Methods
		methodTypes = getAnnotatedMethods(clazz);

		// Create a new hashmap and registry it in rListener
		results = new HashMap<String, Response>();
		rListener.registerProxy(this);
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
		// Local methods only
		String methodName = method.getName();

		// The local methods will be invoked here
		if (method.getDeclaringClass().equals(Remote.class)) {
			if (methodName.equals("getRef")) {
				return getRef();
			} else if (methodName.equals("addListener")) {
				addListener((EventListener) arguments[0]);
				return null;
			} else if (methodName.equals("removeListener")) {
				removeListener((EventListener) arguments[0]);
				return null;
			} else if (methodName.equals("getListeners")) {
				return getListeners();
			} 
			// else if (methodName.equals("notify")) {
			// // notify fanout
			// }
		}

		// If the method was not a local method, it'll be sent in a queue
		// Publish the method we want to invoke
		Request request = createRequest(method, arguments);

		return request.publishRequest(results, env, channel);
	}

	/**
	 * This method creates a new request using a method and it's arguments
	 * 
	 * @param method
	 *            Method to be called
	 * @param arguments
	 *            Arguments of this method
	 * @return request
	 */
	private Request createRequest(Method method, Object[] arguments) {
		String corrId = java.util.UUID.randomUUID().toString();
		String methodName = method.getName();
		Vector<Object> args = new Vector<Object>();

		if (arguments != null) {
			for (Object o : arguments) {
				args.add(o);
			}
		}

		// Since we need to know whether the method is async and if it has to
		// return using an annotation, we'll only check the AsyncMethod
		// annotation
		if (method.getAnnotation(AsyncMethod.class) == null) {
			int retries = 1;
			long timeout = ParameterQueue.DEFAULT_TIMEOUT;
			if (method.getAnnotation(SyncMethod.class) != null) {
				SyncMethod sync = method.getAnnotation(SyncMethod.class);
				retries = sync.retry();
				timeout = sync.timeout();
			}
			return new SyncRequest(this.objUid, corrId, methodName, args, timeout, retries);
		} else {
			String topic = method.getAnnotation(AsyncMethod.class).generateEvent();
			return new AsyncRequest(this.objUid, corrId, methodName, args, topic);
		}
	}

	/**
	 * This method searches annotations in a RemoteInterface. Every time it
	 * finds an annotation saves it in a Map.
	 * 
	 * @param clazz
	 * @return annotatedMethods
	 */
	private Map<Method, CallType> getAnnotatedMethods(Class<?> clazz) {
		Class<?>[] interfaces = clazz.getInterfaces();
		Map<Method, CallType> methodTypes = new HashMap<Method, CallType>();

		// We look for the dermi interface. Once it's found, we read its methods
		// and its annotations and we save them into a Map
		for (Class<?> inter : interfaces) {
			if (inter.getAnnotation(RemoteInterface.class) != null) {
				// Read the methods
				for (Method method : inter.getMethods()) {
					if (method.getAnnotation(AsyncMethod.class) != null) {
						methodTypes.put(method, CallType.ASYNC);
					} else if (method.getAnnotation(SyncMethod.class) != null) {
						methodTypes.put(method, CallType.SYNC);
					}
				}

				break;
			}
		}
		return methodTypes;
	}

	/**
	 * 
	 * @param reference
	 *            RemoteObject reference
	 * @return true if the proxy has been created before or false in the other
	 *         case
	 */
	public static boolean containsProxy(String reference) {
		return proxies.containsKey(reference);
	}

	/**
	 * 
	 * @param reference
	 *            RemoteObject reference
	 * @return a proxy instance
	 * @throws NoContainsInstanceException
	 */
	public static Object getInstance(String reference) throws NoContainsInstanceException {
		if (!containsProxy(reference)) {
			throw new NoContainsInstanceException(reference);
		}
		return proxies.get(reference);
	}

	/**
	 * Returns an instance of a proxy class for the specified interfaces that
	 * dispatches method invocations to the specified invocation handler. * @param
	 * loader
	 * 
	 * @param loader
	 *            the class loader to define the proxy class
	 * 
	 * @param interfaces
	 *            the list of interfaces for the proxy class to implement
	 * @param proxy
	 *            the invocation handler to dispatch method invocations to
	 * @return a proxy instance with the specified invocation handler of a proxy
	 *         class that is defined by the specified class loader and that
	 *         implements the specified interfaces
	 */
	public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, EvoProxy proxy) {
		if (proxies.containsKey(proxy.getRef())) {
			System.out.println("Proxy trobat");
			return proxies.get(proxy.getRef());
		}
		Object value = Proxy.newProxyInstance(loader, interfaces, proxy);
		proxies.put(proxy.getRef(), value);
		return value;
	}

	/**
	 * Gets the Map used internally to retreive the response of the server
	 * 
	 * @return a map with all the keys processed. Every key is a correlation id
	 *         of a method invoked remotely
	 */
	public Map<String, Response> getResults() {
		return results;
	}

	/**
	 * Local method that enables to addListeners to this Proxy
	 */
	public void addListener(EventListener eventListener) throws Exception {
		if (eventListener.getTopic() == null) {
			eventListener.setTopic(objUid);
		}
		listeners.put(eventListener.getTopic(), eventListener);
		dispatcher.addListener(eventListener);
	}

	/**
	 * Local method to remove listeners of this proxy
	 */
	public void removeListener(EventListener eventListener) throws Exception {
		listeners.remove(eventListener.getTopic());
		dispatcher.removeListener(eventListener);
	}

	@Override
	public String getRef() {
		return objUid;
	}

	@Override
	public Response invokeMethod(String methodName, Vector<Object> args) throws Exception {
		return null;
	}

	@Override
	public Collection<EventListener> getListeners() throws Exception {
		return listeners.values();
	}

	@Override
	public void notify(Object obj) {

	}

}
