/**
 * 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.flat;

import android.graphics.Rect;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.OnLayoutEvent;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.ReactShadowNodeImpl;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import javax.annotation.Nullable;

/**
 * FlatShadowNode is a base class for all shadow node used in FlatUIImplementation. It extends
 * {@link LayoutShadowNode} by adding an ability to prepare DrawCommands off the UI thread.
 */
/* package */ class FlatShadowNode extends LayoutShadowNode {

  /* package */ static final FlatShadowNode[] EMPTY_ARRAY = new FlatShadowNode[0];

  private static final String PROP_OPACITY = "opacity";
  private static final String PROP_RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid";
  private static final String PROP_ACCESSIBILITY_LABEL = "accessibilityLabel";
  private static final String PROP_ACCESSIBILITY_COMPONENT_TYPE = "accessibilityComponentType";
  private static final String PROP_ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion";
  private static final String PROP_IMPORTANT_FOR_ACCESSIBILITY = "importantForAccessibility";
  private static final String PROP_TEST_ID = "testID";
  private static final String PROP_TRANSFORM = "transform";
  protected static final String PROP_REMOVE_CLIPPED_SUBVIEWS =
      ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS;
  protected static final String PROP_HORIZONTAL = "horizontal";
  private static final Rect LOGICAL_OFFSET_EMPTY = new Rect();
  // When we first initialize a backing view, we create a view we are going to throw away anyway,
  // so instead initialize with a shared view.
  private static final DrawView EMPTY_DRAW_VIEW = new DrawView(0);

  private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
  private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY;
  private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY;
  private FlatShadowNode[] mNativeChildren = FlatShadowNode.EMPTY_ARRAY;
  private NodeRegion mNodeRegion = NodeRegion.EMPTY;
  private int mNativeParentTag;
  private int mViewLeft;
  private int mViewTop;
  private int mViewRight;
  private int mViewBottom;
  private boolean mBackingViewIsCreated;
  private @Nullable DrawView mDrawView;
  private @Nullable DrawBackgroundColor mDrawBackground;
  private boolean mIsUpdated = true;
  private boolean mForceMountChildrenToView;
  private float mClipLeft;
  private float mClipTop;
  private float mClipRight;
  private float mClipBottom;

  // Used to track whether any of the NodeRegions overflow this Node. This is used to determine
  // whether or not we can detach this Node in the context of a container with
  // setRemoveClippedSubviews enabled.
  private boolean mOverflowsContainer;
  // this Rect contains the offset to get the "logical bounds" (i.e. bounds that include taking
  // into account overflow visible).
  private Rect mLogicalOffset = LOGICAL_OFFSET_EMPTY;

  // last OnLayoutEvent info, only used when shouldNotifyOnLayout() is true.
  private int mLayoutX;
  private int mLayoutY;
  private int mLayoutWidth;
  private int mLayoutHeight;

  // clip radius
  float mClipRadius;
  boolean mClipToBounds = false;

  /* package */ void handleUpdateProperties(ReactStylesDiffMap styles) {
    if (!mountsToView()) {
      // Make sure we mount this FlatShadowNode to a View if any of these properties are present.
      if (styles.hasKey(PROP_OPACITY) ||
          styles.hasKey(PROP_RENDER_TO_HARDWARE_TEXTURE) ||
          styles.hasKey(PROP_TEST_ID) ||
          styles.hasKey(PROP_ACCESSIBILITY_LABEL) ||
          styles.hasKey(PROP_ACCESSIBILITY_COMPONENT_TYPE) ||
          styles.hasKey(PROP_ACCESSIBILITY_LIVE_REGION) ||
          styles.hasKey(PROP_TRANSFORM) ||
          styles.hasKey(PROP_IMPORTANT_FOR_ACCESSIBILITY) ||
          styles.hasKey(PROP_REMOVE_CLIPPED_SUBVIEWS)) {
        forceMountToView();
      }
    }
  }

  /* package */ final void forceMountChildrenToView() {
    if (mForceMountChildrenToView) {
      return;
    }

    mForceMountChildrenToView = true;
    for (int i = 0, childCount = getChildCount(); i != childCount; ++i) {
      ReactShadowNode child = getChildAt(i);
      if (child instanceof FlatShadowNode) {
        ((FlatShadowNode) child).forceMountToView();
      }
    }
  }

  /**
   * Collects DrawCommands produced by this FlatShadowNode.
   */
  protected void collectState(
      StateBuilder stateBuilder,
      float left,
      float top,
      float right,
      float bottom,
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    if (mDrawBackground != null) {
      mDrawBackground = (DrawBackgroundColor) mDrawBackground.updateBoundsAndFreeze(
          left,
          top,
          right,
          bottom,
          clipLeft,
          clipTop,
          clipRight,
          clipBottom);
      stateBuilder.addDrawCommand(mDrawBackground);
    }
  }

  /**
   * Return whether or not this node draws anything
   *
   * This is used to decide whether or not to collect the NodeRegion for this node. This ensures
   * that any FlatShadowNode that does not emit any DrawCommands should not bother handling touch
   * (i.e. if it draws absolutely nothing, it is, for all intents and purposes, a layout only node).
   *
   * @return whether or not this is node draws anything
   */
  boolean doesDraw() {
    // if it mounts to view or draws a background, we can collect it - otherwise, no, unless a
    // child suggests some alternative behavior
    return mDrawView != null || mDrawBackground != null;
  }

  @ReactProp(name = ViewProps.BACKGROUND_COLOR)
  public void setBackgroundColor(int backgroundColor) {
    mDrawBackground = (backgroundColor == 0) ? null : new DrawBackgroundColor(backgroundColor);
    invalidate();
  }

  @Override
  public void setOverflow(String overflow) {
    super.setOverflow(overflow);
    mClipToBounds = "hidden".equals(overflow);
    if (mClipToBounds) {
      mOverflowsContainer = false;
      if (mClipRadius > DrawView.MINIMUM_ROUNDED_CLIPPING_VALUE) {
        // mount to a view if we are overflow: hidden and are clipping, so that we can do one
        // clipPath to clip all the children of this node (both DrawCommands and Views).
        forceMountToView();
      }
    } else {
      updateOverflowsContainer();
    }
    invalidate();
  }

  public final boolean clipToBounds() {
    return mClipToBounds;
  }

  @Override
  public final int getScreenX() {
    return mViewLeft;
  }

  @Override
  public final int getScreenY() {
    return mViewTop;
  }

  @Override
  public final int getScreenWidth() {
    if (mountsToView()) {
      return mViewRight - mViewLeft;
    } else {
      return Math.round(mNodeRegion.getRight() - mNodeRegion.getLeft());
    }
  }

  @Override
  public final int getScreenHeight() {
    if (mountsToView()) {
      return mViewBottom - mViewTop;
    } else {
      return Math.round(mNodeRegion.getBottom() - mNodeRegion.getTop());
    }
  }

  @Override
  public void addChildAt(ReactShadowNodeImpl child, int i) {
    super.addChildAt(child, i);
    if (mForceMountChildrenToView && child instanceof FlatShadowNode) {
      ((FlatShadowNode) child).forceMountToView();
    }
  }

  /**
   * Marks root node as updated to trigger a StateBuilder pass to collect DrawCommands for the node
   * tree. Use it when FlatShadowNode is updated but doesn't require a layout pass (e.g. background
   * color is changed).
   */
  protected final void invalidate() {
    FlatShadowNode node = this;

    while (true) {
      if (node.mountsToView()) {
        if (node.mIsUpdated) {
          // already updated
          return;
        }

        node.mIsUpdated = true;
      }

      ReactShadowNode parent = node.getParent();
      if (parent == null) {
        // not attached to a hierarchy yet
        return;
      }

      node = (FlatShadowNode) parent;
    }
  }

  @Override
  public void markUpdated() {
    super.markUpdated();
    mIsUpdated = true;
    invalidate();
  }

  /* package */ final boolean isUpdated() {
    return mIsUpdated;
  }

  /* package */ final void resetUpdated() {
    mIsUpdated = false;
  }

  /* package */ final boolean clipBoundsChanged(
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    return mClipLeft != clipLeft || mClipTop != clipTop ||
        mClipRight != clipRight || mClipBottom != clipBottom;
  }

  /* package */ final void setClipBounds(
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    mClipLeft = clipLeft;
    mClipTop = clipTop;
    mClipRight = clipRight;
    mClipBottom = clipBottom;
  }

  /**
   * Returns an array of DrawCommands to perform during the View's draw pass.
   */
  /* package */ final DrawCommand[] getDrawCommands() {
    return mDrawCommands;
  }

  /**
   * Sets an array of DrawCommands to perform during the View's draw pass. StateBuilder uses old
   * draw commands to compare to new draw commands and see if the View needs to be redrawn.
   */
  /* package */ final void setDrawCommands(DrawCommand[] drawCommands) {
    mDrawCommands = drawCommands;
  }

  /**
   * Sets an array of AttachDetachListeners to call onAttach/onDetach when they are attached to or
   * detached from a View that this shadow node maps to.
   */
  /* package */ final void setAttachDetachListeners(AttachDetachListener[] listeners) {
    mAttachDetachListeners = listeners;
  }

  /**
   * Returns an array of AttachDetachListeners associated with this shadow node.
   */
  /* package */ final AttachDetachListener[] getAttachDetachListeners() {
    return mAttachDetachListeners;
  }

  /* package */ final FlatShadowNode[] getNativeChildren() {
    return mNativeChildren;
  }

  /* package */ final void setNativeChildren(FlatShadowNode[] nativeChildren) {
    mNativeChildren = nativeChildren;
  }

  /* package */ final int getNativeParentTag() {
    return mNativeParentTag;
  }

  /* package */ final void setNativeParentTag(int nativeParentTag) {
    mNativeParentTag = nativeParentTag;
  }

  /* package */ final NodeRegion[] getNodeRegions() {
    return mNodeRegions;
  }

  /* package */ final void setNodeRegions(NodeRegion[] nodeRegion) {
    mNodeRegions = nodeRegion;
    updateOverflowsContainer();
  }

  /* package */ final void updateOverflowsContainer() {
    boolean overflowsContainer = false;
    int width = (int) (mNodeRegion.getRight() - mNodeRegion.getLeft());
    int height = (int) (mNodeRegion.getBottom() - mNodeRegion.getTop());

    float leftBound = 0;
    float rightBound = width;
    float topBound = 0;
    float bottomBound = height;
    Rect logicalOffset = null;

    // when we are overflow:visible, we try to figure out if any of the children are outside
    // of the bounds of this view. since NodeRegion bounds are relative to their parent (i.e.
    // 0, 0 is always the start), we see how much outside of the bounds we are (negative left
    // or top, or bottom that's more than height or right that's more than width). we set these
    // offsets in mLogicalOffset for being able to more intelligently determine whether or not
    // to clip certain subviews.
    if (!mClipToBounds && height > 0 && width > 0) {
      for (NodeRegion region : mNodeRegions) {
        if (region.getLeft() < leftBound) {
          leftBound = region.getLeft();
          overflowsContainer = true;
        }

        if (region.getRight() > rightBound) {
          rightBound = region.getRight();
          overflowsContainer = true;
        }

        if (region.getTop() < topBound) {
          topBound = region.getTop();
          overflowsContainer = true;
        }

        if (region.getBottom() > bottomBound) {
          bottomBound = region.getBottom();
          overflowsContainer = true;
        }
      }

      if (overflowsContainer) {
        logicalOffset = new Rect(
            (int) leftBound,
            (int) topBound,
            (int) (rightBound - width),
            (int) (bottomBound - height));
      }
    }

    // if we don't overflow, let's check if any of the immediate children overflow.
    // this is "indirectly recursive," since this method is called when setNodeRegions is called,
    // and the children call setNodeRegions before their parent. consequently, when a node deep
    // inside the tree overflows, its immediate parent has mOverflowsContainer set to true, and,
    // by extension, so do all of its ancestors, sufficing here to only check the immediate
    // child's mOverflowsContainer value instead of recursively asking if each child overflows its
    // container.
    if (!overflowsContainer && mNodeRegion != NodeRegion.EMPTY) {
      int children = getChildCount();
      for (int i = 0; i < children; i++) {
        ReactShadowNode node = getChildAt(i);
        if (node instanceof FlatShadowNode && ((FlatShadowNode) node).mOverflowsContainer) {
          Rect childLogicalOffset = ((FlatShadowNode) node).mLogicalOffset;
          if (logicalOffset == null) {
            logicalOffset = new Rect();
          }
          // TODO: t11674025 - improve this - a grandparent may end up having smaller logical
          // bounds than its children (because the grandparent's size may be larger than that of
          // its child, so the grandchild overflows its parent but not its grandparent). currently,
          // if a 100x100 view has a 5x5 view, and inside it has a 10x10 view, the inner most view
          // overflows its parent but not its grandparent - the logical bounds on the grandparent
          // will still be 5x5 (because they're inherited from the child's logical bounds). this
          // has the effect of causing us to clip 5px later than we really have to.
          logicalOffset.union(childLogicalOffset);
          overflowsContainer = true;
        }
      }
    }

    // if things changed, notify the parent(s) about said changes - while in many cases, this will
    // be extra work (since we process this for the parents after the children), in some cases,
    // we may have no new node regions in the parent, but have a new node region in the child, and,
    // as a result, the parent may not get the correct value for overflows container.
    if (mOverflowsContainer != overflowsContainer) {
      mOverflowsContainer = overflowsContainer;
      mLogicalOffset = logicalOffset == null ? LOGICAL_OFFSET_EMPTY : logicalOffset;
    }
  }

  /* package */ void updateNodeRegion(
      float left,
      float top,
      float right,
      float bottom,
      boolean isVirtual) {
    if (!mNodeRegion.matches(left, top, right, bottom, isVirtual)) {
      setNodeRegion(new NodeRegion(left, top, right, bottom, getReactTag(), isVirtual));
    }
  }

  protected final void setNodeRegion(NodeRegion nodeRegion) {
    mNodeRegion = nodeRegion;
    updateOverflowsContainer();
  }

  /* package */ final NodeRegion getNodeRegion() {
    return mNodeRegion;
  }

  /**
   * Sets boundaries of the View that this node maps to relative to the parent left/top coordinate.
   */
  /* package */ final void setViewBounds(int left, int top, int right, int bottom) {
    mViewLeft = left;
    mViewTop = top;
    mViewRight = right;
    mViewBottom = bottom;
  }

  /**
   * Left position of the View this node maps to relative to the parent View.
   */
  /* package */ final int getViewLeft() {
    return mViewLeft;
  }

  /**
   * Top position of the View this node maps to relative to the parent View.
   */
  /* package */ final int getViewTop() {
    return mViewTop;
  }

  /**
   * Right position of the View this node maps to relative to the parent View.
   */
  /* package */ final int getViewRight() {
    return mViewRight;
  }

  /**
   * Bottom position of the View this node maps to relative to the parent View.
   */
  /* package */ final int getViewBottom() {
    return mViewBottom;
  }

  /* package */ final void forceMountToView() {
    if (isVirtual()) {
      return;
    }

    if (mDrawView == null) {
      // Create a new DrawView, but we might not know our react tag yet, so set it to 0 in the
      // meantime.
      mDrawView = EMPTY_DRAW_VIEW;
      invalidate();

      // reset NodeRegion to allow it getting garbage-collected
      mNodeRegion = NodeRegion.EMPTY;
    }
  }

  /* package */ final DrawView collectDrawView(
      float left,
      float top,
      float right,
      float bottom,
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    Assertions.assumeNotNull(mDrawView);
    if (mDrawView == EMPTY_DRAW_VIEW) {
      // This is the first time we have collected this DrawView, but we have to create a new
      // DrawView anyway, as reactTag is final, and our DrawView instance is the static copy.
      mDrawView = new DrawView(getReactTag());
    }

    // avoid path clipping if overflow: visible
    float clipRadius = mClipToBounds ? mClipRadius : 0.0f;
    // We have the correct react tag, but we may need a new copy with updated bounds.  If the bounds
    // match or were never set, the same view is returned.
    mDrawView = mDrawView.collectDrawView(
        left,
        top,
        right,
        bottom,
        left + mLogicalOffset.left,
        top + mLogicalOffset.top,
        right + mLogicalOffset.right,
        bottom + mLogicalOffset.bottom,
        clipLeft,
        clipTop,
        clipRight,
        clipBottom,
        clipRadius);
    return mDrawView;
  }

  @Nullable
  /* package */ final OnLayoutEvent obtainLayoutEvent(int x, int y, int width, int height) {
    if (mLayoutX == x && mLayoutY == y && mLayoutWidth == width && mLayoutHeight == height) {
      return null;
    }

    mLayoutX = x;
    mLayoutY = y;
    mLayoutWidth = width;
    mLayoutHeight = height;

    return OnLayoutEvent.obtain(getReactTag(), x, y, width, height);
  }

  /* package */ final boolean mountsToView() {
    return mDrawView != null;
  }

  /* package */ final boolean isBackingViewCreated() {
    return mBackingViewIsCreated;
  }

  /* package */ final void signalBackingViewIsCreated() {
    mBackingViewIsCreated = true;
  }

  public boolean clipsSubviews() {
    return false;
  }

  public boolean isHorizontal() {
    return false;
  }
}
