/**
 * 
 */
package org.zkoss.zk.ui.select.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;

/**
 * A collection of reflection utilities.
 * @since 6.0.0
 * @author simonpai
 */
public class Reflections {

	// field scanner //
	/**
	 * Traverse through all fields with a certain annotation in a class and 
	 * it super classes. Subclass first.
	 * <p> All the fields with the same name will be scanned too. (since 6.5.1),
	 * before 6.5.1 release, only the subclass will be scanned.
	 */
	public static <A extends Annotation> void forFields(Class<?> clazz, Class<A> annotationClass,
			FieldRunner<A> runner) {
		for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
			for (Field f : c.getDeclaredFields()) {
				A anno = f.getAnnotation(annotationClass);
				if (anno == null)
					continue;
				f.setAccessible(true);
				runner.onField(c, f, anno);
			}
		}
	}

	// method scanner //
	/**
	 * Traverse through all methods with a certain annotation in a class, 
	 * including ones inherited from its super class.
	 */
	public static <A extends Annotation> void forMethods(Class<?> clazz, Class<A> annotationClass,
			MethodRunner<A> runner) {
		for (Method m : clazz.getMethods()) {
			A anno = m.getAnnotation(annotationClass);
			if (anno == null)
				continue;
			m.setAccessible(true);
			runner.onMethod(clazz, m, anno);
		}
	}

	// field information //
	/**
	 * Return true if field is of Collection type, and an object of type clazz
	 * can be added into the Collection.
	 */
	public static boolean isAppendableToCollection(Type type, Object object) {
		if (!Collection.class.isAssignableFrom(getClass(type)))
			return false;

		if (type instanceof ParameterizedType) {
			Class<?> aClass = getClass(
					((ParameterizedType) type).getActualTypeArguments()[0]);
			if (aClass != null)
				return aClass.isAssignableFrom(object.getClass());
		}

		return type instanceof Class<?>;
	}

	// method information //
	/**
	 * Return true if the given arguments can be passed as the arguments of 
	 * the method.
	 */
	public static boolean isPassableToMethod(Method method, Object... arguments) {
		Type[] types = method.getGenericParameterTypes();
		if (types.length != arguments.length)
			return false;
		for (int i = 0; i < types.length; i++) {
			Class<?> aClass = getClass(types[i]);
			if (aClass != null && !aClass.isAssignableFrom(arguments[i].getClass()))
				return false;
		}
		return true;
	}

	// field operation //
	/**
	 * Set a value to a field of the object.
	 */
	public static void setFieldValue(Object bean, Field field, Object value) {
		try {
			field.set(bean, value);

		} catch (IllegalArgumentException e) {
			throw new IllegalStateException("IllegalStateException @ setFieldValue");

		} catch (IllegalAccessException e) {
			throw new IllegalStateException("IllegalAccessException @ setFieldValue");
		}
	}

	/**
	 * Get field value.
	 */
	public static Object getFieldValue(Object bean, Field field) {
		try {
			return field.get(bean);
		} catch (IllegalArgumentException | IllegalAccessException e) {
			throw new IllegalStateException("IllegalStateException @ getFieldValue");
		}
	}

	/**
	 * Add the item to a collection field of an object.
	 */
	@SuppressWarnings("unchecked")
	public static void addToCollectionField(Object owner, Field field, Object item) {
		if (!isAppendableToCollection(field.getGenericType(), item))
			throw new IllegalArgumentException(item + " is not appendable to " + field);
		try {
			field.setAccessible(true);
			Collection c = (Collection) field.get(owner);
			if (c == null)
				throw new IllegalArgumentException(field + " has not been initiated.");
			c.add(item);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Invoke a method reflexively.
	 */
	public static void invokeMethod(Method method, Object bean, Object... arguments) {
		if (!isPassableToMethod(method, arguments))
			throw new IllegalArgumentException("Arguments does not match method signature: " + method);
		method.setAccessible(true);
		try {
			method.invoke(bean, arguments);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		}
	}

	// interface //
	public interface FieldRunner<A extends Annotation> {
		public void onField(Class<?> clazz, Field field, A annotation);
	}

	public interface MethodRunner<A extends Annotation> {
		public void onMethod(Class<?> clazz, Method method, A annotation);
	}

	/**
	 * Return the Class representing the given Type.
	 */
	public static Class<?> getClass(Type type) {
		if (type instanceof Class<?>)
			return (Class<?>) type;

		else if (type instanceof ParameterizedType)
			return getClass(((ParameterizedType) type).getRawType());

		else if (type instanceof GenericArrayType) {
			Class<?> c = getClass(((GenericArrayType) type).getGenericComponentType());
			if (c != null)
				return Array.newInstance(c, 0).getClass();
		}
		return null;
	}

}
