package com.hammurapi.convert;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Mixes interfaces from multiple objects into one proxy.
 * @author Pavel
 *
 */
public class Mixer {
	
	/**
	 * Mixes-in implementation of interfaces into class. I.e. it creates a proxy
	 * which implements all object's interfaces plus interfaces from parameters.
	 * E.g. class has methods compatible with interfaces, but doesn't implement 
	 * the interface itself. This is very similar to duck typing.
	 * This method doesn't check for method compatibility, it simply routes 
	 * interface methods to the object. It is useful when only a sub-set of interface
	 * methods is used by the calling code, so the code would work even if strict 
	 * conversion failed.
	 * @param object
	 * @param interfaces
	 * @return
	 */
	public static Object mixIn(final Object object, Class<?>... interfaces) {
		List<Class<?>> additional = new ArrayList<Class<?>>();
		for (int i = 0; interfaces!=null && i<interfaces.length; ++i) {
			if (!interfaces[i].isInstance(object)) {
				additional.add(interfaces[i]);
			}
		}
		
		if (additional.isEmpty()) {
			return object;
		}
		
		Class<?> objectClass = object.getClass();
		Class<?>[] objectInterfaces = DuckConverterFactory.getClassInterfaces(objectClass);
		Class<?>[] proxyInterfaces = new Class[objectInterfaces.length+additional.size()];
		ClassLoader classLoader = null;
		for (int i=0; i<objectInterfaces.length; ++i) {
			proxyInterfaces[i] = objectInterfaces[i];
			classLoader = DuckConverterFactory.getChildClassLoader(classLoader, objectInterfaces[i].getClassLoader());
		}
		
		final Map<Method, Method> methodMap = new HashMap<Method, Method>();		
		Iterator<Class<?>> it = additional.iterator();
		for (int i=objectInterfaces.length; it.hasNext(); ++i) {
			proxyInterfaces[i] = it.next();
			classLoader = DuckConverterFactory.getChildClassLoader(classLoader, proxyInterfaces[i].getClassLoader());
			DuckConverterFactory.duckMap(proxyInterfaces[i], objectClass, methodMap);
		}
		
		return Proxy.newProxyInstance(
				classLoader == null ? objectClass.getClassLoader() : classLoader, 
				proxyInterfaces,
				new FilterInvocationHandler() {

					public Object invoke(
							Object proxy, 
							Method method,
							Object[] args) throws Throwable {

						Method mappedMethod = methodMap.get(method);
						if (mappedMethod ==null) {
							return method.invoke(object, args);
						}
						
//						CompositeConverter converter = CompositeConverter.getDefaultConverter();
						Object[] convertedArgs;
						if (args==null) {
							convertedArgs = null;
						} else {
							Class<?>[] parameterTypes = mappedMethod.getParameterTypes();
							convertedArgs = new Object[args.length];
							for (int i=0; i<convertedArgs.length; ++i) {
								convertedArgs[i] = ConvertingService.convert(args[i], parameterTypes[i]);
							}
						}
						return mappedMethod.invoke(object, args);
					}

					public Object getMaster() {
						return object;
					}
					
				});
	}
	
