package org.castor.cpa.jpa.processors;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;

import org.castor.core.annotationprocessing.AnnotationTargetException;

/**
 * Tool class that offers methods to retrieve information from
 * {@link AnnotatedElement}s (Methods or Fields).
 * 
 * @author Peter Schmidt
 * @version 12.02.2009
 * 
 */
public final class ReflectionsHelper {

    /**
     * Private Constructor to prevent instantiation.
     */
    private ReflectionsHelper() {
    }

    /**
     * Get the (raw) type of collection from the annotations target and optional
     * check for JPA 1.0 restrictions (only Collection, Map, List and Set are
     * allowed).
     * 
     * @param target
     *            The ManyToMany annotations target.
     * @param jpaTypesOnly
     *            If set to true only Collection types of JPA 1.0 are allowed.
     *            Others will lead to a thrown Exception.
     * @return The raw collection type.
     * @throws AnnotationTargetException
     *             if the raw collection type can not be inferred from the type
     *             definition or if the type is not supported by JPA 1.0 and
     *             jpaTypesOnly was set to true.
     */
    public static Class<?> getCollectionType(final AnnotatedElement target,
            final boolean jpaTypesOnly) throws AnnotationTargetException {
        Class<?> collectionType = null;
        Type fieldType;
        if (target instanceof Field) {
            fieldType = ((Field) target).getGenericType();
        } else {
            fieldType = ((Method) target).getGenericReturnType();
        }

        if (fieldType instanceof ParameterizedType) {
            Type rawType = ((ParameterizedType) fieldType).getRawType();
            if (rawType instanceof Class<?>) {
                collectionType = (Class<?>) rawType;
            } else {
                String message = "Can not infer raw type of generic type definition for field "
                        + ((Member) target).getName()
                        + ". Raw type is not a Class object.";
                throw new AnnotationTargetException(message);
            }
        } else if (fieldType instanceof Class<?>) {
            collectionType = (Class<?>) fieldType;
        }
        String collectionTypeName = collectionType.getSimpleName();
        if (jpaTypesOnly) {
            if ((!collectionTypeName.equals("Collection"))
                    && (!collectionTypeName.equals("Map"))
                    && (!collectionTypeName.equals("List"))
                    && (!collectionTypeName.equals("Set"))) {

                throw new AnnotationTargetException(
                        collectionTypeName
                                + " is not supported by JPA 1.0! Only Collection, Set, "
                                + "List or Map are allowed types of OneToMany properties!");
            }
        }
        return collectionType;
    }

    /**
     * Get the target entity information from the target (necessary if it was
     * not specified by the annotation itself).
     * 
     * @param target
     *            The *ToMany annotations target. This method will fail if
     *            target is not of Type {@link Field} or {@link Method}!
     * @return the relations target entity inferred by the targets generic
     *         definition or null if no generic definition was found at all.
     * @throws AnnotationTargetException
     *             if the generic definition is not sufficient
     */
    public static Class<?> getTargetEntityFromGenerics(
            final AnnotatedElement target) throws AnnotationTargetException {

        Type fieldType = null;
        if (target instanceof Field) {
            fieldType = ((Field) target).getGenericType();
        } else if (target instanceof Method) {
            fieldType = ((Method) target).getGenericReturnType();
        } else {
            throw new IllegalArgumentException(
                    "Can only read generic definitions from Methods or Fields.");
        }

        Class<?> targetEntity = null;
        if (fieldType instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) fieldType)
                    .getActualTypeArguments();
            Type targetType;
            // check wildcard generic => use upper bound
            if (actualTypeArguments[0] instanceof WildcardType) {
                targetType = ((WildcardType) actualTypeArguments[0])
                        .getUpperBounds()[0];
            } else {
                targetType = actualTypeArguments[0];
            }

            if (targetType instanceof Class<?>) {
                targetEntity = (Class<?>) targetType;
            } else {
                // Error => can not infer targetEntity from generic
                // definition
                String message = "Can not infer target entity for ManyToOne relation on "
                        + ((Member) target).getName()
                        + " - use simplier generics or specify targetEntity!";
                throw new AnnotationTargetException(message);
            }

        } else {
            return null;
        }

        return targetEntity;
    }

    /**
     * Little helper to get a field (bean property) name from a getter method. <br/>
     * Example: <code>getFoo() => "foo"</code><br/>
     * Example: <code>isBar() => "bar"</code>
     * 
     * @param getter
     *            the {@link Method} to analyse.
     * @return the field name determined by the methods name or NULL if the
     *         method is not a getter (start with "get" or "is").
     */
    public static String getFieldnameFromGetter(final Method getter) {
        String[] getterPrefixes = {"get", "is"};

        String methodName = getter.getName();
        String fieldName = null;

        for (String prefix : getterPrefixes) {
            if (methodName.startsWith(prefix)) {
                fieldName = methodName.substring(prefix.length());
            }
        }

        if (fieldName == null) {
            return null;
        }

        fieldName = fieldName.substring(0, 1).toLowerCase()
                + fieldName.substring(1);
        return fieldName;
    }

    /**
     * Return a setter {@link Method} for a given getter {@link Method}.
     * 
     * @param getter
     *            The getter {@link Method}
     * @return The setter Method for the given getter Method.
     * @throws NoSuchMethodException
     *             If the setter method does not exist
     * @throws SecurityException
     *             If the setter method is not accessible
     */
    public static Method getSetterMethodFromGetter(final Method getter)
            throws SecurityException, NoSuchMethodException {
        if (!isGetter(getter)) {
            throw new IllegalArgumentException("Method is not a getter method!");
        }

        String[] prefixes = {"get", "is"};

        String setterName;
        for (String prefix : prefixes) {
            if (getter.getName().startsWith(prefix)) {
                String name = getter.getName().substring(prefix.length());
                setterName = "set" + name;
                return getter.getDeclaringClass().getDeclaredMethod(setterName,
                        getter.getReturnType());
            }
        }
        throw new IllegalArgumentException(
                "Method name does not start with 'get' or 'is'!");
    }

    /**
     * Convenience method to check whether a {@link Method} is a getter method,
     * i.e. starts with "get" or "is".
     * 
     * @param method
     *            the {@link Method} to check.
     * @return true if the methods name starts with "get" or "is" (Java Beans
     *         convention).
     */
    public static boolean isGetter(final Method method) {
        if (method.getName().startsWith("get")
                || method.getName().startsWith("is")) {
            return true;
        }
        return false;
    }

}
