package com.hammurapi.convert;

import java.lang.reflect.InvocationHandler;
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.HashSet;
import java.util.Map;
import java.util.Set;

import com.hammurapi.common.ClassHierarchyVisitable;
import com.hammurapi.common.Visitor;



/**
 * Creates converters which use "duck" typing.
 * @author Pavel
 *
 */
public class DuckConverterFactory {
	
	/**
	 * Returns source object unchanged
	 */
	private static ConverterClosure ZERO_CONVERTER = new ConverterClosure() {

		public Object convert(Object source) {			
			return source;
		}
		
	};
	
	/**
	 * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping.
	 */
	private static Map<Collection, Object> converterMap = new HashMap<Collection, Object>();
	
	private static class ProxyConverter implements ConverterClosure {

		/**
		 * Maps target methods to source methods. Unmapped methods
		 * are invoked directly (e.g. equals() or if method in both source and target belongs
		 * to the same interface (partial overlap)).
		 */
		private Map methodMap;
		
		private Class[] targetInterfaces;

		private ClassLoader classLoader;
		
		public ProxyConverter(Class targetInterface, Map methodMap, ClassLoader classLoader) {
			this.methodMap = methodMap;
			this.targetInterfaces = new Class[] {targetInterface};
			this.classLoader = classLoader;
		}
		
		public Object convert(final Object source) {
			
			InvocationHandler ih = new FilterInvocationHandler() {

				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
					Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method));
					return sourceMethod==null ? method.invoke(source, args) : sourceMethod.invoke(source, args);
				}

