/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.component.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Various Reflection utilities.
 * 
 * @version $Id$
 * @since 2.1RC1
 */
public final class ReflectionUtils
{
    /**
     * Utility class.
     */
    private ReflectionUtils()
    {
        // Utility class
    }

    /**
     * @param clazz the class for which to return all fields
     * @return all fields declared by the passed class and its superclasses
     */
    public static Collection<Field> getAllFields(Class< ? > clazz)
    {
        // Note: use a linked hash map to keep the same order as the one used to declare the fields.
        Map<String, Field> fields = new LinkedHashMap<String, Field>();
        Class< ? > targetClass = clazz;
        while (targetClass != null) {
            Field[] targetClassFields;
            try {
                targetClassFields = targetClass.getDeclaredFields();
            } catch (NoClassDefFoundError e) {
                // Provide a better exception message to more easily debug component loading issue.
                // Specifically with this error message we'll known which component failed to be initialized.
                throw new NoClassDefFoundError("Failed to get fields for class [" + targetClass.getName()
                    + "] because the class [" + e.getMessage() + "] couldn't be found in the ClassLoader.");
            }

            for (Field field : targetClassFields) {
                // Make sure that if the same field is declared in a class and its superclass
                // only the field used in the class will be returned. Note that we need to do
                // this check since the Field object doesn't implement the equals method using
                // the field name.
                if (!fields.containsKey(field.getName())) {
                    fields.put(field.getName(), field);
                }
            }
            targetClass = targetClass.getSuperclass();
        }
        return fields.values();
    }

    /**
     * @param clazz the class for which to return all fields
     * @param fieldName the name of the field to get
     * @return the field specified from either the passed class or its superclasses
     * @exception NoSuchFieldException if the field doesn't exist in the class or superclasses
     */
    public static Field getField(Class< ? > clazz, String fieldName) throws NoSuchFieldException
    {
        Field resultField = null;
        Class< ? > targetClass = clazz;
        while (targetClass != null) {
            try {
                resultField = targetClass.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                // Look in superclass
                targetClass = targetClass.getSuperclass();
            }
        }

        if (resultField == null) {
            throw new NoSuchFieldException("No field named [" + fieldName + "] in class [" + clazz.getName()
                + "] or superclasses");
        }

        return resultField;
    }

    /**
     * Extract the main class from the passed {@link Type}.
     * 
     * @param type the generic {@link Type}
     * @return the main Class of the generic {@link Type}
     * @since 4.0M1
     */
    public static Class getTypeClass(Type type)
    {
        Class typeClassClass = null;

        if (type instanceof Class) {
            typeClassClass = (Class) type;
        } else if (type instanceof ParameterizedType) {
            typeClassClass = (Class) ((ParameterizedType) type).getRawType();
        } else if (type instanceof GenericArrayType) {
            Class< ? > arrrayParameter = getTypeClass(((GenericArrayType) type).getGenericComponentType());
            if (arrrayParameter != null) {
                typeClassClass = Array.newInstance(arrrayParameter, 0).getClass();
            }
        }

        return typeClassClass;
    }

    /**
     * Sets a value to a field using reflection even if the field is private.
     * 
     * @param instanceContainingField the object containing the field
     * @param fieldName the name of the field in the object
     * @param fieldValue the value to set for the provided field
     */
    public static void setFieldValue(Object instanceContainingField, String fieldName, Object fieldValue)
    {
        // Find the class containing the field to set
        Class< ? > targetClass = instanceContainingField.getClass();
        while (targetClass != null) {
            for (Field field : targetClass.getDeclaredFields()) {
                if (field.getName().equalsIgnoreCase(fieldName)) {
                    try {
                        boolean isAccessible = field.isAccessible();
                        try {
                            field.setAccessible(true);
                            field.set(instanceContainingField, fieldValue);
                        } finally {
                            field.setAccessible(isAccessible);
                        }
                    } catch (Exception e) {
                        // This shouldn't happen but if it does then the Component manager will not function properly
                        // and we need to abort. It probably means the Java security manager has been configured to
                        // prevent accessing private fields.
                        throw new RuntimeException("Failed to set field [" + fieldName + "] in instance of ["
                            + instanceContainingField.getClass().getName() + "]. The Java Security Manager has "
                            + "probably been configured to prevent settting private field values. XWiki requires "
                            + "this ability to work.", e);
                    }
                    return;
                }
            }
            targetClass = targetClass.getSuperclass();
        }
    }

