package org.apache.maven.surefire.api.util;

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

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author Kristian Rosenvold
 */
public final class ReflectionUtils
{
    private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];

    private ReflectionUtils()
    {
        throw new IllegalStateException( "no instantiable constructor" );
    }

    public static Method getMethod( Object instance, String methodName, Class<?>... parameters )
    {
        return getMethod( instance.getClass(), methodName, parameters );
    }

    public static Method getMethod( Class<?> clazz, String methodName, Class<?>... parameters )
    {
        try
        {
            return clazz.getMethod( methodName, parameters );
        }
        catch ( NoSuchMethodException e )
        {
            throw new RuntimeException( "When finding method " + methodName, e );
        }
    }

    public static Method tryGetMethod( Class<?> clazz, String methodName, Class<?>... parameters )
    {
        try
        {
            return clazz.getMethod( methodName, parameters );
        }
        catch ( NoSuchMethodException e )
        {
            return null;
        }
    }

    public static <T> T invokeGetter( Object instance, String methodName )
    {
        return invokeGetter( instance.getClass(), instance, methodName );
    }

    public static <T> T invokeGetter( Class<?> instanceType, Object instance, String methodName )
    {
        Method method = getMethod( instanceType, methodName );
        return invokeMethodWithArray( instance, method );
    }

    public static Constructor<?> getConstructor( Class<?> clazz, Class<?>... arguments )
    {
        try
        {
            return clazz.getConstructor( arguments );
        }
        catch ( NoSuchMethodException e )
        {
            throw new SurefireReflectionException( e );
        }
    }

    public static <T> Constructor<T> tryGetConstructor( Class<T> clazz, Class<?>... arguments )
    {
        try
        {
            return clazz.getConstructor( arguments );
        }
        catch ( NoSuchMethodException e )
        {
            return null;
        }
    }

    public static <T> T newInstance( Constructor<?> constructor, Object... params )
    {
        try
        {
            //noinspection unchecked
            return (T) constructor.newInstance( params );
        }
        catch ( ReflectiveOperationException e )
        {
            throw new SurefireReflectionException( e );
        }
    }

    public static <T> T instantiate( ClassLoader classLoader, String classname, Class<T> returnType )
    {
        try
        {
            Class<?> clazz = loadClass( classLoader, classname );
            return returnType.cast( clazz.newInstance() );
        }
        catch ( ReflectiveOperationException e )
        {
            throw new SurefireReflectionException( e );
        }
    }

    public static <T> T instantiateOneArg( ClassLoader classLoader, String className, Class<?> param1Class,
                                            Object param1 )
    {
        try
        {
            Class<?> aClass = loadClass( classLoader, className );
            Constructor<?> constructor = getConstructor( aClass, param1Class );
            //noinspection unchecked
            return (T) constructor.newInstance( param1 );
        }
        catch ( InvocationTargetException e )
        {
            throw new SurefireReflectionException( e.getTargetException() );
        }
        catch ( ReflectiveOperationException e )
        {
            throw new SurefireReflectionException( e );
        }
    }

    public static void invokeSetter( Object o, String name, Class<?> value1clazz, Object value )
    {
        Method setter = getMethod( o, name, value1clazz );
        invokeSetter( o, setter, value );
    }

    public static <T> T invokeSetter( Object target, Method method, Object value )
    {
        return invokeMethodWithArray( target, method, value );
    }

    public static <T> T invokeMethodWithArray( Object target, Method method, Object... args )
    {
        try
        {
            //noinspection unchecked
            return (T) method.invoke( target, args );
        }
        catch ( IllegalAccessException e )
        {
            throw new SurefireReflectionException( e );
        }
        catch ( InvocationTargetException e )
        {
            throw new SurefireReflectionException( e.getTargetException() );
        }
    }

    public static <T> T invokeMethodWithArray2( Object target, Method method, Object... args )
        throws InvocationTargetException
    {
        try
        {
            //noinspection unchecked
            return (T) method.invoke( target, args );
        }
        catch ( IllegalAccessException e )
        {
            throw new SurefireReflectionException( e );
        }
    }

    public static <T> T instantiateObject( String className, Class<?>[] types, Object[] params, ClassLoader cl )
    {
        Class<?> clazz = loadClass( cl, className );
        final Constructor<?> constructor = getConstructor( clazz, types );
        return newInstance( constructor, params );
    }

    @SuppressWarnings( "checkstyle:emptyblock" )
    public static Class<?> tryLoadClass( ClassLoader classLoader, String className )
    {
        try
        {
            return classLoader.loadClass( className );
        }
        catch ( NoClassDefFoundError | ClassNotFoundException ignore )
        {
        }
        return null;
    }

    public static Class<?> loadClass( ClassLoader classLoader, String className )
    {
        try
        {
            return classLoader.loadClass( className );
        }
        catch ( NoClassDefFoundError | ClassNotFoundException e )
        {
            throw new SurefireReflectionException( e );
        }
    }

    public static Class<?> reloadClass( ClassLoader classLoader, Object source )
            throws ReflectiveOperationException
    {
        return classLoader.loadClass( source.getClass().getName() );
    }

    /**
     * Invoker of public static no-argument method.
     *
     * @param clazz         class on which public static no-argument {@code methodName} is invoked
     * @param methodName    public static no-argument method to be called
     * @param parameterTypes    method parameter types
     * @param parameters    method parameters
     * @return value returned by {@code methodName}
     * @throws RuntimeException if no such method found
     * @throws SurefireReflectionException if the method could not be called or threw an exception.
     * It has original cause Exception.
     */
    public static <T> T invokeStaticMethod( Class<?> clazz, String methodName,
                                            Class<?>[] parameterTypes, Object[] parameters )
    {
        if ( parameterTypes.length != parameters.length )
        {
            throw new IllegalArgumentException( "arguments length do not match" );
        }
        Method method = getMethod( clazz, methodName, parameterTypes );
        return invokeMethodWithArray( null, method, parameters );
    }

    /**
     * Method chain invoker.
     *
     * @param classesChain        classes to invoke on method chain
     * @param noArgMethodNames    chain of public methods to call
     * @param fallback            returned value if a chain could not be invoked due to an error
     * @return successfully returned value from the last method call; {@code fallback} otherwise
     * @throws IllegalArgumentException if {@code classes} and {@code noArgMethodNames} have different array length
     */
    public static <T> T invokeMethodChain( Class<?>[] classesChain, String[] noArgMethodNames, Object fallback )
    {
        if ( classesChain.length != noArgMethodNames.length )
        {
            throw new IllegalArgumentException( "arrays must have the same length" );
        }
        Object obj = null;
        try
        {
            for ( int i = 0, len = noArgMethodNames.length; i < len; i++ )
            {
                if ( i == 0 )
                {
                    obj = invokeStaticMethod( classesChain[i], noArgMethodNames[i],
                                                    EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY );
                }
                else
                {
                    Method method = getMethod( classesChain[i], noArgMethodNames[i] );
                    obj = invokeMethodWithArray( obj, method );
                }
            }
            //noinspection unchecked
            return (T) obj;
        }
        catch ( RuntimeException e )
        {
            //noinspection unchecked
            return (T) fallback;
        }
    }
}
