/**
 * 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 java.lang.System.arraycopy;

import com.facebook.common.logging.FLog;
import com.facebook.debug.holder.PrinterHolder;
import com.facebook.debug.tags.ReactDebugOverlayTags;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.uimanager.annotations.ReactPropertyHolder;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage;
import com.facebook.yoga.YogaAlign;
import com.facebook.yoga.YogaBaselineFunction;
import com.facebook.yoga.YogaConfig;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDirection;
import com.facebook.yoga.YogaDisplay;
import com.facebook.yoga.YogaEdge;
import com.facebook.yoga.YogaFlexDirection;
import com.facebook.yoga.YogaJustify;
import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaNode;
import com.facebook.yoga.YogaNodeCloneFunction;
import com.facebook.yoga.YogaOverflow;
import com.facebook.yoga.YogaPositionType;
import com.facebook.yoga.YogaValue;
import com.facebook.yoga.YogaWrap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Base node class for representing virtual tree of React nodes. Shadow nodes are used primarily for
 * layouting therefore it extends {@link YogaNode} to allow that. They also help with handling
 * Common base subclass of {@link YogaNode} for all layout nodes for react-based view. It extends
 * {@link YogaNode} by adding additional capabilities.
 *
 * <p>Instances of this class receive property updates from JS via @{link UIManagerModule}.
 * Subclasses may use {@link #updateShadowNode} to persist some of the updated fields in the node
 * instance that corresponds to a particular view type.
 *
 * <p>Subclasses of {@link ReactShadowNodeImpl} should be created only from {@link ViewManager} that
 * corresponds to a certain type of native view. They will be updated and accessed only from JS
 * thread. Subclasses of {@link ViewManager} may choose to use base class {@link
 * ReactShadowNodeImpl} or custom subclass of it if necessary.
 *
 * <p>The primary use-case for {@link ReactShadowNodeImpl} nodes is to calculate layouting. Although
 * this might be extended. For some examples please refer to ARTGroupYogaNode or ReactTextYogaNode.
 *
 * <p>This class allows for the native view hierarchy to not be an exact copy of the hierarchy
 * received from JS by keeping track of both JS children (e.g. {@link #getChildCount()} and
 * separately native children (e.g. {@link #getNativeChildCount()}). See {@link
 * NativeViewHierarchyOptimizer} for more information.
 */
@ReactPropertyHolder
public class ReactShadowNodeImpl implements ReactShadowNode<ReactShadowNodeImpl> {

  private static final boolean DEBUG = ReactBuildConfig.DEBUG || PrinterHolder.getPrinter().shouldDisplayLogMessage(ReactDebugOverlayTags.FABRIC_UI_MANAGER);
  private static final String TAG = ReactShadowNodeImpl.class.getSimpleName();
  private static final YogaConfig sYogaConfig;
  static {
    sYogaConfig = ReactYogaConfigProvider.get();
    sYogaConfig.setOnCloneNode(new YogaNodeCloneFunction() {
      @Override
      public YogaNode cloneNode(YogaNode oldYogaNode,
          YogaNode parent,
          int childIndex) {
        SystraceMessage.beginSection(
          Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
          "FabricReconciler.YogaNodeCloneFunction")
          .flush();
        try {
          ReactShadowNodeImpl parentReactShadowNode = (ReactShadowNodeImpl) parent.getData();
          Assertions.assertNotNull(parentReactShadowNode);
          ReactShadowNodeImpl oldReactShadowNode = (ReactShadowNodeImpl) oldYogaNode.getData();
          Assertions.assertNotNull(oldReactShadowNode);

          if (DEBUG) {
            FLog.d(
              TAG,
              "YogaNode started cloning: oldYogaNode: " + oldReactShadowNode + " - parent: "
                + parentReactShadowNode + " index: " + childIndex);
          }

          ReactShadowNodeImpl newNode = oldReactShadowNode.mutableCopy(oldReactShadowNode.getInstanceHandle());
          parentReactShadowNode.replaceChild(newNode, childIndex);
          return newNode.mYogaNode;
        } finally{
          Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
        }
      }
    });
  }

  private int mReactTag;
  private @Nullable String mViewClassName;
  private int mRootTag;
  private @Nullable ThemedReactContext mThemedContext;
  private boolean mShouldNotifyOnLayout;
  private boolean mNodeUpdated = true;
  private @Nullable ArrayList<ReactShadowNodeImpl> mChildren;
  private @Nullable ReactShadowNodeImpl mParent;

