/*
 * 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 hydra;

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

import org.apache.geode.SystemFailure;

/**
 *
 * A class specialized for executing (via reflection) the receiver/selector pairs found in
 * TestTasks.
 *
 */
public class MethExecutor {

  // @todo lises add static args method

  /**
   * Helper method that searches a class (and its superclasses) for a method with the given name and
   * parameter types.
   *
   * @throws NoSuchMethodException If the method cannot be found
   */
  public static Method getMethod(Class c, String methodName, Class[] paramTypes)
      throws NoSuchMethodException {

    ArrayList matchingMethods = new ArrayList();
    for (Class q = c; q != null; q = q.getSuperclass()) {
      Method[] methods = q.getDeclaredMethods();
      NEXT_METHOD: for (int i = 0; i < methods.length; i++) {
        Method m = methods[i];
        if (!m.getName().equals(methodName)) {
          continue;
        }

        Class[] argTypes = m.getParameterTypes();
        if (argTypes.length != paramTypes.length) {
          continue;
        }

        for (int j = 0; j < argTypes.length; j++) {
          if (paramTypes[j] == null) {
            if (argTypes[j].isPrimitive()) {
              // this parameter is not ok, the parameter is a primative and the value is null
              continue NEXT_METHOD;
            } else {
              // this parameter is ok, the argument is an object and the value is null
              continue;
            }
          }
          if (!argTypes[j].isAssignableFrom(paramTypes[j])) {
            Class argType = argTypes[j];
            Class paramType = paramTypes[j];

            if (argType.isPrimitive()) {
              if ((argType.equals(boolean.class) && paramType.equals(Boolean.class))
                  || (argType.equals(short.class) && paramType.equals(Short.class))
                  || (argType.equals(int.class) && paramType.equals(Integer.class))
                  || (argType.equals(long.class) && paramType.equals(Long.class))
                  || (argType.equals(float.class) && paramType.equals(Float.class))
                  || (argType.equals(double.class) && paramType.equals(Double.class))
                  || (argType.equals(char.class) && paramType.equals(Character.class))
                  || (argType.equals(byte.class) && paramType.equals(Byte.class)) || false) {

                // This parameter is okay, try the next arg
                continue;
              }
            }
            continue NEXT_METHOD;
          }
        }

        matchingMethods.add(m);
      }

      // We want to check to make sure there aren't two
      // ambiguous methods on the same class. But a subclass
      // can still override a method on a super class, so we'll stop
      // if we found a method on the subclass.
      if (matchingMethods.size() > 0) {
        break;
      }
    }

    if (matchingMethods.isEmpty()) {
      StringBuffer sb = new StringBuffer();
      sb.append("Could not find method ");
      sb.append(methodName);
      sb.append(" with ");
      sb.append(paramTypes.length);
      sb.append(" parameters [");
      for (int i = 0; i < paramTypes.length; i++) {
        String name = paramTypes[i] == null ? null : paramTypes[i].getName();
        sb.append(name);
        if (i < paramTypes.length - 1) {
          sb.append(", ");
        }
      }
      sb.append("] in class ");
      sb.append(c.getName());
      throw new NoSuchMethodException(sb.toString());
    }
    if (matchingMethods.size() > 1) {
      StringBuffer sb = new StringBuffer();
      sb.append("Method is ambiguous ");
      sb.append(methodName);
      sb.append(" with ");
      sb.append(paramTypes.length);
      sb.append(" parameters [");
      for (int i = 0; i < paramTypes.length; i++) {
        String name = paramTypes[i] == null ? null : paramTypes[i].getName();
        sb.append(name);
        if (i < paramTypes.length - 1) {
          sb.append(", ");
        }
      }
      sb.append("] in class ");
      sb.append(c.getName());
      sb.append(" methods=" + matchingMethods);
      throw new NoSuchMethodException(sb.toString());
    } else
      return (Method) matchingMethods.get(0);
  }

  /**
   *
   * Send the message "selector" to the class named "receiver". Return the result, including stack
   * trace (if any).
   *
   */
  public static MethExecutorResult execute(String receiver, String selector) {
    return execute(receiver, selector, null);
  }