	/**
	 * Creates a proxy which routes invocations to master unless one of interceptors
	 * has a method with matching signature. Only interface methods are routed
	 * to interceptors.
	 * 
	 * Traditional approach to intercepting in Java is to create filters/wrappers.
	 * This approach might be problematic if the filter master instances might be of 
	 * different subclasses of the base filtered type and if the subclassed functionality
	 * must be propagated through the filter.
	 * 
	 * Returned proxy invokes only interceptor's method. It is responsiblity of the
	 * interceptor to call master's method if needed. 
	 * 
	 * Interceptor methods are matched to master methods based on name and parameter
	 * types equality (as in overriding). Return type and exception types are not
	 * taken in consideration.
	 * 
	 * @param master
	 * @param interceptors
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static Object addInterceptors(final Object master, Object... interceptors) {
		if (master == null) {
			return null;
		}
		
		// Array of collections.
		Collection<Method>[] interceptorMethods = new Collection[interceptors.length];
		for (int i=0; i<interceptors.length; ++i) {
			Class<? extends Object> interceptorClass = interceptors[i].getClass();
			Method[] iMethods = interceptorClass.getMethods();
			interceptorMethods[i] = new ArrayList<Method>();
			for (int m=0; m<iMethods.length; ++m) {
				Method interceptorMethod = iMethods[m];
				
				if (!interceptorMethod.getDeclaringClass().equals(Object.class)) {
					interceptorMethods[i].add(interceptorMethod);
				}
			}
		}
		
		class InterceptorEntry {
			Object object;
			Method method;
			
			InterceptorEntry(Object object, Method method) {
				super();
				this.object = object;
				this.method = method;
			}			
			
			Object invoke(Object[] args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
				return method.invoke(object, args);
			}
		}
		
		Class<?>[] interfaces = DuckConverterFactory.getClassInterfaces(master.getClass());
		final Map<Method, InterceptorEntry> interceptionMap = new HashMap<Method, InterceptorEntry>();
		for (int interfaceIdx=0; interfaceIdx<interfaces.length; ++interfaceIdx) {
			Method[] methods = interfaces[interfaceIdx].getMethods();
			Z: for (int methodIdx = 0; methodIdx < methods.length; ++methodIdx) {
				Method masterMethod = methods[methodIdx];
				if (!masterMethod.getDeclaringClass().equals(Object.class)) {
					for (int interceptorIdx =0; interceptorIdx<interceptors.length; ++interceptorIdx) {
						Iterator<?> interceptorMethodIterator = interceptorMethods[interceptorIdx].iterator();						
						Y: while (interceptorMethodIterator.hasNext()) {
							Method interceptorMethod = (Method) interceptorMethodIterator.next();
							if (masterMethod.getName().equals(interceptorMethod.getName())) {
								Class<?>[] masterParameterTypes = masterMethod.getParameterTypes();
								Class<?>[] interceptorParameterTypes = interceptorMethod.getParameterTypes();
								if (masterParameterTypes.length!=interceptorParameterTypes.length) {
									continue;
								}
								for (int prmIdx = 0; prmIdx<masterParameterTypes.length; ++prmIdx) {
									if (!masterParameterTypes[prmIdx].equals(interceptorParameterTypes[prmIdx])) {
										continue Y;
									}
								}
								
								System.out.println("~~~~~ Mapped method "+masterMethod+" to interceptor "+interceptorIdx);
								interceptionMap.put(masterMethod, new InterceptorEntry(interceptors[interceptorIdx], interceptorMethod));
								continue Z;
							}
						}
					}
				}
			}
		}
		
		return Proxy.newProxyInstance(
				Object.class.getClassLoader(), 
				interfaces,
				new FilterInvocationHandler() {

					public Object invoke(
							Object proxy, 
							Method method,
							Object[] args) throws Throwable {

						InterceptorEntry ie = interceptionMap.get(method);						
						return ie==null ? method.invoke(master, args) : ie.invoke(args);
					}

					public Object getMaster() {
						return master;
					}
					
				});
				
	}
		
	/**
	 * Creates a proxy which implements given interfaces and routes invocations
	 * to appropriate object. If object is not found for a particular interface,
	 * then invocation is routed to the first object.
	 * @param object
	 * @param interfaces
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static Object combine(final Object[] objects, Class<?>[] interfaces) {
		if (objects==null || objects.length==0) {
			return null;
		}
		
		boolean firstDoesIt = true;
		for (int i=0; i<interfaces.length; ++i) {
			if (!interfaces[i].isInstance(objects[0])) {
				firstDoesIt = false;
				break;
			}
		}
		
		if (firstDoesIt) {
			return objects[0];
		}

		if (objects.length==1) {
			return mixIn(objects[0], interfaces);
		}
		
		List<Method> allInterfaceMethods = new ArrayList<Method>();
		for (int i=0; i<interfaces.length; ++i) {
			allInterfaceMethods.addAll(Arrays.asList(interfaces[i].getMethods()));
		}
		
		Method[] aim = allInterfaceMethods.toArray(new Method[allInterfaceMethods.size()]);
		
		final Map<Method, Method>[] methodMaps = new Map[objects.length];
		for (int i=0; i<objects.length; ++i) {
			methodMaps[i] = new HashMap<Method, Method>();
			DuckConverterFactory.duckMap(aim, objects[i].getClass().getMethods(), methodMaps[i]);
		}
		
		ClassLoader classLoader  = null;
		for (int i=0; i<objects.length; ++i) {
			classLoader = DuckConverterFactory.getChildClassLoader(classLoader, objects[i].getClass().getClassLoader());
		}
		for (int i=0; i<interfaces.length; ++i) {
			classLoader = DuckConverterFactory.getChildClassLoader(classLoader, interfaces[i].getClassLoader());
		}
		
		return Proxy.newProxyInstance(
				classLoader == null ? objects[0].getClass().getClassLoader() : classLoader, 
				interfaces, 
				new InvocationHandler() {

					public Object invoke(
							Object proxy, 
							Method method,
							Object[] args) throws Throwable {
						
						Class<?> declaringClass = method.getDeclaringClass();
						for (int i=0; i<objects.length; ++i) {							
							if (declaringClass.isInstance(objects[i])) {
								return method.invoke(objects[i], args);
							}
							Method mappedMethod = (Method) methodMaps[i].get(method);
							if (mappedMethod!=null) {
								return mappedMethod.invoke(objects[i], args);
							}
						}
						
						return method.invoke(objects[0], args);
					}
					
				});
	}
	
	/**
	 * Mixes interfaces from multiple objects into one proxy.
	 * It can be used to "mix-in" interface into another interface, e.g.
	 * mix-in application-specific interfaces into HttpServletRequest.
	 * Different interfaces can be mixed-in depending on context, which is 
	 * difficult to achieve with wrapping.
	 * @param objects
	 * @return
	 */
	public static Object mix(final Object... objects) {
		if (objects==null || objects.length==0) {
			return null;
		}
		
		if (objects.length==1) {
			return objects[0];
		}
		
		
		// Maps interface to implementation.
		final Map<Class<?>, Object> interfaceMap = new HashMap<Class<?>, Object>();
		List<Class<?>> interfaces = new ArrayList<Class<?>>();
		ClassLoader classLoader = null;
		boolean onlyFirstInterfaces = false;
		
		for (int i=0; i<objects.length; ++i) {
			Class<?> sourceClass = objects[i].getClass();
			classLoader=DuckConverterFactory.getChildClassLoader(classLoader, sourceClass.getClassLoader());
			Class<?>[] cia = DuckConverterFactory.getClassInterfaces(sourceClass);
			for (int j=0; j<cia.length; ++j) {
				Class<?> classInterface = cia[j];
				if (!interfaceMap.containsKey(classInterface)) {
					onlyFirstInterfaces = i==0;
					interfaceMap.put(classInterface, objects[i]);
					interfaces.add(classInterface);
				}
			}
		}

		// All interfaces are implemented by the first object, i.e. other objects add nothing.
		if (onlyFirstInterfaces) {
			return objects[0];
		}
		
		return Proxy.newProxyInstance(
				classLoader == null ? objects[0].getClass().getClassLoader() : classLoader,
						interfaces.toArray(new Class[interfaces.size()]), 
						new InvocationHandler() {

							public Object invoke(
									Object proxy, 
									Method method,
									Object[] args) throws Throwable {
								
								Object target = interfaceMap.get(method.getDeclaringClass());
								return method.invoke(target==null ? objects[0] : target, args);
							}
					
				});
	}
	
}