  // layout-only nodes
  private boolean mIsLayoutOnly;
  private int mTotalNativeChildren = 0;
  private @Nullable ReactShadowNodeImpl mNativeParent;
  private @Nullable ArrayList<ReactShadowNodeImpl> mNativeChildren;
  private int mScreenX;
  private int mScreenY;
  private int mScreenWidth;
  private int mScreenHeight;
  private final Spacing mDefaultPadding;
  private final float[] mPadding = new float[Spacing.ALL + 1];
  private final boolean[] mPaddingIsPercent = new boolean[Spacing.ALL + 1];
  private YogaNode mYogaNode;
  private int mGenerationDebugInformation = 1;
  private ReactShadowNode mOriginalReactShadowNode = null;

  private @Nullable ReactStylesDiffMap mNewProps;
  private long mInstanceHandle;
  private boolean mIsSealed = false;

  public ReactShadowNodeImpl() {
    mDefaultPadding = new Spacing(0);
    if (!isVirtual()) {
      YogaNode node = YogaNodePool.get().acquire();
      mYogaNode = node == null ? new YogaNode(sYogaConfig) : node;
      mYogaNode.setData(this);
      Arrays.fill(mPadding, YogaConstants.UNDEFINED);
    } else {
      mYogaNode = null;
    }
  }

  protected ReactShadowNodeImpl(ReactShadowNodeImpl original) {
    mReactTag = original.mReactTag;
    mRootTag = original.mRootTag;
    mViewClassName = original.mViewClassName;
    mThemedContext = original.mThemedContext;
    mShouldNotifyOnLayout = original.mShouldNotifyOnLayout;
    mIsLayoutOnly = original.mIsLayoutOnly;
    mNativeParent = original.mNativeParent;
    mDefaultPadding = new Spacing(original.mDefaultPadding);
    // Cloned nodes should be always updated.
    mNodeUpdated = true;
    // "cached" screen coordinates are not cloned because FabricJS not always clone the last
    // ReactShadowNode that was rendered in the screen.
    mScreenX = 0;
    mScreenY = 0;
    mScreenWidth = 0;
    mScreenHeight = 0;
    mGenerationDebugInformation = original.mGenerationDebugInformation + 1;
    arraycopy(original.mPadding, 0, mPadding, 0, original.mPadding.length);
    arraycopy(original.mPaddingIsPercent, 0, mPaddingIsPercent, 0, original.mPaddingIsPercent.length);
    mNewProps = null;
    mParent = null;
    mOriginalReactShadowNode = original;
    mIsSealed = false;
  }

  private void replaceChild(ReactShadowNodeImpl newNode, int childIndex) {
    mChildren.remove(childIndex);
    mChildren.add(childIndex, newNode);
    newNode.mParent = this;
  }

  /**
   * @return a copy of this object (no including copy of its children or the underlying yogaNode).
   */
  protected ReactShadowNodeImpl copy() {
    return new ReactShadowNodeImpl(this);
  }

  @Override
  public ReactShadowNodeImpl mutableCopy(long instanceHandle) {
    ReactShadowNodeImpl copy = copy();
    Assertions.assertCondition(
        getClass() == copy.getClass(),
        "Copied shadow node must use the same class");
    copy.mInstanceHandle = instanceHandle;
    if (mYogaNode != null) {
      copy.mYogaNode = mYogaNode.clone();
      copy.mYogaNode.setData(copy);
    } else {
      // Virtual ReactShadowNode do not have a YogaNode associated
      copy.mYogaNode = null;
    }
    copy.mTotalNativeChildren = mTotalNativeChildren;
    copy.mNativeChildren = copyChildren(mNativeChildren);
    copy.mChildren = copyChildren(mChildren);

    return copy;
  }

  @Nullable
  private ArrayList<ReactShadowNodeImpl> copyChildren(@Nullable List<ReactShadowNodeImpl> list){
    ArrayList<ReactShadowNodeImpl> result = list == null ? null : new ArrayList<>(list);
    if (result != null) {
      for (ReactShadowNodeImpl child : result) {
        child.mParent = null;
      }
    }
    return result;
  }

  @Override
  public ReactShadowNodeImpl mutableCopyWithNewChildren(long instanceHandle) {
    ReactShadowNodeImpl copy = copy();
    copy.mInstanceHandle = instanceHandle;
    Assertions.assertCondition(
        getClass() == copy.getClass(),
        "Copied shadow node must use the same class");
    if (mYogaNode != null) {
      copy.mYogaNode = mYogaNode.cloneWithNewChildren();
      copy.mYogaNode.setData(copy);
    } else {
      // Virtual ReactShadowNode do not have a YogaNode associated
      copy.mYogaNode = null;
    }
    copy.mNativeChildren = null;
    copy.mChildren = null;
    copy.mTotalNativeChildren = 0;
    return copy;
  }

  @Override
  public ReactShadowNodeImpl mutableCopyWithNewProps(long instanceHandle,
      @Nullable ReactStylesDiffMap newProps) {
    ReactShadowNodeImpl copy = mutableCopy(instanceHandle);
    if (newProps != null) {
      copy.updateProperties(newProps);
      copy.mNewProps = newProps;
    }
    return copy;
  }

