package expo.core;

import android.content.Context;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import expo.core.interfaces.ExpoMethod;

/**
 * Abstract class for exported modules, i. e. modules which export some methods to client code.
 * Use {@link ExpoMethod} or override {@link ExportedModule#getExportedMethods()}
 * to export specific methods to client code and then {@link ExportedModule#invokeExportedMethod(String, Collection)}
 * to support them.
 */
public abstract class ExportedModule {
  public class MethodInfo {
    private Class<?>[] mParameterTypes;

    MethodInfo(Method method) {
      mParameterTypes = method.getParameterTypes();
    }

    public Class<?>[] getParameterTypes() {
      return mParameterTypes;
    }
  }
  private Context mContext;
  private Map<String, Method> mExportedMethods;
  private Map<String, MethodInfo> mExportedMethodInfos;

  public ExportedModule(Context context) {
    mContext = context;
  }

  public abstract String getName();

  public Map<String, Object> getConstants() {
    return Collections.unmodifiableMap(Collections.<String, Object>emptyMap());
  }

  protected Context getContext() {
    return mContext;
  }

  /**
   * Returns a map of { exportedMethodName => methodInfo } so that eg. platform adapter knows
   * what classes of arguments does the method expect.
   */
  public Map<String, MethodInfo> getExportedMethodInfos() {
    if (mExportedMethodInfos != null) {
      return mExportedMethodInfos;
    }

    Map<String, MethodInfo> exportedMethodInfos = new HashMap<>();
    for(Map.Entry<String, Method> entry : getExportedMethods().entrySet()) {
      exportedMethodInfos.put(entry.getKey(), new MethodInfo(entry.getValue()));
    }
    mExportedMethodInfos = exportedMethodInfos;
    return mExportedMethodInfos;
  }

  /**
   * Invokes an exported method
   */
  public Object invokeExportedMethod(String methodName, Collection<Object> arguments) throws NoSuchMethodException, RuntimeException {
    Method method = mExportedMethods.get(methodName);

    if (method  == null) {
      throw new NoSuchMethodException("Module " + getName() + "does not export method " + methodName + ".");
    }

    int expectedArgumentsCount = method.getParameterTypes().length;
    if (arguments.size() != expectedArgumentsCount) {
      throw new IllegalArgumentException(
              "Method " + methodName + " on class " + getName() + " expects " + expectedArgumentsCount + " arguments, "
                      + "whereas " + arguments.size() + " arguments have been provided.");
    }

    Class<?>[] expectedArgumentClasses = method.getParameterTypes();
    Iterator<Object> actualArgumentsIterator = arguments.iterator();
    List<Object> transformedArguments = new ArrayList<>(arguments.size());

    for (int i = 0; i < expectedArgumentsCount; i++) {
      transformedArguments.add(transformArgumentToClass(actualArgumentsIterator.next(), expectedArgumentClasses[i]));
    }

    try {
      return method.invoke(this, transformedArguments.toArray());
    } catch (IllegalAccessException | InvocationTargetException e) {
      e.printStackTrace();
      throw new RuntimeException("Exception occurred while executing exported method " + methodName
              + " on module " + getName() + ": " + e.getMessage(), e);
    }
  }

  protected Object transformArgumentToClass(Object argument, Class<?> expectedArgumentClass) {
    return ArgumentsHelper.validatedArgumentForClass(argument, expectedArgumentClass);
  }

  /**
   * Creates or returns a cached String-keyed map of validated methods exported from {@link ExportedModule},
   * i. e. methods annotated with {@link ExpoMethod}, which should be available in client code land.
   */
  public Map<String, Method> getExportedMethods() {
    if (mExportedMethods != null) {
      return mExportedMethods;
    }

    mExportedMethods = new HashMap<>();
    Method[] declaredMethodsArray = getClass().getDeclaredMethods();

    for (Method method : declaredMethodsArray) {
      if (method.getAnnotation(ExpoMethod.class) != null) {
        String methodName = method.getName();
        Class<?>[] methodParameterTypes = method.getParameterTypes();
        if (methodParameterTypes.length < 1) {
          throw new IllegalArgumentException(
                  "Method " + methodName + " of Java Module " + getName() + " does not define any arguments - minimum argument set is a Promise"
          );
        }

        Class<?> lastParameterClass = methodParameterTypes[methodParameterTypes.length - 1];

        if (lastParameterClass != expo.core.Promise.class) {
          throw new IllegalArgumentException(
                  "Last argument of method " + methodName + " of Java Module " + getName() + " does not expect a Promise"
          );
        }

        if (mExportedMethods.containsKey(methodName)) {
          throw new IllegalArgumentException(
                  "Java Module " + getName() + " method name already registered: " + methodName + "."
          );
        }

        mExportedMethods.put(methodName, method);
      }
    }

    return mExportedMethods;
  }
}

@@ -4109,50 +4109,826 @@
     
-Method%5B%5D declaredMethodsArray = getClass()
+Class klass = getClass();%0A    while (klass != null && ExportedModule.class.isAssignableFrom(klass)) %7B%0A      Map%3CString, Method%3E exportedMethods = getExportedMethods(klass);%0A      for (Map.Entry%3CString, Method%3E methodEntry : exportedMethods.entrySet()) %7B%0A        // Do not overwrite methods from subclasses with methods from superclasses%0A        // (We're iterating from the furthest subclass to ExportedModule.)%0A        if (!mExportedMethods.containsKey(methodEntry.getKey())) %7B%0A          mExportedMethods.put(methodEntry.getKey(), methodEntry.getValue());%0A        %7D%0A      %7D%0A      klass = klass.getSuperclass();%0A    %7D%0A    return mExportedMethods;%0A  %7D%0A%0A  protected Map%3CString, Method%3E getExportedMethods(Class klass) %7B%0A    Map%3CString, Method%3E exportedMethods = new HashMap%3C%3E();%0A    Method%5B%5D declaredMethodsArray = klass
 .get
@@ -5271,36 +5271,32 @@
 (%0A              
-    
 %22Method %22 + meth
@@ -5645,20 +5645,16 @@
         
-    
 %22Last ar
@@ -5777,34 +5777,33 @@
  %7D%0A%0A        if (
-mE
+e
 xportedMethods.c
@@ -5888,20 +5888,16 @@
         
-    
 %22Java Mo
@@ -5996,34 +5996,33 @@
      %7D%0A%0A        
-mE
+e
 xportedMethods.p
@@ -6063,34 +6063,33 @@
   %7D%0A%0A    return 
-mE
+e
 xportedMethods;%0A

Support subclassing ExportedModule without overridding all ExpoMethods (#2916)