/*
 * 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.camel.tooling.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;

/**
 * Helper for working with reflection on classes.
 * <p/>
 * This code is a copy of org.apache.camel.util.ReflectionHelper to avoid cyclic dependencies between artifacts and
 * camel maven plugins. This code is based on org.apache.camel.spring.util.ReflectionUtils class.
 */
public final class ReflectionHelper {

    private ReflectionHelper() {
        // utility class
    }

    /**
     * Callback interface invoked on each field in the hierarchy.
     */
    @FunctionalInterface
    public interface FieldCallback {

        /**
         * Perform an operation using the given field.
         *
         * @param field the field to operate on
         */
        void doWith(Field field) throws IllegalArgumentException, IllegalAccessException;
    }

    /**
     * Action to take on each method.
     */
    @FunctionalInterface
    public interface MethodCallback {

        /**
         * Perform an operation using the given method.
         *
         * @param method the method to operate on
         */
        void doWith(Method method) throws IllegalArgumentException, IllegalAccessException;
    }

    /**
     * Action to take on each class.
     */
    @FunctionalInterface
    public interface ClassCallback {

        /**
         * Perform an operation using the given class.
         *
         * @param clazz the class to operate on
         */
        void doWith(Class clazz) throws IllegalArgumentException, IllegalAccessException;
    }

    /**
     * Perform the given callback operation on the nested (inner) classes.
     *
     * @param clazz class to start looking at
     * @param cc    the callback to invoke for each inner class (excluding the class itself)
     */
    public static void doWithClasses(Class<?> clazz, ClassCallback cc) throws IllegalArgumentException {
        // and then nested classes
        Class[] classes = clazz.getDeclaredClasses();
        for (Class aClazz : classes) {
            try {
                cc.doWith(aClazz);
            } catch (IllegalAccessException ex) {
                throw new IllegalStateException("Shouldn't be illegal to access class '" + aClazz.getName() + "': " + ex);
            }
        }
    }

    /**
     * Invoke the given callback on all fields in the target class, going up the class hierarchy to get all declared
     * fields.
     * 
     * @param clazz the target class to analyze
     * @param fc    the callback to invoke for each field
     */
    public static void doWithFields(Class<?> clazz, FieldCallback fc) throws IllegalArgumentException {
        // Keep backing up the inheritance hierarchy.
        Class<?> targetClass = clazz;
        do {
            Field[] fields = targetClass.getDeclaredFields();
            for (Field field : fields) {
                try {
                    fc.doWith(field);
                } catch (IllegalAccessException ex) {
                    throw new IllegalStateException("Shouldn't be illegal to access field '" + field.getName() + "': " + ex);
                }
            }
            targetClass = targetClass.getSuperclass();
        } while (targetClass != null && targetClass != Object.class);
    }

    /**
     * Perform the given callback operation on all matching methods of the given class and superclasses (or given
     * interface and super-interfaces).
     * <p/>
     * <b>Important:</b> This method does not take the {@link java.lang.reflect.Method#isBridge() bridge methods} into
     * account.
     *
     * @param clazz class to start looking at
     * @param mc    the callback to invoke for each method
     */
    public static void doWithMethods(Class<?> clazz, MethodCallback mc) throws IllegalArgumentException {
        // Keep backing up the inheritance hierarchy.
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (method.isBridge()) {
                // skip the bridge methods which in Java 8 leads to problems with inheritance
                // see https://bugs.openjdk.java.net/browse/JDK-6695379
                continue;
            }
            try {
                mc.doWith(method);
            } catch (IllegalAccessException ex) {
                throw new IllegalStateException("Shouldn't be illegal to access method '" + method.getName() + "': " + ex);
            }
        }
        if (clazz.getSuperclass() != null) {
            doWithMethods(clazz.getSuperclass(), mc);
        } else if (clazz.isInterface()) {
            for (Class<?> superIfc : clazz.getInterfaces()) {
                doWithMethods(superIfc, mc);
            }
        }
    }

    /**
     * Attempt to find a {@link Method} on the supplied class with the supplied name and parameter types. Searches all
     * superclasses up to {@code Object}.
     * <p>
     * Returns {@code null} if no {@link Method} can be found.
     * 
     * @param  clazz      the class to introspect
     * @param  name       the name of the method
     * @param  paramTypes the parameter types of the method (may be {@code null} to indicate any signature)
     * @return            the Method object, or {@code null} if none found
     */
    public static Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
        Objects.requireNonNull(clazz, "Class must not be null");
        Objects.requireNonNull(name, "Method name must not be null");
        Class<?> searchType = clazz;
        while (searchType != null) {
            Method[] methods = searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods();
            for (Method method : methods) {
                if (name.equals(method.getName())
                        && (paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes()))) {
                    return method;
                }
            }
            searchType = searchType.getSuperclass();
        }
        return null;
    }

    /**
     * Attempt to find a {@link Field} on the supplied class with the supplied name. Searches all superclasses up to
     * {@code Object}.
     * <p>
     * Returns {@code null} if no {@link Method} can be found.
     * 
     * @param  clazz the class to introspect
     * @param  name  the name of the field
     * @return       the field object, or {@code null} if none found
     */
    public static Field findField(Class<?> clazz, String name) {
        Objects.requireNonNull(clazz, "Class must not be null");
        Objects.requireNonNull(name, "Field name must not be null");
        Class<?> searchType = clazz;
        while (searchType != null) {
            Field[] fields = searchType.getDeclaredFields();
            for (Field field : fields) {
                if (name.equals(field.getName())) {
                    return field;
                }
            }
            searchType = searchType.getSuperclass();
        }
        return null;
    }

    public static void setField(Field f, Object instance, Object value) {
        try {
            boolean oldAccessible = f.isAccessible();
            boolean shouldSetAccessible = !Modifier.isPublic(f.getModifiers()) && !oldAccessible;
            if (shouldSetAccessible) {
                f.setAccessible(true);
            }
            f.set(instance, value);
            if (shouldSetAccessible) {
                f.setAccessible(oldAccessible);
            }
        } catch (Exception ex) {
            throw new UnsupportedOperationException("Cannot inject value of class: " + value.getClass() + " into: " + f);
        }
    }

    public static Object getField(Field f, Object instance) {
        try {
            boolean oldAccessible = f.isAccessible();
            boolean shouldSetAccessible = !Modifier.isPublic(f.getModifiers()) && !oldAccessible;
            if (shouldSetAccessible) {
                f.setAccessible(true);
            }
            Object answer = f.get(instance);
            if (shouldSetAccessible) {
                f.setAccessible(oldAccessible);
            }
            return answer;
        } catch (Exception ex) {
            // ignore
        }
        return null;
    }

}
