package com.hammurapi.convert;

import java.lang.reflect.InvocationTargetException;
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.Iterator;
import java.util.Map;
import java.util.ServiceLoader;

import com.hammurapi.common.Context;


/**
 * Decorates objects based on their type and decoration providers
 * available. Decoration is done with dynamic proxies.
 * @author Pavel
 */
public class DecoratingService {
	
	/**
	 * Converter which delegates to convert() method.
	 */
	public static final Decorator DECORATOR = new Decorator() {

		public Object decorate(Object source) {
			if (source==null) {
				return null;
			}
			return DecoratingService.decorate(source);
		}
		
	};			
	
	
	/**
	 * Decorates object using static decoration providers from the object's class
	 * classloader. If there are no decorations available or object doesn't 
	 * impement any interfaces, then the object is returned as-is.
	 * @param obj Object to be decorated
	 * @return Decorated object (dynamic proxy instance) or original object if
	 * no decorations are available or object doesn't implement any interfaces.
	 */
	public static Object decorate(Object obj) {
		return decorate(obj, null, null);
	}
	
	/**
	 * Decorates object using static decoration providers from the object's class
	 * classloader. If there are no decorations available or object doesn't 
	 * impement any interfaces, then the object is returned as-is.
	 * @param obj Object to be decorated
	 * @param context Context for dynamic decorators.
	 * @return Decorated object (dynamic proxy instance) or original object if
	 * no decorations are available or object doesn't implement any interfaces.
	 */
	public static Object decorate(Object obj, Context context) {
		return decorate(obj, context, null);
	}
	
	/**
	 * Decorates object using providers from the given classloader.
	 * @param obj Object to decorate.
	 * @param classLoader Class loader.
	 * @return Decorated object (dynamic proxy instance) or original object if
	 * no decorations are available or object doesn't implement any interfaces.
	 */
	public static Object decorate(Object obj, ClassLoader classLoader) {
		return decorate(obj, null, classLoader);
	}
	
	/**
	 * Decorates object using given context for dynamic decorators and given class
	 * loader to load decorating providers. 
	 * @param obj Object to decorate.
	 * @param context Context for dynamic decorators.
	 * @param classLoader Class loader.
	 * @return Decorated object (dynamic proxy instance) or original object if
	 * no decorations are available or object doesn't implement any interfaces.
	 */
	public static Object decorate(final Object obj, Context context, ClassLoader classLoader) {
		if (obj==null) {
			return null;
		}
				
		final Map<Class, Object> interfaceMap = new HashMap<Class, Object>();
		Class[] interfaces = DuckConverterFactory.getClassInterfaces(obj.getClass());
		for (int i=0; i<interfaces.length; ++i) {
			interfaceMap.put(interfaces[i], obj);
		}
		
		if (interfaceMap.isEmpty()) {
			return obj;
		}
		
		if (classLoader==null) {
			classLoader = obj.getClass().getClassLoader();
		}
		
		if (classLoader==null) {
			classLoader = DecoratingService.class.getClassLoader();
		}
					
		ClassLoaderDecoratorsEntry clde;
		synchronized (classLoaderEntries) {
			clde = classLoaderEntries.get(classLoader);
			if (clde == null) {
				clde = new ClassLoaderDecoratorsEntry();
				for (Object provider: ServiceLoader.load(Decorator.class, classLoader)) {
					clde.addDecorators(provider);
				}
				classLoaderEntries.put(classLoader, clde);
			}			
		}
		
		int iSize = interfaceMap.size();
		clde.collectDecorators(obj, interfaceMap, context, DECORATOR);
		if (iSize==interfaceMap.size()) {
			return obj;
		}
		
		Class[] allInterfaces = new Class[interfaceMap.size()];
		FilterInvocationHandler fih = new FilterInvocationHandler() {

			public Object getMaster() {
				return obj;
			}

			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				Object target = interfaceMap.get(method.getDeclaringClass());
				if (target==null) {
					target = obj;
				}
				return method.invoke(target, args);
			}
			
		};
		
