/*
 * 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.bean;

import nuts.core.bean.handlers.ArrayBeanHandler;
import nuts.core.bean.handlers.AtomicBeanHandler;
import nuts.core.bean.handlers.IterableBeanHandler;
import nuts.core.bean.handlers.JavaBeanHandler;
import nuts.core.bean.handlers.ListBeanHandler;
import nuts.core.bean.handlers.MapBeanHandler;
import nuts.core.lang.Arrays;
import nuts.core.lang.Classes;
import nuts.core.lang.Types;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 */
public class Beans {
	/**
	 * instance
	 */
	private static Beans me;

	static {
		try {
			me = (Beans)Classes.newInstance(
					Beans.class.getPackage().getName().toString()
					+ ".FastBeans");
		}
		catch (Throwable e) {
			me = new Beans();
		}
	}

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

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

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

	// ------------------------------------------------------------------------
	public final static String[] RESERVED_PROPERTY_NAMES = { "class", "declaringClass", "metaClass" };
	public final static Class<?>[] RESERVED_PROPERTY_TYPES = { Class.class };
	
	private static Class<?>[] ATOMIC_TYPES = {
		String.class,
		Date.class,
		Calendar.class,
		BigDecimal.class,
		BigInteger.class
	};
	
	public static boolean isReservedProperty(String propertyName) {
		return Arrays.contains(RESERVED_PROPERTY_NAMES, propertyName);
	}
	
	public static boolean isReservedProperty(Type propertyType) {
		return Arrays.contains(RESERVED_PROPERTY_TYPES, propertyType);
	}

	public static boolean isAtomicType(Class<?> type) {
		if (type.isEnum()) {
			return true;
		}
		if (Classes.isPrimitiveOrWrapper(type)) {
			return true;
		}
		return Arrays.contains(ATOMIC_TYPES, type);
	}

	protected static void assertBeanAndName(Object bean, String name) {
		if (bean == null) {
			throw new IllegalArgumentException("argument bean is null.");
		}
		if (name == null) {
			throw new IllegalArgumentException("argument name is null.");
		}
	}
	
	/**
	 * <p>
	 * Return the value of the property of the specified name,
	 * for the specified bean, with no type conversions.
	 * </p>
	 * 
	 * @param bean Bean whose property is to be extracted
	 * @param name the property to be extracted
	 * @return the property value
	 * 
	 * @exception IllegalArgumentException if <code>bean</code> or
	 *                <code>name</code> is null
	 *                throws an exception
	 */
	@SuppressWarnings("unchecked")
	public static Object getBean(Object bean, String name) {
		assertBeanAndName(bean, name);
		
		BeanHandler bh = me().getBeanHandler(bean.getClass());

		return bh.getBeanValue(bean, name);
	}

	/**
	 * <p>
	 * set the value of the property of the specified name.
	 * for the specified bean, with no type conversions.
	 * </p>
	 * 
	 * @param bean Bean whose property is to be set
	 * @param name the property name 
	 * @param value the property value to be set
	 * 
	 * @exception IllegalArgumentException if <code>bean</code> or
	 *                <code>name</code> is null
	 *                throws an exception
	 */
	@SuppressWarnings("unchecked")
	public static void setBean(Object bean, String name, Object value) {
		assertBeanAndName(bean, name);
		
		BeanHandler bh = me().getBeanHandler(bean.getClass());

		bh.setBeanValue(bean, name, value);
	}

	/**
	 * <p>
	 * Return the value of the property of the specified name,
	 * for the specified bean, with no type conversions.
	 * </p>
	 * 
	 * @param bean Bean whose property is to be extracted
	 * @param name the property to be extracted
	 * @return the property value
	 * 
	 * @exception IllegalArgumentException if <code>bean</code> or
	 *                <code>name</code> is null
	 *                throws an exception
	 */
	@SuppressWarnings("unchecked")
	public static Object getProperty(Object bean, String name) {
		assertBeanAndName(bean, name);
		
		BeanHandler bh = me().getBeanHandler(bean.getClass());

		return bh.getPropertyValue(bean, name);
	}