  @Override
  public ReactShadowNodeImpl mutableCopyWithNewChildrenAndProps(long instanceHandle,
      @Nullable ReactStylesDiffMap newProps) {
    ReactShadowNodeImpl copy = mutableCopyWithNewChildren(instanceHandle);
    if (newProps != null) {
      copy.updateProperties(newProps);
      copy.mNewProps = newProps;
    }
    return copy;
  }


  /**
   * Nodes that return {@code true} will be treated as "virtual" nodes. That is, nodes that are not
   * mapped into native views (e.g. nested text node). By default this method returns {@code false}.
   */
  @Override
  public boolean isVirtual() {
    return false;
  }

  /**
   * Nodes that return {@code true} will be treated as a root view for the virtual nodes tree. It
   * means that {@link NativeViewHierarchyManager} will not try to perform {@code manageChildren}
   * operation on such views. Good example is {@code InputText} view that may have children {@code
   * Text} nodes but this whole hierarchy will be mapped to a single android {@link EditText} view.
   */
  @Override
  public boolean isVirtualAnchor() {
    return false;
  }

  /**
   * Nodes that return {@code true} will not manage (and and remove) child Yoga nodes. For example
   * {@link ReactTextInputShadowNode} or {@link ReactTextShadowNode} have child nodes, which do not
   * want Yoga to lay out, so in the eyes of Yoga it is a leaf node. Override this method in
   * subclass to enforce this requirement.
   */
  @Override
  public boolean isYogaLeafNode() {
    return isMeasureDefined();
  }

  @Override
  public final String getViewClass() {
    return Assertions.assertNotNull(mViewClassName);
  }

  @Override
  public final boolean hasUpdates() {
    return mNodeUpdated || hasNewLayout() || isDirty();
  }

  @Override
  public final void markUpdateSeen() {
    assertNotSealed();
    mNodeUpdated = false;
    if (hasNewLayout()) {
      markLayoutSeen();
    }
  }

  @Override
  public void markUpdated() {
    if (mNodeUpdated) {
      return;
    }
    mNodeUpdated = true;
    ReactShadowNodeImpl parent = getParent();
    if (parent != null) {
      parent.markUpdated();
    }
  }

  @Override
  public final boolean hasUnseenUpdates() {
    return mNodeUpdated;
  }

  @Override
  public void dirty() {
    assertNotSealed();
    if (!isVirtual()) {
      mYogaNode.dirty();
    }
  }

  @Override
  public final boolean isDirty() {
    return mYogaNode != null && mYogaNode.isDirty();
  }

  @Override
  public void addChildAt(ReactShadowNodeImpl child, int i) {
    assertNotSealed();
    if (mChildren == null) {
      mChildren = new ArrayList<>(4);
    }
    mChildren.add(i, child);
    child.mParent = this;

    // If a CSS node has measure defined, the layout algorithm will not visit its children. Even
    // more, it asserts that you don't add children to nodes with measure functions.
    if (mYogaNode != null && !isYogaLeafNode()) {
      YogaNode childYogaNode = child.mYogaNode;
      if (childYogaNode == null) {
        throw new RuntimeException(
            "Cannot add a child that doesn't have a YogaNode to a parent without a measure "
                + "function! (Trying to add a '"
                + child.toString()
                + "' to a '"
                + toString()
                + "')");
      }
      mYogaNode.addChildAt(childYogaNode, i);
    }
    markUpdated();

    int increase = child.isLayoutOnly() ? child.getTotalNativeChildren() : 1;
    mTotalNativeChildren += increase;

    updateNativeChildrenCountInParent(increase);
  }

  @Override
  public ReactShadowNodeImpl removeChildAt(int i) {
    assertNotSealed();
    if (mChildren == null) {
      throw new ArrayIndexOutOfBoundsException(
        "Index " + i + " out of bounds: node has no children");
    }
    ReactShadowNodeImpl removed = mChildren.remove(i);
    removed.mParent = null;

    if (mYogaNode != null && !isYogaLeafNode()) {
      mYogaNode.removeChildAt(i);
    }
    markUpdated();

    int decrease = removed.isLayoutOnly() ? removed.getTotalNativeChildren() : 1;
    mTotalNativeChildren -= decrease;
    updateNativeChildrenCountInParent(-decrease);
    return removed;
  }

  @Override
  public final int getChildCount() {
    return mChildren == null ? 0 : mChildren.size();
  }

  @Override
  public final ReactShadowNodeImpl getChildAt(int i) {
    if (mChildren == null) {
      throw new ArrayIndexOutOfBoundsException(
        "Index " + i + " out of bounds: node has no children");
    }
    return mChildren.get(i);
  }

  @Override
  public final int indexOf(ReactShadowNodeImpl child) {
    return mChildren == null ? -1 : mChildren.indexOf(child);
  }

