/*
 * Copyright 2009 the original author or authors.
 *
 * Licensed 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.powermock.api.support.membermodification;

import org.powermock.reflect.Whitebox;
import org.powermock.reflect.exceptions.*;
import org.powermock.reflect.internal.WhiteboxImpl;
import org.powermock.tests.utils.impl.ArrayMergerImpl;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import static java.util.Arrays.asList;

/**
 * Finds members in classes.
 */
public class MemberMatcher {

    /**
     * Get all methods in a class hierarchy of the supplied classes. Both
     * declared an non-declared (no duplicates).
     * 
     * @param cls
     *            The class whose methods to get.
     * @param additionalClasses
     *            Additional classes whose methods to get.
     * @return All methods declared in this class hierarchy.
     */
    public static Method[] methodsDeclaredIn(final Class<?> cls, final Class<?>... additionalClasses) {
        if (cls == null) {
            throw new IllegalArgumentException("You need to supply at least one class.");
        }
        Set<Method> methods = new HashSet<Method>();
        methods.addAll(asList(WhiteboxImpl.getAllMethods(cls)));
        for (Class<?> klass : additionalClasses) {
            methods.addAll(asList(WhiteboxImpl.getAllMethods(klass)));
        }
        return methods.toArray(new Method[methods.size()]);
    }

    /**
     * Get a method when it cannot be determined by methodName or parameter
     * types only.
     * <p>
     * The method will first try to look for a declared method in the same
     * class. If the method is not declared in this class it will look for the
     * method in the super class. This will continue throughout the whole class
     * hierarchy. If the method is not found an {@link IllegalArgumentException}
     * is thrown.
     * 
     * @param declaringClass
     *            The declaringClass of the class where the method is located.
     * @param methodName
     *            The method names.
     * @param parameterTypes
     *            All parameter types of the method (may be <code>null</code>).
     * @return A <code>java.lang.reflect.Method</code>.
     * @throws MethodNotFoundException
     *             If a method cannot be found in the hierarchy.
     */
    public static Method method(Class<?> declaringClass, String methodName, Class<?>... parameterTypes) {
        final Method method = WhiteboxImpl.findMethod(declaringClass, methodName, parameterTypes);
        WhiteboxImpl.throwExceptionIfMethodWasNotFound(declaringClass, methodName, method, (Object[]) parameterTypes);
        return method;
    }

    /**
     * Get a method without having to specify the method name.
     * <p>
     * The method will first try to look for a declared method in the same
     * class. If the method is not declared in this class it will look for the
     * method in the super class. This will continue throughout the whole class
     * hierarchy. If the method is not found an {@link IllegalArgumentException}
     * is thrown. Since the method name is not specified an
     * {@link IllegalArgumentException} is thrown if two or more methods matches
     * the same parameter types in the same class.
     * 
     * @param declaringClass
     *            The declaringClass of the class where the method is located.
     * @param parameterTypes
     *            All parameter types of the method (may be <code>null</code>).
     * @return A <code>java.lang.reflect.Method</code>.
     * @throws MethodNotFoundException
     *             If a method cannot be found in the hierarchy.
     * @throws TooManyMethodsFoundException
     *             If several methods were found.
     */
    public static Method method(Class<?> declaringClass, Class<?>... parameterTypes) {
        return Whitebox.getMethod(declaringClass, parameterTypes);
    }

    /**
     * Get an array of {@link Method}'s that matches the supplied list of method
     * names. Both instance and static methods are taken into account.
     * 
     * @param clazz
     *            The class that should contain the methods.
     * @param methodName
     *            The name of the first method.
     * @param additionalMethodNames
     *            Additional names of the methods that will be returned.
     * @return An array of Method's. May be of length 0 but not
     *         <code>null</code>.
     * @throws MethodNotFoundException
     *             If no method was found.
     */
    public static Method[] methods(Class<?> clazz, String methodName, String... additionalMethodNames) {
        return Whitebox.getMethods(clazz, merge(methodName, additionalMethodNames));
    }

