package omq.server.remote.request;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import omq.Remote;
import omq.common.event.EventListener;
import omq.common.event.EventTrigger;
import omq.common.message.response.CollectionResponse;
import omq.common.message.response.DefaultResponse;
import omq.common.message.response.ExceptionResponse;
import omq.common.message.response.ProxyResponse;
import omq.common.message.response.Response;


import com.rabbitmq.client.Channel;

/**
 * 
 * @author Sergi Toda <sergi.toda@estudiants.urv.cat>
 * 
 */
public abstract class RemoteObject implements Remote {

	private static final long serialVersionUID = -1778953938739846450L;

	private String UID;

	private static final Map<String, Class<?>> primitiveClasses = new HashMap<String, Class<?>>();

	static {
		primitiveClasses.put("byte", Byte.class);
		primitiveClasses.put("short", Short.class);
		primitiveClasses.put("char", Character.class);
		primitiveClasses.put("int", Integer.class);
		primitiveClasses.put("long", Long.class);
		primitiveClasses.put("float", Float.class);
		primitiveClasses.put("double", Double.class);
	}

	public RemoteObject(String UID) throws Exception {
		this.UID = UID;
		registerToRequestListener();
		declareEventTopic();
	}

	public RemoteObject() throws Exception {
		this.UID = java.util.UUID.randomUUID().toString();
		System.out.println("New Object its UID is " + this.UID);
		registerToRequestListener();
		declareEventTopic();
	}

	/**
	 * Registers this object in the RequestListener
	 * 
	 * @throws Exception
	 */
	private void registerToRequestListener() throws Exception {
		RequestListener rListener = RequestListener.getRequestListener();
		rListener.addObj(this);
	}

	private void declareEventTopic() throws Exception {
		RequestListener rListener = RequestListener.getRequestListener();
		Channel channel = rListener.getChannel();
		channel.exchangeDeclare(UID, "fanout");
		channel.close();
	}

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

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

		Object[] arguments = new Object[args.size()];

		for (int i = 0; i < args.size(); i++) {
			Object arg = args.get(i);
			if (arg instanceof Remote) {
				arg = RequestListener.getRequestListener().getObj(((Remote) arg).getRef());
			}
			arguments[i] = arg;
		}

		// Get the specific method identified by methodName and its arguments
		Method method = loadMethod(methodName, args);

		try {
			Object result = method.invoke(this, arguments);
			// TODO see if a result is a collection and if it has some remote
			// objects
			if (result instanceof Remote) {
				resp = getProxyResponse((Remote) result);
			} else if (result instanceof Collection<?>) {
				Collection<?> collection = (Collection<?>) result;
				boolean containsRemote = false;
				for (Object o : collection) {
					if (o instanceof Remote) {
						containsRemote = true;
						break;
					}
				}
				if (containsRemote) {
					Collection<Response> responses = new ArrayList<Response>();
					for (Object o : collection) {
						if (o instanceof Remote) {
							responses.add(getProxyResponse((Remote) o));
						} else {
							responses.add(new DefaultResponse(this.getRef(), o));
						}
					}
					String collectionType = collection.getClass().getCanonicalName();
					resp = new CollectionResponse(this.getRef(), collectionType, responses);
				} else {
					resp = new DefaultResponse(this.getRef(), result);
				}
			} else {
				resp = new DefaultResponse(this.getRef(), result);
			}
		} catch (InvocationTargetException e) {
			resp = new ExceptionResponse(this.getRef(), e.getTargetException());
		}

		return resp;
	}

	private Method loadMethod(String methodName, Vector<Object> args) throws NoSuchMethodException {
		Method m = null;

		// Obtain the class reference
		Class<?> clazz = this.getClass();
		Class<?>[] argArray = new Class<?>[args.size()];

		for (int i = 0; i < args.size(); i++) {
			argArray[i] = args.get(i).getClass();
		}

		try {
			m = clazz.getMethod(methodName, argArray);
		} catch (NoSuchMethodException nsm) {
			m = loadMethodWithPrimitives(methodName, argArray);
		}
		return m;
	}

	private Method loadMethodWithPrimitives(String methodName, Class<?>[] argArray) throws NoSuchMethodException {
		Method[] methods = this.getClass().getMethods();
		int length = argArray.length;

		for (Method method : methods) {
			String name = method.getName();
			int argsLength = method.getParameterTypes().length;

			if (name.equals(methodName) && length == argsLength) {
				// This array can have primitive types inside
				Class<?>[] params = method.getParameterTypes();

				boolean found = true;

				for (int i = 0; i < length; i++) {
					if (params[i].isPrimitive()) {
						Class<?> paramWrapper = primitiveClasses.get(params[i].getName());

						if (!paramWrapper.equals(argArray[i])) {
							found = false;
							break;
						}
					}
				}
				if (found) {
					return method;
				}
			}
		}
		throw new NoSuchMethodException(methodName);
	}

	private Response getProxyResponse(Remote r) {
		String reference = r.getRef();
		String remoteInterface = r.getClass().getInterfaces()[0].getCanonicalName();
		return new ProxyResponse(this.getRef(), reference, remoteInterface);
	}

	@Override
	public void notify(Object obj) throws Exception {
		EventTrigger.publish(UID, obj);
	}

	public void addListener(EventListener eventListener) throws Exception {

	}

	public void removeListener(EventListener eventListener) throws Exception {

	}

	public Collection<EventListener> getListeners() throws Exception {
		return null;
	}

}