  @Override
  public void removeAndDisposeAllChildren() {
    if (getChildCount() == 0) {
      return;
    }

    int decrease = 0;
    for (int i = getChildCount() - 1; i >= 0; i--) {
      if (mYogaNode != null && !isYogaLeafNode()) {
        mYogaNode.removeChildAt(i);
      }
      ReactShadowNodeImpl toRemove = getChildAt(i);
      toRemove.mParent = null;
      toRemove.dispose();

      decrease += toRemove.isLayoutOnly() ? toRemove.getTotalNativeChildren() : 1;
    }
    Assertions.assertNotNull(mChildren).clear();
    markUpdated();

    mTotalNativeChildren -= decrease;
    updateNativeChildrenCountInParent(-decrease);
  }

  private void updateNativeChildrenCountInParent(int delta) {
    if (mIsLayoutOnly) {
      ReactShadowNodeImpl parent = getParent();
      while (parent != null) {
        parent.mTotalNativeChildren += delta;
        if (!parent.isLayoutOnly()) {
          break;
        }
        parent = parent.getParent();
      }
    }
  }

  /**
   * This method will be called by {@link UIManagerModule} once per batch, before calculating
   * layout. Will be only called for nodes that are marked as updated with {@link #markUpdated()} or
   * require layouting (marked with {@link #dirty()}).
   */
  @Override
  public void onBeforeLayout() {}

  @Override
  public final void updateProperties(ReactStylesDiffMap props) {
    ViewManagerPropertyUpdater.updateProps(this, props);
    onAfterUpdateTransaction();
  }

  @Override
  public void onAfterUpdateTransaction() {
    // no-op
  }

  @Override
  @Nullable
  public ReactStylesDiffMap getNewProps() {
    return mNewProps;
  }

  /**
   * Called after layout step at the end of the UI batch from {@link UIManagerModule}. May be used
   * to enqueue additional ui operations for the native view. Will only be called on nodes marked as
   * updated either with {@link #dirty()} or {@link #markUpdated()}.
   *
   * @param uiViewOperationQueue interface for enqueueing UI operations
   */
  @Override
  public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {}

  /** @return true if layout (position or dimensions) changed, false otherwise. */
  @Override
  public boolean dispatchUpdates(
      float absoluteX,
      float absoluteY,
      UIViewOperationQueue uiViewOperationQueue,
      @Nullable NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
    if (mNodeUpdated) {
      onCollectExtraUpdates(uiViewOperationQueue);
    }

    if (hasNewLayout()) {
      float layoutX = getLayoutX();
      float layoutY = getLayoutY();
      int newAbsoluteLeft = Math.round(absoluteX + layoutX);
      int newAbsoluteTop = Math.round(absoluteY + layoutY);
      int newAbsoluteRight = Math.round(absoluteX + layoutX + getLayoutWidth());
      int newAbsoluteBottom = Math.round(absoluteY + layoutY + getLayoutHeight());

      int newScreenX = Math.round(layoutX);
      int newScreenY = Math.round(layoutY);
      int newScreenWidth = newAbsoluteRight - newAbsoluteLeft;
      int newScreenHeight = newAbsoluteBottom - newAbsoluteTop;

      boolean layoutHasChanged =
          newScreenX != mScreenX
              || newScreenY != mScreenY
              || newScreenWidth != mScreenWidth
              || newScreenHeight != mScreenHeight;

      mScreenX = newScreenX;
      mScreenY = newScreenY;
      mScreenWidth = newScreenWidth;
      mScreenHeight = newScreenHeight;

      if (layoutHasChanged) {
        //TODO: T26400974 ReactShadowNode should not depend on nativeViewHierarchyOptimizer
        if (nativeViewHierarchyOptimizer != null) {
          nativeViewHierarchyOptimizer.handleUpdateLayout(this);
        } else {
          uiViewOperationQueue.enqueueUpdateLayout(
            getParent().getReactTag(),
            getReactTag(),
            getScreenX(),
            getScreenY(),
            getScreenWidth(),
            getScreenHeight());
        }
      }

      return layoutHasChanged;
    } else {
      return false;
    }
  }

  @Override
  public final int getReactTag() {
    return mReactTag;
  }

  @Override
  public void setReactTag(int reactTag) {
    assertNotSealed();
    mReactTag = reactTag;
  }

  @Override
  public final int getRootTag() {
    Assertions.assertCondition(mRootTag != 0);
    return mRootTag;
  }

  @Override
  public final void setRootTag(int rootTag) {
    assertNotSealed();
    mRootTag = rootTag;
  }

  @Override
  public final void setViewClassName(String viewClassName) {
    assertNotSealed();
    mViewClassName = viewClassName;
  }

  @Override
  public final @Nullable ReactShadowNodeImpl getParent() {
    return mParent;
  }

