/*
 * Copyright 2004-2012 the original author or authors.
 * 
 * Licensed 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
 * 
 *      https://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.springframework.binding.convert.service;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.binding.convert.ConversionException;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.convert.ConversionExecutorNotFoundException;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.convert.converters.ArrayToArray;
import org.springframework.binding.convert.converters.ArrayToCollection;
import org.springframework.binding.convert.converters.CollectionToCollection;
import org.springframework.binding.convert.converters.Converter;
import org.springframework.binding.convert.converters.ObjectToArray;
import org.springframework.binding.convert.converters.ObjectToCollection;
import org.springframework.binding.convert.converters.ReverseConverter;
import org.springframework.binding.convert.converters.SpringConvertingConverterAdapter;
import org.springframework.binding.convert.converters.TwoWayConverter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
import org.springframework.util.Assert;

/**
 * Base implementation of a conversion service. Initially empty, e.g. no converters are registered by default.
 * 
 * @author Keith Donald
 */
public class GenericConversionService implements ConversionService {

	/**
	 * Spring ConversionService where existing custom {@link Converter} types will be registered through an adapter.
	 */
	private org.springframework.core.convert.ConversionService delegate;

	/**
	 * A map of custom converters. Custom converters are assigned a unique identifier that can be used to lookup the
	 * converter. This allows multiple converters for the same source->target class to be registered.
	 */
	private final Map<String, Converter> customConverters = new HashMap<>();

	/**
	 * Indexes classes by well-known aliases.
	 */
	private final Map<String, Class<?>> aliasMap = new HashMap<>();

	/**
	 * An optional parent conversion service.
	 */
	private ConversionService parent;

	/**
	 * Default constructor.
	 */
	public GenericConversionService() {
		FormattingConversionServiceFactoryBean factoryBean = new FormattingConversionServiceFactoryBean();
		factoryBean.afterPropertiesSet();
		this.delegate = factoryBean.getObject();
	}

	/**
	 * Constructor accepting a specific instance of a Spring ConversionService to delegate to.
	 * @param delegateConversionService the conversion service
	 */
	public GenericConversionService(org.springframework.core.convert.ConversionService delegateConversionService) {
		Assert.notNull(delegateConversionService, "Missing delegate ConversionService");
		this.delegate = delegateConversionService;
	}

	/**
	 * Returns the parent of this conversion service. Could be null.
	 */
	public ConversionService getParent() {
		return parent;
	}

	/**
	 * Set the parent of this conversion service. This is optional.
	 */
	public void setParent(ConversionService parent) {
		this.parent = parent;
	}

	/**
	 * @return the Spring ConverterRegistry
	 */
	public org.springframework.core.convert.ConversionService getDelegateConversionService() {
		return delegate;
	}

	/**
	 * Registers the given converter with the underlying Spring ConversionService with the help of an adapter. The
	 * adapter allows an existing Spring Binding converter to be invoked within Spring's type conversion system.
	 * 
	 * @param converter the converter
	 * 
	 * @see ConverterRegistry
	 * @see org.springframework.core.convert.ConversionService
	 * @see SpringBindingConverterAdapter
	 */
	public void addConverter(Converter converter) {
		((ConverterRegistry) delegate).addConverter(new SpringBindingConverterAdapter(converter));
		if (converter instanceof TwoWayConverter) {
			TwoWayConverter twoWayConverter = (TwoWayConverter) converter;
			((ConverterRegistry) delegate).addConverter(new SpringBindingConverterAdapter(new ReverseConverter(
					twoWayConverter)));
		}
	}

	/**
	 * Add given custom converter to this conversion service.
	 * 
	 * Note: Converters registered through this method will not be involve the Spring type conversion system, which is
	 * now used the default type conversion mechanism. Spring's type conversion does not support named converters. This
	 * method is provided for backwards compatibility.
	 * 
	 * @param id the id of the custom converter instance
	 * @param converter the converter
	 */
	public void addConverter(String id, Converter converter) {
		customConverters.put(id, converter);
	}

