package com.hammurapi.convert;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;

import com.hammurapi.common.Adaptable;
import com.hammurapi.common.Context;

public class ConvertingService {
	
	private interface ConvertersBucket<S, T> {
		
		T convert(S source, Context context);
	}
	
	/**
	 * Converter which delegates to convert() method.
	 */
	public static final Converter CONVERTER = new Converter() {

		@SuppressWarnings("unchecked")
		public Object convert(Object source, Class targetType, Context context) throws ConversionException {
			return ConvertingService.convert(source, targetType, context);
		}
		
	};			
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * Invocation of this method is equivalent to invocation of  
	 * <code>convert(source, targetType, false)</code>
	 * @param source Source object.
	 * @param targetType Target type.
	 * @return Source object converted to target type or null if no converter
	 * is available for source/targetType combination
	 * @throws ConversionException
	 */
	public static <S, T> T convert(S source, Class<T> targetType) throws ConversionException {
		return convert(source, targetType, null, false, null);
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * Invocation of this method is equivalent to invocation of  
	 * <code>convert(source, targetType, false)</code>
	 * @param source Source object.
	 * @param targetType Target type.
	 * @param context Conversion context.
	 * @return Source object converted to target type or null if no converter
	 * is available for source/targetType combination
	 * @throws ConversionException
	 */
	public static <S, T> T convert(S source, Class<T> targetType, Context context) throws ConversionException {
		return convert(source, targetType, context, false, null);
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * Invocation of this method is equivalent to invocation of  
	 * <code>convert(source, targetType, false)</code>
	 * @param source Source object.
	 * @param targetType Target type.
	 * @param classLoader Class loader, source of converters.
	 * @return Source object converted to target type or null if no converter
	 * is available for source/targetType combination
	 * @throws ConversionException
	 */
	public static <S, T> T convert(S source, Class<T> targetType, ClassLoader classLoader) throws ConversionException {
		return convert(source, targetType, null, false, classLoader);
	}
	
	private static class ConverterKey<S,T> {
		Class<S> sourceType;
		Class<T> targetType;
		
		public ConverterKey(Class<S> sourceType, Class<T> targetType) {
			super();
			this.sourceType = sourceType;
			this.targetType = targetType;
		}

		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result	+ ((sourceType == null) ? 0 : sourceType.hashCode());
			result = prime * result	+ ((targetType == null) ? 0 : targetType.hashCode());
			return result;
		}

		@SuppressWarnings("unchecked")
		public boolean equals(Object obj) {
			return 
				sourceType == ((ConverterKey<S, T>) obj).sourceType 
				&& targetType == ((ConverterKey<S, T>) obj).targetType; 
		}		
		
	}
	
	private static class ClassLoaderConvertersEntry {
		private static final String CONVERT = "convert";
		
		private ClassLoader classLoader;
		
		public ClassLoaderConvertersEntry(ClassLoader classLoader) {
			this.classLoader = classLoader;
		}
		
		private Converter master = new Converter() {

			@SuppressWarnings("unchecked")
			public Object convert(Object source, Class targetType, Context context) throws ConversionException {
				if (source==null) {
					return null;
				}
				
				if (targetType.isInstance(source)) {
					return source;
				}
				
				final ConvertersBucket<Object, Object> bucket = findConverter(source.getClass(), targetType);
				if (bucket!=null) {
					Object ret = bucket.convert(source, context);
					if (ret!=null) {
						return ret;
					}
				}
				
				for (ConverterPart part: parts) {
					Object ret = part.convert(source, targetType, context, this);
					if (ret!=null) {
						return ret;
					}
				}

				for (Converter subConverter: subConverters) {
					Object ret = subConverter.convert(source, targetType, context);
					if (ret!=null) {
						return ret;
					}
				}
				
				return null;
			}
			
		};

//		private Converter<?,?> chainingMaster = new Converter<?,?>() {
//
//			public Object convert(Object source, Class targetType, Context context) throws ConversionException {
//				if (source==null) {
//					return null;
//				}
//				
//				if (targetType.isInstance(source)) {
//					return source;
//				}
//				
//				ConvertersBucket bucket = findChainingConverter(source.getClass(), targetType);
//				return bucket==null ? null : bucket.convert(source, context);
//			}
//			
//		};

		Collection<AtomicConverter<? extends Object,? extends Object>> atomicConverters = new ArrayList<AtomicConverter<? extends Object,? extends Object>>();
		
		/**
		 * Map of resolved converters: Collection[source, target] -&gt; ConverterClosure. 
		 */
		Map<ConverterKey<?,?>, Object> converters = new HashMap<ConverterKey<?,?>, Object>(); 
		
		Set<Class<?>> introspectedTargetTypes = new HashSet<Class<?>>();

		Set<Class<?>> introspectedSourceTypes = new HashSet<Class<?>>();
		
		Collection<Converter> subConverters = new ArrayList<Converter>();
		Collection<ConverterPart> parts = new ArrayList<ConverterPart>();
		
		/**
		 * Map of resolved chaining converters: Collection[source, target] -&gt; ConverterClosure. 
		 */
//		Map chainingConverters = new HashMap();
		
		@SuppressWarnings("unchecked")
		synchronized <S, T> ConvertersBucket<S, T> findConverter(Class<S> sourceType, final Class<T> targetType) {
			ConverterKey<S, T> key = new ConverterKey(sourceType, targetType);
			Object ret = converters.get(key);
			if (Boolean.FALSE.equals(ret)) { // Indicates that previous converter resolution didn't yield results.
				return null;
			}
			
			if (ret == null) {
				if (!targetType.isInterface() && introspectedTargetTypes.add(targetType)) {
					atomicConverters.addAll(ReflectionConverter.discoverConstructorConverters(targetType));
				}
				
				class ConverterEntry implements Comparable<ConverterEntry> {
					public ConverterEntry(AtomicConverter<? extends Object,? extends Object> converter, int affinity, int position) {
						super();
						this.converter = converter;
						this.affinity = affinity;
						this.position = position;
					}
					AtomicConverter<? extends Object, ? extends Object> converter;
					int affinity;
					int position;
					public int compareTo(ConverterEntry obj) {
						int cret = affinity - ((ConverterEntry) obj).affinity;
						if (cret==0) {
							return position - ((ConverterEntry) obj).position;
						}
						return cret;
					}
				}
				
				final List<ConverterEntry> theConverters  = new ArrayList<ConverterEntry>();
				
				int position = 0;
				for (AtomicConverter<? extends Object,? extends Object> ac: atomicConverters) {
					if (ac.getSourceType().isAssignableFrom(sourceType) && targetType.isAssignableFrom(ac.getTargetType()) ) {
						Integer newSourceAffinity = DuckConverterFactory.classAffinity(sourceType, ac.getSourceType());
						Integer newTargetAffinity = DuckConverterFactory.classAffinity(ac.getTargetType(), targetType);
						if (newSourceAffinity==null || newTargetAffinity==null) {
							throw new IllegalArgumentException("Something is wrong - source and target affinities are null");
						}
						theConverters.add(new ConverterEntry(ac, newSourceAffinity.intValue()+newTargetAffinity.intValue(), ++position));
					}
				}
				if (theConverters.isEmpty() && parts.isEmpty() && subConverters.isEmpty()) {
					converters.put(key, Boolean.FALSE);
				} else {
					Collections.sort(theConverters);
					ret = new ConvertersBucket<S,T>() {

						public T convert(S source, Context context) {
							for (ConverterEntry ce: theConverters) {
								AtomicConverter<S, T> theConverter = (AtomicConverter<S, T>) ce.converter;
								T ret = (T) theConverter.convert(source, master, context, classLoader);
								if (ret!=null) {
									return ret;
								}																
							}
							
							for (ConverterPart part: parts) {
								Object ret = part.convert(source, targetType, context, master);
								if (ret!=null) {
									return (T) ret;
								}
							}

							for (Converter subConverter: subConverters) {
								Object ret = subConverter.convert(source, targetType, context);
								if (ret!=null) {
									return (T) ret;
								}
							}
														
							return null;
						}
						
					};
					converters.put(key, ret);
				}
			}
			
			return (ConvertersBucket<S, T>) ret;
		}

		synchronized <S, T> ConvertersBucket<S,T> findChainingConverter(Class<S> sourceType, Class<T> targetType) {
			ConvertersBucket<S,T> ret = findConverter(sourceType, targetType);
			if (ret!=null) {
				return ret;
			}
			
			// TODO - Resolve chain.
			return ret;
		}
		
		<S,T> void addAnnotationConverters(Class<S> source) {
			if (introspectedSourceTypes.add(source)) {
				Z: for (Method m: source.getMethods()) {						
					if (m.getAnnotation(ConverterMethod.class)!=null && !void.class.equals(m.getReturnType())) {
						for (Class<?> pt: m.getParameterTypes()) {
							if (!(Context.class.equals(pt) || Converter.class.equals(pt))) {
								continue Z;
							}
						}
						final Method converterMethod = m;
						@SuppressWarnings("unchecked")
						AtomicConverterBase<S, T> converter = new AtomicConverterBase<S, T>(source, (Class<T>) m.getReturnType()) {

							@Override
							public T convert(S source, Converter master, Context context, ClassLoader classLoader) {
								try {
									Class<?>[] pt = converterMethod.getParameterTypes();
									Object[] args = new Object[pt.length];
									for (int i=0; i<pt.length; ++i) {
										if (Context.class.equals(pt[i])) {
											args[i] = context;
										} else if (Converter.class.equals(pt[i])) {
											args[i] = master;
										}											
									}
									return (T) converterMethod.invoke(source, args);
								} catch (Exception e) {
									throw new ConversionException(e);
								}
							}
						};
						atomicConverters.add(converter);
					}
				}
			}
		}
		
		@SuppressWarnings("unchecked")
		void addConverters(final Object provider) {
			if (provider instanceof AtomicConverter) {
				atomicConverters.add((AtomicConverter<Object, Object>) provider);
			} else if (provider instanceof AtomicConvertersBundle) {
				atomicConverters.addAll(((AtomicConvertersBundle) provider).getConverters());
			} else if (provider instanceof Collection<?>) {
				for (Object obj: (Collection<?>) provider) {
					addConverters(obj);
				}
			} else if (provider instanceof Converter) {
				subConverters.add((Converter) provider);
			} else if (provider instanceof ConverterPart) {
				parts.add((ConverterPart) provider);
			} else { // Introspection
				Class pClass = provider.getClass();
				Method[] methods = pClass.getMethods();
				for (int i=0; i<methods.length; ++i) {
					final Method method = methods[i];
					if (CONVERT.equals(method.getName()) && !void.class.equals(method.getReturnType())) {
						Class[] pTypes = method.getParameterTypes();
						if (pTypes.length == 1) {
							atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {

								public Object convert(Object source, Converter master, Context context, ClassLoader classLoader) {
									try {
										return method.invoke(provider, new Object[] {source});
									} catch (IllegalAccessException e) {
										throw new ConversionException(e);
									} catch (InvocationTargetException e) {
										throw new ConversionException(e);
									}
								}
								
							});
						} else if (pTypes.length == 2 && Converter.class.equals(pTypes[1])) {
							atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {

								public Object convert(Object source, Converter master, Context context, ClassLoader classLoader) {
									try {
										return method.invoke(provider, new Object[] {source, master});
									} catch (IllegalAccessException e) {
										throw new ConversionException(e);
									} catch (InvocationTargetException e) {
										throw new ConversionException(e);
									}
								}
								
							});
							
						} else if (pTypes.length == 2 && Context.class.equals(pTypes[1])) {
							atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {

								public Object convert(Object source, Converter master, Context context, ClassLoader classLoader) {
									try {
										return method.invoke(provider, new Object[] {source, context});
									} catch (IllegalAccessException e) {
										throw new ConversionException(e);
									} catch (InvocationTargetException e) {
										throw new ConversionException(e);
									}
								}
								
							});
							
						} else if (pTypes.length == 3 && Converter.class.equals(pTypes[1]) && Context.class.equals(pTypes[2])) {
							atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {

								public Object convert(Object source, Converter master, Context context, ClassLoader classLoader) {
									try {
										return method.invoke(provider, new Object[] {source, master, context});
									} catch (IllegalAccessException e) {
										throw new ConversionException(e);
									} catch (InvocationTargetException e) {
										throw new ConversionException(e);
									}
								}
								
							});
							
						}
						
					}
				}
			}
		}
		
	}
	
	/**
	 * Map of resolved converters: ClassLoader -&gt; ClassLoaderConvertersEntry. 
	 */
	private static Map<ClassLoader, ClassLoaderConvertersEntry> classLoaderEntries = new HashMap<ClassLoader, ClassLoaderConvertersEntry>(); 		

	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * @param source Source object.
	 * @param targetType Target type.
	 * @param context Conversion context.
	 * @param classLoader Class loader.
	 * @return Source object converted to target type or null if no converter
	 * is available for source/targetType combination
	 * @throws ConversionException
	 */
	public static <S, T> T convert(S source, Class<T> targetType, Context context, ClassLoader classLoader) throws ConversionException {
		return convert(source, targetType, context, false, classLoader);
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * @param source Source object.
	 * @param targetType Target type.
	 * @param chain If true, the converter will attempt to build a chain of 
	 * conversions from source type to target type. 
	 * For example, StringBuffer doesn't have a constructor from Integer. If chain
	 * is <code>false</code> then this method will return <code>null</code>. If 
	 * <code>chain</code> is true, then the converter will convert Integer to String
	 * and then will create StringBuffer from it. If multiple conversion chains 
	 * are available, the shortest is chosen.
	 * @return Source object converted to target type or null if no converter
	 * is available for source/targetType combination
	 * @throws ConversionException
	 */
	@SuppressWarnings("unchecked")
	private static <S, T> T convert(S source, Class<T> targetType, Context context, boolean chain, ClassLoader classLoader) throws ConversionException {
		if (source==null) {
			return null;
		}
		
		if (targetType.isInstance(source)) {
			return (T) source;
		}
		
		if (source instanceof Adaptable) {
			T ret = ((Adaptable) source).getAdapter(targetType, context);
			if (ret!=null) {
				return ret;
			}
		}
		
		ConverterClosure<S, T> converter = (ConverterClosure<S, T>) getConverter(source.getClass(), targetType, context, chain, classLoader);
		
		if (converter!=null) {
			return converter.convert(source);
		}
		
		ConverterClosure<S, Adaptable> adaptableConverter = (ConverterClosure<S, Adaptable>) getConverter(source.getClass(), Adaptable.class, context, chain, classLoader);
		if (adaptableConverter==null) {
			return null;
		}
		
		Adaptable adaptable = adaptableConverter.convert(source);		
		return adaptable==null ? null : adaptable.getAdapter(targetType, context);		
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * @param sourceType Source type.
	 * @param targetType Target type.
	 * @return Converter from source type to target type, or null if such converter
	 * is not available.
	 * @throws ConversionException
	 */	
	public static <S, T> ConverterClosure<S, T> getConverter(Class<S> sourceType, Class<T> targetType) { 
		return getConverter(sourceType, targetType, null, false, null);
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * @param sourceType Source type.
	 * @param targetType Target type.
	 * @param classLoader Class loader to retrieve converters from.
	 * @return Converter from source type to target type, or null if such converter
	 * is not available.
	 * @throws ConversionException
	 */	
	public static <S, T> ConverterClosure<S, T> getConverter(Class<S> sourceType, Class<T> targetType, ClassLoader classLoader) { 
		return getConverter(sourceType, targetType, null, false, classLoader);
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * @param sourceType Source type.
	 * @param targetType Target type.
	 * @param context Conversion context.
	 * @return Converter from source type to target type, or null if such converter
	 * is not available.
	 * @throws ConversionException
	 */	
	public static <S, T> ConverterClosure<S,T> getConverter(Class<S> sourceType, Class<T> targetType, Context context) { 
		return getConverter(sourceType, targetType, context, false, null);
	}
	
	/**
	 * Converts source to target type using Java 6 service loading for AtomicConverterBundle, AtomicConverter, and Converter services
	 * If no converter is found, this method performs converter discovery through
	 * constructors if target type is a class and through duck conversion if target
	 * type is an interface. 
	 * @param sourceType Source type.
	 * @param targetType Target type.
	 * @param context Conversion context.
	 * @param classLoader Class loader to retrieve converters from.
	 * @return Converter from source type to target type, or null if such converter
	 * is not available.
	 * @throws ConversionException
	 */	
	public static <S, T> ConverterClosure<S,T> getConverter(Class<S> sourceType, Class<T> targetType, Context context, ClassLoader classLoader) { 
		return getConverter(sourceType, targetType, context, false, classLoader);
	}
	
	private static ClassLoader promote(ClassLoader present, ClassLoader candidate) {
		ClassLoader ret = DuckConverterFactory.getChildClassLoader(present, candidate);
		return ret==null ? present : ret;
	}
	
	/**
	 * 
	 * @param sourceType
	 * @param targetType
	 * @param context
	 * @param chain If true, the converter will attempt to build a chain of 
	 * conversions from source type to target type. 
	 * For example, StringBuffer doesn't have a constructor from Integer. If chain
	 * is <code>false</code> then this method will return <code>null</code>. If 
	 * <code>chain</code> is true, then the converter will convert Integer to String
	 * and then will create StringBuffer from it. If multiple conversion chains 
	 * are available, the shortest is chosen.
	 * @param classLoader
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private static <S, T> ConverterClosure<S, T> getConverter(Class<S> sourceType, Class<T> targetType, final Context context, boolean chain, ClassLoader pClassLoader) {
		if (pClassLoader==null) {
			pClassLoader = ConvertingService.class.getClassLoader();
		}
		ClassLoader classLoader = promote(pClassLoader, promote(targetType.getClassLoader(), sourceType.getClassLoader()));
					
		ClassLoaderConvertersEntry clce;
		synchronized (classLoaderEntries) {
			clce = classLoaderEntries.get(classLoader);
			if (clce == null) {
				clce = new ClassLoaderConvertersEntry(classLoader);
				for (Object provider: ServiceLoader.load(AtomicConvertersBundle.class, classLoader)) {
					clce.addConverters(provider);
				}
				for (Object provider: ServiceLoader.load(AtomicConverter.class, classLoader)) {
					clce.addConverters(provider);
				}
				for (Object provider: ServiceLoader.load(Converter.class, classLoader)) {
					clce.addConverters(provider);
				}
				
				for (Object provider: ServiceLoader.load(ConverterPart.class, classLoader)) {
					clce.addConverters(provider);
				}
				// TODO - Converter factories which may create converter (closure) or return null depending on context.
				classLoaderEntries.put(classLoader, clce);
			}	
			clce.addAnnotationConverters(sourceType);
		}
		final ConvertersBucket bucket = chain ? clce.findChainingConverter(sourceType, targetType) : clce.findConverter(sourceType, targetType);
		return bucket==null ? null : new ConverterClosure() {

			public Object convert(Object source) {
				return bucket.convert(source, context);
			}
			
		};
	}
}
