package io.ray.serve.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

/** Tool class for reflection. */
public class ReflectUtil {

  /**
   * Get types of the parameters in input array, and make the types into a new array.
   *
   * @param parameters The input parameter array
   * @return Type array corresponding to the input parameter array
   */
  @SuppressWarnings("rawtypes")
  private static Class[] getParameterTypes(Object[] parameters) {
    Class[] parameterTypes = null;
    if (ArrayUtils.isEmpty(parameters)) {
      return null;
    }
    parameterTypes = new Class[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
      parameterTypes[i] = parameters[i].getClass();
    }
    return parameterTypes;
  }

  /**
   * Get a constructor whose each parameter's type is the most closed to the corresponding input
   * parameter in terms of Java inheritance system, while {@link Class#getConstructor(Class...)}
   * returns the one based on class's equal.
   *
   * @param targetClass the constructor's class
   * @param parameters the input parameters
   * @return a matching constructor of the target class
   * @throws NoSuchMethodException if a matching method is not found
   */
  @SuppressWarnings("rawtypes")
  public static Constructor getConstructor(Class targetClass, Object... parameters)
      throws NoSuchMethodException {
    return reflect(
        targetClass.getConstructors(), (candidate) -> true, ".<init>", targetClass, parameters);
  }

  /**
   * Get a method whose each parameter's type is the most closed to the corresponding input
   * parameter in terms of Java inheritance system, while {@link Class#getMethod(String, Class...)}
   * returns the one based on class's equal.
   *
   * @param targetClass the constructor's class
   * @param name the specified method's name
   * @param parameters the input parameters
   * @return a matching method of the target class
   * @throws NoSuchMethodException if a matching method is not found
   */
  @SuppressWarnings("rawtypes")
  public static Method getMethod(Class targetClass, String name, Object... parameters)
      throws NoSuchMethodException {
    return reflect(
        targetClass.getMethods(),
        (candidate) -> StringUtils.equals(name, candidate.getName()),
        "." + name,
        targetClass,
        parameters);
  }

  /**
   * Get an object of {@link Executable} of the specified class according to initialization
   * parameters. The result's every parameter's type is the same as, or is a subclass or
   * subinterface of, the type of the corresponding input parameter. This method returns the result
   * whose each parameter's type is the most closed to the corresponding input parameter in terms of
   * Java inheritance system.
   *
   * @param <T> the type of result which extends {@link Executable}
   * @param candidates a set of candidates
   * @param filter the filter deciding whether to select the input candidate
   * @param message a message representing the target executable object
   * @param targetClass the constructor's class
   * @param parameters the input parameters
   * @return a matching executable of the target class
   * @throws NoSuchMethodException if a matching method is not found
   */
  @SuppressWarnings("rawtypes")
  private static <T extends Executable> T reflect(
      T[] candidates,
      Function<T, Boolean> filter,
      String message,
      Class targetClass,
      Object... parameters)
      throws NoSuchMethodException {
    Class[] parameterTypes = getParameterTypes(parameters);
    T result = null;
    for (int i = 0; i < candidates.length; i++) {
      T candidate = candidates[i];
      if (filter.apply(candidate)
          && assignable(parameterTypes, candidate.getParameterTypes())
          && (result == null
              || assignable(candidate.getParameterTypes(), result.getParameterTypes()))) {
        result = candidate;
      }
    }
    if (result == null) {
      throw new NoSuchMethodException(
          targetClass.getName() + message + argumentTypesToString(parameterTypes));
    }
    return result;
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private static boolean assignable(Class[] from, Class[] to) {
    if (from == null) {
      return to == null || to.length == 0;
    }

    if (to == null) {
      return from.length == 0;
    }

    if (from.length != to.length) {
      return false;
    }

    for (int i = 0; i < from.length; i++) {
      if (!to[i].isAssignableFrom(from[i])) {
        return false;
      }
    }

    return true;
  }

  /**
   * It is copied from {@link Class#argumentTypesToString(Class[])}.
   *
   * @param argTypes array of Class object
   * @return Formatted string of the input Class array.
   */
  private static String argumentTypesToString(Class<?>[] argTypes) {
    StringBuilder buf = new StringBuilder();
    buf.append("(");
    if (argTypes != null) {
      for (int i = 0; i < argTypes.length; i++) {
        if (i > 0) {
          buf.append(", ");
        }
        Class<?> c = argTypes[i];
        buf.append((c == null) ? "null" : c.getName());
      }
    }
    buf.append(")");
    return buf.toString();
  }

  /**
   * Get a string representing the specified class's all methods.
   *
   * @param targetClass the input class
   * @return the formatted string of the specified class's all methods.
   */
  @SuppressWarnings("rawtypes")
  public static List<String> getMethodStrings(Class targetClass) {
    if (targetClass == null) {
      return null;
    }
    Method[] methods = targetClass.getMethods();
    if (methods == null || methods.length == 0) {
      return null;
    }
    List<String> methodStrings = new ArrayList<>();
    for (int i = 0; i < methods.length; i++) {
      methodStrings.add(methods[i].toString());
    }
    return methodStrings;
  }

  @SuppressWarnings("unchecked")
  public static <T> List<T> getInstancesByClassNames(String classNames, Class<T> cls)
      throws ClassNotFoundException, InstantiationException, IllegalAccessException,
          IllegalArgumentException, InvocationTargetException, NoSuchMethodException,
          SecurityException {
    String[] classNameArray = StringUtils.split(classNames, ";");
    List<T> isntances = new ArrayList<>();
    for (String className : classNameArray) {
      isntances.add((T) Class.forName(className).getConstructor().newInstance());
    }
    return isntances;
  }
}