    /**
     * Extract the last generic type from the passed field. For example {@code private List&lt;A, B&gt; field} would
     * return the {@code B} class.
     * 
     * @param field the field from which to extract the generic type
     * @return the class of the last generic type or null if the field doesn't have a generic type
     */
    public static Class< ? > getLastGenericFieldType(Field field)
    {
        return getTypeClass(getLastFieldGenericArgument(field));
    }

    /**
     * Extract the last generic type from the passed field. For example {@code private List&lt;A, B&gt; field} would
     * return the {@code B} class.
     * 
     * @param field the field from which to extract the generic type
     * @return the type of the last generic type or null if the field doesn't have a generic type
     * @since 4.0M1
     */
    public static Type getLastFieldGenericArgument(Field field)
    {
        return getLastTypeGenericArgument(field.getGenericType());
    }

    /**
     * Extract the last generic type from the passed Type. For example {@code private List&lt;A, B&gt; field} would
     * return the {@code B} class.
     * 
     * @param type the type from which to extract the generic type
     * @return the type of the last generic type or null if the field doesn't have a generic type
     * @since 4.0M1
     */
    public static Type getLastTypeGenericArgument(Type type)
    {
        if (type instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) type;
            Type[] types = pType.getActualTypeArguments();
            if (types.length > 0) {
                return types[types.length - 1];
            }
        }

