package aQute.lib.converter;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;

import aQute.lib.base64.Base64;

/**
 * General Java type converter from an object to any type. Supports number
 * conversion
 */
@SuppressWarnings({
		"unchecked", "rawtypes"
})
public class Converter {
	public interface Hook {
		Object convert(Type dest, Object o) throws Exception;
	}

	boolean			fatal	= true;
	Map<Type,Hook>	hooks;
	List<Hook>		allHooks;

	public <T> T convert(Class<T> type, Object o) throws Exception {
		// Is it a compatible type?
		if (o != null && type.isAssignableFrom(o.getClass()))
			return (T) o;
		return (T) convert((Type) type, o);
	}

	public <T> T convert(TypeReference<T> type, Object o) throws Exception {
		return (T) convert(type.getType(), o);
	}

	public Object convert(Type type, Object o) throws Exception {
		Class resultType = getRawClass(type);
		if (o == null) {
			if (resultType.isPrimitive() || Number.class.isAssignableFrom(resultType))
				return convert(type, 0);

			return null; // compatible with any
		}

		if (allHooks != null) {
			for (Hook hook : allHooks) {
				Object r = hook.convert(type, o);
				if (r != null)
					return r;
			}
		}

		if (hooks != null) {
			Hook hook = hooks.get(type);
			if (hook != null) {
				Object value = hook.convert(type, o);
				if (value != null)
					return value;
			}
		}

		Class< ? > actualType = o.getClass();

		// We can always make a string

		if (resultType == String.class) {
			if (actualType.isArray()) {
				if (actualType == char[].class)
					return new String((char[]) o);
				if (actualType == byte[].class)
					return Base64.encodeBase64((byte[]) o);
				int l = Array.getLength(o);
				StringBuilder sb = new StringBuilder("[");
				String del = "";
				for (int i = 0; i < l; i++) {
					sb.append(del);
					del = ",";
					sb.append(convert(String.class, Array.get(o, i)));
				}
				sb.append("]");
				return sb.toString();
			}
			return o.toString();
		}

		// or make a UUID
		if (resultType == UUID.class) {
			return UUID.fromString(o.toString());
		}

		//
		// In case we have a Dictionary that is not also a map
		// this is kind of opportune in OSGi because of the silly
		// dictionaries we're still having
		//

		if (o instanceof Dictionary && !(o instanceof Map)) {
			Dictionary< ? , ? > dict = (Dictionary< ? , ? >) o;
			Map<Object,Object> map = new HashMap<Object,Object>();
			Enumeration< ? > e = dict.keys();
			while (e.hasMoreElements()) {
				Object k = e.nextElement();
				Object v = dict.get(k);
				map.put(k, v);
			}
			o = map;
		}

		if (Collection.class.isAssignableFrom(resultType))
			return collection(type, resultType, o);

		if (Map.class.isAssignableFrom(resultType))
			return map(type, resultType, o);

		if (type instanceof GenericArrayType) {
			GenericArrayType gType = (GenericArrayType) type;
			return array(gType.getGenericComponentType(), o);
		}

		if (resultType.isArray()) {
			if (actualType == String.class) {
				String s = (String) o;
				if (byte[].class == resultType)
					return Base64.decodeBase64(s);

				if (char[].class == resultType)
					return s.toCharArray();
			}
			if (byte[].class == resultType) {
				// Sometimes classes implement toByteArray
				try {
					Method m = actualType.getMethod("toByteArray");
					if (m.getReturnType() == byte[].class)
						return m.invoke(o);

				} catch (Exception e) {
					// Ignore
				}
			}

			return array(resultType.getComponentType(), o);
		}

		if (resultType.isAssignableFrom(o.getClass()))
			return o;

		if (Map.class.isAssignableFrom(actualType) && resultType.isInterface()) {
			return proxy(resultType, (Map) o);
		}

		// Simple type coercion

		if (resultType == boolean.class || resultType == Boolean.class) {
			if (actualType == boolean.class || actualType == Boolean.class)
				return o;
			Number n = number(o);
			if (n != null)
				return n.longValue() == 0 ? false : true;

			resultType = Boolean.class;
		} else if (resultType == byte.class || resultType == Byte.class) {
			Number n = number(o);
			if (n != null)
				return n.byteValue();
			resultType = Byte.class;
		} else if (resultType == char.class || resultType == Character.class) {
			Number n = number(o);
			if (n != null)
				return (char) n.shortValue();
			resultType = Character.class;
		} else if (resultType == short.class || resultType == Short.class) {
			Number n = number(o);
			if (n != null)
				return n.shortValue();

			resultType = Short.class;
		} else if (resultType == int.class || resultType == Integer.class) {
			Number n = number(o);
			if (n != null)
				return n.intValue();

			resultType = Integer.class;
		} else if (resultType == long.class || resultType == Long.class) {
			Number n = number(o);
			if (n != null)
				return n.longValue();

			resultType = Long.class;
		} else if (resultType == float.class || resultType == Float.class) {
			Number n = number(o);
			if (n != null)
				return n.floatValue();

			resultType = Float.class;
		} else if (resultType == double.class || resultType == Double.class) {
			Number n = number(o);
			if (n != null)
				return n.doubleValue();

			resultType = Double.class;
		}

		assert !resultType.isPrimitive();

		if (actualType == String.class) {
			String input = (String) o;
			if (resultType == char[].class)
				return input.toCharArray();

			if (resultType == byte[].class)
				return Base64.decodeBase64(input);

			if (Enum.class.isAssignableFrom(resultType)) {
				try {
					return Enum.valueOf((Class<Enum>) resultType, input);
				} catch (Exception e) {
					input = input.toUpperCase();
					return Enum.valueOf((Class<Enum>) resultType, input);
				}
			}
			if (resultType == Pattern.class) {
				return Pattern.compile(input);
			}
			if (resultType == URI.class) {
				return new URI(sanitizeInputForURI(input));
			}

			try {
				Constructor< ? > c = resultType.getConstructor(String.class);
				return c.newInstance(o.toString());
			} catch (Throwable t) {}
			try {
				Method m = resultType.getMethod("valueOf", String.class);
				if (Modifier.isStatic(m.getModifiers()))
					return m.invoke(null, o.toString());
			} catch (Throwable t) {}

			if (resultType == Character.class && input.length() == 1)
				return input.charAt(0);
		}
		Number n = number(o);
		if (n != null) {
			if (Enum.class.isAssignableFrom(resultType)) {
				try {
					Method values = resultType.getMethod("values");
					Enum[] vs = (Enum[]) values.invoke(null);
					int nn = n.intValue();
					if (nn > 0 && nn < vs.length)
						return vs[nn];
				} catch (Exception e) {
					// Ignore
				}
			}
		}

		// Translate arrays with length 1 by picking the single element
		if (actualType.isArray() && Array.getLength(o) == 1) {
			return convert(type, Array.get(o, 0));
		}

		// Translate collections with size 1 by picking the single element
		if (o instanceof Collection) {
			Collection col = (Collection) o;
			if (col.size() == 1)
				return convert(type, col.iterator().next());
		}

		if (o instanceof Map) {
			String key = null;
			try {
				Map<Object,Object> map = (Map) o;
				Object instance = resultType.getConstructor().newInstance();
				for (Map.Entry e : map.entrySet()) {
					key = (String) e.getKey();
					try {
						Field f = resultType.getField(key);
						Object value = convert(f.getGenericType(), e.getValue());
						f.set(instance, value);
					} catch (Exception ee) {

						// We cannot find the key, so try the __extra field
						Field f = resultType.getField("__extra");
						Map<String,Object> extra = (Map<String,Object>) f.get(instance);
						if (extra == null) {
							extra = new HashMap<String,Object>();
							f.set(instance, extra);
						}
						extra.put(key, convert(Object.class, e.getValue()));

					}
				}
				return instance;
			} catch (Exception e) {
				return error(
						"No conversion found for " + o.getClass() + " to " + type + ", error " + e + " on key " + key);
			}
		}

		return error("No conversion found for " + o.getClass() + " to " + type);
	}