	/**
	 * Add an alias for given target type.
	 */
	public void addAlias(String alias, Class<?> targetType) {
		aliasMap.put(alias, targetType);
	}

	public ConversionExecutor getConversionExecutor(Class<?> sourceClass, Class<?> targetClass)
			throws ConversionExecutorNotFoundException {
		Assert.notNull(sourceClass, "The source class to convert from is required");
		Assert.notNull(targetClass, "The target class to convert to is required");
		sourceClass = convertToWrapperClassIfNecessary(sourceClass);
		targetClass = convertToWrapperClassIfNecessary(targetClass);
		if (targetClass.isAssignableFrom(sourceClass)) {
			return new StaticConversionExecutor(sourceClass, targetClass, new NoOpConverter(sourceClass, targetClass));
		}
		if (delegate.canConvert(sourceClass, targetClass)) {
			return new StaticConversionExecutor(sourceClass, targetClass, new SpringConvertingConverterAdapter(
					sourceClass, targetClass, delegate));
		} else if (parent != null) {
			return parent.getConversionExecutor(sourceClass, targetClass);
		} else {
			throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
					"No ConversionExecutor found for converting from sourceClass [" + sourceClass.getName()
							+ "] to target class [" + targetClass.getName() + "]");
		}
	}

	public ConversionExecutor getConversionExecutor(String id, Class<?> sourceClass, Class<?> targetClass)
			throws ConversionExecutorNotFoundException {
		Assert.hasText(id, "The id of the custom converter is required");
		Assert.notNull(sourceClass, "The source class to convert from is required");
		Assert.notNull(targetClass, "The target class to convert to is required");
		Converter converter = customConverters.get(id);
		if (converter == null) {
			if (parent != null) {
				return parent.getConversionExecutor(id, sourceClass, targetClass);
			} else {
				throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
						"No custom ConversionExecutor found with id '" + id + "' for converting from sourceClass ["
								+ sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]");
			}
		}
		sourceClass = convertToWrapperClassIfNecessary(sourceClass);
		targetClass = convertToWrapperClassIfNecessary(targetClass);
		if (sourceClass.isArray()) {
			Class<?> sourceComponentType = sourceClass.getComponentType();
			if (targetClass.isArray()) {
				Class<?> targetComponentType = targetClass.getComponentType();
				if (converter.getSourceClass().isAssignableFrom(sourceComponentType)) {
					if (!converter.getTargetClass().isAssignableFrom(targetComponentType)) {
						throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
								"Custom ConversionExecutor with id '" + id
										+ "' cannot convert from an array storing elements of type ["
										+ sourceComponentType.getName() + "] to an array of storing elements of type ["
										+ targetComponentType.getName() + "]");
					}
					ConversionExecutor elementConverter = new StaticConversionExecutor(sourceComponentType,
							targetComponentType, converter);
					return new StaticConversionExecutor(sourceClass, targetClass, new ArrayToArray(elementConverter));
				} else if (converter.getTargetClass().isAssignableFrom(sourceComponentType)
						&& converter instanceof TwoWayConverter) {
					TwoWayConverter twoWay = (TwoWayConverter) converter;
					ConversionExecutor elementConverter = new StaticConversionExecutor(sourceComponentType,
							targetComponentType, new ReverseConverter(twoWay));
					return new StaticConversionExecutor(sourceClass, targetClass, new ArrayToArray(elementConverter));
				} else {
					throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
							"Custom ConversionExecutor with id '" + id
									+ "' cannot convert from an array storing elements of type ["
									+ sourceComponentType.getName() + "] to an array storing elements of type ["
									+ targetComponentType.getName() + "]");
				}
			} else if (Collection.class.isAssignableFrom(targetClass)) {
				if (!targetClass.isInterface() && Modifier.isAbstract(targetClass.getModifiers())) {
					throw new IllegalArgumentException("Conversion target class [" + targetClass.getName()
							+ "] is invalid; cannot convert to abstract collection types--"
							+ "request an interface or concrete implementation instead");
				}
				if (converter.getSourceClass().isAssignableFrom(sourceComponentType)) {
					// type erasure has prevented us from getting the concrete type, this is best we can do for now
					ConversionExecutor elementConverter = new StaticConversionExecutor(sourceComponentType,
							converter.getTargetClass(), converter);
					return new StaticConversionExecutor(sourceClass, targetClass, new ArrayToCollection(
							elementConverter));
				} else if (converter.getTargetClass().isAssignableFrom(sourceComponentType)
						&& converter instanceof TwoWayConverter) {
					TwoWayConverter twoWay = (TwoWayConverter) converter;
					ConversionExecutor elementConverter = new StaticConversionExecutor(sourceComponentType,
							converter.getSourceClass(), new ReverseConverter(twoWay));
					return new StaticConversionExecutor(sourceClass, targetClass, new ArrayToCollection(
							elementConverter));
				} else {
					throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
							"Custom ConversionExecutor with id '" + id
									+ "' cannot convert from array an storing elements type ["
									+ sourceComponentType.getName() + "] to a collection of type ["
									+ targetClass.getName() + "]");
				}
			}
		}
		if (targetClass.isArray()) {
			Class<?> targetComponentType = targetClass.getComponentType();
			if (Collection.class.isAssignableFrom(sourceClass)) {
				// type erasure limits us here as well
				if (converter.getTargetClass().isAssignableFrom(targetComponentType)) {
					ConversionExecutor elementConverter = new StaticConversionExecutor(converter.getSourceClass(),
							targetComponentType, converter);
					Converter collectionToArray = new ReverseConverter(new ArrayToCollection(elementConverter));
					return new StaticConversionExecutor(sourceClass, targetClass, collectionToArray);
				} else if (converter.getSourceClass().isAssignableFrom(targetComponentType)
						&& converter instanceof TwoWayConverter) {
					TwoWayConverter twoWay = (TwoWayConverter) converter;
					ConversionExecutor elementConverter = new StaticConversionExecutor(converter.getTargetClass(),
							targetComponentType, new ReverseConverter(twoWay));
					Converter collectionToArray = new ReverseConverter(new ArrayToCollection(elementConverter));
					return new StaticConversionExecutor(sourceClass, targetClass, collectionToArray);
				} else {
					throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
							"Custom ConversionExecutor with id '" + id + "' cannot convert from collection of type ["
									+ sourceClass.getName() + "] to an array storing elements of type ["
									+ targetComponentType.getName() + "]");
				}
			} else {
				if (converter.getSourceClass().isAssignableFrom(sourceClass)) {
					if (!converter.getTargetClass().isAssignableFrom(targetComponentType)) {
						throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
								"Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass ["
										+ sourceClass.getName() + "] to array holding elements of type ["
										+ targetComponentType.getName() + "]");
					}
					ConversionExecutor elementConverter = new StaticConversionExecutor(sourceClass,
							targetComponentType, converter);
					return new StaticConversionExecutor(sourceClass, targetClass, new ObjectToArray(elementConverter));
				} else if (converter.getTargetClass().isAssignableFrom(sourceClass)
						&& converter instanceof TwoWayConverter) {
					if (!converter.getSourceClass().isAssignableFrom(targetComponentType)) {
						throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
								"Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass ["
										+ sourceClass.getName() + "] to array holding elements of type ["
										+ targetComponentType.getName() + "]");
					}
					TwoWayConverter twoWay = (TwoWayConverter) converter;
					ConversionExecutor elementConverter = new StaticConversionExecutor(sourceClass,
							targetComponentType, new ReverseConverter(twoWay));
					return new StaticConversionExecutor(sourceClass, targetClass, new ObjectToArray(elementConverter));
				}
			}
		}
		if (Collection.class.isAssignableFrom(targetClass)) {
			if (Collection.class.isAssignableFrom(sourceClass)) {
				ConversionExecutor elementConverter;
				// type erasure forces us to do runtime checks of list elements
				if (converter instanceof TwoWayConverter) {
					elementConverter = new TwoWayCapableConversionExecutor(converter.getSourceClass(),
							converter.getTargetClass(), (TwoWayConverter) converter);
				} else {
					elementConverter = new StaticConversionExecutor(converter.getSourceClass(),
							converter.getTargetClass(), converter);
				}
				return new StaticConversionExecutor(sourceClass, targetClass, new CollectionToCollection(
						elementConverter));
			} else {
				ConversionExecutor elementConverter;
				// type erasure forces us to do runtime checks of list elements
				if (converter instanceof TwoWayConverter) {
					elementConverter = new TwoWayCapableConversionExecutor(sourceClass, converter.getTargetClass(),
							(TwoWayConverter) converter);
				} else {
					elementConverter = new StaticConversionExecutor(sourceClass, converter.getTargetClass(), converter);
				}
				if (!Collection.class.isAssignableFrom(converter.getTargetClass())) {
					elementConverter = new StaticConversionExecutor(sourceClass, targetClass, new ObjectToCollection(
							elementConverter));
				}
				return elementConverter;
			}
		}
		if (converter.getSourceClass().isAssignableFrom(sourceClass)) {
			if (!converter.getTargetClass().isAssignableFrom(targetClass)) {
				throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
						"Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass ["
								+ sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]");
			}
			return new StaticConversionExecutor(sourceClass, targetClass, converter);
		} else if (converter.getTargetClass().isAssignableFrom(sourceClass) && converter instanceof TwoWayConverter) {
			if (!converter.getSourceClass().isAssignableFrom(targetClass)) {
				throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
						"Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass ["
								+ sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]");
			}
			TwoWayConverter twoWay = (TwoWayConverter) converter;
			return new StaticConversionExecutor(sourceClass, targetClass, new ReverseConverter(twoWay));
		} else {
			throw new ConversionExecutorNotFoundException(sourceClass, targetClass,
					"Custom ConversionExecutor with id '" + id + "' cannot convert from sourceClass ["
							+ sourceClass.getName() + "] to targetClass [" + targetClass.getName() + "]");
		}
	}

	public Object executeConversion(Object source, Class<?> targetClass) throws ConversionException {
		if (source != null) {
			ConversionExecutor conversionExecutor = getConversionExecutor(source.getClass(), targetClass);
			return conversionExecutor.execute(source);
		} else {
			return null;
		}
	}

	public Object executeConversion(String converterId, Object source, Class<?> targetClass) throws ConversionException {
		if (source != null) {
			ConversionExecutor conversionExecutor = getConversionExecutor(converterId, source.getClass(), targetClass);
			return conversionExecutor.execute(source);
		} else {
			return null;
		}
	}

	public Class<?> getClassForAlias(String name) throws IllegalArgumentException {
		Class<?> clazz = aliasMap.get(name);
		if (clazz != null) {
			return clazz;
		} else {
			if (parent != null) {
				return parent.getClassForAlias(name);
			} else {
				return null;
			}
		}
	}

	// internal helpers

	private Class<?> convertToWrapperClassIfNecessary(Class<?> targetType) {
		if (targetType.isPrimitive()) {
			if (targetType.equals(int.class)) {
				return Integer.class;
			} else if (targetType.equals(short.class)) {
				return Short.class;
			} else if (targetType.equals(long.class)) {
				return Long.class;
			} else if (targetType.equals(float.class)) {
				return Float.class;
			} else if (targetType.equals(double.class)) {
				return Double.class;
			} else if (targetType.equals(byte.class)) {
				return Byte.class;
			} else if (targetType.equals(boolean.class)) {
				return Boolean.class;
			} else if (targetType.equals(char.class)) {
				return Character.class;
			} else {
				throw new IllegalStateException("Should never happen - primitive type is not a primitive?");
			}
		} else {
			return targetType;
		}
	}

}
