/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.webbeans.util;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.enterprise.inject.spi.InjectionPoint;

import org.apache.webbeans.exception.WebBeansConfigurationException;
import org.apache.webbeans.exception.WebBeansException;


/**
 * Utility classes with respect to the class operations.
 *
 * @author <a href="mailto:gurkanerdogdu@yahoo.com">Gurkan Erdogdu</a>
 * @since 1.0
 */
public final class ClassUtil
{
    public static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPERS_MAP;
    public static final Map<Class<?>, Object> DEFAULT_VALUES_MAP;

    static
    {
        Map<Class<?>, Class<?>> primitiveToWrappersMap = new HashMap<Class<?>, Class<?>>();
        primitiveToWrappersMap.put(Integer.TYPE,Integer.class);
        primitiveToWrappersMap.put(Float.TYPE,Float.class);
        primitiveToWrappersMap.put(Double.TYPE,Double.class);
        primitiveToWrappersMap.put(Character.TYPE,Character.class);
        primitiveToWrappersMap.put(Long.TYPE,Long.class);
        primitiveToWrappersMap.put(Byte.TYPE,Byte.class);
        primitiveToWrappersMap.put(Short.TYPE,Short.class);
        primitiveToWrappersMap.put(Boolean.TYPE,Boolean.class);
        primitiveToWrappersMap.put(Void.TYPE,Void.class);
        PRIMITIVE_TO_WRAPPERS_MAP = Collections.unmodifiableMap(primitiveToWrappersMap);
        Map<Class<?>, Object> defaultValuesMap = new HashMap<Class<?>, Object>();
        defaultValuesMap.put(Integer.TYPE, Integer.valueOf(0));
        defaultValuesMap.put(Float.TYPE, Float.valueOf(0F));
        defaultValuesMap.put(Double.TYPE, Double.valueOf(0D));
        defaultValuesMap.put(Character.TYPE, Character.valueOf('\u0000'));
        defaultValuesMap.put(Long.TYPE, Long.valueOf(0L));
        defaultValuesMap.put(Byte.TYPE, Byte.valueOf((byte)0));
        defaultValuesMap.put(Short.TYPE, Short.valueOf((short)0));
        defaultValuesMap.put(Boolean.TYPE, Boolean.FALSE);
        DEFAULT_VALUES_MAP = Collections.unmodifiableMap(defaultValuesMap);
    }

    public static final Type[] NO_TYPES = new Type[0];

    /*
     * Private constructor
     */
    private ClassUtil()
    {
        throw new UnsupportedOperationException();
    }

    public static Class<?> getClassFromName(String name)
    {
        Class<?> clazz;
        ClassLoader loader;
        try
        {
            loader = WebBeansUtil.getCurrentClassLoader();
            clazz = Class.forName(name, true , loader);
            return clazz;

        }
        catch (ClassNotFoundException e)
        {
            try
            {
                loader = ClassUtil.class.getClassLoader(); 
                clazz = Class.forName(name, true , loader);

                return clazz;

            }
            catch (ClassNotFoundException e1)
            {
                try
                {
                    loader = ClassLoader.getSystemClassLoader();
                    clazz = Class.forName(name, true , loader);

                    return clazz;

                }
                catch (ClassNotFoundException e2)
                {
                    return null;
                }
            }
        }
    }

    /**
     * Check the class is inner or not
     * 
     * @param clazz to check
     * @return true or false
     */
    public static boolean isInnerClazz(Class<?> clazz)
    {
        Asserts.nullCheckForClass(clazz);

        return clazz.isMemberClass();
    }

    public static boolean isSame(Type type1, Type type2)
    {
        if ((type1 instanceof Class) && ((Class<?>)type1).isPrimitive())
        {
            type1 = PRIMITIVE_TO_WRAPPERS_MAP.get(type1);
        }
        if ((type2 instanceof Class) && ((Class<?>)type2).isPrimitive())
        {
            type2 = PRIMITIVE_TO_WRAPPERS_MAP.get(type2);
        }
        return type1 == type2;
    }

