/*
 * 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.dao.sql.adapter;

import nuts.core.lang.Classes;
import nuts.core.lang.Types;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * a factory class for TypeHandler objects.
 */
public class TypeHandlers {
	private final Map<Type, Map<String, TypeHandler>> typeHandlerMap = new HashMap<Type, Map<String, TypeHandler>>();

	private final TypeHandler unknownTypeHandler = new UnknownTypeHandler(this);

	private final Map<String, String> typeAliases = new HashMap<String, String>();

	private static TypeHandlers me = new TypeHandlers();
	
	/**
	 * @return instance
	 */
	public static TypeHandlers me() {
		return me;
	}

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

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

	/**
	 * Constructor
	 */
	public TypeHandlers() {
		register(null, unknownTypeHandler);

		TypeHandler handler;

		register(Boolean.class, new BooleanTypeHandler());
		register(Byte.class, new ByteTypeHandler());
		register(Character.class, new CharacterTypeHandler());
		register(Short.class, new ShortTypeHandler());
		register(Integer.class, new IntegerTypeHandler());
		register(Long.class, new LongTypeHandler());
		register(Float.class, new FloatTypeHandler());
		register(Double.class, new DoubleTypeHandler());
		register(String.class, new StringTypeHandler());

		handler = new ClobTypeHandler();
		register(String.class, "CLOB", handler);
		register(String.class, "LONGVARCHAR", handler);

		register(BigDecimal.class, new BigDecimalTypeHandler());

		register(byte[].class, new ByteArrayTypeHandler());
		
		handler = new BlobTypeHandler();
		register(byte[].class, "BLOB", handler);
		register(byte[].class, "LONGVARBINARY", handler);

		handler = new ObjectTypeHandler();
		register(Object.class, handler);
		register(Object.class, "OBJECT", handler);

		register(Date.class, new DateTypeHandler());
		register(Date.class, "DATE", new DateOnlyTypeHandler());
		register(Date.class, "TIME", new TimeOnlyTypeHandler());

		register(java.sql.Date.class, new SqlDateTypeHandler());
		register(java.sql.Time.class, new SqlTimeTypeHandler());
		register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());