	private String sanitizeInputForURI(String input) {
		int newline = input.indexOf("\n");
		if (newline > -1)
			return input.substring(0, newline).trim();
		return input;
	}

	private Number number(Object o) {
		if (o instanceof Number)
			return (Number) o;

		if (o instanceof Boolean)
			return ((Boolean) o).booleanValue() ? 1 : 0;

		if (o instanceof Character)
			return (int) ((Character) o).charValue();

		if (o instanceof String) {
			String s = (String) o;
			try {
				return Double.parseDouble(s);
			} catch (Exception e) {
				// Ignore
			}
		}
		return null;
	}

	private Collection collection(Type collectionType, Class< ? extends Collection> rawClass, Object o)
			throws Exception {
		Collection collection;
		if (rawClass.isInterface() || Modifier.isAbstract(rawClass.getModifiers())) {
			if (rawClass.isAssignableFrom(ArrayList.class))
				collection = new ArrayList();
			else if (rawClass.isAssignableFrom(HashSet.class))
				collection = new HashSet();
			else if (rawClass.isAssignableFrom(TreeSet.class))
				collection = new TreeSet();
			else if (rawClass.isAssignableFrom(LinkedList.class))
				collection = new LinkedList();
			else if (rawClass.isAssignableFrom(Vector.class))
				collection = new Vector();
			else if (rawClass.isAssignableFrom(Stack.class))
				collection = new Stack();
			else if (rawClass.isAssignableFrom(ConcurrentLinkedQueue.class))
				collection = new ConcurrentLinkedQueue();
			else
				return (Collection) error("Cannot find a suitable collection for the collection interface " + rawClass);
		} else
			collection = rawClass.getConstructor().newInstance();

		Type subType = Object.class;
		if (collectionType instanceof ParameterizedType) {
			ParameterizedType ptype = (ParameterizedType) collectionType;
			subType = ptype.getActualTypeArguments()[0];
		}

		Collection input = toCollection(o);

		for (Object i : input)
			collection.add(convert(subType, i));

		return collection;
	}