    /**
     * Get an array of {@link Field}'s.
     * 
     * @param method
     *            The first field.
     * @param additionalMethods
     *            Additional fields
     * @return An array of {@link Field}.
     */
    public static Method[] methods(Method method, Method... additionalMethods) {
        return merge(method, additionalMethods);
    }

    /**
     * Get an array of {@link Method}'s that matches the supplied list of method
     * names. Both instance and static methods are taken into account.
     * 
     * @param clazz
     *            The class that should contain the methods.
     * @param methodNames
     *            The names of the methods.
     * @return An array of Method's. May be of length 0 but not
     *         <code>null</code>.
     * @throws MethodNotFoundException
     *             If no method was found.
     */
    public static Method[] methods(Class<?> clazz, String[] methodNames) {
        return Whitebox.getMethods(clazz, methodNames);
    }

    /**
     * Get a field from a class.
     * <p>
     * The method will first try to look for a declared field in the same class.
     * If the method is not declared in this class it will look for the field in
     * the super class. This will continue throughout the whole class hierarchy.
     * If the field is not found an {@link IllegalArgumentException} is thrown.
     * 
     * @param declaringClass
     *            The declaringClass of the class where the method is located.
     * @param fieldName
     *            The method names.
     * @return A <code>java.lang.reflect.Field</code>.
     * @throws FieldNotFoundException
     *             If a field cannot be found in the hierarchy.
     */
    public static Field field(Class<?> declaringClass, String fieldName) {
        return Whitebox.getField(declaringClass, fieldName);
    }

    /**
     * Get an array of {@link Field}'s that matches the supplied list of field
     * names.
     * 
     * @param clazz
     *            The class that should contain the fields.
     * @param firstFieldName
     *            The name of the first field.
     * @param additionalfieldNames
     *            The additional names of the fields that will be returned.
     * @return An array of Field's. May be of length 0 but not <code>null</code>
     * 
     */
    public static Field[] fields(Class<?> clazz, String firstFieldName, String... additionalfieldNames) {
        return Whitebox.getFields(clazz, merge(firstFieldName, additionalfieldNames));
    }

    /**
     * Get all fields in a class hierarchy.
     * 
     * @param clazz
     *            The class that should contain the fields.
     * @param firstFieldName
     *            The name of the first field.
     * @param additionalfieldNames
     *            The additional names of the fields that will be returned.
     * @return An array of Field's. May be of length 0 but not <code>null</code>
     * 
     */
    public static Field[] fields(Class<?> clazz) {
        return WhiteboxImpl.getAllFields(clazz);
    }

    /**
     * Get an array of {@link Field}'s.
     * 
     * @param field
     *            The first field.
     * @param additionalFields
     *            Additional fields
     * @return An array of {@link Field}.
     */
    public static Field[] fields(Field field, Field... additionalFields) {
        return merge(field, additionalFields);
    }

    /**
     * Get an array of {@link Field}'s that matches the supplied list of field
     * names.
     * 
     * @param clazz
     *            The class that should contain the fields.
     * @param fieldNames
     *            The names of the fields that will be returned.
     * @return An array of Field's. May be of length 0 but not <code>null</code>
     * 
     */
    public static Field[] fields(Class<?> clazz, String[] fieldNames) {
        return Whitebox.getFields(clazz, fieldNames);
    }

    /**
     * Returns a constructor specified in declaringClass.
     * 
     * @param declaringClass
     *            The declaringClass of the class where the constructor is
     *            located.
     * @param parameterTypes
     *            All parameter types of the constructor (may be
     *            <code>null</code>).
     * @return A <code>java.lang.reflect.Constructor</code>.
     * @throws ConstructorNotFoundException
     *             if the constructor cannot be found.
     */
    @SuppressWarnings("unchecked")
    public static <T> Constructor<T> constructor(Class<T> declaringClass, Class<?>... parameterTypes) {
        return (Constructor<T>) WhiteboxImpl.findUniqueConstructorOrThrowException(declaringClass,
                (Object[]) parameterTypes);
    }