		register(List.class, "LIST", new CollectionTypeHandler(ArrayList.class));
		register(Map.class, "MAP", new CollectionTypeHandler(LinkedHashMap.class));
		register(Set.class, "SET", new CollectionTypeHandler(LinkedHashSet.class));

		
		putTypeAlias("string", String.class.getName());
		putTypeAlias("byte", Byte.class.getName());
		putTypeAlias("char", Character.class.getName());
		putTypeAlias("long", Long.class.getName());
		putTypeAlias("short", Short.class.getName());
		putTypeAlias("int", Integer.class.getName());
		putTypeAlias("integer", Integer.class.getName());
		putTypeAlias("double", Double.class.getName());
		putTypeAlias("float", Float.class.getName());
		putTypeAlias("boolean", Boolean.class.getName());
		putTypeAlias("date", Date.class.getName());
		putTypeAlias("decimal", BigDecimal.class.getName());
		putTypeAlias("object", Object.class.getName());
		putTypeAlias("map", Map.class.getName());
		putTypeAlias("hashmap", HashMap.class.getName());
		putTypeAlias("list", List.class.getName());
		putTypeAlias("arraylist", ArrayList.class.getName());
		putTypeAlias("set", Set.class.getName());
		putTypeAlias("hashset", HashSet.class.getName());
		putTypeAlias("collection", Collection.class.getName());
		putTypeAlias("iterator", Iterator.class.getName());
		putTypeAlias("cursor", java.sql.ResultSet.class.getName());
	}

	/**
	 * Get a TypeHandler for a class
	 * 
	 * @param type - the class you want a TypeHandler for
	 * 
	 * @return - the handler
	 */
	public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
		return getTypeHandler((Type)type, null);
	}

	/**
	 * Get a TypeHandler for a class and a JDBC type
	 * 
	 * @param type - the class
	 * @param jdbcType - the jdbc type
	 * 
	 * @return - the handler
	 */
	public <T> TypeHandler<T> getTypeHandler(Class<T> type, String jdbcType) {
		return getTypeHandler((Type)type, jdbcType);
	}
	
	/**
	 * Get a TypeHandler for a class
	 * 
	 * @param type - the class you want a TypeHandler for
	 * 
	 * @return - the handler
	 */
	public <T> TypeHandler<T> getTypeHandler(Type type) {
		return getTypeHandler(type, null);
	}

	/**
	 * Get a TypeHandler for a class and a JDBC type
	 * 
	 * @param type - the class
	 * @param jdbcType - the jdbc type
	 * 
	 * @return - the handler
	 */
	@SuppressWarnings("unchecked")
	public <T> TypeHandler<T> getTypeHandler(Type type, String jdbcType) {
		TypeHandler handler = null;

		Map<String, TypeHandler> jdbcHandlerMap = (Map<String, TypeHandler>) typeHandlerMap.get(type);
		if (jdbcHandlerMap != null) {
			handler = jdbcHandlerMap.get(jdbcType);
			if (handler == null) {
				handler = jdbcHandlerMap.get(null);
			}
		}
		
		if (handler == null && type != null) {
			Class clazz = Types.getRawType(type);
			if (clazz.isEnum()) {
				handler = new EnumTypeHandler(clazz);
			}
			else if ("LIST".equalsIgnoreCase(jdbcType) && List.class.isAssignableFrom(clazz)) {
				handler = new CollectionTypeHandler(type);
			}
			else if ("SET".equalsIgnoreCase(jdbcType) && Set.class.isAssignableFrom(clazz)) {
				handler = new CollectionTypeHandler(type);
			}
			else if ("MAP".equalsIgnoreCase(jdbcType) && Map.class.isAssignableFrom(clazz)) {
				handler = new CollectionTypeHandler(type);
			}
		}
		return handler;
	}

	/**
	 * When in doubt, get the "unknown" type handler
	 * 
	 * @return - if I told you, it would not be unknown, would it?
	 */
	public TypeHandler getUnkownTypeHandler() {
		return unknownTypeHandler;
	}

	/**
	 * Tells you if a particular class has a TypeHandler
	 * 
	 * @param type - the class
	 * 
	 * @return - true if there is a TypeHandler
	 */
	public boolean hasTypeHandler(Type type) {
		return type != null
				&& (getTypeHandler(type) != null 
				|| Types.getRawType(type).isEnum());
	}

	/**
	 * Register (add) a type handler for a class
	 * 
	 * @param type - the class
	 * @param handler - the handler instance
	 */
	public void register(Type type, TypeHandler handler) {
		register(type, null, handler);
	}

	/**
	 * Register (add) a type handler for a class and JDBC type
	 * 
	 * @param type - the class
	 * @param jdbcType - the JDBC type
	 * @param handler - the handler instance
	 */
	public void register(Type type, String jdbcType, TypeHandler handler) {
		Map<String, TypeHandler> map = typeHandlerMap.get(type);
		if (map == null) {
			map = new HashMap<String, TypeHandler>();
			typeHandlerMap.put(type, map);
		}
		map.put(jdbcType, handler);

		if (type instanceof Class && Classes.isPrimitiveWrapper((Class)type)) {
			register(Classes.wrapper2Primitive((Class)type), jdbcType, handler);
		}
	}

	/**
	 * Lookup an aliased class and return it's REAL name
	 * 
	 * @param string - the alias
	 * 
	 * @return - the REAL name
	 */
	public String resolveAlias(String string) {
		String key = null;
		if (string != null) {
			key = string.toLowerCase();
		}
		
		String value = null;
		if (typeAliases.containsKey(key)) {
			value = typeAliases.get(key);
		}
		else {
			value = string;
		}

		return value;
	}

	/**
	 * Adds a type alias that is case insensitive.  All of the following String, string, StRiNg will equate to the same alias.
	 * @param alias - the alias
	 * @param value - the real class name
	 */
	public void putTypeAlias(String alias, String value) {
		String key = null;
		if (alias != null)
			key = alias.toLowerCase();
		if (typeAliases.containsKey(key) && !typeAliases.get(key).equals(value)) {
			throw new IllegalArgumentException(
					"Alias name conflict occurred.  The alias '"
							+ key + "' is already mapped to the value '"
							+ typeAliases.get(alias) + "'.");
		}
		typeAliases.put(key, value);
	}

}