				public Object getMaster() {
					return source;
				}
				
			};
			
			return Proxy.newProxyInstance(classLoader, targetInterfaces, ih);
		}
		
	}

	/**
	 * @param sourceClass
	 * @param targetInterface
	 * @param lenient If true converter is created even not all interface methods could be mapped to class methods.
	 * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible.
	 * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored.
	 */
	public static ConverterClosure getConverter(Class sourceClass, Class targetInterface, boolean lenient) {
		if (targetInterface.isAssignableFrom(sourceClass)) {
			return ZERO_CONVERTER;
		}
		
		Collection key=new ArrayList();
		key.add(sourceClass);
		key.add(targetInterface);
		key.add(lenient ? Boolean.TRUE : Boolean.FALSE);
		synchronized (converterMap) {
			Object value = converterMap.get(key);
			if (Boolean.FALSE.equals(value)) {
				return null;
			}
			
			if (value==null) {
				Method[] targetMethods = targetInterface.getMethods();
				Method[] sourceMethods = sourceClass.getMethods();
				Map<Method, Method> methodMap = new HashMap<Method, Method>();
				
				if (!(duckMap(targetMethods, sourceMethods, methodMap) || lenient)) {
					converterMap.put(key, Boolean.FALSE); // Strict mapping - to indicate that we tried and failed.
					return null;
				}
				
				ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader());
				if (cl==null) {
					converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
					return null;					
				}
				
				value = new ProxyConverter(targetInterface, methodMap.isEmpty() ? null : methodMap, cl);
				converterMap.put(key, value);
			}
			return (ConverterClosure) value;
		}
	}

	/**
	 * Duck maps source (interface methods) to compatible target (class) methods. 
	 * @param interfaceMethods Interface methods.
	 * @param classMethods Class methods
	 * @param methodMap Method map (interface to class methods).
	 * @return true if all source methods have been mapped.
	 */
	public static boolean duckMap(Class theInterface, Class theClass, Map<Method, Method> methodMap) {
		return duckMap(theInterface.getMethods(), theClass.getMethods(), methodMap);
	}
	
	/**
	 * Duck maps source (interface methods) to compatible target (class) methods. 
	 * Mapped interface entries are set to null.
	 * @param interfaceMethods Interface methods.
	 * @param classMethods Class methods
	 * @param methodMap Method map (interface to class methods).
	 * @return true if all source methods have been mapped.
	 */
	public static boolean duckMap(Method[] interfaceMethods, Method[] classMethods, Map<Method, Method> methodMap) {
		boolean ret = true;
		
		Z: 
		for (int i=0, l=interfaceMethods.length; i<l; ++i) {
			if (interfaceMethods[i] == null) {
				continue; // Method was mapped in previous invocations.
			}
			
			if (Object.class.equals(interfaceMethods[i].getDeclaringClass())) { 
				continue;
			}					
			
			Method targetMethod = interfaceMethods[i];
			int candidateIndex=-1;
			Method candidateMethod = null;
			
			Y:
			for (int j=0, m=classMethods.length; j<m; ++j) {
				Method sourceMethod = classMethods[j];
				if (sourceMethod!=null) {
					if (targetMethod.equals(sourceMethod)) { // No mapping necessary
						continue Z;
					}
					
					if (targetMethod.getName().equals(sourceMethod.getName()) 
							&& targetMethod.getParameterTypes().length == sourceMethod.getParameterTypes().length) {
						// Check for compatibility
						
						// Return type shall "widen"
						if (!(targetMethod.getReturnType().isAssignableFrom(sourceMethod.getReturnType()) 
								|| void.class.equals(targetMethod.getReturnType()))) {
							continue;
						}
						
						Class[] targetParameterTypes = targetMethod.getParameterTypes();
						Class[] sourceParameterTypes = sourceMethod.getParameterTypes();
						for (int k=0, n=targetParameterTypes.length; k<n; ++k) {
							if (!sourceParameterTypes[k].isAssignableFrom(targetParameterTypes[k])) {
								continue Y;
							}
						}
						
						if (candidateMethod!=null) {
							
							Class[] candidateParameterTypes = candidateMethod.getParameterTypes();
							for (int k=0, n=sourceParameterTypes.length; k<n; ++k) {
								Integer oldAffinity = classAffinity(targetParameterTypes[k], candidateParameterTypes[k]);
								Integer newAffinity = classAffinity(targetParameterTypes[k], sourceParameterTypes[k]);
								if (oldAffinity!=null && (newAffinity==null || oldAffinity.intValue()<newAffinity.intValue())) {
									continue Y;
								}
							}
							
							Integer oldAffinity = classAffinity(candidateMethod.getReturnType(), targetMethod.getReturnType());
							Integer newAffinity = classAffinity(sourceMethod.getReturnType(), targetMethod.getReturnType());
							if (oldAffinity!=null && (newAffinity==null || oldAffinity.intValue()<newAffinity.intValue())) {
								continue;
							}
							
							classMethods[candidateIndex] = candidateMethod; // return method back
						}
						
						candidateMethod=sourceMethod;
						candidateIndex=j;
					}
				}
			}	
			
			ret = false;
			if (candidateMethod!=null) {
				methodMap.put(targetMethod, candidateMethod);
				interfaceMethods[i] = null;
			}
			
		}
		return ret;
	}

	/**
	 * Calculates how close is subclass to superclass in class hierarchy.
	 * @param subClass
	 * @param superClass
	 * @return affinity, or null if classes don't belong to the same class hierarchy.
	 */
	public static Integer classAffinity(Class subClass, final Class superClass) {
		if (superClass.equals(subClass)) {
			return 0;
		}
		
		if (superClass.isAssignableFrom(subClass)) {
			final int[] caffinity={0};
			new ClassHierarchyVisitable(subClass).accept(new Visitor() {
	
				public boolean visit(Object target) {
					if (target.equals(superClass)) {
						return false;
					} 
					
					caffinity[0]++;
					return true;
				}
				
			});
			return new Integer(caffinity[0]);
		}
		
		return null;		
	}
	
	/**
	 * @param cl1
	 * @param cl2
	 * @return Child classloader or null if classloaders are not related
	 */
	public static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) {
		
		if (cl1==null) {
			return cl2;
		}
		if (cl2==null) {
			return cl1;
		}
		for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
			if (cl2.equals(cl)) {
				return cl1;
			}
		}
		for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
			if (cl1.equals(cl)) {
				return cl2;
			}
		}
		return null;
	}
	
	private static Map<Class, Class[]> interfaceMap=new HashMap<Class, Class[]>();
	
	/**
	 * @param sourceClass
	 * @return all interfaces implemented by this class
	 */
	public static Class[] getClassInterfaces(Class sourceClass) {
		synchronized (interfaceMap) {
			Class[] ret=interfaceMap.get(sourceClass);
			if (ret==null) {
				Set<Class> set=new HashSet<Class>();
				getClassInterfaces(sourceClass, set);
				ret = new ArrayList<Class>(set).toArray(new Class[set.size()]);
				interfaceMap.put(sourceClass, ret);			
			}
			return ret;
		}
	}

	/**
	 * @param sourceClass
	 */
	private static void getClassInterfaces(Class sourceClass, Collection<Class> set) {
		if (sourceClass!=null) {
			Class[] interfaces = sourceClass.getInterfaces();
			for (int i=0; interfaces!=null && i<interfaces.length; i++) {
				getClassInterfaces(interfaces[i]);
			}
			getClassInterfaces(sourceClass.getSuperclass(), set);
			set.addAll(Arrays.asList(interfaces));
		}
	}
	
		
}