	/**
	 * <p>
	 * set the value of the property of the specified name.
	 * for the specified bean, with no type conversions.
	 * </p>
	 * 
	 * @param bean Bean whose property is to be set
	 * @param name the property name 
	 * @param value the property value to be set
	 * 
	 * @exception IllegalArgumentException if <code>bean</code> or
	 *                <code>name</code> is null
	 *                throws an exception
	 */
	@SuppressWarnings("unchecked")
	public static void setProperty(Object bean, String name, Object value) {
		assertBeanAndName(bean, name);
		
		BeanHandler bh = me().getBeanHandler(bean.getClass());

		bh.setPropertyValue(bean, name, value);
	}

	// ------------------------------------------------------------------------
	/**
	 * handler map
	 */
	protected Map<Type, BeanHandler> handlers = new ConcurrentHashMap<Type, BeanHandler>();

	/**
	 * prepareBeanHandler
	 * 
	 * @param type class type
	 */
	public void prepareBeanHandler(Type type) {
		getBeanHandler(type);
	}
	
	/**
	 * prepareBeanHandler
	 * 
	 * @param types class type array
	 */
	public void prepareBeanHandler(Type[] types) {
		for (Type type : types) {
			prepareBeanHandler(type);
		}
	}

	/**
	 * prepareBeanHandler
	 * 
	 * @param types class type collection
	 */
	public void prepareBeanHandler(Collection<Type> types) {
		for (Type type : types) {
			prepareBeanHandler(type);
		}
	}

	/**
	 * Register (add) a bean handler for a class
	 * 
	 * @param type - the class
	 * @param handler - the handler instance
	 */
	public void register(Type type, BeanHandler handler) {
		handlers.put(type, handler);
	}
	
	/**
	 * Unregister (remove) a bean handler for a class
	 * 
	 * @param type - the class
	 */
	public void unregister(Type type) {
		handlers.remove(type);
	}
	
	/**
	 * clear bean handlers
	 */
	public void clear() {
		handlers.clear();
	}

	/**
	 * getBeanHandler
	 * @param type bean type
	 * @return BeanHandler
	 */
	public <T> BeanHandler<T> getBeanHandler(Class<T> type) {
		return getBeanHandler((Type)type);
	}
	
	/**
	 * getBeanHandler
	 * @param type bean type
	 * @return BeanHandler
	 */
	@SuppressWarnings("unchecked")
	public <T> BeanHandler<T> getBeanHandler(Type type) {
		if (type == null) {
			return new AtomicBeanHandler(Object.class); 
		}

		BeanHandler<T> handler = handlers.get(type);
		if (handler == null) {
			if (Types.isArrayType(type)) {
				handler = new ArrayBeanHandler(this, type);
			}
			else if (Types.isAssignable(type, Map.class)) {
				handler = new MapBeanHandler(this, type);
			}
			else if (Types.isAssignable(type, List.class)) {
				handler = new ListBeanHandler(this, type);
			}
			else if (Types.isAssignable(type, Iterable.class)) {
				handler = new IterableBeanHandler(this, type);
			}
			else if (isAtomicJavaType(type)) {
				handler = new AtomicBeanHandler(type); 
			}
			else {
				handler = createJavaBeanHandler(type);
				register(type, handler);
			}
		}
		return handler;
	}

	/**
	 * is primitive java type
	 * @param type class type
	 * @return true if the type is a simple java type
	 */
	protected boolean isAtomicJavaType(Type type) {
		Class clazz = Types.getRawType(type);
		return Beans.isAtomicType(clazz);
	}
	
	/**
	 * create java bean handler
	 * @param type bean type
	 * @return BeanHandler
	 */
	protected BeanHandler createJavaBeanHandler(Type type) {
		return new JavaBeanHandler(this, type);
	}
}