  /**
   * Executes the given static method on the given class with the given arguments.
   */
  public static MethExecutorResult execute(String receiver, String selector, Object[] args) {
    try {
      // get the class
      Class receiverClass = Class.forName(receiver);

      // invoke the method
      Object res = null;
      try {
        Class[] paramTypes;
        if (args == null) {
          paramTypes = new Class[0];

        } else {
          paramTypes = new Class[args.length];
          for (int i = 0; i < args.length; i++) {
            if (args[i] == null) {
              paramTypes[i] = null;

            } else {
              paramTypes[i] = args[i].getClass();
            }
          }
        }

        Method theMethod = getMethod(receiverClass, selector, paramTypes);
        theMethod.setAccessible(true);
        res = theMethod.invoke(receiverClass, args);
        return new MethExecutorResult(res);

      } catch (InvocationTargetException invTargEx) {
        Throwable targEx = invTargEx.getTargetException();
        if (targEx == null) {
          return new MethExecutorResult(res);

        } else {
          return new MethExecutorResult(targEx);
        }
      }

    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable t) {
      // String s = "While trying to invoke " + receiver + "." +
      // selector;
      // t = new HydraConfigException(s, t);
      return new MethExecutorResult(t);
    }
  }

  /**
   *
   * Send the message "selector" to the object "target". Return the result, including stack trace
   * (if any).
   *
   */
  public static MethExecutorResult executeObject(Object target, String selector) {
    return executeObject(target, selector, null);
  }

  /**
   * Executes the given instance method on the given object with the given arguments.
   */
  public static MethExecutorResult executeObject(Object target, String selector, Object[] args) {
    try {
      // get the class
      Class receiverClass = target.getClass();

      // invoke the method
      Object res = null;
      try {
        Class[] paramTypes;
        if (args == null) {
          paramTypes = new Class[0];

        } else {
          paramTypes = new Class[args.length];
          for (int i = 0; i < args.length; i++) {
            if (args[i] == null) {
              paramTypes[i] = Object.class;

            } else {
              paramTypes[i] = args[i].getClass();
            }
          }
        }

        Method theMethod = getMethod(receiverClass, selector, paramTypes);
        theMethod.setAccessible(true);
        res = theMethod.invoke(target, args);
        return new MethExecutorResult(res);

      } catch (InvocationTargetException invTargEx) {
        Throwable targEx = invTargEx.getTargetException();
        if (targEx == null) {
          return new MethExecutorResult(res);

        } else {
          return new MethExecutorResult(targEx);
        }
      }

    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable t) {
      return new MethExecutorResult(t);
    }
  }

  /**
   *
   * Send the message "selector" to an instance of the class named "receiver". Return the result,
   * including stack trace (if any).
   *
   */
  public static MethExecutorResult executeInstance(String receiver, String selector) {

    try {
      // get the class
      Class receiverClass = Class.forName(receiver);
      Object target = receiverClass.newInstance();

      // invoke the method
      Object res = null;
      try {
        Method theMethod = getMethod(receiverClass, selector, new Class[0]);
        res = theMethod.invoke(target, new Object[0]);
        return new MethExecutorResult(res);

      } catch (InvocationTargetException invTargEx) {
        Throwable targEx = invTargEx.getTargetException();
        if (targEx == null) {
          return new MethExecutorResult(res);
        } else {
          return new MethExecutorResult(targEx);
        }
      }

    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable t) {
      return new MethExecutorResult(t);
    }
  }

  /**
   *
   * Send the message "selector" to an instance of the class named "receiver". Return the result,
   * including stack trace (if any).
   *
   */
  public static MethExecutorResult executeInstance(String receiver, String selector, Class[] types,
      Object[] args) {

    try {
      // get the class
      Class receiverClass = Class.forName(receiver);
      Constructor init = receiverClass.getDeclaredConstructor(new Class[0]);
      init.setAccessible(true);
      Object target = init.newInstance(new Object[0]);

      // invoke the method
      Object res = null;
      try {
        Method theMethod = getMethod(receiverClass, selector, types);
        res = theMethod.invoke(target, args);
        return new MethExecutorResult(res);

      } catch (InvocationTargetException invTargEx) {
        Throwable targEx = invTargEx.getTargetException();
        if (targEx == null) {
          return new MethExecutorResult(res);

        } else {
          return new MethExecutorResult(targEx);
        }
      }

    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable t) {
      return new MethExecutorResult(t);
    }
  }

  /**
   *
   * A small program for testing this class.
   *
   */
  public static String testMethod1() {
    return "The result is: " + System.currentTimeMillis();
  }

  public static String testMethod2() {
    throw new ArrayIndexOutOfBoundsException("frip");
  }

  public static void main(String[] args) {
    MethExecutorResult result = null;
    result = MethExecutor.execute("hydra.MethExecutor", "testMethod1");
    System.out.println(result.toString());
    result = MethExecutor.execute("hydra.MethExecutor", "testMethod2");
    System.out.println(result.toString());
  }
}
