package org.apache.commons.beanutils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class MethodUtils {
  private static Log log = LogFactory.getLog(MethodUtils.class);
  
  private static boolean loggedAccessibleWarning = false;
  
  private static final Class[] emptyClassArray = new Class[0];
  
  private static final Object[] emptyObjectArray = new Object[0];
  
  public static Object invokeMethod(Object object, String methodName, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    Object[] args = { arg };
    return invokeMethod(object, methodName, args);
  }
  
  public static Object invokeMethod(Object object, String methodName, Object[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (args == null)
      args = emptyObjectArray; 
    int arguments = args.length;
    Class[] parameterTypes = new Class[arguments];
    for (int i = 0; i < arguments; i++)
      parameterTypes[i] = args[i].getClass(); 
    return invokeMethod(object, methodName, args, parameterTypes);
  }
  
  public static Object invokeMethod(Object object, String methodName, Object[] args, Class[] parameterTypes) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (parameterTypes == null)
      parameterTypes = emptyClassArray; 
    if (args == null)
      args = emptyObjectArray; 
    Method method = getMatchingAccessibleMethod(object.getClass(), methodName, parameterTypes);
    if (method == null)
      throw new NoSuchMethodException("No such accessible method: " + methodName + "() on object: " + object.getClass().getName()); 
    return method.invoke(object, args);
  }
  
  public static Object invokeExactMethod(Object object, String methodName, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    Object[] args = { arg };
    return invokeExactMethod(object, methodName, args);
  }
  
  public static Object invokeExactMethod(Object object, String methodName, Object[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (args == null)
      args = emptyObjectArray; 
    int arguments = args.length;
    Class[] parameterTypes = new Class[arguments];
    for (int i = 0; i < arguments; i++)
      parameterTypes[i] = args[i].getClass(); 
    return invokeExactMethod(object, methodName, args, parameterTypes);
  }
  
  public static Object invokeExactMethod(Object object, String methodName, Object[] args, Class[] parameterTypes) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (args == null)
      args = emptyObjectArray; 
    if (parameterTypes == null)
      parameterTypes = emptyClassArray; 
    Method method = getAccessibleMethod(object.getClass(), methodName, parameterTypes);
    if (method == null)
      throw new NoSuchMethodException("No such accessible method: " + methodName + "() on object: " + object.getClass().getName()); 
    return method.invoke(object, args);
  }
  
  public static Method getAccessibleMethod(Class clazz, String methodName, Class parameterType) {
    Class[] parameterTypes = { parameterType };
    return getAccessibleMethod(clazz, methodName, parameterTypes);
  }
  
  public static Method getAccessibleMethod(Class clazz, String methodName, Class[] parameterTypes) {
    try {
      return getAccessibleMethod(clazz.getMethod(methodName, parameterTypes));
    } catch (NoSuchMethodException e) {
      return null;
    } 
  }
  
  public static Method getAccessibleMethod(Method method) {
    if (method == null)
      return null; 
    if (!Modifier.isPublic(method.getModifiers()))
      return null; 
    Class clazz = method.getDeclaringClass();
    if (Modifier.isPublic(clazz.getModifiers()))
      return method; 
    method = getAccessibleMethodFromInterfaceNest(clazz, method.getName(), method.getParameterTypes());
    return method;
  }
  
  private static Method getAccessibleMethodFromInterfaceNest(Class clazz, String methodName, Class[] parameterTypes) {
    Method method = null;
    for (; clazz != null; clazz = clazz.getSuperclass()) {
      Class[] interfaces = clazz.getInterfaces();
      for (int i = 0; i < interfaces.length; i++) {
        if (Modifier.isPublic(interfaces[i].getModifiers())) {
          try {
            method = interfaces[i].getDeclaredMethod(methodName, parameterTypes);
          } catch (NoSuchMethodException e) {}
          if (method != null)
            break; 
          method = getAccessibleMethodFromInterfaceNest(interfaces[i], methodName, parameterTypes);
          if (method != null)
            break; 
        } 
      } 
    } 
    if (method != null)
      return method; 
    return null;
  }
  
  public static Method getMatchingAccessibleMethod(Class clazz, String methodName, Class[] parameterTypes) {
    if (log.isTraceEnabled())
      log.trace("Matching name=" + methodName + " on " + clazz); 
    try {
      Method method = clazz.getMethod(methodName, parameterTypes);
      if (log.isTraceEnabled()) {
        log.trace("Found straight match: " + method);
        log.trace("isPublic:" + Modifier.isPublic(method.getModifiers()));
      } 
      try {
        method.setAccessible(true);
      } catch (SecurityException se) {
        if (!loggedAccessibleWarning) {
          log.warn("Cannot use JVM pre-1.4 access bug workaround die to restrictive security manager.");
          loggedAccessibleWarning = true;
        } 
        log.debug("Cannot setAccessible on method. Therefore cannot use jvm access bug workaround.", se);
      } 
      return method;
    } catch (NoSuchMethodException e) {
      int paramSize = parameterTypes.length;
      Method[] methods = clazz.getMethods();
      for (int i = 0, size = methods.length; i < size; i++) {
        if (methods[i].getName().equals(methodName)) {
          if (log.isTraceEnabled()) {
            log.trace("Found matching name:");
            log.trace(methods[i]);
          } 
          Class[] methodsParams = methods[i].getParameterTypes();
          int methodParamSize = methodsParams.length;
          if (methodParamSize == paramSize) {
            boolean match = true;
            for (int n = 0; n < methodParamSize; n++) {
              if (log.isTraceEnabled()) {
                log.trace("Param=" + parameterTypes[n].getName());
                log.trace("Method=" + methodsParams[n].getName());
              } 
              if (!isAssignmentCompatible(methodsParams[n], parameterTypes[n])) {
                if (log.isTraceEnabled())
                  log.trace(methodsParams[n] + " is not assignable from " + parameterTypes[n]); 
                match = false;
                break;
              } 
            } 
            if (match) {
              Method method = getAccessibleMethod(methods[i]);
              if (method != null) {
                if (log.isTraceEnabled())
                  log.trace(method + " accessible version of " + methods[i]); 
                try {
                  method.setAccessible(true);
                } catch (SecurityException se) {
                  if (!loggedAccessibleWarning) {
                    log.warn("Cannot use JVM pre-1.4 access bug workaround die to restrictive security manager.");
                    loggedAccessibleWarning = true;
                  } 
                  log.debug("Cannot setAccessible on method. Therefore cannot use jvm access bug workaround.", se);
                } 
                return method;
              } 
              log.trace("Couldn't find accessible method.");
            } 
          } 
        } 
      } 
      log.trace("No match found.");
      return null;
    } 
  }
  
  protected static final boolean isAssignmentCompatible(Class parameterType, Class parameterization) {
    if (parameterType.isAssignableFrom(parameterization))
      return true; 
    if (parameterType.isPrimitive()) {
      if (boolean.class.equals(parameterType))
        return Boolean.class.equals(parameterization); 
      if (float.class.equals(parameterType))
        return Float.class.equals(parameterization); 
      if (long.class.equals(parameterType))
        return Long.class.equals(parameterization); 
      if (int.class.equals(parameterType))
        return Integer.class.equals(parameterization); 
      if (double.class.equals(parameterType))
        return Double.class.equals(parameterization); 
    } 
    return false;
  }
}
