package com.hammurapi.convert;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.hammurapi.common.Context;


/**
 * Converts one type to another using accessor/constructor combination.
 * @author Pavel
 *
 */
public class ReflectionConverter<S, T> implements AtomicConverter<S, T> {
	private static final Logger logger = Logger.getLogger(ReflectionConverter.class.getName());
	
	Method accessor;
	Constructor<? extends T> constructor;
	private Class<? extends T> targetType;
	private Class<S> sourceType;
	private boolean singleArg;
	
	/**
	 * @param accessor Source object method to invoke to get target type or
	 * constructor parameter for the target type. If it is null, then source itself
	 * is passed to constructor. One of parameters may be null, but not both.
	 * @param constructor Target class constructor which takes one parameter, either
	 * source type or return type of source type accessor.
	 */
	@SuppressWarnings("unchecked")
	public ReflectionConverter(Method accessor, Constructor<? extends T> constructor) {
		super();
		this.accessor = accessor;
		this.constructor = constructor;
		singleArg = constructor!=null && constructor.getParameterTypes().length==1;
		if (accessor==null) {
			sourceType = (Class<S>) constructor.getParameterTypes()[0];
		} else {
			sourceType = (Class<S>) accessor.getDeclaringClass();
		}
		
		if (constructor==null) {
			targetType = (Class<T>) accessor.getReturnType();
		} else {
			targetType = constructor.getDeclaringClass();
		}		
	}
	
	@SuppressWarnings("unchecked")
	public T convert(S source, Converter master, Context context, ClassLoader classLoader) {
		try {
			Object param = accessor==null ? source : accessor.invoke(source);
			if (constructor==null) {
				return (T) param;
			}
			
			if (singleArg) {
				return constructor.newInstance(new Object[] {param});
			}
			
			return constructor.newInstance(new Object[] {param, context});
		} catch (Exception e) {
			logger.log(Level.FINE, "Cannot convert "+source.getClass().getName()+" to " + constructor.getDeclaringClass()+": "+e, e);
			return null;
		}
	}

	public Class<S> getSourceType() {
		return sourceType;
	}

	public Class<? extends T> getTargetType() {
		return targetType;
	}
	
	/**
	 * Discovers constructor conversions.
	 * @param target Target type
	 * @return Collection of discovered converters.
	 */
	@SuppressWarnings("unchecked")
	public static <T> Collection<ReflectionConverter<?, T>> discoverConstructorConverters(Class<T> target) {
		Collection<ReflectionConverter<?, T>> ret=new ArrayList<ReflectionConverter<?, T>>();
		if (!target.isInterface()) {
			for (int i=0, cc=target.getConstructors().length; i<cc; i++) {
				Constructor<?> constructor = target.getConstructors()[i];
				Class<?>[] parameterTypes = constructor.getParameterTypes();
				if (parameterTypes.length==1 && !target.isAssignableFrom(parameterTypes[0])) {
					
					ret.add(new ReflectionConverter(null, constructor));
				} else if (parameterTypes.length==2
						&& !target.isAssignableFrom(parameterTypes[0])
						&& Context.class.equals(parameterTypes[1])) {
						
					ret.add(new ReflectionConverter(null, constructor));
				}
			}
		}
		return ret;
	}	
	
	public String toString() {
		return this.getClass().getName() + " ("+sourceType+" -> "+targetType+", accessor: "+accessor+", constructor: "+constructor+")";
	}
	
}
