/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.uimanager;

import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_CONSTANTS_END;
import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_CONSTANTS_START;
import static com.facebook.react.uimanager.common.UIManagerType.DEFAULT;

import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
import android.media.AudioManager;
import com.facebook.common.logging.FLog;
import com.facebook.debug.holder.PrinterHolder;
import com.facebook.debug.tags.ReactDebugOverlayTags;
import com.facebook.react.animation.Animation;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.OnBatchCompleteListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.common.MeasureSpecProvider;
import com.facebook.react.uimanager.common.SizeMonitoringFrameLayout;
import com.facebook.react.uimanager.common.ViewUtil;
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * <p>Native module to allow JS to create and update native Views.</p>
 *
 * <p>
 * <h2>== Transactional Requirement ==</h2>
 * A requirement of this class is to make sure that transactional UI updates occur all at once,
 * meaning that no intermediate state is ever rendered to the screen. For example, if a JS
 * application update changes the background of View A to blue and the width of View B to 100, both
 * need to appear at once. Practically, this means that all UI update code related to a single
 * transaction must be executed as a single code block on the UI thread. Executing as multiple code
 * blocks could allow the platform UI system to interrupt and render a partial UI state.
 * </p>
 *
 * <p>To facilitate this, this module enqueues operations that are then applied to native view
 * hierarchy through {@link NativeViewHierarchyManager} at the end of each transaction.
 *
 * <p>
 * <h2>== CSSNodes ==</h2>
 * In order to allow layout and measurement to occur on a non-UI thread, this module also
 * operates on intermediate CSSNodeDEPRECATED objects that correspond to a native view. These CSSNodeDEPRECATED are able
 * to calculate layout according to their styling rules, and then the resulting x/y/width/height of
 * that layout is scheduled as an operation that will be applied to native view hierarchy at the end
 * of current batch.
 * </p>
 *
 * TODO(5241856): Investigate memory usage of creating many small objects in UIManageModule and
 *                consider implementing a pool
 * TODO(5483063): Don't dispatch the view hierarchy at the end of a batch if no UI changes occurred
 */