		return Proxy.newProxyInstance(classLoader, allInterfaces, fih);
	}
	
	/**
	 * Creates decorating closure.
	 * @return Decorating closure.
	 */
	public static Decorator getDecorator() {
		return getDecorator(null, null);
	}
	
	/**
	 * Creates decorating closure.
	 * @param classLoader Class loader.
	 * @return Decorating closure.
	 */
	public static Decorator getDecorator(ClassLoader classLoader) {
		return getDecorator(null, classLoader);
	}
	
	/**
	 * Creates decorating closure.
	 * @param context Context for dynamic decorators.
	 * @return Decorating closure.
	 */
	public static Decorator getDecorator(Context context) {
		return getDecorator(context, null);
	}
	
	/**
	 * Creates decorating closure.
	 * @param context Context for dynamic decorators.
	 * @param classLoader Class loader.
	 * @return Decorating closure.
	 */
	public static Decorator getDecorator(final Context context, final ClassLoader classLoader) {
		return new Decorator() {

			public Object decorate(Object source) {
				return DecoratingService.decorate(source, context, classLoader);
			}
			
		};
	}
	
	private static class ClassLoaderDecoratorsEntry {
		private static final String DECORATE = "decorate";
		
		Collection<Object> atomicDecorators = new ArrayList<Object>();
						
		void addDecorators(final Object provider) {
			if (provider instanceof AtomicDecorator) {
				atomicDecorators.add(provider);
			} else if (provider instanceof AtomicDecoratorsBundle) {
				atomicDecorators.addAll(((AtomicDecoratorsBundle) provider).getDecorators());
			} else if (provider instanceof Collection) {
				Iterator it = ((Collection) provider).iterator();
				while (it.hasNext()) {
					addDecorators(it.next());
				}
			} else { // Introspection
				Class<? extends Object> pClass = provider.getClass();
				Method[] methods = pClass.getMethods();
				for (int i=0; i<methods.length; ++i) {
					final Method method = methods[i];
					if (DECORATE.equals(method.getName()) && !void.class.equals(method.getReturnType())) {
						final Class[] pTypes = method.getParameterTypes();
						if (pTypes.length == 1) {
							atomicDecorators.add(new AtomicDecorator() {

								public Object decorate(Object source, Context context, Decorator master) {
									try {
										return method.invoke(provider, new Object[] {source});
									} catch (IllegalAccessException e) {
										throw new DecorationException(e);
									} catch (InvocationTargetException e) {
										throw new DecorationException(e);
									}
								}

								public Class getSourceType() {
									return pTypes[0];
								}
								
							});
						} else if (pTypes.length == 2 && pTypes[1].equals(Decorator.class)) {
							atomicDecorators.add(new AtomicDecorator() {

								public Object decorate(Object source, Context context, Decorator master) {
									try {
										return method.invoke(provider, new Object[] {source, master});
									} catch (IllegalAccessException e) {
										throw new DecorationException(e);
									} catch (InvocationTargetException e) {
										throw new DecorationException(e);
									}
								}

								public Class getSourceType() {
									return pTypes[0];
								}
								
							});							
						} else if (pTypes.length == 2 && pTypes[1].equals(Context.class)) {
							atomicDecorators.add(new AtomicDecorator() {

								public Object decorate(Object source, Context context, Decorator master) {
									try {
										return method.invoke(provider, new Object[] {source, context});
									} catch (IllegalAccessException e) {
										throw new DecorationException(e);
									} catch (InvocationTargetException e) {
										throw new DecorationException(e);
									}
								}

								public Class getSourceType() {
									return pTypes[0];
								}
								
							});							
						} else if (pTypes.length == 3 && pTypes[1].equals(Context.class) && pTypes[1].equals(Decorator.class)) {
							atomicDecorators.add(new AtomicDecorator() {

								public Object decorate(Object source, Context context, Decorator master) {
									try {
										return method.invoke(provider, new Object[] {source, context, master});
									} catch (IllegalAccessException e) {
										throw new DecorationException(e);
									} catch (InvocationTargetException e) {
										throw new DecorationException(e);
									}
								}

								public Class getSourceType() {
									return pTypes[0];
								}
								
							});							
						}
						
					}
				}
			}
		}

		void collectDecorators(Object obj, Map<Class, Object> interfaceMap, Context context, Decorator master) {
			Iterator<Object> it = atomicDecorators.iterator();
			while (it.hasNext()) {
				AtomicDecorator<Object> ad = (AtomicDecorator<Object>) it.next();
				if (ad.getSourceType().isInstance(obj)) {
					Object decoration = ad.decorate(obj, context, master);
					if (decoration!=null) {
						boolean cascade = false;
						Class[] dia = DuckConverterFactory.getClassInterfaces(decoration.getClass());
						for (int i=0; i<dia.length; ++i) {
							if (!interfaceMap.containsKey(dia[i])) {
								cascade=true;
								interfaceMap.put(dia[i], decoration);
							}
						}
						if (cascade) {
							collectDecorators(decoration, interfaceMap, context, master);
						}						
					}
				}
			}
		}
		
	}
	
	/**
	 * Map of resolved converters: ClassLoader -&gt; ClassLoaderConvertersEntry. 
	 */
	private static Map<ClassLoader, ClassLoaderDecoratorsEntry> classLoaderEntries = new HashMap<ClassLoader, ClassLoaderDecoratorsEntry>(); 


}