	private Map map(Type mapType, Class< ? extends Map< ? , ? >> rawClass, Object o) throws Exception {
		Map result;
		if (rawClass.isInterface() || Modifier.isAbstract(rawClass.getModifiers())) {
			if (rawClass.isAssignableFrom(HashMap.class))
				result = new HashMap();
			else if (rawClass.isAssignableFrom(TreeMap.class))
				result = new TreeMap();
			else if (rawClass.isAssignableFrom(ConcurrentHashMap.class))
				result = new ConcurrentHashMap();
			else {
				return (Map) error("Cannot find suitable map for map interface " + rawClass);
			}
		} else
			result = rawClass.getConstructor().newInstance();

		Map< ? , ? > input = toMap(o);

		Type keyType = Object.class;
		Type valueType = Object.class;
		if (mapType instanceof ParameterizedType) {
			ParameterizedType ptype = (ParameterizedType) mapType;
			keyType = ptype.getActualTypeArguments()[0];
			valueType = ptype.getActualTypeArguments()[1];
		}

		for (Map.Entry< ? , ? > entry : input.entrySet()) {
			Object key = convert(keyType, entry.getKey());
			Object value = convert(valueType, entry.getValue());
			if (key == null)
				error("Key for map must not be null: " + input);
			else
				result.put(key, value);
		}

		return result;
	}

	public Object array(Type type, Object o) throws Exception {
		Collection< ? > input = toCollection(o);
		Class< ? > componentClass = getRawClass(type);
		Object array = Array.newInstance(componentClass, input.size());

		int i = 0;
		for (Object next : input) {
			Array.set(array, i++, convert(type, next));
		}
		return array;
	}