  /**
   * Get the {@link ThemedReactContext} associated with this {@link ReactShadowNodeImpl}. This will
   * never change during the lifetime of a {@link ReactShadowNodeImpl} instance, but different
   * instances can have different contexts; don't cache any calculations based on theme values
   * globally.
   */
  @Override
  public final ThemedReactContext getThemedContext() {
    return Assertions.assertNotNull(mThemedContext);
  }

  @Override
  public void setThemedContext(ThemedReactContext themedContext) {
    mThemedContext = themedContext;
  }

  @Override
  public final boolean shouldNotifyOnLayout() {
    return mShouldNotifyOnLayout;
  }

  @Override
  public void calculateLayout() {
    mYogaNode.calculateLayout(YogaConstants.UNDEFINED, YogaConstants.UNDEFINED);
  }

  @Override
  public final boolean hasNewLayout() {
    return mYogaNode != null && mYogaNode.hasNewLayout();
  }

  @Override
  public final void markLayoutSeen() {
    assertNotSealed();
    if (mYogaNode != null) {
      mYogaNode.markLayoutSeen();
    }
  }

  /**
   * Adds a child that the native view hierarchy will have at this index in the native view
   * corresponding to this node.
   */
  @Override
  public final void addNativeChildAt(ReactShadowNodeImpl child, int nativeIndex) {
    assertNotSealed();
    Assertions.assertCondition(!mIsLayoutOnly);
    Assertions.assertCondition(!child.mIsLayoutOnly);

    if (mNativeChildren == null) {
      mNativeChildren = new ArrayList<>(4);
    }

    mNativeChildren.add(nativeIndex, child);
    child.mNativeParent = this;
  }

  @Override
  public final ReactShadowNodeImpl removeNativeChildAt(int i) {
    Assertions.assertNotNull(mNativeChildren);
    ReactShadowNodeImpl removed = mNativeChildren.remove(i);
    removed.mNativeParent = null;
    return removed;
  }

  @Override
  public final void removeAllNativeChildren() {
    if (mNativeChildren != null) {
      for (int i = mNativeChildren.size() - 1; i >= 0; i--) {
        mNativeChildren.get(i).mNativeParent = null;
      }
      mNativeChildren.clear();
    }
  }

  @Override
  public final int getNativeChildCount() {
    return mNativeChildren == null ? 0 : mNativeChildren.size();
  }

  @Override
  public final int indexOfNativeChild(ReactShadowNodeImpl nativeChild) {
    Assertions.assertNotNull(mNativeChildren);
    return mNativeChildren.indexOf(nativeChild);
  }

  @Override
  public final @Nullable ReactShadowNodeImpl getNativeParent() {
    return mNativeParent;
  }

  /**
   * Sets whether this node only contributes to the layout of its children without doing any drawing
   * or functionality itself.
   */
  @Override
  public final void setIsLayoutOnly(boolean isLayoutOnly) {
    assertNotSealed();
    Assertions.assertCondition(getParent() == null, "Must remove from no opt parent first");
    Assertions.assertCondition(mNativeParent == null, "Must remove from native parent first");
    Assertions.assertCondition(getNativeChildCount() == 0, "Must remove all native children first");
    mIsLayoutOnly = isLayoutOnly;
  }

  @Override
  public final boolean isLayoutOnly() {
    return mIsLayoutOnly;
  }

  @Override
  public final int getTotalNativeChildren() {
    return mTotalNativeChildren;
  }

  @Override
  public boolean isDescendantOf(ReactShadowNodeImpl ancestorNode) {
    ReactShadowNodeImpl parentNode = getParent();

    boolean isDescendant = false;

    while (parentNode != null) {
      if (parentNode == ancestorNode) {
        isDescendant = true;
        break;
      } else {
        parentNode = parentNode.getParent();
      }
    }

    return isDescendant;
  }

  @Override
  public String toString() {
    return "[" + mViewClassName + " " + getReactTag() + "]";
  }

  /*
   * 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 node.
   * Please respect one-directional data flow of React.
   * Use  {@link ReactUIManagerModule#setViewLocalData} to set this property
   * (to provide local/environmental data for a shadow node) from the main thread.
   */
  public void setLocalData(Object data) {}

