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.Collection;
import java.util.HashMap;
import java.util.Map;

import com.hammurapi.common.Context;
import com.hammurapi.common.MutableContext;



/**
 * Creates converters from Context and MutableContext to interfaces which have only setters and getters (beans)
 * @author Pavel
 *
 */
public class ContextConverterFactory {
	
	private static final String SET = "set";

	private static final String GET = "get";

	/**
	 * Returns source object unchanged
	 */
	private static ConverterClosure<?,?> ZERO_CONVERTER = new ConverterClosure<Object,Object>() {

		public Object convert(Object source) {			
			return source;
		}
		
	};
	
	/**
	 * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping.
	 */
	private static Map<Collection<Class<?>>, Object> converterMap = new HashMap<Collection<Class<?>>, Object>();
	
	/**
	 * Converts object to target interface using dynamic proxy.
	 * @author Pavel Vlasov
	 *
	 */
	private static class ProxyConverter<S, T> implements ConverterClosure<S, T> {

		/**
		 * 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<Method,Method> methodMap;
		
		private Class<?>[] targetInterfaces;

		private ClassLoader classLoader;
		
		public ProxyConverter(Class<T> targetInterface, Map<Method,Method> methodMap, ClassLoader classLoader) {
			this.methodMap = methodMap;
			this.targetInterfaces = new Class[] {targetInterface};
			this.classLoader = classLoader;
		}
		
		@SuppressWarnings("unchecked")
		public T convert(final S source) {
			
			InvocationHandler ih = new InvocationHandler() {

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

					if (method.getName().startsWith(GET)) {
						Object ret = sourceMethod.invoke(source, new Object[] {method.getName().substring(GET.length())});						
						return ConvertingService.convert(ret, method.getReturnType());						
					}
					
					return sourceMethod.invoke(source, new Object[] {method.getName().substring(SET.length()), args[0]});
				}
				
			};
			
			return (T) Proxy.newProxyInstance(classLoader, targetInterfaces, ih);
		}
		
	}

	/**
	 * @param sourceClass
	 * @param targetInterface
	 * @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.
	 */
	@SuppressWarnings("unchecked")
	public static <S, T> ConverterClosure<S, T> getConverter(Class<S> sourceClass, Class<T> targetInterface) {
		if (targetInterface.isAssignableFrom(sourceClass)) {
			return (ConverterClosure<S, T>) ZERO_CONVERTER;
		}
		
		Collection<Class<?>> key=new ArrayList<Class<?>>();
		key.add(sourceClass);
		key.add(targetInterface);
		synchronized (converterMap) {
			Object value = converterMap.get(key);
			if (Boolean.FALSE.equals(value)) {
				return null;
			}
			
			if (!Context.class.isAssignableFrom(sourceClass)) {
				converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
				return null;				
			}
			
			Method getter;
			try {
				getter = Context.class.getMethod("lookup", new Class[] {String.class});
			} catch (SecurityException e) {
				throw new ConversionException(e);
			} catch (NoSuchMethodException e) {
				throw new ConversionException(e);
			}
			
			Method setter;
			try {
				setter = MutableContext.class.isAssignableFrom(sourceClass) ? MutableContext.class.getMethod("bind", new Class[] {String.class, Object.class}) : null;
			} catch (SecurityException e) {
				throw new ConversionException(e);
			} catch (NoSuchMethodException e) {
				throw new ConversionException(e);
			}			
						
			if (value==null) {
				Method[] targetMethods = targetInterface.getMethods();
				
				Map<Method, Method> methodMap = new HashMap<Method, Method>();
				
				for (int i=0, l=targetMethods.length; i<l; ++i) {
					if (Object.class.equals(targetMethods[i].getDeclaringClass())) { 
						continue;
					}
										
					Method targetMethod = targetMethods[i];
					if (targetMethod.getName().startsWith("get") 
							&& targetMethod.getParameterTypes().length==0) {
						methodMap.put(targetMethod, getter);
					} else if (targetMethod.getName().startsWith("set") 
							&& targetMethod.getParameterTypes().length==1
							&& setter!=null) {
						methodMap.put(targetMethod, setter);
					} else {
						converterMap.put(key, Boolean.FALSE); // 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<S, T>(targetInterface, methodMap.isEmpty() ? null : methodMap, cl);
				converterMap.put(key, value);
			}
			return (ConverterClosure<S, T>) value;
		}
	}

	/**
	 * @param cl1
	 * @param cl2
	 * @return Child classloader or null if classloaders are not related
	 */
	private 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;
	}		
}