	private Class< ? > getRawClass(Type type) {
		if (type instanceof Class)
			return (Class< ? >) type;

		if (type instanceof ParameterizedType)
			return (Class< ? >) ((ParameterizedType) type).getRawType();

		if (type instanceof GenericArrayType) {
			Type componentType = ((GenericArrayType) type).getGenericComponentType();
			return Array.newInstance(getRawClass(componentType), 0).getClass();
		}

		if (type instanceof TypeVariable) {
			Type componentType = ((TypeVariable) type).getBounds()[0];
			return Array.newInstance(getRawClass(componentType), 0).getClass();
		}

		if (type instanceof WildcardType) {
			Type componentType = ((WildcardType) type).getUpperBounds()[0];
			return Array.newInstance(getRawClass(componentType), 0).getClass();
		}

		return Object.class;
	}

	public Collection< ? > toCollection(Object o) {
		if (o instanceof Collection)
			return (Collection< ? >) o;

		if (o.getClass().isArray()) {
			if (o.getClass().getComponentType().isPrimitive()) {
				int length = Array.getLength(o);
				List<Object> result = new ArrayList<Object>(length);
				for (int i = 0; i < length; i++) {
					result.add(Array.get(o, i));
				}
				return result;
			}
			return Arrays.asList((Object[]) o);
		}

		return Arrays.asList(o);
	}

	public Map< ? , ? > toMap(Object o) throws Exception {
		if (o instanceof Map)
			return (Map< ? , ? >) o;
		Map result = new HashMap();
		Field fields[] = o.getClass().getFields();
		for (Field f : fields)
			result.put(f.getName(), f.get(o));
		if (result.isEmpty())
			return null;

		return result;
	}

	private Object error(String string) {
		if (fatal)
			throw new IllegalArgumentException(string);
		return null;
	}

	public void setFatalIsException(boolean b) {
		fatal = b;
	}

	public Converter hook(Type type, Hook hook) {
		if (type != null) {
			if (hooks == null)
				hooks = new HashMap<Type,Converter.Hook>();
			this.hooks.put(type, hook);
		} else {
			if (allHooks == null)
				allHooks = new ArrayList<Converter.Hook>();
			allHooks.add(hook);
		}

		return this;
	}

	/**
	 * Convert a map to an interface.
	 * 
	 * @param interfc
	 * @param properties
	 * @return proxy object for map
	 */
	public <T> T proxy(Class<T> interfc, final Map< ? , ? > properties) {
		return (T) Proxy.newProxyInstance(interfc.getClassLoader(), new Class[] {
				interfc
		}, new InvocationHandler() {

			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				Object o = properties.get(method.getName());
				if (o == null)
					o = properties.get(mangleMethodName(method.getName()));

				if (o == null) {
					if (args != null && args.length == 1) {
						o = args[0];
					} else {
						o = method.getDefaultValue();
					}
				}

				return convert(method.getGenericReturnType(), o);
			}

		});
	}

	public static String mangleMethodName(String id) {
		StringBuilder sb = new StringBuilder(id);
		for (int i = 0; i < sb.length(); i++) {
			char c = sb.charAt(i);
			boolean twice = i < sb.length() - 1 && sb.charAt(i + 1) == c;
			if (c == '$' || c == '_') {
				if (twice)
					sb.deleteCharAt(i + 1);
				else if (c == '$')
					sb.deleteCharAt(i--); // Remove dollars
				else
					sb.setCharAt(i, '.'); // Make _ into .
			}
		}
		return sb.toString();
	}

	public static <T> T cnv(TypeReference<T> tr, Object source) throws Exception {
		return new Converter().convert(tr, source);
	}

	public static <T> T cnv(Class<T> tr, Object source) throws Exception {
		return new Converter().convert(tr, source);
	}

	public static Object cnv(Type tr, Object source) throws Exception {
		return new Converter().convert(tr, source);
	}

}