  /**
   * Returns the offset within the native children owned by all layout-only nodes in the subtree
   * rooted at this node for the given child. Put another way, this returns the number of native
   * nodes (nodes not optimized out of the native tree) that are a) to the left (visited before by a
   * DFS) of the given child in the subtree rooted at this node and b) do not have a native parent
   * in this subtree (which means that the given child will be a sibling of theirs in the final
   * native hierarchy since they'll get attached to the same native parent).
   *
   * <p>Basically, a view might have children that have been optimized away by {@link
   * NativeViewHierarchyOptimizer}. Since those children will then add their native children to this
   * view, we now have ranges of native children that correspond to single unoptimized children. The
   * purpose of this method is to return the index within the native children that corresponds to
   * the **start** of the native children that belong to the given child. Also, note that all of the
   * children of a view might be optimized away, so this could return the same value for multiple
   * different children.
   *
   * <p>Example. Native children are represented by (N) where N is the no-opt child they came from.
   * If no children are optimized away it'd look like this: (0) (1) (2) (3) ... (n)
   *
   * <p>In case some children are optimized away, it might look like this: (0) (1) (1) (1) (3) (3)
   * (4)
   *
   * <p>In that case: getNativeOffsetForChild(Node 0) => 0 getNativeOffsetForChild(Node 1) => 1
   * getNativeOffsetForChild(Node 2) => 4 getNativeOffsetForChild(Node 3) => 4
   *
   * <p>getNativeOffsetForChild(Node 4) => 6
   */
  @Override
  public final int getNativeOffsetForChild(ReactShadowNodeImpl child) {
    int index = 0;
    boolean found = false;
    for (int i = 0; i < getChildCount(); i++) {
      ReactShadowNodeImpl current = getChildAt(i);
      if (child == current) {
        found = true;
        break;
      }
      index += (current.isLayoutOnly() ? current.getTotalNativeChildren() : 1);
    }
    if (!found) {
      throw new RuntimeException(
          "Child " + child.getReactTag() + " was not a child of " + mReactTag);
    }
    return index;
  }

  @Override
  public final float getLayoutX() {
    return mYogaNode.getLayoutX();
  }

  @Override
  public final float getLayoutY() {
    return mYogaNode.getLayoutY();
  }

  @Override
  public final float getLayoutWidth() {
    return mYogaNode.getLayoutWidth();
  }

  @Override
  public final float getLayoutHeight() {
    return mYogaNode.getLayoutHeight();
  }

  /** @return the x position of the corresponding view on the screen, rounded to pixels */
  @Override
  public int getScreenX() {
    return mScreenX;
  }

  /** @return the y position of the corresponding view on the screen, rounded to pixels */
  @Override
  public int getScreenY() {
    return mScreenY;
  }

  /** @return width corrected for rounding to pixels. */
  @Override
  public int getScreenWidth() {
    return mScreenWidth;
  }

  /** @return height corrected for rounding to pixels. */
  @Override
  public int getScreenHeight() {
    return mScreenHeight;
  }

  @Override
  public final YogaDirection getLayoutDirection() {
    return mYogaNode.getLayoutDirection();
  }

  @Override
  public void setLayoutDirection(YogaDirection direction) {
    assertNotSealed();
    mYogaNode.setDirection(direction);
  }

  @Override
  public final YogaValue getStyleWidth() {
    return mYogaNode.getWidth();
  }

  @Override
  public void setStyleWidth(float widthPx) {
    assertNotSealed();
    mYogaNode.setWidth(widthPx);
  }

  @Override
  public void setStyleWidthPercent(float percent) {
    assertNotSealed();
    mYogaNode.setWidthPercent(percent);
  }

  @Override
  public void setStyleWidthAuto() {
    assertNotSealed();
    mYogaNode.setWidthAuto();
  }

  @Override
  public void setStyleMinWidth(float widthPx) {
    assertNotSealed();
    mYogaNode.setMinWidth(widthPx);
  }

  @Override
  public void setStyleMinWidthPercent(float percent) {
    assertNotSealed();
    mYogaNode.setMinWidthPercent(percent);
  }

  @Override
  public void setStyleMaxWidth(float widthPx) {
    assertNotSealed();
    mYogaNode.setMaxWidth(widthPx);
  }

  @Override
  public void setStyleMaxWidthPercent(float percent) {
    assertNotSealed();
    mYogaNode.setMaxWidthPercent(percent);
  }

  @Override
  public final YogaValue getStyleHeight() {
    return mYogaNode.getHeight();
  }

  @Override
  public void setStyleHeight(float heightPx) {
    assertNotSealed();
    mYogaNode.setHeight(heightPx);
  }

  @Override
  public void setStyleHeightPercent(float percent) {
    assertNotSealed();
    mYogaNode.setHeightPercent(percent);
  }

  @Override
  public void setStyleHeightAuto() {
    assertNotSealed();
    mYogaNode.setHeightAuto();
  }

  @Override
  public void setStyleMinHeight(float widthPx) {
    assertNotSealed();
    mYogaNode.setMinHeight(widthPx);
  }

  @Override
  public void setStyleMinHeightPercent(float percent) {
    assertNotSealed();
    mYogaNode.setMinHeightPercent(percent);
  }

  @Override
  public void setStyleMaxHeight(float widthPx) {
    assertNotSealed();
    mYogaNode.setMaxHeight(widthPx);
  }

