package org.antforge;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;

/**
 * A proxy factory supports the rapid development of a transportation
 * layer based on RMI for given clean interfaces. The interface classes
 * do not need to be "cluttered" with specifics of the transportation
 * implementation. This means no more annoying appearances of classes
 * like java.rmi.Remote or java.rmi.RemotException in your interfaces.
 * <br>
 * Interfaces can explicitly be marked as "remote interfaces". All methods
 * exposed by such an interface can be invoked from a remote site. All
 * parameters which are subclasses of such interfaces, will be passed
 * by-reference and will not be serialized. This is just the same behavior
 * as if the interface would extend java.rmi.Remote in the RMI world.
 * The actual remote interfaces are generated on-demand at runtime.
 * <br>
 * A proxy is a transparent object implementing both, the local and the
 * generated remote interface. Both interfaces are usable:
 * <ol>
 * <li>Cast the proxy to java.rmi.Remote and use it with any naming or
 *     registry service available to the RMI world.</li>
 * <li>Cast the proxy to your local interface and don't bother whether
 *    it actually targets a local or a remote site.</li>
 * </ol>
 * The decision how an invocation actually is dispatched can be solely
 * based on whether the target object of a proxy is a remote or a local
 * one. This decision is hidden inside the transportation layer.
 * <br>
 * This is my attempt to show how RMI should have been designed in the
 * first place. Please feel free to comment, improve, ignore or flame.
 * 
 * @author Michael Starzinger
 * @version 0.1-20100406
 */
public class ProxyFactory
{
	private static ProxyFactory singleton;

	private List<Class<?>> markedInterfaces;
	private Map<Class<?>, String> generatedNames;
	private Map<Class<?>, Class<?>> generatedInterfaces;
	private Map<Object, Object> createdProxies;

	private ProxyFactory() {
		// XXX We probably should use weak references here!
		super();
		this.markedInterfaces = new ArrayList<Class<?>>();
		this.generatedNames = new HashMap<Class<?>, String>();
		this.generatedInterfaces = new HashMap<Class<?>, Class<?>>();
		this.createdProxies = new HashMap<Object, Object>();
	}

	/**
	 * At the moment we only allow one proxy factory per VM because classes
	 * are generated during runtime and we have to ensure they are unique.
	 * To resolve this issue properly, move interface generation into a
	 * singleton and allow multiple proxy factories. Also remember that
	 * this factory is not at all thread-safe.
	 * @return The proxy factory singleton.
	 */
	public static ProxyFactory getFactory() {
		if (singleton == null)
			singleton = new ProxyFactory();
		return singleton;
	}

	/**
	 * Marks the given interface as being a "remote interface".
	 * @param localInterface A plain-old interface class.
	 */
	public void markAsRemote(Class<?> localInterface) {
		// Check if given class really is an interface.
		if (!localInterface.isInterface())
			throw new ProxyException("Given class is not an interface.");

		// Check if given interface is a remote interface.
		if (Remote.class.isAssignableFrom(localInterface))
			throw new ProxyException("Given interface already is a remote interface.");

		// Check if given interface is already marked as remote.
		if (markedInterfaces.contains(localInterface))
			return;

		// Mark given interface as remote.
		markedInterfaces.add(localInterface);
	}

	private String getRemoteInterfaceName(Class<?> localInterface) {
		// Check if name for remote interface already generated.
		if (generatedNames.containsKey(localInterface))
			return generatedNames.get(localInterface);
		
		// Otherwise trigger interface generation.
		Class<?> remoteInterface = getRemoteInterface(localInterface);
		return remoteInterface.getName();
	}

	private Class<?> getRemoteInterface(Class<?> localInterface) {
		// Check if remote interface already generated.
		if (generatedInterfaces.containsKey(localInterface))
			return generatedInterfaces.get(localInterface);

		// Generate a new remote interface.
		Class<?> remoteInterface;
		try {
			remoteInterface = generateRemoteInterface(localInterface);
		} catch (NotFoundException e) {
			throw new ProxyException(e);
		} catch (CannotCompileException e) {
			e.printStackTrace();
			throw new ProxyException(e);
		}
		
		// Remember the newly generated remote interface and return it.
		generatedInterfaces.put(localInterface, remoteInterface);
		return remoteInterface;
	}

	private Class<?> getOriginalInterface(Class<?> remoteInterface) {
		// Check if remote interface was generated before.
		for (Class<?> localInterface : generatedInterfaces.keySet())
			if (generatedInterfaces.get(localInterface).equals(remoteInterface))
				return localInterface;

		// If we did not generate the remote interface, something is wrong.
		throw new ProxyException("Unable to find original interface type.");
	}

	private int uniqueCounter = 1;
	private String generateUniqueName() {
		return "$Dynamic" + uniqueCounter++;
	}

