/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.uimanager;

import javax.annotation.Nullable;

import android.util.SparseBooleanArray;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMapKeySetIterator;

/**
 * Class responsible for optimizing the native view hierarchy while still respecting the final UI
 * product specified by JS. Basically, JS sends us a hierarchy of nodes that, while easy to reason
 * about in JS, are very inefficient to translate directly to native views. This class sits in
 * between {@link UIManagerModule}, which directly receives view commands from JS, and
 * {@link UIViewOperationQueue}, which enqueues actual operations on the native view hierarchy. It
 * is able to take instructions from UIManagerModule and output instructions to the native view
 * hierarchy that achieve the same displayed UI but with fewer views.
 *
 * Currently this class is only used to remove layout-only views, that is to say views that only
 * affect the positions of their children but do not draw anything themselves. These views are
 * fairly common because 1) containers are used to do layouting via flexbox and 2) the return of
 * each Component#render() call in JS must be exactly one view, which means views are often wrapped
 * in a unnecessary layer of hierarchy.
 *
 * This optimization is implemented by keeping track of both the unoptimized JS hierarchy and the
 * optimized native hierarchy in {@link ReactShadowNode}.
 *
 * This optimization is important for view hierarchy depth (which can cause stack overflows during
 * view traversal for complex apps), memory usage, amount of time spent during GCs,
 * and time-to-display.
 *
 * Some examples of the optimizations this class will do based on commands from JS:
 * - Create a view with only layout props: a description of that view is created as a
 *   {@link ReactShadowNode} in UIManagerModule, but this class will not output any commands to
 *   create the view in the native view hierarchy.
 * - Update a layout-only view to have non-layout props: before issuing the updateShadowNode call
 *   to the native view hierarchy, issue commands to create the view we optimized away move it into
 *   the view hierarchy
 * - Manage the children of a view: multiple manageChildren calls for various parent views may be
 *   issued to the native view hierarchy depending on where the views being added/removed are
 *   attached in the optimized hierarchy
 */
public class NativeViewHierarchyOptimizer {

  private static final boolean ENABLED = true;

  private final UIViewOperationQueue mUIViewOperationQueue;
  private final ShadowNodeRegistry mShadowNodeRegistry;
  private final SparseBooleanArray mTagsWithLayoutVisited = new SparseBooleanArray();

  public NativeViewHierarchyOptimizer(
      UIViewOperationQueue uiViewOperationQueue,
      ShadowNodeRegistry shadowNodeRegistry) {
    mUIViewOperationQueue = uiViewOperationQueue;
    mShadowNodeRegistry = shadowNodeRegistry;
  }

  /**
   * Handles a createView call. May or may not actually create a native view.
   */
  public void handleCreateView(
      ReactShadowNode node,
      ThemedReactContext themedContext,
      @Nullable ReactStylesDiffMap initialProps) {
    if (!ENABLED) {
      int tag = node.getReactTag();
      mUIViewOperationQueue.enqueueCreateView(
          themedContext,
          tag,
          node.getViewClass(),
          initialProps);
      return;
    }

    boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) &&
        isLayoutOnlyAndCollapsable(initialProps);
    node.setIsLayoutOnly(isLayoutOnly);