  @Override
  public void setStyleMaxHeightPercent(float percent) {
    assertNotSealed();
    mYogaNode.setMaxHeightPercent(percent);
  }

  @Override
  public void setFlex(float flex) {
    assertNotSealed();
    mYogaNode.setFlex(flex);
  }

  @Override
  public void setFlexGrow(float flexGrow) {
    assertNotSealed();
    mYogaNode.setFlexGrow(flexGrow);
  }

  @Override
  public void setFlexShrink(float flexShrink) {
    assertNotSealed();
    mYogaNode.setFlexShrink(flexShrink);
  }

  @Override
  public void setFlexBasis(float flexBasis) {
    assertNotSealed();
    mYogaNode.setFlexBasis(flexBasis);
  }

  @Override
  public void setFlexBasisAuto() {
    assertNotSealed();
    mYogaNode.setFlexBasisAuto();
  }

  @Override
  public void setFlexBasisPercent(float percent) {
    assertNotSealed();
    mYogaNode.setFlexBasisPercent(percent);
  }

  @Override
  public void setStyleAspectRatio(float aspectRatio) {
    assertNotSealed();
    mYogaNode.setAspectRatio(aspectRatio);
  }

  @Override
  public void setFlexDirection(YogaFlexDirection flexDirection) {
    assertNotSealed();
    mYogaNode.setFlexDirection(flexDirection);
  }

  @Override
  public void setFlexWrap(YogaWrap wrap) {
    assertNotSealed();
    mYogaNode.setWrap(wrap);
  }

  @Override
  public void setAlignSelf(YogaAlign alignSelf) {
    assertNotSealed();
    mYogaNode.setAlignSelf(alignSelf);
  }

  @Override
  public void setAlignItems(YogaAlign alignItems) {
    assertNotSealed();
    mYogaNode.setAlignItems(alignItems);
  }

  @Override
  public void setAlignContent(YogaAlign alignContent) {
    assertNotSealed();
    mYogaNode.setAlignContent(alignContent);
  }

  @Override
  public void setJustifyContent(YogaJustify justifyContent) {
    assertNotSealed();
    mYogaNode.setJustifyContent(justifyContent);
  }

  @Override
  public void setOverflow(YogaOverflow overflow) {
    assertNotSealed();
    mYogaNode.setOverflow(overflow);
  }

  @Override
  public void setDisplay(YogaDisplay display) {
    assertNotSealed();
    mYogaNode.setDisplay(display);
  }

  @Override
  public void setMargin(int spacingType, float margin) {
    assertNotSealed();
    mYogaNode.setMargin(YogaEdge.fromInt(spacingType), margin);
  }

  @Override
  public void setMarginPercent(int spacingType, float percent) {
    assertNotSealed();
    mYogaNode.setMarginPercent(YogaEdge.fromInt(spacingType), percent);
  }

  @Override
  public void setMarginAuto(int spacingType) {
    assertNotSealed();
    mYogaNode.setMarginAuto(YogaEdge.fromInt(spacingType));
  }

  @Override
  public final float getPadding(int spacingType) {
    return mYogaNode.getLayoutPadding(YogaEdge.fromInt(spacingType));
  }

  @Override
  public final YogaValue getStylePadding(int spacingType) {
    return mYogaNode.getPadding(YogaEdge.fromInt(spacingType));
  }

  @Override
  public void setDefaultPadding(int spacingType, float padding) {
    assertNotSealed();
    mDefaultPadding.set(spacingType, padding);
    updatePadding();
  }

  @Override
  public void setPadding(int spacingType, float padding) {
    assertNotSealed();
    mPadding[spacingType] = padding;
    mPaddingIsPercent[spacingType] = false;
    updatePadding();
  }

  @Override
  public void setPaddingPercent(int spacingType, float percent) {
    assertNotSealed();
    mPadding[spacingType] = percent;
    mPaddingIsPercent[spacingType] = !YogaConstants.isUndefined(percent);
    updatePadding();
  }

  private void updatePadding() {
    assertNotSealed();
    for (int spacingType = Spacing.LEFT; spacingType <= Spacing.ALL; spacingType++) {
      if (spacingType == Spacing.LEFT
          || spacingType == Spacing.RIGHT
          || spacingType == Spacing.START
          || spacingType == Spacing.END) {
        if (YogaConstants.isUndefined(mPadding[spacingType])
            && YogaConstants.isUndefined(mPadding[Spacing.HORIZONTAL])
            && YogaConstants.isUndefined(mPadding[Spacing.ALL])) {
          mYogaNode.setPadding(YogaEdge.fromInt(spacingType), mDefaultPadding.getRaw(spacingType));
          continue;
        }
      } else if (spacingType == Spacing.TOP || spacingType == Spacing.BOTTOM) {
        if (YogaConstants.isUndefined(mPadding[spacingType])
            && YogaConstants.isUndefined(mPadding[Spacing.VERTICAL])
            && YogaConstants.isUndefined(mPadding[Spacing.ALL])) {
          mYogaNode.setPadding(YogaEdge.fromInt(spacingType), mDefaultPadding.getRaw(spacingType));
          continue;
        }
      } else {
        if (YogaConstants.isUndefined(mPadding[spacingType])) {
          mYogaNode.setPadding(YogaEdge.fromInt(spacingType), mDefaultPadding.getRaw(spacingType));
          continue;
        }
      }

      if (mPaddingIsPercent[spacingType]) {
        mYogaNode.setPaddingPercent(YogaEdge.fromInt(spacingType), mPadding[spacingType]);
      } else {
        mYogaNode.setPadding(YogaEdge.fromInt(spacingType), mPadding[spacingType]);
      }
    }
  }