	private Class<?> generateRemoteInterface(Class<?> localInterface) throws NotFoundException, CannotCompileException {
		ClassPool pool = ClassPool.getDefault();
		
		// Get all the RMI specific classes.
		CtClass ccRemote = pool.get("java.rmi.Remote");
		CtClass ccRemoteException = pool.get("java.rmi.RemoteException");

		// Generate a new unique name for the remote interface and remember it
		// separately to prevent infinite loops.
		String name = generateUniqueName();
		generatedNames.put(localInterface, name);

		// Create a remote interface which is a subclass of java.rmi.Remote just
		// like RMI wants.
		CtClass cc = pool.makeInterface(name, ccRemote);
		
		// Iterate over all methods of the interface.
		for (Method method : localInterface.getDeclaredMethods()) {

			// Replace all parameter types which are subclasses of an interface
			// marked as being remote by their generated counterparts so that they
			// can be passed by reference.
			// XXX Subclass relationships are currently ignored.
			ArrayList<CtClass> parameters = new ArrayList<CtClass>();
			for (Class<?> parameterType : method.getParameterTypes()) {
				if (markedInterfaces.contains(parameterType))
					parameters.add(pool.get(getRemoteInterfaceName(parameterType)));
				else
					parameters.add(pool.get(parameterType.getName()));
			}
			
			// Create a new method with the above parameters and make the method throw
			// a java.rmi.RemoteException just like RMI wants.
			CtMethod cm = CtNewMethod.abstractMethod(
					pool.get(method.getReturnType().getName()),
					method.getName(),
					parameters.toArray(new CtClass[0]),
					new CtClass[] { ccRemoteException },
					cc);
			
			// Add method to the newly created interface.
			cc.addMethod(cm);
		}

		// Actually create and load the generated remote interface.
		Class<?> remoteInterface = cc.toClass();

		//System.out.println("GENERATED REMOTE INTERFACE");
		//System.out.println("  LOCAL-TYPE: " + localInterface);
		//System.out.println("  REMOTE-TYPE: " + remoteInterface);

		return remoteInterface;
	}

	/**
	 * Returns a transparent proxy object implementing both, the local and
	 * the generated remote interface.
	 * @param object The target object which will receive invocations.
	 * @param localInterface A plain-old interface class which has been
	 * marked as being remote before.
	 * @return The transparent proxy object.
	 */
	public Object getProxy(Object object, Class<?> localInterface) {
		// Check if there already is a proxy for the given object.
		if (createdProxies.containsKey(object))
			return createdProxies.get(object);

		// Check if the target object itself already is a proxy.
		if (createdProxies.containsValue(object))
			throw new ProxyException("Given object already is a transparent proxy.");

		// Check if the given interface is marked as remote.
		if (!markedInterfaces.contains(localInterface))
			throw new ProxyException("Given interface is not marked as remote.");

		// The given object must either implement the local or the
		// generated remote interface.
		Class<?> remoteInterface = getRemoteInterface(localInterface);
		if (!localInterface.isInstance(object) && !remoteInterface.isInstance(object))
			throw new ProxyException("Given object does not implement the interface.");

		// Create a new proxy implementing both of the given interfaces.
		// XXX Users might want to proxify several interfaces at once.
		Object proxy = Proxy.newProxyInstance(
				object.getClass().getClassLoader(),
				new Class<?>[] { localInterface, remoteInterface },
				new ProxyInvocationHandler(object));

		//System.out.println("GENERATED PROXY FOR " + object.getClass());
		//System.out.println("  LOCAL-TYPE: " + localInterface);
		//System.out.println("  REMOTE-TYPE: " + remoteInterface);

		// Export the created proxy as a remote object. This is necessary
		// because proxies will be transported by-reference and thus need
		// to be reachable from a remote site.
		try {
			UnicastRemoteObject.exportObject((Remote) proxy, 0);
		} catch (RemoteException e) {
			throw new ProxyException("Unable to export created proxy");
		}

		// Remember the proxy and return it.
		createdProxies.put(object, proxy);
		return proxy;
	}

	/**
	 * All invocations received by a proxy will be dispatched by this
	 * invocation handler.
	 */
	private class ProxyInvocationHandler implements InvocationHandler {
		private final Object target;

		public ProxyInvocationHandler(Object target) {
			super();
			this.target = target;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			Class<?>[] parameterTypes = method.getParameterTypes();

			// We need to assemble a new list of parameter types and
			// arguments for the target object.
			List<Object> targetArguments = new ArrayList<Object>();
			List<Class<?>> targetParameterTypes = new ArrayList<Class<?>>();

			// Map parameter types and arguments for target method by
			// iterating over all parameters.
			for (int i = 0; i < parameterTypes.length; i++) {
				Object argument = args[i];
				Class<?> parameterType = parameterTypes[i];

				// If the target is a remote object and the parameter is a
				// subclass of an interface marked as remote, we need to map
				// to the generated remote interface.
				// XXX Subclass relationships are currently ignored.
				if (Remote.class.isInstance(target) && markedInterfaces.contains(parameterType)) {
					targetArguments.add(getProxy(argument, parameterType));
					targetParameterTypes.add(getRemoteInterface(parameterType));
					continue;
				}

				// If the target is a local object and the parameter is a
				// generated remote interface, we need to map back to the
				// original local interface.
				if (!Remote.class.isInstance(target) && generatedInterfaces.containsValue(parameterType)) {
					Class<?> localInterface = getOriginalInterface(parameterType);
					targetArguments.add(getProxy(argument, localInterface));
					targetParameterTypes.add(localInterface);
					continue;
				}

				// The default behavior is to just pass the argument without
				// any mapping.
				targetArguments.add(argument);
				targetParameterTypes.add(parameterType);
			}

			//System.out.println("PERFORMING INVOCATION INSIDE PROXY");
			//System.out.println("  INTERFACE-METHOD: " + method);

			// Find the target method.
			Method targetMethod = target.getClass().getMethod(
					method.getName(),
					targetParameterTypes.toArray(new Class<?>[0]));

			//System.out.println("  TARGET-METHOD: " + targetMethod);

			// Actually invoke the target method.
			// XXX We probably need to proxify the returned object.
			Object result = targetMethod.invoke(target, targetArguments.toArray());
			return result;
		}
	}

	/**
	 * All errors happening inside the transport layer will be passed out
	 * using this exception type. Since it is an unchecked exception we
	 * ensure "clutter-free" code.
	 */
	private static class ProxyException extends RuntimeException {
		private static final long serialVersionUID = 1L;

		public ProxyException(String message) {
			super(message);
		}

		public ProxyException(Throwable cause) {
			super(cause);
		}
	}
}