    if (!isLayoutOnly) {
      mUIViewOperationQueue.enqueueCreateView(
          themedContext,
          node.getReactTag(),
          node.getViewClass(),
          initialProps);
    }
  }

  /**
   * Handles native children cleanup when css node is removed from hierarchy
   */
  public static void handleRemoveNode(ReactShadowNode node) {
    node.removeAllNativeChildren();
  }

  /**
   * Handles an updateView call. If a view transitions from being layout-only to not (or vice-versa)
   * this could result in some number of additional createView and manageChildren calls. If the
   * view is layout only, no updateView call will be dispatched to the native hierarchy.
   */
  public void handleUpdateView(
      ReactShadowNode node,
      String className,
      ReactStylesDiffMap props) {
    if (!ENABLED) {
      mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
      return;
    }

    boolean needsToLeaveLayoutOnly = node.isLayoutOnly() && !isLayoutOnlyAndCollapsable(props);
    if (needsToLeaveLayoutOnly) {
      transitionLayoutOnlyViewToNativeView(node, props);
    } else if (!node.isLayoutOnly()) {
      mUIViewOperationQueue.enqueueUpdateProperties(node.getReactTag(), className, props);
    }
  }

  /**
   * Handles a manageChildren call. This may translate into multiple manageChildren calls for
   * multiple other views.
   *
   * NB: the assumption for calling this method is that all corresponding ReactShadowNodes have
   * been updated **but tagsToDelete have NOT been deleted yet**. This is because we need to use
   * the metadata from those nodes to figure out the correct commands to dispatch. This is unlike
   * all other calls on this class where we assume all operations on the shadow hierarchy have
   * already completed by the time a corresponding method here is called.
   */
  public void handleManageChildren(
      ReactShadowNode nodeToManage,
      int[] indicesToRemove,
      int[] tagsToRemove,
      ViewAtIndex[] viewsToAdd,
      int[] tagsToDelete) {
    if (!ENABLED) {
      mUIViewOperationQueue.enqueueManageChildren(
          nodeToManage.getReactTag(),
          indicesToRemove,
          viewsToAdd,
          tagsToDelete);
      return;
    }

    // We operate on tagsToRemove instead of indicesToRemove because by the time this method is
    // called, these views have already been removed from the shadow hierarchy and the indices are
    // no longer useful to operate on
    for (int i = 0; i < tagsToRemove.length; i++) {
      int tagToRemove = tagsToRemove[i];
      boolean delete = false;
      for (int j = 0; j < tagsToDelete.length; j++) {
        if (tagsToDelete[j] == tagToRemove) {
          delete = true;
          break;
        }
      }
      ReactShadowNode nodeToRemove = mShadowNodeRegistry.getNode(tagToRemove);
      removeNodeFromParent(nodeToRemove, delete);
    }

    for (int i = 0; i < viewsToAdd.length; i++) {
      ViewAtIndex toAdd = viewsToAdd[i];
      ReactShadowNode nodeToAdd = mShadowNodeRegistry.getNode(toAdd.mTag);
      addNodeToNode(nodeToManage, nodeToAdd, toAdd.mIndex);
    }
  }

  /**
   * Handles a setChildren call.  This is a simplification of handleManagerChildren that only adds
   * children in index order of the childrenTags array
   */
  public void handleSetChildren(
    ReactShadowNode nodeToManage,
    ReadableArray childrenTags
  ) {
    if (!ENABLED) {
      mUIViewOperationQueue.enqueueSetChildren(
        nodeToManage.getReactTag(),
        childrenTags);
      return;
    }

    for (int i = 0; i < childrenTags.size(); i++) {
      ReactShadowNode nodeToAdd = mShadowNodeRegistry.getNode(childrenTags.getInt(i));
      addNodeToNode(nodeToManage, nodeToAdd, i);
    }
  }

  /**
   * Handles an updateLayout call. All updateLayout calls are collected and dispatched at the end
   * of a batch because updateLayout calls to layout-only nodes can necessitate multiple
   * updateLayout calls for all its children.
   */
  public void handleUpdateLayout(ReactShadowNode node) {
    if (!ENABLED) {
      mUIViewOperationQueue.enqueueUpdateLayout(
          Assertions.assertNotNull(node.getParent()).getReactTag(),
          node.getReactTag(),
          node.getScreenX(),
          node.getScreenY(),
          node.getScreenWidth(),
          node.getScreenHeight());
      return;
    }

    applyLayoutBase(node);
  }

  /**
   * Processes the shadow hierarchy to dispatch all necessary updateLayout calls to the native
   * hierarchy. Should be called after all updateLayout calls for a batch have been handled.
   */
  public void onBatchComplete() {
    mTagsWithLayoutVisited.clear();
  }

  private void addNodeToNode(ReactShadowNode parent, ReactShadowNode child, int index) {
    int indexInNativeChildren = parent.getNativeOffsetForChild(parent.getChildAt(index));
    boolean parentIsLayoutOnly = parent.isLayoutOnly();
    boolean childIsLayoutOnly = child.isLayoutOnly();

    // Switch on the four cases of:
    //   add (layout-only|not layout-only) to (layout-only|not layout-only)
    if (!parentIsLayoutOnly && !childIsLayoutOnly) {
      addNonLayoutNodeToNonLayoutNode(parent, child, indexInNativeChildren);
    } else if (!childIsLayoutOnly) {
      addNonLayoutOnlyNodeToLayoutOnlyNode(parent, child, indexInNativeChildren);
    } else if (!parentIsLayoutOnly) {
      addLayoutOnlyNodeToNonLayoutOnlyNode(parent, child, indexInNativeChildren);
    } else {
      addLayoutOnlyNodeToLayoutOnlyNode(parent, child, indexInNativeChildren);
    }
  }

  /**
   * For handling node removal from manageChildren. In the case of removing a layout-only node, we
   * need to instead recursively remove all its children from their native parents.
   */
  private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDelete) {
    ReactShadowNode nativeNodeToRemoveFrom = nodeToRemove.getNativeParent();

    if (nativeNodeToRemoveFrom != null) {
      int index = nativeNodeToRemoveFrom.indexOfNativeChild(nodeToRemove);
      nativeNodeToRemoveFrom.removeNativeChildAt(index);

      mUIViewOperationQueue.enqueueManageChildren(
          nativeNodeToRemoveFrom.getReactTag(),
          new int[]{index},
          null,
          shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null);
    } else {
      for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
        removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
      }
    }
  }

  private void addLayoutOnlyNodeToLayoutOnlyNode(
      ReactShadowNode parent,
      ReactShadowNode child,
      int index) {
    ReactShadowNode parentParent = parent.getParent();

    // If the parent hasn't been attached to its parent yet, don't issue commands to the native
    // hierarchy. We'll do that when the parent node actually gets attached somewhere.
    if (parentParent == null) {
      return;
    }

    int transformedIndex = index + parentParent.getNativeOffsetForChild(parent);
    if (parentParent.isLayoutOnly()) {
      addLayoutOnlyNodeToLayoutOnlyNode(parentParent, child, transformedIndex);
    } else {
      addLayoutOnlyNodeToNonLayoutOnlyNode(parentParent, child, transformedIndex);
    }
  }

  private void addNonLayoutOnlyNodeToLayoutOnlyNode(
      ReactShadowNode layoutOnlyNode,
      ReactShadowNode nonLayoutOnlyNode,
      int index) {
    ReactShadowNode parent = layoutOnlyNode.getParent();

    // If the parent hasn't been attached to its parent yet, don't issue commands to the native
    // hierarchy. We'll do that when the parent node actually gets attached somewhere.
    if (parent == null) {
      return;
    }

    int transformedIndex = index + parent.getNativeOffsetForChild(layoutOnlyNode);
    if (parent.isLayoutOnly()) {
      addNonLayoutOnlyNodeToLayoutOnlyNode(parent, nonLayoutOnlyNode, transformedIndex);
    } else {
      addNonLayoutNodeToNonLayoutNode(parent, nonLayoutOnlyNode, transformedIndex);
    }
  }

  private void addLayoutOnlyNodeToNonLayoutOnlyNode(
      ReactShadowNode nonLayoutOnlyNode,
      ReactShadowNode layoutOnlyNode,
      int index) {
    // Add all of the layout-only node's children to its parent instead
    int currentIndex = index;
    for (int i = 0; i < layoutOnlyNode.getChildCount(); i++) {
      ReactShadowNode childToAdd = layoutOnlyNode.getChildAt(i);
      Assertions.assertCondition(childToAdd.getNativeParent() == null);

      if (childToAdd.isLayoutOnly()) {
        // Adding this layout-only child could result in adding multiple native views
        int childCountBefore = nonLayoutOnlyNode.getNativeChildCount();
        addLayoutOnlyNodeToNonLayoutOnlyNode(
            nonLayoutOnlyNode,
            childToAdd,
            currentIndex);
        int childCountAfter = nonLayoutOnlyNode.getNativeChildCount();
        currentIndex += childCountAfter - childCountBefore;
      } else {
        addNonLayoutNodeToNonLayoutNode(nonLayoutOnlyNode, childToAdd, currentIndex);
        currentIndex++;
      }
    }
  }

  private void addNonLayoutNodeToNonLayoutNode(
      ReactShadowNode parent,
      ReactShadowNode child,
      int index) {
    parent.addNativeChildAt(child, index);
    mUIViewOperationQueue.enqueueManageChildren(
        parent.getReactTag(),
        null,
        new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)},
        null);
  }

  private void applyLayoutBase(ReactShadowNode node) {
    int tag = node.getReactTag();
    if (mTagsWithLayoutVisited.get(tag)) {
      return;
    }
    mTagsWithLayoutVisited.put(tag, true);

    ReactShadowNode parent = node.getParent();

    // We use screenX/screenY (which round to integer pixels) at each node in the hierarchy to
    // emulate what the layout would look like if it were actually built with native views which
    // have to have integral top/left/bottom/right values
    int x = node.getScreenX();
    int y = node.getScreenY();

    while (parent != null && parent.isLayoutOnly()) {
      // TODO(7854667): handle and test proper clipping
      x += Math.round(parent.getLayoutX());
      y += Math.round(parent.getLayoutY());

      parent = parent.getParent();
    }

    applyLayoutRecursive(node, x, y);
  }

  private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) {
    if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) {
      int tag = toUpdate.getReactTag();
      mUIViewOperationQueue.enqueueUpdateLayout(
          toUpdate.getNativeParent().getReactTag(),
          tag,
          x,
          y,
          toUpdate.getScreenWidth(),
          toUpdate.getScreenHeight());
      return;
    }

    for (int i = 0; i < toUpdate.getChildCount(); i++) {
      ReactShadowNode child = toUpdate.getChildAt(i);
      int childTag = child.getReactTag();
      if (mTagsWithLayoutVisited.get(childTag)) {
        continue;
      }
      mTagsWithLayoutVisited.put(childTag, true);

      int childX = child.getScreenX();
      int childY = child.getScreenY();

      childX += x;
      childY += y;

      applyLayoutRecursive(child, childX, childY);
    }
  }

  private void transitionLayoutOnlyViewToNativeView(
      ReactShadowNode node,
      @Nullable ReactStylesDiffMap props) {
    ReactShadowNode parent = node.getParent();
    if (parent == null) {
      node.setIsLayoutOnly(false);
      return;
    }

    // First, remove the node from its parent. This causes the parent to update its native children
    // count. The removeNodeFromParent call will cause all the view's children to be detached from
    // their native parent.
    int childIndex = parent.indexOf(node);
    parent.removeChildAt(childIndex);
    removeNodeFromParent(node, false);

    node.setIsLayoutOnly(false);

    // Create the view since it doesn't exist in the native hierarchy yet
    mUIViewOperationQueue.enqueueCreateView(
        node.getRootNode().getThemedContext(),
        node.getReactTag(),
        node.getViewClass(),
        props);

    // Add the node and all its children as if we are adding a new nodes
    parent.addChildAt(node, childIndex);
    addNodeToNode(parent, node, childIndex);
    for (int i = 0; i < node.getChildCount(); i++) {
      addNodeToNode(node, node.getChildAt(i), i);
    }

    // Update layouts since the children of the node were offset by its x/y position previously.
    // Bit of a hack: we need to update the layout of this node's children now that it's no longer
    // layout-only, but we may still receive more layout updates at the end of this batch that we
    // don't want to ignore.
    Assertions.assertCondition(mTagsWithLayoutVisited.size() == 0);
    applyLayoutBase(node);
    for (int i = 0; i < node.getChildCount(); i++) {
      applyLayoutBase(node.getChildAt(i));
    }
    mTagsWithLayoutVisited.clear();
  }

  private static boolean isLayoutOnlyAndCollapsable(@Nullable ReactStylesDiffMap props) {
    if (props == null) {
      return true;
    }

    if (props.hasKey(ViewProps.COLLAPSABLE) && !props.getBoolean(ViewProps.COLLAPSABLE, true)) {
      return false;
    }

    ReadableMapKeySetIterator keyIterator = props.mBackingMap.keySetIterator();
    while (keyIterator.hasNextKey()) {
      if (!ViewProps.isLayoutOnly(keyIterator.nextKey())) {
        return false;
      }
    }
    return true;
  }
}