    public static Class<?> getPrimitiveWrapper(Class<?> clazz)
    {
        Asserts.nullCheckForClass(clazz);
        
        return PRIMITIVE_TO_WRAPPERS_MAP.get(clazz);

    }

    public static Object getDefaultValue(Class<?> type)
    {
        return DEFAULT_VALUES_MAP.get(type);
    }

    /**
     * Gets the class of the given type arguments.
     * <p>
     * If the given type {@link Type} parameters is an instance of the
     * {@link ParameterizedType}, it returns the raw type otherwise it return
     * the casted {@link Class} of the type argument.
     * </p>
     * 
     * @param type class or parametrized type
     * @return
     */
    public static Class<?> getClass(Type type)
    {
        return getClazz(type);
    }


    /**
     * Check method throws checked exception or not.
     * 
     * @param method method instance
     */
    public static boolean isMethodHasCheckedException(Method method)
    {
        Asserts.nullCheckForMethod(method);

        Class<?>[] et = method.getExceptionTypes();

        if (et.length > 0)
        {
            for (Class<?> type : et)
            {
                if (!Error.class.isAssignableFrom(type) && !RuntimeException.class.isAssignableFrom(type))
                {
                    return true;
                }
            }

        }

        return false;
    }

    /**
     * Call method on the instance with given arguments.
     * 
     * @param method method instance
     * @param instance object instance
     * @param args arguments
     * @return the method result
     */
    public static Object callInstanceMethod(Method method, Object instance, Object[] args)
    {
        Asserts.nullCheckForMethod(method);

        try
        {
            if (args == null)
            {
                args = new Object[] {};
            }
            return method.invoke(instance, args);

        }
        catch (Exception e)
        {
            throw new WebBeansException("Exception occurs in the method call with method : " + method.getName() + " in class : " + instance.getClass().getName(), e);
        }

    }

    private static Set<String> getObjectMethodNames()
    {
        if (objectMethodNames == null)
        {
            // not much synchronisation needed...
            Set<String> list = new HashSet<String>();
            Class<?> clazz = Object.class;

            Method[] methods = SecurityUtil.doPrivilegedGetDeclaredMethods(clazz);
            for (Method method : methods)
            {
                list.add(method.getName());
            }
            objectMethodNames = list;
        }

        return objectMethodNames;
    }
    private static volatile Set<String> objectMethodNames= null;

    /**
     * collect all non-private, non-static and non-abstract methods from the given class.
     * This method removes any overloaded methods from the list automatically.
     * We also do skip bridge methods as they exist for and are handled solely
     * by the JVM itself.
     *
     * The returned Map contains the methods divided by the methodName as key in the map
     * following all the methods with the same methodName in a List.
     *
     * There is some special rule for package-private methods. Any non-visible
     * package-private method will get skipped and treated similarly to private methods.
     *
     * Note: we filter out the {@link Object#finalize()} method as users must not deal with it.
     * @param topClass the class to start with. Then move up the hierarchy
     * @param excludeFinalMethods whether final classes should get excluded from the result
     */
    public static List<Method> getNonPrivateMethods(Class<?> topClass, boolean excludeFinalMethods)
    {
        Map<String, List<Method>> methodMap = new HashMap<String, List<Method>>();
        List<Method> allMethods = new ArrayList<Method>(10);

        Class<?> clazz = topClass;

        if (!clazz.isAnnotation() && clazz.isInterface())
        {
            addNonPrivateMethods(topClass, excludeFinalMethods, methodMap, allMethods, clazz);
            for (final Class<?> parent : clazz.getInterfaces())
            {
                addNonPrivateMethods(topClass, excludeFinalMethods, methodMap, allMethods, parent);
            }
        }
        else
        {
            while (clazz != null)
            {
                addNonPrivateMethods(topClass, excludeFinalMethods, methodMap, allMethods, clazz);
                clazz = clazz.getSuperclass();
            }
        }

        return allMethods;
    }