        return null;
    }

    /**
     * Extract the last generic type from the passed class. For example
     * {@code public Class MyClass implements FilterClass&lt;A, B&gt;, SomeOtherClass&lt;C&gt;} will return {@code B}.
     * 
     * @param clazz the class to extract from
     * @param filterClass the class of the generic type we're looking for
     * @return the last generic type from the interfaces of the passed class, filtered by the passed filter class
     */
    public static Class< ? > getLastGenericClassType(Class clazz, Class filterClass)
    {
        Type type = getGenericClassType(clazz, filterClass);

        if (type instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) type;
            if (filterClass.isAssignableFrom((Class) pType.getRawType())) {
                Type[] actualTypes = pType.getActualTypeArguments();
                if (actualTypes.length > 0 && actualTypes[actualTypes.length - 1] instanceof Class) {
                    return (Class) actualTypes[actualTypes.length - 1];
                }
            }
        }

        return null;
    }

    /**
     * Extract the real Type from the passed class. For example
     * {@code public Class MyClass implements FilterClass&lt;A, B&gt;, SomeOtherClass&lt;C&gt;} will return
     * {@code FilterClass&lt;A, B&gt;, SomeOtherClass&lt;C&gt;}.
     * 
     * @param clazz the class to extract from
     * @param filterClass the class of the generic type we're looking for
     * @return the real Type from the interfaces of the passed class, filtered by the passed filter class
     * @since 4.0M1
     */
    public static Type getGenericClassType(Class clazz, Class filterClass)
    {
        // Get all interfaces implemented and find the one that's a Provider with a Generic type
        for (Type type : clazz.getGenericInterfaces()) {
            if (type == filterClass) {
                return type;
            } else if (type instanceof ParameterizedType) {
                ParameterizedType pType = (ParameterizedType) type;
                if (filterClass.isAssignableFrom((Class) pType.getRawType())) {
                    return type;
                }
            }
        }

        return null;
    }

    /**
     * @param parameters the parameters of a direct superclass or interface
     * @param childType a extending class as Type
     * @return the actual parameters of the direct superclass or interface, return null if it's impossible to resolve
     */
    public static Type[] resolveSuperArguments(Type[] parameters, Type childType)
    {
        Type[] resolvedPrameters = null;

        if (parameters != null && childType instanceof ParameterizedType) {
            ParameterizedType parameterizedChildType = (ParameterizedType) childType;

            return resolveSuperArguments(parameters, parameterizedChildType.getClass(),
                parameterizedChildType.getActualTypeArguments());
        }

        return resolvedPrameters;
    }

    /**
     * @param parameters the parameters of a direct superclass or interface
     * @param childClass an extending class
     * @param childParameters the actual parameters of the extending class
     * @return the actual parameters of the direct superclass or interface, return null if it's impossible to resolve
     */
    public static Type[] resolveSuperArguments(Type[] parameters, Class childClass, Type[] childParameters)
    {
        Map<TypeVariable, Type> typeMapping;
        if (childParameters != null) {
            TypeVariable<Class>[] declaredChildParameters = childClass.getTypeParameters();

            typeMapping = new HashMap<TypeVariable, Type>();
            for (int i = 0; i < declaredChildParameters.length; ++i) {
                typeMapping.put(declaredChildParameters[i], childParameters[i]);
            }
        } else {
            typeMapping = Collections.emptyMap();
        }

        return resolveTypes(parameters, typeMapping);
    }

    /**
     * @param type the type to resolve
     * @param typeMapping the mapping between TypeVariable and real type
     * @return the resolved type, the passed type it does not need to be resolved or null if it can't be resolved
     */
    public static Type resolveType(Type type, Map<TypeVariable, Type> typeMapping)
    {
        Type resolvedType = type;

        if (type instanceof TypeVariable) {
            if (typeMapping == null) {
                return null;
            }

            resolvedType = typeMapping.get(type);
        } else if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;

            Type[] arguments = parameterizedType.getActualTypeArguments();
            Type[] resolvedArguments = resolveTypes(arguments, typeMapping);

            if (resolvedArguments != arguments) {
                resolvedType =
                    new DefaultParameterizedType(parameterizedType.getOwnerType(),
                        (Class< ? >) parameterizedType.getRawType(), resolvedArguments);
            } else {
                resolvedType = type;
            }
        }

        return resolvedType;
    }

    /**
     * @param types the types to resolve
     * @param typeMapping the mapping between TypeVariable and real type
     * @return the resolved types, the passed types if nothing need to be resolved or null if it can't be fully resolved
     */
    private static Type[] resolveTypes(Type[] types, Map<TypeVariable, Type> typeMapping)
    {
        Type[] resolvedTypes = types;

        for (int i = 0; i < types.length; ++i) {
            Type type = types[i];
            Type resovedType = resolveType(type, typeMapping);

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

            if (resovedType != type) {
                if (resolvedTypes == types) {
                    resolvedTypes = new Type[types.length];
                    for (int j = 0; j < i; ++j) {
                        resolvedTypes[j] = types[j];
                    }
                }
            }

            if (resolvedTypes != types) {
                resolvedTypes[i] = resovedType;
            }
        }

        return resolvedTypes;
    }

    /**
     * @param type the type for which to resolve the parameters
     * @param childType an extending class as Type
     * @return the Type with resolved parameters
     */
    public static Type resolveType(Type type, Type childType)
    {
        Type resolvedType = type;

        if (type instanceof ParameterizedType) {
            if (childType instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;

                Class< ? > parameterizedTypeClass = (Class) parameterizedType.getRawType();
                Type[] parameterizedTypeArguments = parameterizedType.getActualTypeArguments();

                Type[] resolvedParameters =
                    ReflectionUtils.resolveSuperArguments(parameterizedTypeArguments, childType);

                if (resolvedParameters == null) {
                    resolvedType = parameterizedTypeClass;
                } else if (resolvedParameters != parameterizedTypeArguments) {
                    resolvedType =
                        new DefaultParameterizedType(parameterizedType.getOwnerType(), parameterizedTypeClass,
                            resolvedParameters);
                }
            } else {
                resolvedType = getTypeClass(type);
            }
        }

        return resolvedType;
    }

    /**
     * Retrieve a {@link Type} object from it's serialized form.
     *
     * @param serializedType the serialized form of the {@link Type} to retrieve
     * @param classLoader the {@link ClassLoader} to look into to find the given {@link Type}
     * @return the type built from the given {@link String}
     * @throws ClassNotFoundException if no class corresponding to the passed serialized type can be found
     */
    public static Type unserializeType(String serializedType, ClassLoader classLoader) throws ClassNotFoundException
    {
        String sType = serializedType.replaceAll(" ", "");
        String inferior = "<";
        Type type = null;

        // A real parser could be used here but it would probably be overkill.
        if (sType.contains(inferior)) {
            // Parameterized type
            int firstInferior = sType.indexOf(inferior);
            int lastSuperior = sType.lastIndexOf(">");
            String rawType = sType.substring(0, firstInferior);
            String sArguments = sType.substring(firstInferior + 1, lastSuperior);
            List<Type> argumentTypes = new ArrayList<Type>();
            int nestedArgsDepth = 0;
            int previousSplit = 0;
            // We'll go through all the Type arguments and they will be unserialized, since arguments can be
            // ParameterizedTypes themselves we need to avoid parsing their arguments, that's why we need the
            // nestedArgsDepth counter.
            for (int i = 0; i < sArguments.length(); i++) {
                char current = sArguments.charAt(i);
                switch (current) {
                    case '<':
                        nestedArgsDepth++;
                        break;
                    case '>':
                        nestedArgsDepth--;
                        break;
                    case ',':
                        if (nestedArgsDepth == 0) {
                            argumentTypes.add(unserializeType(sArguments.substring(previousSplit, i), classLoader));
                            previousSplit = i + 1;
                        }
                        break;
                    default:
                        break;
                }
                if (i == sArguments.length() - 1) {
                    // We're at the end of the parameter list, we need to unserialize the Type of the last element.
                    // If there was only one argument it will be unserialized here.
                    argumentTypes.add(unserializeType(sArguments.substring(previousSplit), classLoader));
                }
            }

            type = new DefaultParameterizedType(null, Class.forName(rawType, false, classLoader),
                argumentTypes.toArray(new Type[1]));
        } else {
            // This was a simple type, no type arguments were found.
            type = Class.forName(sType, false, classLoader);
        }

        return type;
    }

    /**
     * Get the first found annotation with the provided class directly assigned to the provided {@link AnnotatedElement}
     * .
     * 
     * @param <T> the type of the annotation
     * @param annotationClass the annotation class
     * @param element the class on which annotation are assigned
     * @return the found annotation or null if there is none
     */
    public static <T extends Annotation> T getDirectAnnotation(Class<T> annotationClass, AnnotatedElement element)
    {
        // Handle interfaces directly declared in the passed component class
        for (Annotation annotation : element.getDeclaredAnnotations()) {
            if (annotation.annotationType() == annotationClass) {
                return (T) annotation;
            }
        }

        return null;
    }

    /**
     * @param type the type from which to extract super type and interfaces
     * @return the direct super type and interfaces for the provided type
     */
    public static List<Type> getDirectTypes(Type type)
    {
        Class< ? > clazz = getTypeClass(type);

        if (clazz == null) {
            return Collections.emptyList();
        }

        List<Type> types = new LinkedList<Type>();

        for (Type interfaceType : clazz.getGenericInterfaces()) {
            types.add(resolveType(interfaceType, type));
        }

        Type superType = clazz.getGenericSuperclass();
        if (superType != null) {
            types.add(resolveType(superType, type));
        }

        return types;
    }
}