/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 * 
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.core.castor;

import nuts.core.bean.BeanHandler;
import nuts.core.bean.Beans;
import nuts.core.castor.castors.ArrayCastor;
import nuts.core.castor.castors.JavaBeanCastor;
import nuts.core.castor.castors.CollectionCastor;
import nuts.core.castor.castors.DateTypeCastor;
import nuts.core.castor.castors.MapCastor;
import nuts.core.castor.castors.NumberTypeCastor;
import nuts.core.castor.castors.ObjectCastor;
import nuts.core.castor.castors.PrimitiveTypeCastor;
import nuts.core.castor.castors.PrimitiveWrapCastor;
import nuts.core.castor.castors.StringTypeCastor;
import nuts.core.lang.Asserts;
import nuts.core.lang.Types;
import nuts.core.lang.collection.MultiKey;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class Castors {
	private static Castors me = new Castors();

	/**
	 * @return instance
	 */
	public static Castors me() {
		return me;
	}

	/**
	 * @return instance
	 */
	public static Castors getMe() {
		return me;
	}

	/**
	 * @param instance the instance to set
	 */
	public static void setMe(Castors instance) {
		Castors.me = instance;
	}

	// ------------------------------------------------------------------------
	private Beans beans = Beans.me();
	private Map<MultiKey, Castor> castors = new ConcurrentHashMap<MultiKey, Castor>();
	
	/**
	 * Constructor
	 */
	public Castors() {
		register(Object.class, Boolean.TYPE, new PrimitiveTypeCastor.BooleanCastor());
		register(Object.class, Byte.TYPE, new PrimitiveTypeCastor.ByteCastor());
		register(Object.class, Character.TYPE, new PrimitiveTypeCastor.CharacterCastor());
		register(Object.class, Double.TYPE, new PrimitiveTypeCastor.DoubleCastor());
		register(Object.class, Float.TYPE, new PrimitiveTypeCastor.FloatCastor());
		register(Object.class, Integer.TYPE, new PrimitiveTypeCastor.IntegerCastor());
		register(Object.class, Long.TYPE, new PrimitiveTypeCastor.LongCastor());
		register(Object.class, Short.TYPE, new PrimitiveTypeCastor.ShortCastor());

		register(new PrimitiveWrapCastor.BooleanCastor());
		register(new PrimitiveWrapCastor.ByteCastor());
		register(new PrimitiveWrapCastor.CharacterCastor());
		register(new PrimitiveWrapCastor.DoubleCastor());
		register(new PrimitiveWrapCastor.FloatCastor());
		register(new PrimitiveWrapCastor.IntegerCastor());
		register(new PrimitiveWrapCastor.LongCastor());
		register(new PrimitiveWrapCastor.ShortCastor());

		register(new NumberTypeCastor.NumberCastor());
		register(new NumberTypeCastor.BigIntegerCastor());
		register(new NumberTypeCastor.BigDecimalCastor());

		DateTypeCastor.DateCastor dc = new DateTypeCastor.DateCastor();
		register(dc);
		register(new DateTypeCastor.CalendarCastor(dc));
		
		register(new StringTypeCastor.StringCastor(dc));
		register(new StringTypeCastor.StringBufferCastor(dc));
		register(new StringTypeCastor.StringBuilderCastor(dc));
	}
	
	public Beans getBeans() {
		return beans;
	}

	public void setBeans(Beans beans) {
		this.beans = beans;
	}

	/**
	 * Register (add) a castor for a class
	 * 
	 * @param castor - the castor instance
	 */
	public void register(AbstractCastor castor) {
		register(castor.getFromType(), castor.getToType(), castor);
	}

	/**
	 * Register (add) a castor for a class
	 * 
	 * @param toType - the class
	 * @param castor - the castor instance
	 */
	public void register(Type fromType, Type toType, Castor castor) {
		castors.put(new MultiKey(fromType, toType), castor);
	}
	
	/**
	 * Unregister (remove) a castor for a class
	 * 
	 * @param toType - the class
	 */
	public void unregister(Type fromType, Type toType) {
		castors.remove(new MultiKey(fromType, toType));
	}
	
	/**
	 * clear converters
	 */
	public void clear() {
		castors.clear();
	}
	
	/**
	 * getCastor
	 * @param type object type
	 * @return Castor
	 */
	public <T> Castor<Object, T> getCastor(Class<T> type) {
		return getCastor((Type)type);
	}
	
	/**
	 * getCastor
	 * @param toType object type
	 * @return Castor
	 */
	public <T> Castor<Object, T> getCastor(Type toType) {
		return getCastor(Object.class, toType);
	}
	
	/**
	 * getCastor
	 * @param toType object type
	 * @return Castor
	 */
	@SuppressWarnings("unchecked")
	public <S, T> Castor<S, T> getCastor(Type fromType, Type toType) {
		Asserts.notNull(fromType, "The from type is null");
		Asserts.notNull(toType, "The to type is null");

		// type -> type castor
		Castor<S, T> castor = castors.get(new MultiKey(fromType, toType));
		if (castor != null) {
			return castor;
		}
		
		// object -> type castor
		if (!Object.class.equals(fromType)) {
			castor = castors.get(new MultiKey(Object.class, toType));
			if (castor != null) {
				return castor;
			}
		}
		
		// default castor
		if (Object.class.equals(toType)) {
			return new ObjectCastor();
		}
		else if (Types.isArrayType(toType)) {
			castor = new ArrayCastor(fromType, toType, this);
		}
		else if (Types.isAssignable(toType, Number.class)) {
			castor = (Castor<S, T>)new NumberTypeCastor();
		}
		else if (Types.isAbstractType(toType)) {
			if (toType instanceof ParameterizedType) {
				ParameterizedType pt = (ParameterizedType)toType;
				Type rawType = pt.getRawType();
				if (Types.isAssignable(rawType, List.class)) {
					rawType = ArrayList.class;
				}
				else if (Types.isAssignable(rawType, Map.class)) {
					rawType = LinkedHashMap.class;
				}
				else if (Types.isAssignable(rawType, Set.class)) {
					rawType = LinkedHashSet.class;
				}
				else {
					return new ObjectCastor();
				}
				
				toType = Types.paramTypeOfOwner(pt.getOwnerType(), rawType, pt.getActualTypeArguments());
				return (Castor<S, T>)getCastor(toType);
			}

			if (Types.isAssignable(toType, List.class)) {
				castor = (Castor<S, T>)new CollectionCastor(fromType, ArrayList.class, this);
			}
			else if (Types.isAssignable(toType, Map.class)) {
				castor = (Castor<S, T>)new MapCastor(fromType, LinkedHashMap.class, this);
			}
			else if (Types.isAssignable(toType, Set.class)) {
				castor = (Castor<S, T>)new CollectionCastor(fromType, LinkedHashSet.class, this);
			}
			else {
				castor = new ObjectCastor();
			}
		}
		else if (Types.isAssignable(toType, Map.class)) {
			castor = (Castor<S, T>)new MapCastor(fromType, toType, this);
		}
		else if (Types.isAssignable(toType, Collection.class)) {
			castor = (Castor<S, T>)new CollectionCastor(fromType, toType, this);
		}
		else {
			castor = new JavaBeanCastor(fromType, toType, this);
		}

		return castor;
	}

	public <T> BeanHandler<T> getBeanHandler(Type type) {
		return beans.getBeanHandler(type);
	}

	public <T> T cast(Object value, Class<T> toType) {
		return cast(value, toType, new CastContext());
	}

	public <T> T cast(Object value, Class<T> toType, CastContext context) {
		Castor<Object, T> c = getCastor(value == null ? Object.class : value.getClass(), toType);
		return c.cast(value, context);
	}

	public <T> T cast(Object value, Type toType) {
		return cast(value, toType, new CastContext());
	}

	public <T> T cast(Object value, Type toType, CastContext context) {
		Castor<Object, T> c = getCastor(value == null ? Object.class : value.getClass(), toType);
		return c.cast(value, context);
	}
}