    private static void addNonPrivateMethods(Class<?> topClass, boolean excludeFinalMethods,
                                             Map<String, List<Method>> methodMap, List<Method> allMethods,
                                             Class<?> clazz)
    {
        List<Method> temp = new ArrayList<Method>(Arrays.asList(clazz.getMethods()));
        for (Method method : clazz.getDeclaredMethods())
        {
            if (!temp.contains(method))
            {
                temp.add(method);
            }
        }

        for (Method method : temp)
        {
            if (allMethods.contains(method))
            {
                continue;
            }

            if (method.isBridge())
            {
                // we have no interest in generics bridge methods
                continue;
            }

            final int modifiers = method.getModifiers();

            if (Modifier.isPrivate(modifiers) || Modifier.isStatic(modifiers))
            {
                continue;
            }
            if (excludeFinalMethods && Modifier.isFinal(modifiers))
            {
                continue;
            }

            if ("finalize".equals(method.getName()))
            {
                // we do not proxy finalize()
                continue;
            }

            // check for package-private methods from a different package
            if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers))
            {
                // private already got handled above, so we only had to check for not public nor protected
                // we cannot see those methods if they are not in the same package as the rootClazz
                if (!clazz.getPackage().getName().equals(topClass.getPackage().getName()))
                {
                    continue;
                }

            }

