// Copyright 2004-present Facebook. All Rights Reserved.

package com.facebook.react.uimanager;

import javax.annotation.Nullable;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import android.view.View;

import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;

/**
 * This class is responsible for holding view manager property setters and is used in a process of
 * updating views with the new properties set in JS.
 */
/*package*/ class ViewManagersPropertyCache {

  private static final Map<Class, Map<String, PropSetter>> CLASS_PROPS_CACHE = new HashMap<>();
  private static final Map<String, PropSetter> EMPTY_PROPS_MAP = new HashMap<>();

  /*package*/ static abstract class PropSetter {

    protected final String mPropName;
    protected final String mPropType;
    protected final Method mSetter;
    protected final @Nullable Integer mIndex; /* non-null only for group setters */

    // The following Object arrays are used to prevent extra allocations from varargs when we call
    // Method.invoke. It's safe for those objects to be static as we update properties in a single
    // thread sequentially
    private static final Object[] VIEW_MGR_ARGS = new Object[2];
    private static final Object[] VIEW_MGR_GROUP_ARGS = new Object[3];
    private static final Object[] SHADOW_ARGS = new Object[1];
    private static final Object[] SHADOW_GROUP_ARGS = new Object[2];

    private PropSetter(ReactProp prop, String defaultType, Method setter) {
      mPropName = prop.name();
      mPropType = ReactProp.USE_DEFAULT_TYPE.equals(prop.customType()) ?
          defaultType : prop.customType();
      mSetter = setter;
      mIndex = null;
    }

    private PropSetter(ReactPropGroup prop, String defaultType, Method setter, int index) {
      mPropName = prop.names()[index];
      mPropType = ReactPropGroup.USE_DEFAULT_TYPE.equals(prop.customType()) ?
          defaultType : prop.customType();
      mSetter = setter;
      mIndex = index;
    }

    public String getPropName() {
      return mPropName;
    }

    public String getPropType() {
      return mPropType;
    }

    public void updateViewProp(
        ViewManager viewManager,
        View viewToUpdate,
        ReactStylesDiffMap props) {
      try {
        if (mIndex == null) {
          VIEW_MGR_ARGS[0] = viewToUpdate;
          VIEW_MGR_ARGS[1] = extractProperty(props);
          mSetter.invoke(viewManager, VIEW_MGR_ARGS);
          Arrays.fill(VIEW_MGR_ARGS, null);
        } else {
          VIEW_MGR_GROUP_ARGS[0] = viewToUpdate;
          VIEW_MGR_GROUP_ARGS[1] = mIndex;
          VIEW_MGR_GROUP_ARGS[2] = extractProperty(props);
          mSetter.invoke(viewManager, VIEW_MGR_GROUP_ARGS);
          Arrays.fill(VIEW_MGR_GROUP_ARGS, null);
        }
      } catch (Throwable t) {
        FLog.e(ViewManager.class, "Error while updating prop " + mPropName, t);
        throw new JSApplicationIllegalArgumentException("Error while updating property '" +
            mPropName + "' of a view managed by: " + viewManager.getName(), t);
      }
    }

    public void updateShadowNodeProp(
        ReactShadowNode nodeToUpdate,
        ReactStylesDiffMap props) {
      try {
        if (mIndex == null) {
          SHADOW_ARGS[0] = extractProperty(props);
          mSetter.invoke(nodeToUpdate, SHADOW_ARGS);
          Arrays.fill(SHADOW_ARGS, null);
        } else {
          SHADOW_GROUP_ARGS[0] = mIndex;
          SHADOW_GROUP_ARGS[1] = extractProperty(props);
          mSetter.invoke(nodeToUpdate, SHADOW_GROUP_ARGS);
          Arrays.fill(SHADOW_GROUP_ARGS, null);
        }
      } catch (Throwable t) {
        FLog.e(ViewManager.class, "Error while updating prop " + mPropName, t);
        throw new JSApplicationIllegalArgumentException("Error while updating property '" +
            mPropName + "' in shadow node of type: " + nodeToUpdate.getViewClass(), t);
      }
    }

    protected abstract @Nullable Object extractProperty(ReactStylesDiffMap props);
  }

  private static class IntPropSetter extends PropSetter {

    private final int mDefaultValue;

    public IntPropSetter(ReactProp prop, Method setter, int defaultValue) {
      super(prop, "number", setter);
      mDefaultValue = defaultValue;
    }

    public IntPropSetter(ReactPropGroup prop, Method setter, int index, int defaultValue) {
      super(prop, "number", setter, index);
      mDefaultValue = defaultValue;
    }

    @Override
    protected Object extractProperty(ReactStylesDiffMap props) {
      return props.getInt(mPropName, mDefaultValue);
    }
  }

  private static class DoublePropSetter extends PropSetter {

    private final double mDefaultValue;

    public DoublePropSetter(ReactProp prop, Method setter, double defaultValue) {
      super(prop, "number", setter);
      mDefaultValue = defaultValue;
    }

    public DoublePropSetter(ReactPropGroup prop, Method setter, int index, double defaultValue) {
      super(prop, "number", setter, index);
      mDefaultValue = defaultValue;
    }

    @Override
    protected Object extractProperty(ReactStylesDiffMap props) {
      return props.getDouble(mPropName, mDefaultValue);
    }
  }

  private static class BooleanPropSetter extends PropSetter {

    private final boolean mDefaultValue;

    public BooleanPropSetter(ReactProp prop, Method setter, boolean defaultValue) {
      super(prop, "boolean", setter);
      mDefaultValue = defaultValue;
    }

    @Override
    protected Object extractProperty(ReactStylesDiffMap props) {
      return props.getBoolean(mPropName, mDefaultValue) ? Boolean.TRUE : Boolean.FALSE;
    }
  }

  private static class FloatPropSetter extends PropSetter {

    private final float mDefaultValue;

    public FloatPropSetter(ReactProp prop, Method setter, float defaultValue) {
      super(prop, "number", setter);
      mDefaultValue = defaultValue;
    }

    public FloatPropSetter(ReactPropGroup prop, Method setter, int index, float defaultValue) {
      super(prop, "number", setter, index);
      mDefaultValue = defaultValue;
    }

    @Override
    protected Object extractProperty(ReactStylesDiffMap props) {
      return props.getFloat(mPropName, mDefaultValue);
    }
  }

  private static class ArrayPropSetter extends PropSetter {

    public ArrayPropSetter(ReactProp prop, Method setter) {
      super(prop, "Array", setter);
    }

    @Override
    protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
      return props.getArray(mPropName);
    }
  }

  private static class MapPropSetter extends PropSetter {

    public MapPropSetter(ReactProp prop, Method setter) {
      super(prop, "Map", setter);
    }

    @Override
    protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
      return props.getMap(mPropName);
    }
  }

  private static class StringPropSetter extends PropSetter {

    public StringPropSetter(ReactProp prop, Method setter) {
      super(prop, "String", setter);
    }

    @Override
    protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
      return props.getString(mPropName);
    }
  }

  private static class BoxedBooleanPropSetter extends PropSetter {

    public BoxedBooleanPropSetter(ReactProp prop, Method setter) {
      super(prop, "boolean", setter);
    }

    @Override
    protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
      if (!props.isNull(mPropName)) {
        return props.getBoolean(mPropName, /* ignored */ false) ? Boolean.TRUE : Boolean.FALSE;
      }
      return null;
    }
  }

  private static class BoxedIntPropSetter extends PropSetter {

    public BoxedIntPropSetter(ReactProp prop, Method setter) {
      super(prop, "number", setter);
    }

    public BoxedIntPropSetter(ReactPropGroup prop, Method setter, int index) {
      super(prop, "number", setter, index);
    }

    @Override
    protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
      if (!props.isNull(mPropName)) {
        return props.getInt(mPropName, /* ignored */ 0);
      }
      return null;
    }
  }

  /*package*/ static Map<String, String> getNativePropsForView(
      Class<? extends ViewManager> viewManagerTopClass,
      Class<? extends ReactShadowNode> shadowNodeTopClass) {
    Map<String, String> nativeProps = new HashMap<>();

    Map<String, PropSetter> viewManagerProps =
        getNativePropSettersForViewManagerClass(viewManagerTopClass);
    for (PropSetter setter : viewManagerProps.values()) {
      nativeProps.put(setter.getPropName(), setter.getPropType());
    }

    Map<String, PropSetter> shadowNodeProps =
        getNativePropSettersForShadowNodeClass(shadowNodeTopClass);
    for (PropSetter setter : shadowNodeProps.values()) {
      nativeProps.put(setter.getPropName(), setter.getPropType());
    }

    return nativeProps;
  }

  /**
   * Returns map from property name to setter instances for all the property setters annotated with
   * {@link ReactProp} in the given {@link ViewManager} class plus all the setter declared by its
   * parent classes.
   */
  /*package*/ static Map<String, PropSetter> getNativePropSettersForViewManagerClass(
      Class<? extends ViewManager> cls) {
    if (cls == ViewManager.class) {
      return EMPTY_PROPS_MAP;
    }
    Map<String, PropSetter> props = CLASS_PROPS_CACHE.get(cls);
    if (props != null) {
      return props;
    }
    // This is to include all the setters from parent classes. Once calculated the result will be
    // stored in CLASS_PROPS_CACHE so that we only scan for @ReactProp annotations once per class.
    props = new HashMap<>(
        getNativePropSettersForViewManagerClass(
            (Class<? extends ViewManager>) cls.getSuperclass()));
    extractPropSettersFromViewManagerClassDefinition(cls, props);
    CLASS_PROPS_CACHE.put(cls, props);
    return props;
  }

  /**
   * Returns map from property name to setter instances for all the property setters annotated with
   * {@link ReactProp} (or {@link ReactPropGroup} in the given {@link ReactShadowNode} subclass plus
   * all the setters declared by its parent classes up to {@link ReactShadowNode} which is treated
   * as a base class.
   */
  /*package*/ static Map<String, PropSetter> getNativePropSettersForShadowNodeClass(
      Class<? extends ReactShadowNode> cls) {
    if (cls == ReactShadowNode.class) {
      return EMPTY_PROPS_MAP;
    }
    Map<String, PropSetter> props = CLASS_PROPS_CACHE.get(cls);
    if (props != null) {
      return props;
    }
    // This is to include all the setters from parent classes up to ReactShadowNode class
    props = new HashMap<>(
        getNativePropSettersForShadowNodeClass(
            (Class<? extends ReactShadowNode>) cls.getSuperclass()));
    extractPropSettersFromShadowNodeClassDefinition(cls, props);
    CLASS_PROPS_CACHE.put(cls, props);
    return props;
  }

  private static PropSetter createPropSetter(
      ReactProp annotation,
      Method method,
      Class<?> propTypeClass) {
    if (propTypeClass == boolean.class) {
      return new BooleanPropSetter(annotation, method, annotation.defaultBoolean());
    } else if (propTypeClass == int.class) {
      return new IntPropSetter(annotation, method, annotation.defaultInt());
    } else if (propTypeClass == float.class) {
      return new FloatPropSetter(annotation, method, annotation.defaultFloat());
    } else if (propTypeClass == double.class) {
      return new DoublePropSetter(annotation, method, annotation.defaultDouble());
    } else if (propTypeClass == String.class) {
      return new StringPropSetter(annotation, method);
    } else if (propTypeClass == Boolean.class) {
      return new BoxedBooleanPropSetter(annotation, method);
    } else if (propTypeClass == Integer.class) {
      return new BoxedIntPropSetter(annotation, method);
    } else if (propTypeClass == ReadableArray.class) {
      return new ArrayPropSetter(annotation, method);
    } else if (propTypeClass == ReadableMap.class) {
      return new MapPropSetter(annotation, method);
    } else {
      throw new RuntimeException("Unrecognized type: " + propTypeClass + " for method: " +
          method.getDeclaringClass().getName() + "#" + method.getName());
    }
  }

  private static void createPropSetters(
      ReactPropGroup annotation,
      Method method,
      Class<?> propTypeClass,
      Map<String, PropSetter> props) {
    String[] names = annotation.names();
    if (propTypeClass == int.class) {
      for (int i = 0; i < names.length; i++) {
        props.put(
            names[i],
            new IntPropSetter(annotation, method, i, annotation.defaultInt()));
      }
    } else if (propTypeClass == float.class) {
      for (int i = 0; i < names.length; i++) {
        props.put(
            names[i],
            new FloatPropSetter(annotation, method, i, annotation.defaultFloat()));
      }
    } else if (propTypeClass == double.class) {
      for (int i = 0; i < names.length; i++) {
        props.put(
            names[i],
            new DoublePropSetter(annotation, method, i, annotation.defaultDouble()));
      }
    } else if (propTypeClass == Integer.class) {
      for (int i = 0; i < names.length; i++) {
        props.put(
            names[i],
            new BoxedIntPropSetter(annotation, method, i));
      }
    } else {
      throw new RuntimeException("Unrecognized type: " + propTypeClass + " for method: " +
          method.getDeclaringClass().getName() + "#" + method.getName());
    }
  }

  private static void extractPropSettersFromViewManagerClassDefinition(
      Class<? extends ViewManager> cls,
      Map<String, PropSetter> props) {
    Method[] declaredMethods = cls.getDeclaredMethods();
    for (int i = 0; i < declaredMethods.length; i++) {
      Method method = declaredMethods[i];
      ReactProp annotation = method.getAnnotation(ReactProp.class);
      if (annotation != null) {
        Class<?>[] paramTypes = method.getParameterTypes();
        if (paramTypes.length != 2) {
          throw new RuntimeException("Wrong number of args for prop setter: " +
              cls.getName() + "#" + method.getName());
        }
        if (!View.class.isAssignableFrom(paramTypes[0])) {
          throw new RuntimeException("First param should be a view subclass to be updated: " +
              cls.getName() + "#" + method.getName());
        }
        props.put(annotation.name(), createPropSetter(annotation, method, paramTypes[1]));
      }

      ReactPropGroup groupAnnotation = method.getAnnotation(ReactPropGroup.class);
      if (groupAnnotation != null) {
        Class<?> [] paramTypes = method.getParameterTypes();
        if (paramTypes.length != 3) {
          throw new RuntimeException("Wrong number of args for group prop setter: " +
              cls.getName() + "#" + method.getName());
        }
        if (!View.class.isAssignableFrom(paramTypes[0])) {
          throw new RuntimeException("First param should be a view subclass to be updated: " +
              cls.getName() + "#" + method.getName());
        }
        if (paramTypes[1] != int.class) {
          throw new RuntimeException("Second argument should be property index: " +
              cls.getName() + "#" + method.getName());
        }
        createPropSetters(groupAnnotation, method, paramTypes[2], props);
      }
    }
  }

  private static void extractPropSettersFromShadowNodeClassDefinition(
      Class<? extends ReactShadowNode> cls,
      Map<String, PropSetter> props) {
    for (Method method : cls.getDeclaredMethods()) {
      ReactProp annotation = method.getAnnotation(ReactProp.class);
      if (annotation != null) {
        Class<?>[] paramTypes = method.getParameterTypes();
        if (paramTypes.length != 1) {
          throw new RuntimeException("Wrong number of args for prop setter: " +
              cls.getName() + "#" + method.getName());
        }
        props.put(annotation.name(), createPropSetter(annotation, method, paramTypes[0]));
      }

      ReactPropGroup groupAnnotation = method.getAnnotation(ReactPropGroup.class);
      if (groupAnnotation != null) {
        Class<?> [] paramTypes = method.getParameterTypes();
        if (paramTypes.length != 2) {
          throw new RuntimeException("Wrong number of args for group prop setter: " +
              cls.getName() + "#" + method.getName());
        }
        if (paramTypes[0] != int.class) {
          throw new RuntimeException("Second argument should be property index: " +
              cls.getName() + "#" + method.getName());
        }
        createPropSetters(groupAnnotation, method, paramTypes[1], props);
      }
    }
  }
}