@ReactModule(name = UIManagerModule.NAME)
public class UIManagerModule extends ReactContextBaseJavaModule implements
    OnBatchCompleteListener, LifecycleEventListener, UIManager {

  /**
   * Enables lazy discovery of a specific {@link ViewManager} by its name.
   */
  public interface ViewManagerResolver {
    /**
     * {@class UIManagerModule} class uses this method to get a ViewManager by its name.
     * This is the same name that comes from JS by {@code UIManager.ViewManagerName} call.
     */
    @Nullable ViewManager getViewManager(String viewManagerName);

    /**
     * Provides a list of view manager names to register in JS as {@code UIManager.ViewManagerName}
     */
    List<String> getViewManagerNames();
  }

  /**
   * Resolves a name coming from native side to a name of the event that is exposed to JS.
   */
  public interface CustomEventNamesResolver {
    /**
     * Returns custom event name by the provided event name.
     */
    @Nullable String resolveCustomEventName(String eventName);
  }

  protected static final String NAME = "UIManager";

  private static final boolean DEBUG =
      PrinterHolder.getPrinter().shouldDisplayLogMessage(ReactDebugOverlayTags.UI_MANAGER);

  private final EventDispatcher mEventDispatcher;
  private final Map<String, Object> mModuleConstants;
  private final Map<String, Object> mCustomDirectEvents;
  private final UIImplementation mUIImplementation;
  private final MemoryTrimCallback mMemoryTrimCallback = new MemoryTrimCallback();
  private final List<UIManagerModuleListener> mListeners = new ArrayList<>();

  private int mBatchId = 0;

  public UIManagerModule(
      ReactApplicationContext reactContext,
      ViewManagerResolver viewManagerResolver,
      int minTimeLeftInFrameForNonBatchedOperationMs) {
    super(reactContext);
    DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext);
    mEventDispatcher = new EventDispatcher(reactContext);
    mModuleConstants = createConstants(viewManagerResolver);
    mCustomDirectEvents = UIManagerModuleConstants.getDirectEventTypeConstants();
    mUIImplementation =
        new UIImplementation(
            reactContext,
            viewManagerResolver,
            mEventDispatcher,
            minTimeLeftInFrameForNonBatchedOperationMs);

    reactContext.addLifecycleEventListener(this);
  }

  public UIManagerModule(
      ReactApplicationContext reactContext,
      List<ViewManager> viewManagersList,
      int minTimeLeftInFrameForNonBatchedOperationMs) {
    super(reactContext);
    DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext);
    mEventDispatcher = new EventDispatcher(reactContext);
    mCustomDirectEvents = MapBuilder.newHashMap();
    mModuleConstants = createConstants(viewManagersList, null, mCustomDirectEvents);
    mUIImplementation =
        new UIImplementation(
            reactContext,
            viewManagersList,
            mEventDispatcher,
            minTimeLeftInFrameForNonBatchedOperationMs);

    reactContext.addLifecycleEventListener(this);
  }
  /**
   * This method gives an access to the {@link UIImplementation} object that can be used to execute
   * operations on the view hierarchy.
   */
  public UIImplementation getUIImplementation() {
    return mUIImplementation;
  }

  @Override
  public String getName() {
    return NAME;
  }

  @Override
  public Map<String, Object> getConstants() {
    return mModuleConstants;
  }

  @Override
  public void initialize() {
    getReactApplicationContext().registerComponentCallbacks(mMemoryTrimCallback);
    mEventDispatcher.registerEventEmitter(
      DEFAULT,
      getReactApplicationContext().getJSModule(RCTEventEmitter.class));
  }

  @Override
  public void onHostResume() {

    mUIImplementation.onHostResume();
  }

  @Override
  public void onHostPause() {
    mUIImplementation.onHostPause();
  }

  @Override
  public void onHostDestroy() {
    mUIImplementation.onHostDestroy();
  }

  @Override
  public void onCatalystInstanceDestroy() {
    super.onCatalystInstanceDestroy();
    mEventDispatcher.onCatalystInstanceDestroyed();

    getReactApplicationContext().unregisterComponentCallbacks(mMemoryTrimCallback);
    YogaNodePool.get().clear();
    ViewManagerPropertyUpdater.clear();
  }

  private static Map<String, Object> createConstants(ViewManagerResolver viewManagerResolver) {
    ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_START);
    Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "CreateUIManagerConstants");
    try {
      return UIManagerModuleConstantsHelper.createConstants(viewManagerResolver);
    } finally {
      Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
      ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_END);
    }
  }

  private static Map<String, Object> createConstants(
      List<ViewManager> viewManagers,
      @Nullable Map<String, Object> customBubblingEvents,
      @Nullable Map<String, Object> customDirectEvents) {
    ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_START);
    Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "CreateUIManagerConstants");
    try {
      return UIManagerModuleConstantsHelper.createConstants(
          viewManagers, customBubblingEvents, customDirectEvents);
    } finally {
      Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
      ReactMarker.logMarker(CREATE_UI_MANAGER_MODULE_CONSTANTS_END);
    }
  }

  @ReactMethod(isBlockingSynchronousMethod = true)
  public @Nullable WritableMap getConstantsForViewManager(final String viewManagerName) {
    ViewManager targetView =
        viewManagerName != null ? mUIImplementation.resolveViewManager(viewManagerName) : null;
    if (targetView == null) {
      return null;
    }

    SystraceMessage.beginSection(
            Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIManagerModule.getConstantsForViewManager")
        .arg("ViewManager", targetView.getName())
        .arg("Lazy", true)
        .flush();
    try {
      Map<String, Object> viewManagerConstants =
          UIManagerModuleConstantsHelper.createConstantsForViewManager(
              targetView, null, null, null, mCustomDirectEvents);
      if (viewManagerConstants != null) {
        return Arguments.makeNativeMap(viewManagerConstants);
      }
      return null;
    } finally {
      SystraceMessage.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE).flush();
    }
  }

  @ReactMethod(isBlockingSynchronousMethod = true)
  public WritableMap getDefaultEventTypes() {
    return Arguments.makeNativeMap(UIManagerModuleConstantsHelper.getDefaultExportableEventTypes());
  }

  /** Resolves Direct Event name exposed to JS from the one known to the Native side. */
  public CustomEventNamesResolver getDirectEventNamesResolver() {
    return new CustomEventNamesResolver() {
      @Override
      public @Nullable String resolveCustomEventName(String eventName) {
        Map<String, String> customEventType =
            (Map<String, String>) mCustomDirectEvents.get(eventName);
        if (customEventType != null) {
          return customEventType.get("registrationName");
        }
        return eventName;
      }
    };
  }

  @Override
  public void profileNextBatch() {
    mUIImplementation.profileNextBatch();
  }

  @Override
  public Map<String, Long> getPerformanceCounters() {
    return mUIImplementation.getProfiledBatchPerfCounters();
  }

  /**
   * Registers a new root view. JS can use the returned tag with manageChildren to add/remove
   * children to this view.
   *
   * <p>Note that this must be called after getWidth()/getHeight() actually return something. See
   * CatalystApplicationFragment as an example.
   *
   * <p>TODO(6242243): Make addRootView thread safe NB: this method is horribly not-thread-safe.
   */
  @Override
  public <T extends SizeMonitoringFrameLayout & MeasureSpecProvider> int addRootView(
      final T rootView) {
    Systrace.beginSection(
      Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
      "UIManagerModule.addRootView");
    final int tag = ReactRootViewTagGenerator.getNextRootViewTag();
    final ReactApplicationContext reactApplicationContext = getReactApplicationContext();
    final ThemedReactContext themedRootContext =
      new ThemedReactContext(reactApplicationContext, rootView.getContext());

    mUIImplementation.registerRootView(rootView, tag, themedRootContext);

    rootView.setOnSizeChangedListener(
      new SizeMonitoringFrameLayout.OnSizeChangedListener() {
        @Override
        public void onSizeChanged(final int width, final int height, int oldW, int oldH) {
          reactApplicationContext.runOnNativeModulesQueueThread(
            new GuardedRunnable(reactApplicationContext) {
              @Override
              public void runGuarded() {
                updateNodeSize(tag, width, height);
              }
            });
        }
      });

    Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
    return tag;
  }

  @ReactMethod
  public void removeRootView(int rootViewTag) {
    mUIImplementation.removeRootView(rootViewTag);
  }

  public void updateNodeSize(int nodeViewTag, int newWidth, int newHeight) {
    getReactApplicationContext().assertOnNativeModulesQueueThread();

    mUIImplementation.updateNodeSize(nodeViewTag, newWidth, newHeight);
  }

  /**
   * Sets local data for a shadow node corresponded with given tag.
   * In some cases we need a way to specify some environmental data to shadow node
   * to improve layout (or do something similar), so {@code localData} serves these needs.
   * For example, any stateful embedded native views may benefit from this.
   * Have in mind that this data is not supposed to interfere with the state of
   * the shadow view.
   * Please respect one-directional data flow of React.
   */
  public void setViewLocalData(final int tag, final Object data) {
    final ReactApplicationContext reactApplicationContext = getReactApplicationContext();

    reactApplicationContext.assertOnUiQueueThread();

    reactApplicationContext.runOnNativeModulesQueueThread(
        new GuardedRunnable(reactApplicationContext) {
          @Override
          public void runGuarded() {
            mUIImplementation.setViewLocalData(tag, data);
          }
        });
  }

  @ReactMethod
  public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
    if (DEBUG) {
      String message =
          "(UIManager.createView) tag: " + tag + ", class: " + className + ", props: " + props;
      FLog.d(ReactConstants.TAG, message);
      PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.UI_MANAGER, message);
    }
    mUIImplementation.createView(tag, className, rootViewTag, props);
  }

  @ReactMethod
  public void updateView(int tag, String className, ReadableMap props) {
    if (DEBUG) {
      String message =
          "(UIManager.updateView) tag: " + tag + ", class: " + className + ", props: " + props;
      FLog.d(ReactConstants.TAG, message);
      PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.UI_MANAGER, message);
    }
    mUIImplementation.updateView(tag, className, props);
  }

  /**
   * Interface for adding/removing/moving views within a parent view from JS.
   *
   * @param viewTag the view tag of the parent view
   * @param moveFrom a list of indices in the parent view to move views from
   * @param moveTo parallel to moveFrom, a list of indices in the parent view to move views to
   * @param addChildTags a list of tags of views to add to the parent
   * @param addAtIndices parallel to addChildTags, a list of indices to insert those children at
   * @param removeFrom a list of indices of views to permanently remove. The memory for the
   *        corresponding views and data structures should be reclaimed.
   */
  @ReactMethod
  public void manageChildren(
      int viewTag,
      @Nullable ReadableArray moveFrom,
      @Nullable ReadableArray moveTo,
      @Nullable ReadableArray addChildTags,
      @Nullable ReadableArray addAtIndices,
      @Nullable ReadableArray removeFrom) {
    if (DEBUG) {
      String message =
          "(UIManager.manageChildren) tag: "
              + viewTag
              + ", moveFrom: "
              + moveFrom
              + ", moveTo: "
              + moveTo
              + ", addTags: "
              + addChildTags
              + ", atIndices: "
              + addAtIndices
              + ", removeFrom: "
              + removeFrom;
      FLog.d(ReactConstants.TAG, message);
      PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.UI_MANAGER, message);
    }
    mUIImplementation.manageChildren(
        viewTag,
        moveFrom,
        moveTo,
        addChildTags,
        addAtIndices,
        removeFrom);
  }

  /**
   * Interface for fast tracking the initial adding of views.  Children view tags are assumed to be
   * in order
   *
   * @param viewTag the view tag of the parent view
   * @param childrenTags An array of tags to add to the parent in order
   */
  @ReactMethod
  public void setChildren(
    int viewTag,
    ReadableArray childrenTags) {
    if (DEBUG) {
      String message = "(UIManager.setChildren) tag: " + viewTag + ", children: " + childrenTags;
      FLog.d(ReactConstants.TAG, message);
      PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.UI_MANAGER, message);
    }
    mUIImplementation.setChildren(viewTag, childrenTags);
  }

  /**
   * Replaces the View specified by oldTag with the View specified by newTag within oldTag's parent.
   * This resolves to a simple {@link #manageChildren} call, but React doesn't have enough info in
   * JS to formulate it itself.
   */
  @ReactMethod
  public void replaceExistingNonRootView(int oldTag, int newTag) {
    mUIImplementation.replaceExistingNonRootView(oldTag, newTag);
  }

  /**
   * Method which takes a container tag and then releases all subviews for that container upon
   * receipt.
   * TODO: The method name is incorrect and will be renamed, #6033872
   * @param containerTag the tag of the container for which the subviews must be removed
   */
  @ReactMethod
  public void removeSubviewsFromContainerWithID(int containerTag) {
    mUIImplementation.removeSubviewsFromContainerWithID(containerTag);
  }

  /**
   * Determines the location on screen, width, and height of the given view and returns the values
   * via an async callback.
   */
  @ReactMethod
  public void measure(int reactTag, Callback callback) {
    mUIImplementation.measure(reactTag, callback);
  }

  /**
   * Determines the location on screen, width, and height of the given view relative to the device
   * screen and returns the values via an async callback.  This is the absolute position including
   * things like the status bar
   */
  @ReactMethod
  public void measureInWindow(int reactTag, Callback callback) {
    mUIImplementation.measureInWindow(reactTag, callback);
  }

  /**
   * Measures the view specified by tag relative to the given ancestorTag. This means that the
   * returned x, y are relative to the origin x, y of the ancestor view. Results are stored in the
   * given outputBuffer. We allow ancestor view and measured view to be the same, in which case
   * the position always will be (0, 0) and method will only measure the view dimensions.
   *
   * NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
   * window which can cause unexpected results when measuring relative to things like ScrollViews
   * that can have offset content on the screen.
   */
  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Callback errorCallback,
      Callback successCallback) {
    mUIImplementation.measureLayout(tag, ancestorTag, errorCallback, successCallback);
  }

  /**
   * Like {@link #measure} and {@link #measureLayout} but measures relative to the immediate parent.
   *
   * NB: Unlike {@link #measure}, this will measure relative to the view layout, not the visible
   * window which can cause unexpected results when measuring relative to things like ScrollViews
   * that can have offset content on the screen.
   */
  @ReactMethod
  public void measureLayoutRelativeToParent(
      int tag,
      Callback errorCallback,
      Callback successCallback) {
    mUIImplementation.measureLayoutRelativeToParent(tag, errorCallback, successCallback);
  }

  /**
   * Find the touch target child native view in  the supplied root view hierarchy, given a react
   * target location.
   *
   * This method is currently used only by Element Inspector DevTool.
   *
   * @param reactTag the tag of the root view to traverse
   * @param point an array containing both X and Y target location
   * @param callback will be called if with the identified child view react ID, and measurement
   *        info. If no view was found, callback will be invoked with no data.
   */
  @ReactMethod
  public void findSubviewIn(
      final int reactTag,
      final ReadableArray point,
      final Callback callback) {
    mUIImplementation.findSubviewIn(
      reactTag,
      Math.round(PixelUtil.toPixelFromDIP(point.getDouble(0))),
      Math.round(PixelUtil.toPixelFromDIP(point.getDouble(1))),
      callback);
  }

  /**
   *  Check if the first shadow node is the descendant of the second shadow node
   */
  @ReactMethod
  public void viewIsDescendantOf(
      final int reactTag,
      final int ancestorReactTag,
      final Callback callback) {
    mUIImplementation.viewIsDescendantOf(reactTag, ancestorReactTag, callback);
  }

  /**
   * Registers a new Animation that can then be added to a View using {@link #addAnimation}.
   */
  public void registerAnimation(Animation animation) {
    mUIImplementation.registerAnimation(animation);
  }

  /**
   * Adds an Animation previously registered with {@link #registerAnimation} to a View and starts it
   */
  public void addAnimation(int reactTag, int animationID, Callback onSuccess) {
    mUIImplementation.addAnimation(reactTag, animationID, onSuccess);
  }

  /**
   * Removes an existing Animation, canceling it if it was in progress.
   */
  public void removeAnimation(int reactTag, int animationID) {
    mUIImplementation.removeAnimation(reactTag, animationID);
  }

  @Override
  @ReactMethod
  public void setJSResponder(int reactTag, boolean blockNativeResponder) {
    mUIImplementation.setJSResponder(reactTag, blockNativeResponder);
  }

  @Override
  @ReactMethod
  public void clearJSResponder() {
    mUIImplementation.clearJSResponder();
  }

  @ReactMethod
  public void dispatchViewManagerCommand(int reactTag, int commandId, @Nullable ReadableArray commandArgs) {
    //TODO: this is a temporary approach to support ViewManagerCommands in Fabric until
    // the dispatchViewManagerCommand() method is supported by Fabric JS API.
    UIManagerHelper.getUIManager(getReactApplicationContext(), ViewUtil.getUIManagerType(reactTag))
      .dispatchCommand(reactTag, commandId, commandArgs);
  }

  @Override
  public void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray commandArgs) {
    mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
  }

  @ReactMethod
  public void playTouchSound() {
    AudioManager audioManager = (AudioManager) getReactApplicationContext().getSystemService(Context.AUDIO_SERVICE);
    if (audioManager != null) {
      audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
    }
  }

  /**
   * Show a PopupMenu.
   *
   * @param reactTag the tag of the anchor view (the PopupMenu is displayed next to this view); this
   *        needs to be the tag of a native view (shadow views can not be anchors)
   * @param items the menu items as an array of strings
   * @param error will be called if there is an error displaying the menu
   * @param success will be called with the position of the selected item as the first argument, or
   *        no arguments if the menu is dismissed
   */
  @ReactMethod
  public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Callback success) {
    mUIImplementation.showPopupMenu(reactTag, items, error, success);
  }

  @ReactMethod
  public void dismissPopupMenu() {
    mUIImplementation.dismissPopupMenu();
  }

  /**
   * LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
   * explicitly in order to avoid regression in existing application written for iOS using this API.
   *
   * Warning : This method will be removed in future version of React Native, and layout animation
   * will be enabled by default, so always check for its existence before invoking it.
   *
   * TODO(9139831) : remove this method once layout animation is fully stable.
   *
   * @param enabled whether layout animation is enabled or not
   */
  @ReactMethod
  public void setLayoutAnimationEnabledExperimental(boolean enabled) {
    mUIImplementation.setLayoutAnimationEnabledExperimental(enabled);
  }

  /**
   * Configure an animation to be used for the native layout changes, and native views
   * creation. The animation will only apply during the current batch operations.
   *
   * TODO(7728153) : animating view deletion is currently not supported.
   * TODO(7613721) : callbacks are not supported, this feature will likely be killed.
   *
   * @param config the configuration of the animation for view addition/removal/update.
   * @param success will be called when the animation completes, or when the animation get
   *        interrupted. In this case, callback parameter will be false.
   * @param error will be called if there was an error processing the animation
   */
  @ReactMethod
  public void configureNextLayoutAnimation(
      ReadableMap config,
      Callback success,
      Callback error) {
    mUIImplementation.configureNextLayoutAnimation(config, success, error);
  }

  /**
   * To implement the transactional requirement mentioned in the class javadoc, we only commit
   * UI changes to the actual view hierarchy once a batch of JS->Java calls have been completed.
   * We know this is safe because all JS->Java calls that are triggered by a Java->JS call (e.g.
   * the delivery of a touch event or execution of 'renderApplication') end up in a single
   * JS->Java transaction.
   *
   * A better way to do this would be to have JS explicitly signal to this module when a UI
   * transaction is done. Right now, though, this is how iOS does it, and we should probably
   * update the JS and native code and make this change at the same time.
   *
   * TODO(5279396): Make JS UI library explicitly notify the native UI module of the end of a UI
   *                transaction using a standard native call
   */
  @Override
  public void onBatchComplete() {
    int batchId = mBatchId;
    mBatchId++;

    SystraceMessage.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "onBatchCompleteUI")
          .arg("BatchId", batchId)
          .flush();
    for (UIManagerModuleListener listener : mListeners) {
      listener.willDispatchViewUpdates(this);
    }
    try {
      mUIImplementation.dispatchViewUpdates(batchId);
    } finally {
      Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
    }
  }

  public void setViewHierarchyUpdateDebugListener(
      @Nullable NotThreadSafeViewHierarchyUpdateDebugListener listener) {
    mUIImplementation.setViewHierarchyUpdateDebugListener(listener);
  }

  public EventDispatcher getEventDispatcher() {
    return mEventDispatcher;
  }

  @ReactMethod
  public void sendAccessibilityEvent(int tag, int eventType) {
    mUIImplementation.sendAccessibilityEvent(tag, eventType);
  }

  /**
   * Schedule a block to be executed on the UI thread. Useful if you need to execute
   * view logic after all currently queued view updates have completed.
   *
   * @param block that contains UI logic you want to execute.
   *
   * Usage Example:

   UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
   uiManager.addUIBlock(new UIBlock() {
     public void execute (NativeViewHierarchyManager nvhm) {
       View view = nvhm.resolveView(tag);
       // ...execute your code on View (e.g. snapshot the view)
     }
   });
     */
  public void addUIBlock(UIBlock block) {
    mUIImplementation.addUIBlock(block);
  }

  /**
   * Schedule a block to be executed on the UI thread. Useful if you need to execute
   * view logic before all currently queued view updates have completed.
   *
   * @param block that contains UI logic you want to execute.
   */
  public void prependUIBlock(UIBlock block) {
    mUIImplementation.prependUIBlock(block);
  }

  public void addUIManagerListener(UIManagerModuleListener listener) {
    mListeners.add(listener);
  }

  public void removeUIManagerListener(UIManagerModuleListener listener) {
    mListeners.remove(listener);
  }

  /**
   * Given a reactTag from a component, find its root node tag, if possible.
   * Otherwise, this will return 0. If the reactTag belongs to a root node, this
   * will return the same reactTag.
   *
   * @param reactTag the component tag
   *
   * @return the rootTag
   */
  public int resolveRootTagFromReactTag(int reactTag) {
    return mUIImplementation.resolveRootTagFromReactTag(reactTag);
  }

  /** Dirties the node associated with the given react tag */
  public void invalidateNodeLayout(int tag) {
    ReactShadowNode node = mUIImplementation.resolveShadowNode(tag);
    if (node == null) {
      FLog.w(
          ReactConstants.TAG,
          "Warning : attempted to dirty a non-existent react shadow node. reactTag=" + tag);
      return;
    }
    node.dirty();
    mUIImplementation.dispatchViewUpdates(-1);
  }

  /**
   * Updates the styles of the {@link ReactShadowNode} based on the Measure specs received by
   * parameters.
   */
  public void updateRootLayoutSpecs(int rootViewTag, int widthMeasureSpec, int heightMeasureSpec) {
    mUIImplementation.updateRootView(rootViewTag, widthMeasureSpec, heightMeasureSpec);
    mUIImplementation.dispatchViewUpdates(-1);
  }

  /** Listener that drops the CSSNode pool on low memory when the app is backgrounded. */
  private class MemoryTrimCallback implements ComponentCallbacks2 {

    @Override
    public void onTrimMemory(int level) {
      if (level >= TRIM_MEMORY_MODERATE) {
        YogaNodePool.get().clear();
      }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
    }

    @Override
    public void onLowMemory() {
    }
  }
}