    /**
     * Returns any one constructor specified in declaringClass. Is is useful when you only have ONE constructor
     * declared in <code>declaringClass</code> but you don't care which parameters it take.
     * 
     * @param declaringClass
     *            The declaringClass of the class where the constructor is
     *            located.
     * @return A <code>java.lang.reflect.Constructor</code>.
     * @throws TooManyConstructorsFoundException
     *             If more than one constructor was present in
     *             <code>declaringClass</code>
     */
    @SuppressWarnings("unchecked")
    public static <T> Constructor<T> constructor(Class<T> declaringClass) {
        return (Constructor<T>) WhiteboxImpl.findConstructorOrThrowException(declaringClass);
    }

    /**
     * Returns the default constructor in <code>declaringClass</code>
     *
     * @param declaringClass
     *            The declaringClass of the class where the constructor is
     *            located.
     * @return A <code>java.lang.reflect.Constructor</code>.
     * @throws ConstructorNotFoundException
     *             If no default constructor was found in  <code>declaringClass</code>
     */
    @SuppressWarnings("unchecked")
    public static <T> Constructor<T> defaultConstructorIn(Class<T> declaringClass) {
        return (Constructor<T>) WhiteboxImpl.findDefaultConstructorOrThrowException(declaringClass);
    }

    /**
     * Get all constructors in the supplied class(es).
     * 
     * @param cls
     *            The class whose constructors to get.
     * @param additionalClasses
     *            Additional classes whose constructors to get.
     * @return All constructors declared in this class.
     */
    public static Constructor<?>[] constructorsDeclaredIn(final Class<?> cls, final Class<?>... additionalClasses) {
        if (cls == null) {
            throw new IllegalArgumentException("You need to supply at least one class.");
        }
        Set<Constructor<?>> constructors = new HashSet<Constructor<?>>();
        constructors.addAll(asList(WhiteboxImpl.getAllConstructors(cls)));
        for (Class<?> klass : additionalClasses) {
            constructors.addAll(asList(WhiteboxImpl.getAllConstructors(klass)));
        }
        return constructors.toArray(new Constructor[constructors.size()]);
    }

    /**
     * Convenience method to get a constructor from a class.
     * 
     * @param constructor
     *            The first constructor.
     * @param additionalConstructors
     *            Additional constructors
     * @return An array of <code>java.lang.reflect.Constructor</code>.
     */
    public static Constructor<?>[] constructors(Constructor<?> constructor, Constructor<?>... additionalConstructors) {
        return merge(constructor, additionalConstructors);
    }

    /**
     * Get all constructors and methods in the supplied class(es).
     * 
     * @param cls
     *            The class whose constructors and methods to get.
     * @param additionalClasses
     *            Additional classes whose constructors and methods to get.
     * @return All constructors and methods declared in this class.
     */
    public static AccessibleObject[] everythingDeclaredIn(final Class<?> cls, final Class<?>... additionalClasses) {
        if (cls == null) {
            throw new IllegalArgumentException("You need to supply at least one class.");
        }
        Set<AccessibleObject> accessibleObjects = new HashSet<AccessibleObject>();
        accessibleObjects.addAll(Collections.unmodifiableCollection(asList(methodsDeclaredIn(cls, additionalClasses))));
        accessibleObjects.addAll(Collections.unmodifiableCollection(asList(constructorsDeclaredIn(cls,
                additionalClasses))));
        return accessibleObjects.toArray(new AccessibleObject[accessibleObjects.size()]);
    }

    private static String[] merge(String first, String... additional) {
        return new ArrayMergerImpl().mergeArrays(String.class, new String[] { first }, additional);
    }

    private static Method[] merge(Method first, Method... additional) {
        return new ArrayMergerImpl().mergeArrays(Method.class, new Method[] { first }, additional);
    }

    private static Field[] merge(Field first, Field... additional) {
        return new ArrayMergerImpl().mergeArrays(Field.class, new Field[] { first }, additional);
    }

    private static Constructor<?>[] merge(Constructor<?> first, Constructor<?>... additional) {
        return new ArrayMergerImpl().mergeArrays(Constructor.class, new Constructor<?>[] { first }, additional);
    }
}