            List<Method> methods = methodMap.get(method.getName());
            if (methods == null)
            {
                methods = new ArrayList<Method>();
                methods.add(method);
                allMethods.add(method);
                methodMap.put(method.getName(), methods);
            }
            else
            {
                if (!isOverridden(methods, method))
                {
                    // method is not overridden, so add it
                    methods.add(method);
                    allMethods.add(method);
                }
            }
        }
    }

    /**
     * collect all abstract methods for the given class if the given class
     * is {@link Modifier#ABSTRACT}
     *
     * @param clazz {@link Class} to check
     *
     * @return {@link List} with all abstract methods of the given class or an empty list
     *         if the given class is not abstract or doesn't contain an abstract method
     */
    public static List<Method> getAbstractMethods(Class<?> clazz)
    {
        if (!Modifier.isAbstract(clazz.getModifiers()))
        {
            return Collections.emptyList();
        }

        List<Method> methods = getNonPrivateMethods(clazz, true);
        if (!methods.isEmpty())
        {
            Iterator<Method> iterator = methods.iterator();
            while (iterator.hasNext())
            {
                if (!Modifier.isAbstract(iterator.next().getModifiers()))
                {
                    iterator.remove();
                }
            }
        }

        return methods;
    }

    /**
     * Checks, if the given {@link Class} declares the {@link Method} with the
     * given name und parameterTypes.
     *
     * @param clazz to check
     * @param name of the method
     * @param parameterTypes of the method
     *
     * @return {@code} true if the given class contains a method with the given name and parameterTypes,
     *         otherwise {@code false}
     */
    public static boolean isMethodDeclared(Class<?> clazz, String name, Class<?>... parameterTypes)
    {
        try
        {
            return clazz.getMethod(name, parameterTypes) != null;
        }
        catch (NoSuchMethodException e)
        {
            return false;
        }
    }

    /**
     * Check if the method is already defined in a subclass
     * @param subclassMethods
     * @param superclassMethod
     */
    public static boolean isOverridden(final List<Method> subclassMethods, final Method superclassMethod)
    {
        for (final Method m : subclassMethods)
        {
            if (isOverridden(m, superclassMethod))
            {
                return true;
            }
        }
        return false;
    }


    /**
     * Checks if the given method if from Object.class
     * @param methodName
     * @return <code>true</code> if the given method is from Object.class (either directly or overloaded)
     */
    public static boolean isObjectMethod(String methodName)
    {
        return getObjectMethodNames().contains(methodName);
    }

    /**
     * Returns true if type is an instance of <code>ParameterizedType</code>
     * else otherwise.
     * 
     * @param type type of the artifact
     * @return true if type is an instance of <code>ParameterizedType</code>
     */
    public static boolean isParametrizedType(Type type)
    {
        return type instanceof ParameterizedType;
    }
    
    /**
     * Returns true if type is an instance of <code>WildcardType</code>
     * else otherwise.
     * 
     * @param type type of the artifact
     * @return true if type is an instance of <code>WildcardType</code>
     */    
    public static boolean isWildCardType(Type type)
    {
        Asserts.assertNotNull(type, "type");
        
        return type instanceof WildcardType;
    }
    
    public static boolean isUnboundedTypeVariable(Type type)
    {
        Asserts.assertNotNull(type, "type");
        
        if (type instanceof TypeVariable)
        {
            TypeVariable wc = (TypeVariable)type;
            Type[] upper = wc.getBounds();            
            
            
            if(upper.length > 1)
            {
                return false;
            }            
            else
            {
                Type arg = upper[0];
                if(!(arg instanceof Class))
                {
                    return false;
                }
                else
                {
                    Class<?> clazz = (Class<?>)arg;
                    if(!clazz.equals(Object.class))
                    {
                        return false;
                    }                    
                }
            }            
        }
        else
        {
            return false;
        }

        return true;
    }
    
    
    /**
     * Returns true if type is an instance of <code>TypeVariable</code>
     * else otherwise.
     * 
     * @param type type of the artifact
     * @return true if type is an instance of <code>TypeVariable</code>
     */    
    public static boolean isTypeVariable(Type type)
    {
        Asserts.assertNotNull(type, "type");

        return type instanceof TypeVariable;
    }
    

    /**
     * Return true if the class is not abstract and interface.
     *     
     * @param clazz class type
     * @return true if the class is not abstract and interface
     */
    public static boolean isConcrete(Class<?> clazz)
    {
        Asserts.nullCheckForClass(clazz);

        Integer modifier = clazz.getModifiers();

        return !Modifier.isAbstract(modifier) && !Modifier.isInterface(modifier);
    }
    
    
    /**
     * Returns true if rhs is assignable type
     * to the lhs, false otherwise.
     * 
     * @param lhs left hand side class
     * @param rhs right hand side class
     * @return true if rhs is assignable to lhs
     */
    public static boolean isClassAssignableFrom(Class<?> lhs, Class<?> rhs)
    {
        Asserts.assertNotNull(lhs, "lhs");
        Asserts.assertNotNull(rhs, "rhs");

        if(lhs.isPrimitive())
        {
            lhs = getPrimitiveWrapper(lhs);
        }
        
        if(rhs.isPrimitive())
        {
            rhs = getPrimitiveWrapper(rhs);
        }

        if (lhs.isAssignableFrom(rhs))
        {
            return true;
        }

        return false;
    }

    /**
     * Check bean type and required type.
     * <p>
     * Required type is a wildcard type.
     * </p>
     * @param beanTypeArg bean type
     * @param requiredTypeArg required type
     * @return true if condition satisfies
     * @since 1.1.1
     */
    public static boolean checkRequiredTypeIsWildCard(Type beanTypeArg, Type requiredTypeArg)
    {
        WildcardType wctRequiredTypeArg = (WildcardType)requiredTypeArg;
        Type upperBoundRequiredTypeArg =  wctRequiredTypeArg.getUpperBounds()[0];
        Type[] lowerBoundRequiredTypeArgs =  wctRequiredTypeArg.getLowerBounds();
        
        if(beanTypeArg instanceof Class)
        {
            Class<?> clazzBeanTypeArg = (Class<?>)beanTypeArg;
            if(upperBoundRequiredTypeArg instanceof Class)
            {
                //Check upper bounds
                Class<?> clazzUpperBoundTypeArg = (Class<?>)upperBoundRequiredTypeArg;
                if(clazzUpperBoundTypeArg != Object.class)
                {
                    if(!clazzUpperBoundTypeArg.isAssignableFrom(clazzBeanTypeArg))
                    {                                       
                        return false;
                    }
                }
                
                //Check lower bounds
                if(lowerBoundRequiredTypeArgs.length > 0 &&  lowerBoundRequiredTypeArgs[0] instanceof Class)
                {
                    Class<?> clazzLowerBoundTypeArg = (Class<?>)lowerBoundRequiredTypeArgs[0];
                    
                    if(clazzLowerBoundTypeArg != Object.class)
                    {
                        if(!clazzBeanTypeArg.isAssignableFrom(clazzLowerBoundTypeArg))
                        {
                            return false;
                        }                                
                    }
                }
            }                    
        }
        else if(ClassUtil.isTypeVariable(beanTypeArg))
        {
            TypeVariable<?> tvBeanTypeArg = (TypeVariable<?>)beanTypeArg;
            Type tvBound = tvBeanTypeArg.getBounds()[0];
            
            if(tvBound instanceof Class)
            {
                Class<?> clazzTvBound = (Class<?>)tvBound;
                
                if(upperBoundRequiredTypeArg instanceof Class)
                {
                    Class<?> clazzUpperBoundTypeArg = (Class<?>)upperBoundRequiredTypeArg;                    
                    if(clazzUpperBoundTypeArg != Object.class && clazzTvBound != Object.class)
                    {
                        if(!clazzUpperBoundTypeArg.isAssignableFrom(clazzTvBound))
                        {   
                            return false;
                        }                       
                    }
                    
                    //Check lower bounds
                    if(lowerBoundRequiredTypeArgs.length > 0 &&  lowerBoundRequiredTypeArgs[0] instanceof Class)
                    {
                        Class<?> clazzLowerBoundTypeArg = (Class<?>)lowerBoundRequiredTypeArgs[0];
                        
                        if(clazzLowerBoundTypeArg != Object.class)
                        {
                            if(!clazzTvBound.isAssignableFrom(clazzLowerBoundTypeArg))
                            {
                                return false;
                            }                                    
                        }
                    }                    
                }                                    
            }
        }
         
        return true;
    }

    /**
     * Returns declared type arguments if {@code type} is a
     * {@link ParameterizedType} instance, else an empty array.
     * Get the actual type arguments of a type.
     * @param type
     * @return array of type arguments available
     * @since 1.1.1
     */
    public static Type[] getActualTypeArguments(Type type)
    {
        Asserts.assertNotNull(type, "type");

        if (type instanceof ParameterizedType)
        {
            return ((ParameterizedType) type).getActualTypeArguments();

        }
        else
        {
            return NO_TYPES;
        }
    }

    /**
     * Return raw class type for given type.
     * @param type base type instance
     * @return class type for given type
     */
    public static Class<?> getClazz(Type type)
    {
        if(type instanceof ParameterizedType)
        {
            ParameterizedType pt = (ParameterizedType)type;
            return (Class<?>)pt.getRawType();                
        }
        else if(type instanceof Class)
        {
            return (Class<?>)type;
        }
        else if(type instanceof GenericArrayType)
        {
            GenericArrayType arrayType = (GenericArrayType)type;
            return Array.newInstance(getClazz(arrayType.getGenericComponentType()), 0).getClass();
        }
        else if (type instanceof WildcardType)
        {
            WildcardType wildcardType = (WildcardType)type;
            Type[] bounds = wildcardType.getUpperBounds();
            if (bounds.length > 1)
            {
                throw new WebBeansConfigurationException("Illegal use of wild card type with more than one upper bound: " + wildcardType);
            }
            else if (bounds.length == 0)
            {
                return Object.class;
            }
            else
            {
                return getClass(bounds[0]);
            }
        }
        else if (type instanceof TypeVariable)
        {
            TypeVariable<?> typeVariable = (TypeVariable<?>)type;
            if (typeVariable.getBounds().length > 1)
            {
                throw new WebBeansConfigurationException("Illegal use of type variable with more than one bound: " + typeVariable);
            }
            else
            {
                Type[] bounds = typeVariable.getBounds();
                if (bounds.length == 0)
                {
                    return Object.class;
                }
                else
                {
                    return getClass(bounds[0]);
                }
            }
        }
        else
        {
            throw new WebBeansConfigurationException("Unsupported type " + type);
        }
    }

    /**
     * Return true if it does not contain type variable for wildcard type
     * false otherwise.
     * 
     * @param pType parameterized type
     * @return true if it does not contain type variable for wildcard type
     */
    public static boolean checkParametrizedType(ParameterizedType pType)
    {
        Asserts.assertNotNull(pType, "pType");
        
        Type[] types = pType.getActualTypeArguments();

        for (Type type : types)
        {
            if (type instanceof ParameterizedType)
            {
                return checkParametrizedType((ParameterizedType) type);
            }
            else if ((type instanceof TypeVariable) || (type instanceof WildcardType))
            {
                return false;
            }
        }

        return true;
    }


    /**
     * Returns injection point raw type.
     * 
     * @param injectionPoint injection point definition
     * @return injection point raw type
     */
    public static Class<?> getRawTypeForInjectionPoint(InjectionPoint injectionPoint)
    {
        Class<?> rawType = null;
        Type type = injectionPoint.getType();
        
        if(type instanceof Class)
        {
            rawType = (Class<?>) type;
        }
        else if(type instanceof ParameterizedType)
        {
            ParameterizedType pt = (ParameterizedType)type;            
            rawType = (Class<?>)pt.getRawType();                                                
        }
        
        return rawType;
    }

    /**
     * Check whether <code>superClassMethod</code> is overridden by <code>subClassMethod</code>.
     * @param subClassMethod potentially overriding
     * @param superClassMethod potentially overridden
     * @return true if overridden
     * @since 1.1.1
     */
    public static boolean isOverridden(Method subClassMethod, Method superClassMethod)
    {
        if (isSuperClass(superClassMethod.getDeclaringClass(), subClassMethod.getDeclaringClass())
                && subClassMethod.getName().equals(superClassMethod.getName())
                && Arrays.equals(subClassMethod.getParameterTypes(), superClassMethod.getParameterTypes()))
        {
            int modifiers = superClassMethod.getModifiers();
            if(Modifier.isPrivate(modifiers) || Modifier.isStatic(modifiers))
            {
                return false;
            }
            
            if(!Modifier.isProtected(modifiers) && !Modifier.isPublic(modifiers))
            {
                Class<?> superClass = superClassMethod.getDeclaringClass();
                Class<?> subClass = subClassMethod.getDeclaringClass();
                
                //Same package
                if(!subClass.getPackage().getName().equals(superClass.getPackage().getName()))
                {
                    return false;
                }
            }
            
            return true;
        }
        
        return false;
    }

    private static boolean isSuperClass(Class<?> superClass, Class<?> subClass)
    {
        return superClass.isAssignableFrom(subClass) && !superClass.equals(subClass);
    }

    public static boolean isRawClassEquals(Type ipType, Type apiType)
    {
        Class ipClass = getRawPrimitiveType(ipType);
        Class apiClass  = getRawPrimitiveType(apiType);

        if (ipClass == null || apiClass == null)
        {
            // we found some illegal types
            return false;
        }

        return ipClass.equals(apiClass);
    }

    private static Class getRawPrimitiveType(Type type)
    {
        if (type instanceof Class)
        {
            if (((Class) type).isPrimitive())
            {
                return getPrimitiveWrapper((Class) type);
            }
            return (Class) type;
        }

        if (type instanceof ParameterizedType)
        {
            return getRawPrimitiveType(((ParameterizedType) type).getRawType());
        }

        return null;
    }
}
