/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.osgi.util.converter;

import java.lang.annotation.Annotation;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * @author $Id: e9d558ae48970ee045c84f19e1e7bafc79efa0fb $
 */
class ConvertingImpl extends AbstractSpecifying<Converting>
		implements Converting, InternalConverting {
	private static final Map<Class< ? >,Class< ? >>	INTERFACE_IMPLS;
	// Interfaces with no methods are also not considered
	private static final Collection<Class< ? >>		NO_MAP_VIEW_TYPES;
	static {

		Map<Class< ? >,Class< ? >> cim = new HashMap<>();
		cim.put(Collection.class, ArrayList.class);
		// Lists
		cim.put(List.class, ArrayList.class);
		// Sets
		cim.put(Set.class, LinkedHashSet.class); // preserves insertion order
		cim.put(NavigableSet.class, TreeSet.class);
		cim.put(SortedSet.class, TreeSet.class);
		// Queues
		cim.put(Queue.class, LinkedList.class);
		cim.put(Deque.class, LinkedList.class);

		Map<Class< ? >,Class< ? >> iim = new HashMap<>(cim);
		// Maps
		iim.put(Map.class, LinkedHashMap.class); // preserves insertion order
		iim.put(ConcurrentMap.class, ConcurrentHashMap.class);
		iim.put(ConcurrentNavigableMap.class, ConcurrentSkipListMap.class);
		iim.put(NavigableMap.class, TreeMap.class);
		iim.put(SortedMap.class, TreeMap.class);

		Set<Class< ? >> nmv = new HashSet<>(cim.keySet());
		nmv.addAll(Arrays.<Class< ? >> asList(String.class, Class.class,
				Comparable.class, CharSequence.class, Map.Entry.class));

		INTERFACE_IMPLS = Collections.unmodifiableMap(iim);
		NO_MAP_VIEW_TYPES = Collections.unmodifiableSet(nmv);
	}

	volatile InternalConverter	converter;
	private volatile Object		object;
	private volatile Class< ? >	sourceClass;
	private volatile Class< ? >	targetClass;
	private volatile Type[]		typeArguments;
	private volatile Type		targetType;

	ConvertingImpl(InternalConverter c, Object obj) {
		converter = c;
		object = obj;
	}

	@Override
	public void setConverter(Converter c) {
		if (c instanceof InternalConverter)
			converter = (InternalConverter) c;
		else
			throw new IllegalStateException(
					"Incorrect converter used. Should implement "
							+ InternalConverter.class + " but was " + c);
	}

	@Override
	public <T> T to(Class<T> cls) {
		Type type = cls;
		return to(type);
	}

	@Override
	public <T> T to(TypeReference<T> ref) {
		return to(ref.getType());
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T to(Type type) {
		Class< ? > cls = null;
		if (type instanceof Class) {
			cls = (Class< ? >) type;
		} else if (type instanceof ParameterizedType) {
			ParameterizedType pt = (ParameterizedType) type;
			Type rt = pt.getRawType();
			typeArguments = pt.getActualTypeArguments();
			if (rt instanceof Class)
				cls = (Class< ? >) rt;
		} else if (type instanceof GenericArrayType) {
			GenericArrayType pt = (GenericArrayType) type;
			Type rt = pt.getGenericComponentType();
			if (rt instanceof Class)
				cls = (Class< ? >) rt;
			else if (rt instanceof ParameterizedType) {
				Type rt2 = ((ParameterizedType) rt).getRawType();
				if (rt2 instanceof Class) {
					cls = (Class< ? >) rt2;
				}
			}

		}
		targetType = type;
		if (cls == null)
			return null;

		if (object == null)
			return (T) handleNull(cls);

		targetClass = Util.primitiveToBoxed(cls);
		if (targetAsClass == null)
			targetAsClass = targetClass;

		sourceClass = sourceAsClass != null ? sourceAsClass : object.getClass();

		if (!isCopyRequiredType(targetAsClass)
				&& targetAsClass.isAssignableFrom(sourceClass)) {
			return (T) object;
		}

		Object res = trySpecialCases();
		if (res != null)
			return (T) res;

		if (targetAsClass.isArray()) {
			return convertToArray(targetAsClass.getComponentType(),
					targetAsClass.getComponentType());
		} else if (type instanceof GenericArrayType) {
			return convertToArray(targetAsClass,
					((GenericArrayType) type).getGenericComponentType());
		} else if (Collection.class.isAssignableFrom(targetAsClass)) {
			return convertToCollectionType();
		} else if (isMapType(targetAsClass, targetAsJavaBean, targetAsDTO)) {
			return (T) convertToMapType();
		}

		// At this point we know that the target is a 'singular' type: not a
		// map, collection or array
		if (Collection.class.isAssignableFrom(sourceClass)) {
			return (T) convertCollectionToSingleValue(targetAsClass);
		} else if (isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) {
			return (T) convertMapToSingleValue(targetAsClass);
		} else if (object instanceof Map.Entry) {
			return (T) convertMapEntryToSingleValue(targetAsClass);
		} else if ((object = asBoxedArray(object)) instanceof Object[]) {
			return (T) convertArrayToSingleValue(targetAsClass);
		}

		Object res2 = tryStandardMethods();
		if (res2 != null) {
			return (T) res2;
		} else {
			if (hasDefault)
				return (T) converter.convert(defaultValue)
						.sourceAs(sourceAsClass)
						.targetAs(targetAsClass)
						.to(targetClass);
			else
				throw new ConversionException(
						"Cannot convert " + object + " to " + targetAsClass);
		}
	}

	private Object convertArrayToSingleValue(Class< ? > cls) {
		Object[] arr = (Object[]) object;
		if (arr.length == 0)
			return null;
		else
			return converter.convert(arr[0]).to(cls);
	}

	private Object convertCollectionToSingleValue(Class< ? > cls) {
		Collection< ? > coll = (Collection< ? >) object;
		if (coll.size() == 0)
			return null;
		else
			return converter.convert(coll.iterator().next()).to(cls);
	}

	private Object convertMapToSingleValue(Class< ? > cls) {
		Map< ? , ? > m = mapView(object, sourceClass, converter);
		if (m.size() > 0) {
			return converter.convert(m.entrySet().iterator().next()).to(cls);
		} else {
			return null;
		}
	}

	@SuppressWarnings("rawtypes")
	private Object convertMapEntryToSingleValue(Class< ? > cls) {
		Map.Entry entry = (Map.Entry) object;

		Class keyCls = entry.getKey() != null ? entry.getKey().getClass()
				: null;
		Class valueCls = entry.getValue() != null ? entry.getValue().getClass()
				: null;

		if (cls.equals(keyCls)) {
			return converter.convert(entry.getKey()).to(cls);
		} else if (cls.equals(valueCls)) {
			return converter.convert(entry.getValue()).to(cls);
		} else if (cls.isAssignableFrom(keyCls)) {
			return converter.convert(entry.getKey()).to(cls);
		} else if (cls.isAssignableFrom(valueCls)) {
			return converter.convert(entry.getValue()).to(cls);
		} else if (entry.getKey() instanceof String) {
			return converter.convert(entry.getKey()).to(cls);
		} else if (entry.getValue() instanceof String) {
			return converter.convert(entry.getValue()).to(cls);
		}

		return converter
				.convert(converter.convert(entry.getKey()).to(String.class))
				.to(cls);
	}

	@SuppressWarnings("unchecked")
	private <T> T convertToArray(Class< ? > componentClz, Type componentType) {
		Collection< ? > collectionView = collectionView();
		Iterator< ? > itertor = collectionView.iterator();
		try {
			Object array = Array.newInstance(componentClz,
					collectionView.size());
			for (int i = 0; i < collectionView.size()
					&& itertor.hasNext(); i++) {
				Object next = itertor.next();
				Object converted = converter.convert(next)
						.to(componentType);
				Array.set(array, i, converted);
			}
			return (T) array;
		} catch (Exception e) {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	private <T> T convertToCollectionType() {
		Collection< ? > res = convertToCollectionDelegate();
		if (res != null)
			return (T) res;

		return convertToCollection();
	}

	private Collection< ? > convertToCollectionDelegate() {
		if (!liveView)
			return null;

		if (List.class.equals(targetClass)
				|| Collection.class.equals(targetClass)) {
			if (sourceClass.isArray()) {
				return ListDelegate.forArray(object, this);
			} else if (Collection.class.isAssignableFrom(sourceClass)) {
				return ListDelegate.forCollection((Collection< ? >) object,
						this);
			}
		} else if (Set.class.equals(targetClass)) {
			if (sourceClass.isArray()) {
				return SetDelegate.forCollection(
						ListDelegate.forArray(object, this), this);
			} else if (Collection.class.isAssignableFrom(sourceClass)) {
				return SetDelegate.forCollection((Collection< ? >) object,
						this);
			}
		}
		return null;
	}

	@SuppressWarnings({
			"rawtypes", "unchecked"
	})
	private <T> T convertToCollection() {
		Collection< ? > cv = collectionView();
		Class< ? > targetElementType = null;
		if (typeArguments != null && typeArguments.length > 0
				&& typeArguments[0] instanceof Class) {
			targetElementType = (Class< ? >) typeArguments[0];
		}

		Class< ? > ctrCls = INTERFACE_IMPLS.get(targetAsClass);
		Class< ? > targetCls;
		if (ctrCls != null)
			targetCls = ctrCls;
		else
			targetCls = targetAsClass;

		Collection instance = (Collection) createMapOrCollection(targetCls,
				cv.size());
		if (instance == null)
			return null;

		for (Object o : cv) {
			if (targetElementType != null) {
				try {
					o = converter.convert(o).to(targetElementType);
				} catch (ConversionException ce) {
					if (hasDefault) {
						return (T) defaultValue;
					}
				}
			}

			instance.add(o);
		}

		return (T) instance;
	}

	@SuppressWarnings({
			"rawtypes", "unchecked"
	})
	private <T> T convertToDTO(Class< ? > sourceCls, Class< ? > targetAsCls) {
		Map m = mapView(object, sourceCls, converter);

		try {
			String prefix = Util.getPrefix(targetAsCls);

			T dto = (T) targetClass.newInstance();

			List<String> names = getNames(targetAsClass);
			for (Map.Entry entry : (Set<Map.Entry>) m.entrySet()) {
				Object key = entry.getKey();
				if (key == null)
					continue;

				String fieldName = Util.mangleName(prefix, key.toString(), names);
				if (fieldName == null)
					continue;

				Field f = null;
				try {
					f = targetAsCls.getDeclaredField(fieldName);
				} catch (NoSuchFieldException e) {
					try {
						f = targetAsCls.getField(fieldName);
					} catch (NoSuchFieldException | NullPointerException e1) {
						// There is no field with this name
						if (keysIgnoreCase) {
							// If enabled, try again but now ignore case
							for (Field fs : targetAsCls.getDeclaredFields()) {
								if (fs.getName().equalsIgnoreCase(fieldName)) {
									f = fs;
									break;
								}
							}

							if (f == null) {
								for (Field fs : targetAsCls.getFields()) {
									if (fs.getName()
											.equalsIgnoreCase(fieldName)) {
										f = fs;
										break;
									}
								}
							}
						}
					}
				}

				if (f != null) {
					Object val = entry.getValue();
					if (sourceAsDTO && DTOUtil.isDTOType(f.getType()))
						val = converter.convert(val).sourceAsDTO().to(
								f.getType());
					else {
						Type genericType = reifyType(f.getGenericType(),
								targetAsClass, typeArguments);
						val = converter.convert(val).to(genericType);
					}
					f.set(dto, val);
				}
			}

			return dto;
		} catch (Exception e) {
			throw new ConversionException("Cannot create DTO " + targetClass,
					e);
		}
	}

	static Type reifyType(Type typeToReify, Class< ? > ownerClass,
			Type[] typeArgs) {

		if (typeToReify instanceof TypeVariable) {
			String name = ((TypeVariable< ? >) typeToReify).getName();
			for (int i = 0; i < ownerClass.getTypeParameters().length; i++) {
				TypeVariable< ? > typeVariable = ownerClass
						.getTypeParameters()[i];
				if (typeVariable.getName().equals(name)) {
					return typeArgs[i];
				}
			}

			// The direct type variable wasn't found, maybe it was already
			// bound in this class.

			Type currentType = ownerClass;
			while (currentType != null) {
				if (currentType instanceof Class) {
					currentType = ((Class< ? >) currentType)
							.getGenericSuperclass();
				} else if (currentType instanceof ParameterizedType) {
					currentType = ((ParameterizedType) currentType)
							.getRawType();
				}

				if (currentType instanceof ParameterizedType) {
					ParameterizedType pt = (ParameterizedType) currentType;
					Type rawType = pt.getRawType();
					if (rawType instanceof Class) {
						return reifyType(typeToReify, (Class< ? >) rawType,
								pt.getActualTypeArguments());
					}
				}
			}
		} else if (typeToReify instanceof ParameterizedType) {
			final ParameterizedType parameterizedType = (ParameterizedType) typeToReify;
			Type[] parameters = parameterizedType.getActualTypeArguments();
			boolean useCopy = false;
			final Type[] copiedParameters = new Type[parameters.length];

			for (int i = 0; i < parameters.length; i++) {
				copiedParameters[i] = reifyType(parameters[i], ownerClass,
						typeArgs);
				useCopy |= copiedParameters[i] != parameters[i];
			}

			if (useCopy) {
				return new ParameterizedType() {

					@Override
					public Type getRawType() {
						return parameterizedType.getRawType();
					}

					@Override
					public Type getOwnerType() {
						return parameterizedType.getOwnerType();
					}

					@Override
					public Type[] getActualTypeArguments() {
						return Arrays.copyOf(copiedParameters,
								copiedParameters.length);
					}
				};
			}
		} else if (typeToReify instanceof GenericArrayType) {
			GenericArrayType type = (GenericArrayType) typeToReify;
			Type genericComponentType = type.getGenericComponentType();
			final Type reifiedType = reifyType(genericComponentType, ownerClass,
					typeArgs);

			if (reifiedType != genericComponentType) {
				return new GenericArrayType() {

					@Override
					public Type getGenericComponentType() {
						return reifiedType;
					}
				};
			}
		}

		return typeToReify;
	}

	private List<String> getNames(Class< ? > cls) {
		List<String> names = new ArrayList<>();
		for (Field field : cls.getDeclaredFields()) {
			int modifiers = field.getModifiers();
			if (Modifier.isStatic(modifiers))
				continue;
			if (!Modifier.isPublic(modifiers))
				continue;

			String name = field.getName();
			if (!names.contains(name))
				names.add(name);

		}
		return names;
	}

	@SuppressWarnings({
			"rawtypes", "unchecked"
	})
	private Map convertToMap() {
		Map m = mapView(object, sourceClass, converter);
		if (m == null)
			return null;

		Class< ? > ctrCls = INTERFACE_IMPLS.get(targetClass);
		if (ctrCls == null)
			ctrCls = targetClass;

		Map instance = (Map) createMapOrCollection(ctrCls, m.size());
		if (instance == null)
			return null;

		for (Map.Entry entry : (Set<Entry>) m.entrySet()) {
			Object key = entry.getKey();
			Object value = entry.getValue();
			key = convertMapKey(key);
			value = convertMapValue(value);
			instance.put(key, value);
		}

		return instance;
	}

	Object convertCollectionValue(Object element) {
		Type type = null;
		if (typeArguments != null && typeArguments.length > 0) {
			type = typeArguments[0];
		}

		if (element != null) {
			if (type != null) {
				element = converter.convert(element).to(type);
			} else {
				Class< ? > cls = element.getClass();
				if (isCopyRequiredType(cls)) {
					cls = getConstructableType(cls);
				}

				if (sourceAsDTO || DTOUtil.isDTOType(cls))
					element = converter.convert(element).sourceAsDTO().to(cls);
				else
					element = converter.convert(element).to(cls);
			}
		}
		return element;
	}

	Object convertMapKey(Object key) {
		return convertMapElement(key, 0);
	}

	Object convertMapValue(Object value) {
		return convertMapElement(value, 1);
	}

	private Object convertMapElement(Object element, int typeIdx) {
		Type type = null;
		if (typeArguments != null && typeArguments.length > typeIdx) {
			type = typeArguments[typeIdx];
		}

		if (element != null) {
			if (type != null) {
				element = converter.convert(element).to(type);
			} else {
				Class< ? > cls = element.getClass();
				if (isCopyRequiredType(cls)) {
					cls = getConstructableType(cls);
				}

				if (sourceAsDTO || DTOUtil.isDTOType(cls))
					element = converter.convert(element).sourceAsDTO().to(cls);
				else
					element = converter.convert(element).to(cls);
			}
		}
		return element;
	}

	@SuppressWarnings({
			"unchecked", "rawtypes"
	})
	private Map convertToMapDelegate() {
		if (Map.class.isAssignableFrom(sourceClass)) {
			return MapDelegate.forMap((Map) object, this);
		} else if (Dictionary.class.isAssignableFrom(sourceClass)) {
			return MapDelegate.forDictionary((Dictionary) object, this);
		} else if (DTOUtil.isDTOType(sourceClass) || sourceAsDTO) {
			return MapDelegate.forDTO(object, sourceClass, this);
		} else if (sourceAsJavaBean) {
			return MapDelegate.forBean(object, sourceClass, this);
		} else if (hasGetProperties(sourceClass)) {
			return null; // Handled in convertToMap()
		}

		// Assume it's an interface
		Set<Class< ? >> interfaces = getInterfaces(sourceClass);
		if (interfaces.size() > 0) {
			return MapDelegate.forInterface(object,
					interfaces.iterator().next(), this);
		}
		return null;
	}

	@SuppressWarnings("rawtypes")
	private Object convertToMapType() {
		if (!isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) {
			throw new ConversionException(
					"Cannot convert " + object + " to " + targetAsClass);
		}

		if (Map.class.equals(targetClass) && liveView) {
			Map res = convertToMapDelegate();
			if (res != null)
				return res;
		}

		if (Map.class.isAssignableFrom(targetAsClass))
			return convertToMap();
		else if (Dictionary.class.isAssignableFrom(targetAsClass))
			return convertToDictionary();
		else if (targetAsDTO || DTOUtil.isDTOType(targetAsClass))
			return convertToDTO(sourceClass, targetAsClass);
		else if (targetAsClass.isInterface())
			return convertToInterface(sourceClass, targetAsClass);
		else if (targetAsJavaBean)
			return convertToJavaBean(sourceClass, targetAsClass);
		throw new ConversionException(
				"Cannot convert " + object + " to " + targetAsClass);
	}

	@SuppressWarnings({
			"unchecked", "rawtypes"
	})
	private Object convertToDictionary() {
		return new Hashtable(
				(Map) converter.convert(object).to(new ParameterizedType() {
					@Override
					public Type getRawType() {
						return HashMap.class;
					}

					@Override
					public Type getOwnerType() {
						return null;
					}

					@SuppressWarnings("synthetic-access")
					@Override
					public Type[] getActualTypeArguments() {
						return typeArguments;
					}
				}));
	}

	private Object convertToJavaBean(Class< ? > sourceCls,
			Class< ? > targetCls) {
		String prefix = Util.getPrefix(targetCls);

		@SuppressWarnings("rawtypes")
		Map m = mapView(object, sourceCls, converter);
		try {
			Object res = targetClass.newInstance();
			for (Method setter : getSetters(targetCls)) {
				String setterName = setter.getName();
				StringBuilder propName = new StringBuilder(Character
						.valueOf(Character.toLowerCase(setterName.charAt(3)))
						.toString());
				if (setterName.length() > 4)
					propName.append(setterName.substring(4));

				Class< ? > setterType = setter.getParameterTypes()[0];
				String key = propName.toString();
				Object val = m.get(Util.unMangleName(prefix, key));
				setter.invoke(res, converter.convert(val).to(setterType));
			}
			return res;
		} catch (Exception e) {
			throw new ConversionException(
					"Cannot convert to class: " + targetCls.getName()
							+ ". Not a JavaBean with a Zero-arg Constructor.",
					e);
		}
	}

	@SuppressWarnings("rawtypes")
	private Object convertToInterface(Class< ? > sourceCls,
			final Class< ? > targetCls) {
		InternalConverting ic = converter.convert(object);
		ic.sourceAs(sourceAsClass).view();
		if (sourceAsDTO)
			ic.sourceAsDTO();
		if (sourceAsJavaBean)
			ic.sourceAsBean();
		final Map m = ic.to(Map.class);

		return createProxy(targetCls, m);
	}

	private Object createProxy(final Class< ? > cls, final Map< ? , ? > data) {
		return Proxy.newProxyInstance(cls.getClassLoader(), new Class[] {
				cls
		}, new InvocationHandler() {
			@SuppressWarnings("boxing")
			@Override
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				Class< ? > mdDecl = method.getDeclaringClass();
				if (mdDecl.equals(Object.class))
					switch (method.getName()) {
						case "equals" :
							return proxy == args[0];
						case "hashCode" :
							return System.identityHashCode(proxy);
						case "toString" :
							return "Proxy for " + cls;
						default :
							throw new UnsupportedOperationException("Method "
									+ method + " not supported on proxy for "
									+ cls);
					}
				if (mdDecl.equals(Annotation.class)) {
					if ("annotationType".equals(method.getName())
							&& method.getParameterTypes().length == 0) {
						return cls;
					}
				}

				String propName = Util.getInterfacePropertyName(method,
						Util.getSingleElementAnnotationKey(cls, proxy),
						proxy);
				if (propName == null)
					return null;

				Object val = data.get(propName);
				if (val == null && keysIgnoreCase) {
					// try in a case-insensitive way
					for (Iterator< ? > it = data.keySet().iterator(); it
							.hasNext()
							&& val == null;) {
						String k = it.next().toString();
						if (propName.equalsIgnoreCase(k)) {
							val = data.get(k);
						}
					}
				}

				// If no value is available take the default if specified
				if (val == null) {
					if (cls.isAnnotation()) {
						val = method.getDefaultValue();
					}

					if (val == null) {
						if (args != null && args.length == 1) {
							val = args[0];
						} else {
							throw new ConversionException(
									"No value for property: " + propName);
						}
					}
				}

				@SuppressWarnings("synthetic-access")
				Type genericType = reifyType(method.getGenericReturnType(),
						targetAsClass, typeArguments);
				return converter.convert(val).to(genericType);
			}
		});
	}

	@SuppressWarnings("boxing")
	private Object handleNull(Class< ? > cls) {
		if (hasDefault)
			return converter.convert(defaultValue).to(cls);

		Class< ? > boxed = Util.primitiveToBoxed(cls);
		if (boxed.equals(cls)) {
			if (cls.isArray()) {
				return new Object[] {};
			} else if (Collection.class.isAssignableFrom(cls)) {
				return converter.convert(Collections.emptyList()).to(cls);
			}
			// This is not a primitive, just return null
			return null;
		}

		return converter.convert(0).to(cls);
	}

	private static boolean isMapType(Class< ? > cls, boolean asJavaBean,
			boolean asDTO) {
		if (asDTO)
			return true;

		// All interface types that are not Collections are treated as maps
		if (Map.class.isAssignableFrom(cls))
			return true;
		else if (getInterfaces(cls).size() > 0)
			return true;
		else if (DTOUtil.isDTOType(cls))
			return true;
		else if (asJavaBean && isWriteableJavaBean(cls))
			return true;
		else
			return Dictionary.class.isAssignableFrom(cls);
	}

	@SuppressWarnings("boxing")
	private Object trySpecialCases() {
		if (Boolean.class.equals(targetAsClass)) {
			if (object instanceof Collection
					&& ((Collection< ? >) object).size() == 0) {
				return Boolean.FALSE;
			}
		} else if (Number.class.isAssignableFrom(targetAsClass)) {
			if (object instanceof Boolean) {
				return ((Boolean) object).booleanValue() ? 1 : 0;
			} else if (object instanceof Number) {
				if (Byte.class.isAssignableFrom(targetAsClass)) {
					return ((Number) object).byteValue();
				} else if (Short.class.isAssignableFrom(targetAsClass)) {
					return ((Number) object).shortValue();
				} else if (Integer.class.isAssignableFrom(targetAsClass)) {
					return ((Number) object).intValue();
				} else if (Long.class.isAssignableFrom(targetAsClass)) {
					return ((Number) object).longValue();
				} else if (Float.class.isAssignableFrom(targetAsClass)) {
					return ((Number) object).floatValue();
				} else if (Double.class.isAssignableFrom(targetAsClass)) {
					return ((Number) object).doubleValue();
				}
			}
		} else if (Enum.class.isAssignableFrom(targetAsClass)) {
			if (object instanceof Number) {
				try {
					Method m = targetAsClass.getMethod("values");
					Object[] values = (Object[]) m.invoke(null);
					return values[((Number) object).intValue()];
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			} else {
				try {
					Method m = targetAsClass.getMethod("valueOf", String.class);
					return m.invoke(null, object.toString());
				} catch (Exception e) {
					try {
						// Case insensitive fallback
						Method m = targetAsClass.getMethod("values");
						for (Object v : (Object[]) m.invoke(null)) {
							if (v.toString()
									.equalsIgnoreCase(object.toString())) {
								return v;
							}
						}
					} catch (Exception e1) {
						throw new RuntimeException(e1);
					}
				}
			}
		} else if (Annotation.class.isAssignableFrom(sourceClass)
				&& isMarkerAnnotation(sourceClass)) {
			// Special treatment for marker annotations
			Class< ? > ann = Util.getAnnotationType(sourceClass, object);
			String key = Util.toSingleElementAnnotationKey(ann.getSimpleName());
			return converter
					.convert(Collections.singletonMap(key, Boolean.TRUE))
					.targetAs(targetAsClass)
					.to(targetType);
		} else if (Annotation.class.isAssignableFrom(targetAsClass)
				&& isMarkerAnnotation(targetAsClass)) {
			Map<String,Boolean> representation = Converters.standardConverter()
					.convert(object)
					.to(new TypeReference<Map<String,Boolean>>() {
						/* empty subclass */
					});
			if (Boolean.TRUE.equals(
					representation.get(Util.toSingleElementAnnotationKey(
							targetAsClass.getSimpleName())))) {
				return createProxy(targetClass, Collections.emptyMap());
			} else {
				throw new ConversionException("Cannot convert " + object
						+ " to marker annotation " + targetAsClass);
			}
		}
		return null;
	}

	private boolean isMarkerAnnotation(Class< ? > annClass) {
		for (Method m : annClass.getDeclaredMethods()) {
			try {
				if (Annotation.class
						.getMethod(m.getName(), m.getParameterTypes())
						.getReturnType()
						.equals(m.getReturnType()))
					// this is a base annotation method
					continue;
			} catch (Exception ex) {
				// Method not found, not a marker annotation
			}
			return false;
		}
		return true;
	}

	@SuppressWarnings("unchecked")
	private <T> T tryStandardMethods() {
		try {
			Method m = targetAsClass.getDeclaredMethod("valueOf", String.class);
			if (m != null) {
				return (T) m.invoke(null, object.toString());
			}
		} catch (Exception e) {
			try {
				Constructor< ? > ctr = targetAsClass
						.getConstructor(String.class);
				return (T) ctr.newInstance(object.toString());
			} catch (Exception e2) {
				// Ignore
			}
		}
		return null;
	}

	private Collection< ? > collectionView() {
		if (object == null)
			return null;

		Collection< ? > c = asCollection();
		if (c == null)
			return Collections.singleton(object);
		else
			return c;
	}

	private Collection< ? > asCollection() {
		if (object instanceof Collection)
			return (Collection< ? >) object;
		else if ((object = asBoxedArray(object)) instanceof Object[])
			return Arrays.asList((Object[]) object);
		else if (isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO))
			return mapView(object, sourceClass, converter).entrySet();
		else
			return null;
	}

	private static Object asBoxedArray(Object obj) {
		Class< ? > objClass = obj.getClass();
		if (!objClass.isArray())
			return obj;

		int len = Array.getLength(obj);
		Object arr = Array.newInstance(
				Util.primitiveToBoxed(objClass.getComponentType()), len);
		for (int i = 0; i < len; i++) {
			Object val = Array.get(obj, i);
			Array.set(arr, i, val);
		}
		return arr;
	}

	@SuppressWarnings("rawtypes")
	private static Map createMapFromBeanAccessors(Object obj,
			Class< ? > sourceCls) {
		Set<String> invokedMethods = new HashSet<>();

		Map result = new HashMap();
		for (Method md : sourceCls.getDeclaredMethods()) {
			handleBeanMethod(obj, md, invokedMethods, result);
		}

		return result;
	}

	@SuppressWarnings("rawtypes")
	private Map createMapFromDTO(Object obj, InternalConverter ic) {
		Set<String> handledFields = new HashSet<>();

		Map result = new HashMap();
		// Do we need 'declaredfields'? We only need to look at the public
		// ones...
		for (Field f : obj.getClass().getDeclaredFields()) {
			handleDTOField(obj, f, handledFields, result, ic);
		}
		for (Field f : obj.getClass().getFields()) {
			handleDTOField(obj, f, handledFields, result, ic);
		}
		return result;
	}

	@SuppressWarnings("rawtypes")
	private static Map createMapFromInterface(Object obj, Class< ? > srcCls) {
		Map result = new HashMap();

		for (Class i : getInterfaces(srcCls)) {
			for (Method md : i.getMethods()) {
				handleInterfaceMethod(obj, i, md, new HashSet<String>(),
						result);
			}
			if (result.size() > 0)
				return result;
		}
		throw new ConversionException("Cannot be converted to map: " + obj);
	}

	@SuppressWarnings("boxing")
	private static Object createMapOrCollection(Class< ? > cls,
			int initialSize) {
		try {
			Constructor< ? > ctor = cls.getConstructor(int.class);
			return ctor.newInstance(initialSize);
		} catch (Exception e1) {
			try {
				Constructor< ? > ctor2 = cls.getConstructor();
				return ctor2.newInstance();
			} catch (Exception e2) {
				// ignore
			}
		}
		return null;
	}

	private static Class< ? > getConstructableType(Class< ? > targetCls) {
		if (targetCls.isArray())
			return targetCls;

		Class< ? > cls = targetCls;
		do {
			try {
				cls.getConstructor(int.class);
				return cls; // If no exception the constructor is there
			} catch (NoSuchMethodException e) {
				try {
					cls.getConstructor();
					return cls; // If no exception the constructor is there
				} catch (NoSuchMethodException e1) {
					// There is no constructor with this name
				}
			}
			for (Class< ? > intf : cls.getInterfaces()) {
				Class< ? > impl = INTERFACE_IMPLS.get(intf);
				if (impl != null)
					return impl;
			}

			cls = cls.getSuperclass();
		} while (!Object.class.equals(cls));

		return null;
	}

	// Returns an ordered set
	private static Set<Class< ? >> getInterfaces(Class< ? > cls) {
		if (NO_MAP_VIEW_TYPES.contains(cls))
			return Collections.emptySet();

		Set<Class< ? >> interfaces = getInterfaces0(cls);
		for (Iterator<Class< ? >> it = interfaces.iterator(); it.hasNext();) {
			Class< ? > intf = it.next();
			if (intf.getDeclaredMethods().length == 0)
				it.remove();
		}

		interfaces.removeAll(NO_MAP_VIEW_TYPES);

		return interfaces;
	}

	// Returns an ordered set
	private static Set<Class< ? >> getInterfaces0(Class< ? > cls) {
		if (cls == null)
			return Collections.emptySet();

		Set<Class< ? >> classes = new LinkedHashSet<>();
		if (cls.isInterface()) {
			classes.add(cls);
		} else {
			classes.addAll(Arrays.asList(cls.getInterfaces()));
		}

		classes.addAll(getInterfaces(cls.getSuperclass()));

		return classes;
	}

	@SuppressWarnings({
			"rawtypes", "unchecked"
	})
	private void handleDTOField(Object obj, Field field,
			Set<String> handledFields, Map result, InternalConverter ic) {
		String fn = Util.getDTOKey(field);
		if (fn == null)
			return;

		if (handledFields.contains(fn))
			return; // Field with this name was already handled

		try {
			Object fVal = field.get(obj);
			result.put(fn, fVal);
			handledFields.add(fn);
		} catch (Exception e) {
			// Ignore
		}
	}

	@SuppressWarnings({
			"rawtypes", "unchecked"
	})
	private static void handleBeanMethod(Object obj, Method md,
			Set<String> invokedMethods, Map res) {
		String bp = Util.getBeanKey(md);
		if (bp == null)
			return;

		if (invokedMethods.contains(bp))
			return; // method with this name already invoked

		try {
			res.put(bp, md.invoke(obj));
			invokedMethods.add(bp);
		} catch (Exception e) {
			// Ignore
		}
	}

	@SuppressWarnings({
			"rawtypes", "unchecked"
	})
	private static void handleInterfaceMethod(Object obj, Class< ? > intf,
			Method md, Set<String> invokedMethods, Map res) {
		String mn = md.getName();
		if (invokedMethods.contains(mn))
			return; // method with this name already invoked

		String propName = Util.getInterfacePropertyName(md,
				Util.getSingleElementAnnotationKey(intf, obj), obj);
		if (propName == null)
			return;

		try {
			Object r = Util.getInterfaceProperty(obj, md);
			if (r == null)
				return;

			res.put(propName, r);
			invokedMethods.add(mn);
		} catch (Exception e) {
			// Ignore
		}
	}

	private Map< ? , ? > mapView(Object obj, Class< ? > sourceCls,
			InternalConverter ic) {
		if (Map.class.isAssignableFrom(sourceCls)
				|| (DTOUtil.isDTOType(sourceCls) && obj instanceof Map))
			return (Map< ? , ? >) obj;
		else if (Dictionary.class.isAssignableFrom(sourceCls))
			return MapDelegate.forDictionary((Dictionary< ? , ? >) object,
					this);
		else if (DTOUtil.isDTOType(sourceCls) || sourceAsDTO)
			return createMapFromDTO(obj, ic);
		else if (sourceAsJavaBean) {
			Map< ? , ? > m = createMapFromBeanAccessors(obj, sourceCls);
			if (m.size() > 0)
				return m;
		} else if (hasGetProperties(sourceCls)) {
			return getPropertiesDelegate(obj, sourceCls);
		}
		return createMapFromInterface(obj, sourceClass);
	}

	private boolean hasGetProperties(Class< ? > cls) {
		try {
			Method m = cls.getDeclaredMethod("getProperties");
			if (m == null)
				m = cls.getMethod("getProperties");
			return m != null;
		} catch (Exception e) {
			return false;
		}
	}

	private Map< ? , ? > getPropertiesDelegate(Object obj, Class< ? > cls) {
		try {
			Method m = cls.getDeclaredMethod("getProperties");
			if (m == null)
				m = cls.getMethod("getProperties");

			return converter.convert(m.invoke(obj)).to(Map.class);
		} catch (Exception e) {
			return Collections.emptyMap();
		}
	}

	private static boolean isCopyRequiredType(Class< ? > cls) {
		if (cls.isEnum())
			return false;
		return Map.class.isAssignableFrom(cls)
				|| Collection.class.isAssignableFrom(cls)
				|| DTOUtil.isDTOType(cls) || cls.isArray();
	}

	private static boolean isWriteableJavaBean(Class< ? > cls) {
		boolean hasNoArgCtor = false;
		for (Constructor< ? > ctor : cls.getConstructors()) {
			if (ctor.getParameterTypes().length == 0)
				hasNoArgCtor = true;
		}
		if (!hasNoArgCtor)
			return false; // A JavaBean must have a public no-arg constructor

		return getSetters(cls).size() > 0;
	}

	private static Set<Method> getSetters(Class< ? > cls) {
		Set<Method> setters = new HashSet<>();
		while (!Object.class.equals(cls)) {
			Set<Method> methods = new HashSet<>();
			methods.addAll(Arrays.asList(cls.getDeclaredMethods()));
			methods.addAll(Arrays.asList(cls.getMethods()));
			for (Method md : methods) {
				if (md.getParameterTypes().length != 1)
					continue; // Only setters with a single argument
				String name = md.getName();
				if (name.length() < 4)
					continue;
				if (name.startsWith("set")
						&& Character.isUpperCase(name.charAt(3)))
					setters.add(md);
			}
			cls = cls.getSuperclass();
		}
		return setters;
	}
}