  @Override
  public void setBorder(int spacingType, float borderWidth) {
    assertNotSealed();
    mYogaNode.setBorder(YogaEdge.fromInt(spacingType), borderWidth);
  }

  @Override
  public void setPosition(int spacingType, float position) {
    assertNotSealed();
    mYogaNode.setPosition(YogaEdge.fromInt(spacingType), position);
  }

  @Override
  public void setPositionPercent(int spacingType, float percent) {
    assertNotSealed();
    mYogaNode.setPositionPercent(YogaEdge.fromInt(spacingType), percent);
  }

  @Override
  public void setPositionType(YogaPositionType positionType) {
    assertNotSealed();
    mYogaNode.setPositionType(positionType);
  }

  @Override
  public void setShouldNotifyOnLayout(boolean shouldNotifyOnLayout) {
    assertNotSealed();
    mShouldNotifyOnLayout = shouldNotifyOnLayout;
  }

  @Override
  public void setBaselineFunction(YogaBaselineFunction baselineFunction) {
    assertNotSealed();
    mYogaNode.setBaselineFunction(baselineFunction);
  }

  @Override
  public void setMeasureFunction(YogaMeasureFunction measureFunction) {
    assertNotSealed();
    mYogaNode.setMeasureFunction(measureFunction);
  }

  @Override
  public boolean isMeasureDefined() {
    return mYogaNode.isMeasureDefined();
  }

  @Override
  public String getHierarchyInfo() {
    StringBuilder sb = new StringBuilder();
    getHierarchyInfoWithIndentation(sb, 0);
    return sb.toString();
  }

  private void getHierarchyInfoWithIndentation(StringBuilder result, int level) {
    // Spaces and tabs are dropped by IntelliJ logcat integration, so rely on __ instead.
    for (int i = 0; i < level; ++i) {
      result.append("  ");
    }

    result.append("<").append(getClass().getSimpleName()).append(" view='").append(getViewClass())
      .append("' tag=").append(getReactTag()).append(" gen=").append(mGenerationDebugInformation);
    if (mYogaNode != null) {
      result.append(" layout='x:").append(getScreenX())
        .append(" y:").append(getScreenY()).append(" w:").append(getLayoutWidth()).append(" h:")
        .append(getLayoutHeight()).append("'");
    } else {
      result.append("(virtual node)");
    }
    result.append(">\n");

    if (getChildCount() == 0) {
      return;
    }

    for (int i = 0; i < getChildCount(); i++) {
      getChildAt(i).getHierarchyInfoWithIndentation(result, level + 1);
    }
  }

  @Override
  public void dispose() {
    if (mYogaNode != null) {
      mYogaNode.reset();
      YogaNodePool.get().release(mYogaNode);
    }
  }

  @Nullable
  @Override
  public List<ReactShadowNode> getChildrenList() {
    return mChildren == null ? null : Collections.<ReactShadowNode>unmodifiableList(mChildren);
  }

  @Override
  public ReactShadowNode getOriginalReactShadowNode() {
    return mOriginalReactShadowNode;
  }

  @Override
  public void setOriginalReactShadowNode(ReactShadowNode node) {
    mOriginalReactShadowNode = node;
  }

  @Override
  public long getInstanceHandle() {
    return mInstanceHandle;
  }

  @Override
  public void setInstanceHandle(long instanceHandle) {
    assertNotSealed();
    mInstanceHandle = instanceHandle;
  }

  @Override
  public void markAsSealed() {
    mIsSealed = true;
  }

  @Override
  public boolean isSealed() {
    return mIsSealed;
  }

  private void assertNotSealed() {
    if (mIsSealed) {
      throw new IllegalStateException("Can not modify sealed node " + toString());
    }
  }

  @Override
  public void updateScreenLayout(ReactShadowNode prevNode) {
    mScreenHeight = prevNode.getScreenHeight();
    mScreenWidth = prevNode.getScreenWidth();
    mScreenX = prevNode.getScreenX();
    mScreenY = prevNode.getScreenY();
  }
}
