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

import javax.annotation.Nullable;

import java.lang.ref.WeakReference;
import java.util.ArrayList;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.touch.OnInterceptTouchEventListener;
import com.facebook.react.touch.ReactHitSlopView;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactCompoundViewGroup;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.views.image.ImageLoadEvent;
import com.facebook.react.uimanager.ReactClippingViewGroup;

/**
 * A view that the {@link FlatShadowNode} hierarchy maps to.  Can mount and draw native views as
 * well as draw commands.  We reuse some of Android's ViewGroup logic, but in Nodes we try to
 * minimize the amount of shadow nodes that map to native children, so we have a lot of logic
 * specific to draw commands.
 *
 * In a very simple case with no Android children, the FlatViewGroup will receive:
 *
 *   flatViewGroup.mountDrawCommands(...);
 *   flatViewGroup.dispatchDraw(...);
 *
 * The draw commands are mounted, then draw iterates through and draws them one by one.
 *
 * In a simple case where there are native children:
 *
 *   flatViewGroup.mountDrawCommands(...);
 *   flatViewGroup.detachAllViewsFromParent(...);
 *   flatViewGroup.mountViews(...);
 *   flatViewGroup.dispatchDraw(...);
 *
 * Draw commands are mounted, with a draw view command for each mounted view.  As an optimization
 * we then detach all views from the FlatViewGroup, then allow mountViews to selectively reattach
 * and add views in order.  We do this as adding a single view is a O(n) operation (On average you
 * have to move all the views in the array to the right one position), as is dropping and re-adding
 * all views (One pass to clear the array and one pass to re-attach detached children and add new
 * children).
 *
 * FlatViewGroups also have arrays of node regions, which are little more than a rects that
 * represents a touch target.  Native views contain their own touch logic, but not all react tags
 * map to native views.  We use node regions to find touch targets among commands as well as nodes
 * which map to native views.
 *
 * In the case of clipping, much of the underlying logic for is handled by
 * {@link DrawCommandManager}.  This lets us separate logic, while also allowing us to save on
 * memory for data structures only used in clipping.  In a case of a clipping FlatViewGroup which
 * is scrolling:
 *
 *   flatViewGroup.setRemoveClippedSubviews(true);
 *   flatViewGroup.mountClippingDrawCommands(...);
 *   flatViewGroup.detachAllViewsFromParent(...);
 *   flatViewGroup.mountViews(...);
 *   flatViewGroup.updateClippingRect(...);
 *   flatViewGroup.dispatchDraw(...);
 *   flatViewGroup.updateClippingRect(...);
 *   flatViewGroup.dispatchDraw(...);
 *   flatViewGroup.updateClippingRect(...);
 *   flatViewGroup.dispatchDraw(...);
 *
 * Setting remove clipped subviews creates a {@link DrawCommandManager} to handle clipping, which
 * allows the rest of the methods to simply call in to draw command manager to handle the clipping
 * logic.
 */
/* package */ final class FlatViewGroup extends ViewGroup
    implements ReactInterceptingViewGroup, ReactClippingViewGroup,
    ReactCompoundViewGroup, ReactHitSlopView, ReactPointerEventsView, FlatMeasuredViewGroup {
  /**
   * Helper class that allows our AttachDetachListeners to invalidate the hosting View.  When a
   * listener gets an attach it is passed an invalidate callback for the FlatViewGroup it is being
   * attached to.
   */
  static final class InvalidateCallback extends WeakReference<FlatViewGroup> {

    private InvalidateCallback(FlatViewGroup view) {
      super(view);
    }

    /**
     * Propagates invalidate() call up to the hosting View (if it's still alive)
     */
    public void invalidate() {
      FlatViewGroup view = get();
      if (view != null) {
        view.invalidate();
      }
    }

    /**
     * Propogates image load events to javascript if the hosting view is still alive.
     *
     * @param reactTag The view id.
     * @param imageLoadEvent The event type.
     */
    public void dispatchImageLoadEvent(int reactTag, int imageLoadEvent) {
      FlatViewGroup view = get();
      if (view == null) {
        return;
      }

      ReactContext reactContext = ((ReactContext) view.getContext());
      UIManagerModule uiManagerModule = reactContext.getNativeModule(UIManagerModule.class);
      uiManagerModule.getEventDispatcher().dispatchEvent(
          new ImageLoadEvent(reactTag, imageLoadEvent));
    }
  }

  // Draws the name of the draw commands at the bottom right corner of it's bounds.
  private static final boolean DEBUG_DRAW_TEXT = false;
  // Draws colored rectangles over known performance issues.
  /* package */ static final boolean DEBUG_HIGHLIGHT_PERFORMANCE_ISSUES = false;
  // Force layout bounds drawing.  This can also be enabled by turning on layout bounds in Android.
  private static final boolean DEBUG_DRAW = DEBUG_DRAW_TEXT || DEBUG_HIGHLIGHT_PERFORMANCE_ISSUES;
  // Resources for debug drawing.
  private boolean mAndroidDebugDraw;
  private static Paint sDebugTextPaint;
  private static Paint sDebugTextBackgroundPaint;
  private static Paint sDebugRectPaint;
  private static Paint sDebugCornerPaint;
  private static Rect sDebugRect;

  private static final ArrayList<FlatViewGroup> LAYOUT_REQUESTS = new ArrayList<>();
  private static final Rect VIEW_BOUNDS = new Rect();

  // An invalidate callback singleton for this FlatViewGroup.
  private @Nullable InvalidateCallback mInvalidateCallback;
  private DrawCommand[] mDrawCommands = DrawCommand.EMPTY_ARRAY;
  private AttachDetachListener[] mAttachDetachListeners = AttachDetachListener.EMPTY_ARRAY;
  private NodeRegion[] mNodeRegions = NodeRegion.EMPTY_ARRAY;

  // The index of the next native child to draw.  This is used in dispatchDraw to check that we are
  // actually drawing all of our attached children, then is reset to 0.
  private int mDrawChildIndex = 0;
  private boolean mIsAttached = false;
  private boolean mIsLayoutRequested = false;
  private boolean mNeedsOffscreenAlphaCompositing = false;
  private Drawable mHotspot;
  private PointerEvents mPointerEvents = PointerEvents.AUTO;
  private long mLastTouchDownTime;
  private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;

  private static final SparseArray<View> EMPTY_DETACHED_VIEWS = new SparseArray<>(0);
  // Provides clipping, drawing and node region finding logic if subview clipping is enabled.
  private @Nullable DrawCommandManager mDrawCommandManager;

  private @Nullable Rect mHitSlopRect;

  /* package */ FlatViewGroup(Context context) {
    super(context);
    setClipChildren(false);
  }

  @Override
  protected void detachAllViewsFromParent() {
    super.detachAllViewsFromParent();
  }

  @Override
  @SuppressLint("MissingSuperCall")
  public void requestLayout() {
    if (mIsLayoutRequested) {
      return;
    }

    mIsLayoutRequested = true;
    LAYOUT_REQUESTS.add(this);
  }

  @Override
  public int reactTagForTouch(float touchX, float touchY) {
    /*
     * Make sure we don't find any children if the pointer events are set to BOX_ONLY.
     * There is no need to special-case any other modes, because if PointerEvents are set to:
     * a) PointerEvents.AUTO - all children are included, nothing to exclude
     * b) PointerEvents.NONE - this method will NOT be executed, because the View will be filtered
     *    out by TouchTargetHelper.
     * c) PointerEvents.BOX_NONE - TouchTargetHelper will make sure that {@link #reactTagForTouch()}
     *     doesn't return getId().
     */
    SoftAssertions.assertCondition(
        mPointerEvents != PointerEvents.NONE,
        "TouchTargetHelper should not allow calling this method when pointer events are NONE");

    if (mPointerEvents != PointerEvents.BOX_ONLY) {
      NodeRegion nodeRegion = virtualNodeRegionWithinBounds(touchX, touchY);
      if (nodeRegion != null) {
        return nodeRegion.getReactTag(touchX, touchY);
      }
    }

    // no children found
    return getId();
  }

  @Override
  public boolean interceptsTouchEvent(float touchX, float touchY) {
    NodeRegion nodeRegion = anyNodeRegionWithinBounds(touchX, touchY);
    return nodeRegion != null && nodeRegion.mIsVirtual;
  }

  /**
   * Secretly Overrides the hidden ViewGroup.onDebugDraw method.  This is hidden in the Android
   * ViewGroup, but still gets called in super.dispatchDraw.  Overriding here allows us to draw
   * layout bounds for Nodes when android is drawing layout bounds.
   */
  protected void onDebugDraw(Canvas canvas) {
    // Android is drawing layout bounds, so we should as well.
    mAndroidDebugDraw = true;
  }

  /**
   * Draw FlatViewGroup on a canvas.  Also checks that all children are drawn, as a draw view calls
   * back to the FlatViewGroup to draw each child.
   *
   * @param canvas The canvas to draw on.
   */
  @Override
  public void dispatchDraw(Canvas canvas) {
    mAndroidDebugDraw = false;
    super.dispatchDraw(canvas);

    if (mDrawCommandManager != null) {
      mDrawCommandManager.draw(canvas);
    } else {
      for (DrawCommand drawCommand : mDrawCommands) {
        drawCommand.draw(this, canvas);
      }
    }

    if (mDrawChildIndex != getChildCount()) {
      throw new RuntimeException(
          "Did not draw all children: " + mDrawChildIndex + " / " + getChildCount());
    }
    mDrawChildIndex = 0;

    if (DEBUG_DRAW || mAndroidDebugDraw) {
      initDebugDrawResources();
      debugDraw(canvas);
    }

    if (mHotspot != null) {
      mHotspot.draw(canvas);
    }
  }

  /**
   * Draws layout bounds for debug.  Optionally can draw the name of the DrawCommand so you can
   * distinguish commands easier.
   *
   * @param canvas The canvas to draw on.
   */
  private void debugDraw(Canvas canvas) {
    if (mDrawCommandManager != null) {
      mDrawCommandManager.debugDraw(canvas);
    } else {
      for (DrawCommand drawCommand : mDrawCommands) {
        drawCommand.debugDraw(this, canvas);
      }
    }
    mDrawChildIndex = 0;
  }

  /**
   * This override exists to suppress the default drawing behaviour of the ViewGroup.  dispatchDraw
   * calls super.dispatchDraw, which lets Android perform some of our child management logic.
   * super.dispatchDraw then calls our drawChild, which is suppressed.
   *
   * dispatchDraw within the FlatViewGroup then calls super.drawChild, which actually draws the
   * child.
   *
   *   // Pseudocode example.
   *   Class FlatViewGroup {
   *     void dispatchDraw() {
   *       super.dispatchDraw(); // Eventually calls our drawChild, which is a no op.
   *       super.drawChild();    // Calls the actual drawChild.
   *     }
   *
   *     boolean drawChild(...) {
   *       // No op.
   *     }
   *   }
   *
   *   Class ViewGroup {
   *     void dispatchDraw() {
   *       drawChild(); // No op.
   *     }
   *
   *     boolean drawChild(...) {
   *       getChildAt(...).draw();
   *     }
   *   }
   *
   * @return false, as we are suppressing drawChild.
   */
  @Override
  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    // suppress
    // no drawing -> no invalidate -> return false
    return false;
  }

  /**
   * Draw layout bounds for the next child.
   *
   * @param canvas The canvas to draw on.
   */
  /* package */ void debugDrawNextChild(Canvas canvas) {
    View child = getChildAt(mDrawChildIndex);
    // Draw FlatViewGroups a different color than regular child views.
    int color = child instanceof FlatViewGroup ? Color.DKGRAY : Color.RED;
    debugDrawRect(
        canvas,
        color,
        child.getLeft(),
        child.getTop(),
        child.getRight(),
        child.getBottom());
    ++mDrawChildIndex;
  }

  // Used in debug drawing.
  /* package */ int dipsToPixels(int dips) {
    float scale = getResources().getDisplayMetrics().density;
    return (int) (dips * scale + 0.5f);
  }

  // Used in debug drawing.
  private static void fillRect(Canvas canvas, Paint paint, float x1, float y1, float x2, float y2) {
    if (x1 != x2 && y1 != y2) {
      if (x1 > x2) {
        float tmp = x1; x1 = x2; x2 = tmp;
      }
      if (y1 > y2) {
        float tmp = y1; y1 = y2; y2 = tmp;
      }
      canvas.drawRect(x1, y1, x2, y2, paint);
    }
  }

  // Used in debug drawing.
  private static int sign(float x) {
    return (x >= 0) ? 1 : -1;
  }

  // Used in debug drawing.
  private static void drawCorner(
      Canvas c,
      Paint paint,
      float x1,
      float y1,
      float dx,
      float dy,
      float lw) {
    fillRect(c, paint, x1, y1, x1 + dx, y1 + lw * sign(dy));
    fillRect(c, paint, x1, y1, x1 + lw * sign(dx), y1 + dy);
  }

  // Used in debug drawing.
  private static void drawRectCorners(
      Canvas canvas,
      float x1,
      float y1,
      float x2,
      float y2,
      Paint paint,
      int lineLength,
      int lineWidth) {
    drawCorner(canvas, paint, x1, y1, lineLength, lineLength, lineWidth);
    drawCorner(canvas, paint, x1, y2, lineLength, -lineLength, lineWidth);
    drawCorner(canvas, paint, x2, y1, -lineLength, lineLength, lineWidth);
    drawCorner(canvas, paint, x2, y2, -lineLength, -lineLength, lineWidth);
  }

  /**
   * Makes sure that we only initialize one instance of each of our layout bounds drawing
   * resources.
   */
  private void initDebugDrawResources() {
    if (sDebugTextPaint == null) {
      sDebugTextPaint = new Paint();
      sDebugTextPaint.setTextAlign(Paint.Align.RIGHT);
      sDebugTextPaint.setTextSize(dipsToPixels(9));
      sDebugTextPaint.setTypeface(Typeface.MONOSPACE);
      sDebugTextPaint.setAntiAlias(true);
      sDebugTextPaint.setColor(Color.RED);
    }
    if (sDebugTextBackgroundPaint == null) {
      sDebugTextBackgroundPaint = new Paint();
      sDebugTextBackgroundPaint.setColor(Color.WHITE);
      sDebugTextBackgroundPaint.setAlpha(200);
      sDebugTextBackgroundPaint.setStyle(Paint.Style.FILL);
    }
    if (sDebugRectPaint == null) {
      sDebugRectPaint = new Paint();
      sDebugRectPaint.setAlpha(100);
      sDebugRectPaint.setStyle(Paint.Style.STROKE);
    }
    if (sDebugCornerPaint == null) {
      sDebugCornerPaint = new Paint();
      sDebugCornerPaint.setAlpha(200);
      sDebugCornerPaint.setColor(Color.rgb(63, 127, 255));
      sDebugCornerPaint.setStyle(Paint.Style.FILL);
    }
    if (sDebugRect == null) {
      sDebugRect = new Rect();
    }
  }

  /**
   * Used in drawing layout bounds, draws a layout bounds rectangle similar to the Android default
   * implementation, with a specifiable border color.
   *
   * @param canvas The canvas to draw on.
   * @param color The border color of the layout bounds.
   * @param left Left bound of the rectangle.
   * @param top Top bound of the rectangle.
   * @param right Right bound of the rectangle.
   * @param bottom Bottom bound of the rectangle.
   */
  private void debugDrawRect(
      Canvas canvas,
      int color,
      float left,
      float top,
      float right,
      float bottom) {
    debugDrawNamedRect(canvas, color, "", left, top, right, bottom);
  }

  /**
   * Used in drawing layout bounds, draws a layout bounds rectangle similar to the Android default
   * implementation, with a specifiable border color.  Also draws a name text in the bottom right
   * corner of the rectangle if DEBUG_DRAW_TEXT is set.
   *
   * @param canvas The canvas to draw on.
   * @param color The border color of the layout bounds.
   * @param name Name to be drawn on top of the rectangle if DEBUG_DRAW_TEXT is set.
   * @param left Left bound of the rectangle.
   * @param top Top bound of the rectangle.
   * @param right Right bound of the rectangle.
   * @param bottom Bottom bound of the rectangle.
   */
  /* package */ void debugDrawNamedRect(
      Canvas canvas,
      int color,
      String name,
      float left,
      float top,
      float right,
      float bottom) {
    if (DEBUG_DRAW_TEXT && !name.isEmpty()) {
      sDebugTextPaint.getTextBounds(name, 0, name.length(), sDebugRect);
      int inset = dipsToPixels(2);
      float textRight = right - inset - 1;
      float textBottom = bottom - inset - 1;
      canvas.drawRect(
          textRight - sDebugRect.right - inset,
          textBottom + sDebugRect.top - inset,
          textRight + inset,
          textBottom + inset,
          sDebugTextBackgroundPaint);
      canvas.drawText(name, textRight, textBottom, sDebugTextPaint);
    }
    // Retain the alpha component.
    sDebugRectPaint.setColor((sDebugRectPaint.getColor() & 0xFF000000) | (color & 0x00FFFFFF));
    sDebugRectPaint.setAlpha(100);
    canvas.drawRect(
        left,
        top,
        right - 1,
        bottom - 1,
        sDebugRectPaint);
    drawRectCorners(
        canvas,
        left,
        top,
        right,
        bottom,
        sDebugCornerPaint,
        dipsToPixels(8),
        dipsToPixels(1));
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // nothing to do here
  }

  @Override
  @SuppressLint("MissingSuperCall")
  protected boolean verifyDrawable(Drawable who) {
    return true;
  }

  @Override
  protected void onAttachedToWindow() {
    if (mIsAttached) {
      // This is possible, unfortunately.
      return;
    }

    mIsAttached = true;

    super.onAttachedToWindow();
    dispatchOnAttached(mAttachDetachListeners);

    // This is a no op if we aren't clipping, so let updateClippingRect handle the check for us.
    updateClippingRect();
  }

  @Override
  protected void onDetachedFromWindow() {
    if (!mIsAttached) {
      throw new RuntimeException("Double detach");
    }

    mIsAttached = false;

    super.onDetachedFromWindow();
    dispatchOnDetached(mAttachDetachListeners);
  }

  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    if (mHotspot != null) {
      mHotspot.setBounds(0, 0, w, h);
      invalidate();
    }

    // This is a no op if we aren't clipping, so let updateClippingRect handle the check for us.
    updateClippingRect();
  }

  @Override
  public void dispatchDrawableHotspotChanged(float x, float y) {
    if (mHotspot != null) {
      mHotspot.setHotspot(x, y);
      invalidate();
    }
  }

  @Override
  protected void drawableStateChanged() {
    super.drawableStateChanged();

    if (mHotspot != null && mHotspot.isStateful()) {
        mHotspot.setState(getDrawableState());
    }
  }

  @Override
  public void jumpDrawablesToCurrentState() {
    super.jumpDrawablesToCurrentState();
    if (mHotspot != null) {
        mHotspot.jumpToCurrentState();
    }
  }

  @Override
  public void invalidate() {
    // By default, invalidate() only invalidates the View's boundaries, which works great in most
    // cases but may fail with overflow: visible (i.e. View clipping disabled) when View width or
    // height is 0. This is because invalidate() has an optimization where it will not invalidate
    // empty Views at all. A quick fix is to invalidate a slightly larger region to make sure we
    // never hit that optimization.
    //
    // Another thing to note is that this may not work correctly with software rendering because
    // in software, Android tracks dirty regions to redraw. We would need to collect information
    // about all children boundaries (recursively) to track dirty region precisely.
    invalidate(0, 0, getWidth() + 1, getHeight() + 1);
  }

  /**
   * We override this to allow developers to determine whether they need offscreen alpha compositing
   * or not. See the documentation of needsOffscreenAlphaCompositing in View.js.
   */
  @Override
  public boolean hasOverlappingRendering() {
    return mNeedsOffscreenAlphaCompositing;
  }

  @Override
  public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) {
    mOnInterceptTouchEventListener = listener;
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    final long downTime = ev.getDownTime();
    if (downTime != mLastTouchDownTime) {
      mLastTouchDownTime = downTime;
      if (interceptsTouchEvent(ev.getX(), ev.getY())) {
        return true;
      }
    }

    if (mOnInterceptTouchEventListener != null &&
        mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) {
      return true;
    }
    // We intercept the touch event if the children are not supposed to receive it.
    if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) {
      return true;
    }
    return super.onInterceptTouchEvent(ev);
  }

  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    // We do not accept the touch event if this view is not supposed to receive it.
    if (mPointerEvents == PointerEvents.NONE) {
      return false;
    }

    if (mPointerEvents == PointerEvents.BOX_NONE) {
      // We cannot always return false here because some child nodes could be flatten into this View
      NodeRegion nodeRegion = virtualNodeRegionWithinBounds(ev.getX(), ev.getY());
      if (nodeRegion == null) {
        // no child to handle this touch event, bailing out.
        return false;
      }
    }

    // The root view always assumes any view that was tapped wants the touch
    // and sends the event to JS as such.
    // We don't need to do bubbling in native (it's already happening in JS).
    // For an explanation of bubbling and capturing, see
    // http://javascript.info/tutorial/bubbling-and-capturing#capturing
    return true;
  }

  @Override
  public PointerEvents getPointerEvents() {
    return mPointerEvents;
  }

  /*package*/ void setPointerEvents(PointerEvents pointerEvents) {
    mPointerEvents = pointerEvents;
  }

  /**
   * See the documentation of needsOffscreenAlphaCompositing in View.js.
   */
  /* package */ void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) {
    mNeedsOffscreenAlphaCompositing = needsOffscreenAlphaCompositing;
  }

  /* package */ void setHotspot(Drawable hotspot) {
    if (mHotspot != null) {
      mHotspot.setCallback(null);
      unscheduleDrawable(mHotspot);
    }

    if (hotspot != null) {
      hotspot.setCallback(this);
      if (hotspot.isStateful()) {
        hotspot.setState(getDrawableState());
      }
    }

    mHotspot = hotspot;
    invalidate();
  }

  /**
   * Draws the next child of the FlatViewGroup.  Each draw view calls FlatViewGroup.drawNextChild,
   * which keeps track of the current child index to draw.
   *
   * @param canvas The canvas to draw on.
   */
  /* package */ void drawNextChild(Canvas canvas) {
    View child = getChildAt(mDrawChildIndex);
    if (child instanceof FlatViewGroup) {
      super.drawChild(canvas, child, getDrawingTime());
    } else {
      // Make sure non-React Views clip properly.
      canvas.save(Canvas.CLIP_SAVE_FLAG);
      child.getHitRect(VIEW_BOUNDS);
      canvas.clipRect(VIEW_BOUNDS);
      super.drawChild(canvas, child, getDrawingTime());
      canvas.restore();
    }

    ++mDrawChildIndex;
  }

  /**
   * Mount a list of draw commands to this FlatViewGroup.  Draw commands sometimes map to a view,
   * as in the case of {@link DrawView}, and sometimes to a simple canvas operation.  We only
   * receive a call to mount draw commands when our commands have changed, so we always invalidate.
   *
   * A call to mount draw commands will only be followed by a call to mount views if the draw view
   * commands within the draw command array have changed since last mount.
   *
   * @param drawCommands The draw commands to mount.
   */
  /* package */ void mountDrawCommands(DrawCommand[] drawCommands) {
    mDrawCommands = drawCommands;
    invalidate();
  }

  /**
   * Mount a list of draw commands to this FlatViewGroup, which is clipping subviews.  Clipping
   * logic is handled by a {@link DrawCommandManager}, which provides a better explanation of
   * these arguments and logic.
   *
   * A call to mount draw commands will only be followed by a call to mount views if the draw view
   * commands within the draw command array have changed since last mount, which is indicated here
   * by willMountViews.
   *
   * @param drawCommands The draw commands to mount.
   * @param drawViewIndexMap See {@link DrawCommandManager}.
   * @param maxBottom See {@link DrawCommandManager}.
   * @param minTop See {@link DrawCommandManager}.
   * @param willMountViews True if we will also receive a mountViews call.  If we are going to
   *   receive a call to mount views, that will take care of updating the commands that are
   *   currently onscreen, otherwise we need to update the onscreen commands.
   */
  /* package */ void mountClippingDrawCommands(
      DrawCommand[] drawCommands,
      SparseIntArray drawViewIndexMap,
      float[] maxBottom,
      float[] minTop,
      boolean willMountViews) {
    Assertions.assertNotNull(mDrawCommandManager).mountDrawCommands(
        drawCommands,
        drawViewIndexMap,
        maxBottom,
        minTop,
        willMountViews);
    invalidate();
  }

  /**
   * Handle a subview being dropped
   * In most cases, we are informed about a subview being dropped via mountViews, but in some
   * cases (such as when both the child and parent get explicit removes in the same frame),
   * we may not find out, so this is called when the child is dropped so the parent can clean up
   * strong references to the child.
   *
   * @param view the view being dropped
   */
  void onViewDropped(View view) {
    if (mDrawCommandManager != null) {
      // for now, we only care about clearing clipped subview references
      mDrawCommandManager.onClippedViewDropped(view);
    }
  }

  /**
   * Return the NodeRegion which matches a reactTag, or EMPTY if none match.
   *
   * @param reactTag The reactTag to look for
   * @return The matching NodeRegion, or NodeRegion.EMPTY if none match.
   */
  /* package */ NodeRegion getNodeRegionForTag(int reactTag) {
    for (NodeRegion region : mNodeRegions) {
      if (region.matchesTag(reactTag)) {
        return region;
      }
    }
    return NodeRegion.EMPTY;
  }

  /**
   * Return a list of FlatViewGroups that are detached (due to being clipped) but that we have a
   * strong reference to. This is used by the FlatNativeViewHierarchyManager to explicitly clean up
   * those views when removing this parent.
   *
   * @return A Collection of Views to clean up.
   */
  /* package */ SparseArray<View> getDetachedViews() {
    if (mDrawCommandManager == null) {
      return EMPTY_DETACHED_VIEWS;
    }
    return mDrawCommandManager.getDetachedViews();
  }

  /**
   * Remove the detached view from the parent
   * This is used in the DrawCommandManagers and during cleanup to trigger onDetachedFromWindow on
   * any views that were in a temporary detached state due to them being clipped. This is called
   * for cleanup of said views by FlatNativeViewHierarchyManager.
   *
   * @param view the detached View to remove
   */
  void removeDetachedView(View view) {
    removeDetachedView(view, false);
  }

  @Override
  public void removeAllViewsInLayout() {
    // whenever we want to remove all views in a layout, we also want to remove all the
    // DrawCommands, otherwise, we can have a mismatch between the DrawView DrawCommands
    // and the Views to draw (note that because removeAllViewsInLayout doesn't call invalidate,
    // we don't actually need to modify mDrawCommands, but we do it just in case).
    mDrawCommands = DrawCommand.EMPTY_ARRAY;
    super.removeAllViewsInLayout();
  }

  /**
   * Mounts attach detach listeners to a FlatViewGroup.  The Nodes spec states that children and
   * commands deal gracefully with multiple attaches and detaches, and as long as:
   *
   *   attachCount - detachCount > 0
   *
   * Then children still consider themselves as attached.
   *
   * @param listeners The listeners to mount.
   */
  /* package */ void mountAttachDetachListeners(AttachDetachListener[] listeners) {
    if (mIsAttached) {
      // Ordering of the following 2 statements is very important. While logically it makes sense to
      // detach old listeners first, and only then attach new listeners, this is not very efficient,
      // because a listener can be in both lists. In this case, it will be detached first and then
      // re-attached immediately. This is undesirable for a couple of reasons:
      // 1) performance. Detaching is slow because it may cancel an ongoing network request
      // 2) it may cause flicker: an image that was already loaded may get unloaded.
      //
      // For this reason, we are attaching new listeners first. What this means is that listeners
      // that are in both lists need to gracefully handle a secondary attach and detach events,
      // (i.e. onAttach() being called when already attached, followed by a detach that should be
      // ignored) turning them into no-ops. This will result in no performance loss and no flicker,
      // because ongoing network requests don't get cancelled.
      dispatchOnAttached(listeners);
      dispatchOnDetached(mAttachDetachListeners);
    }
    mAttachDetachListeners = listeners;
  }

  /**
   * Mount node regions to a FlatViewGroup.  A node region is a touch target for a react tag.  As
   * not all react tags map to a view, we use node regions to determine whether a non-native region
   * should receive a touch.
   *
   * @param nodeRegions The node regions to mount.
   */
  /* package */ void mountNodeRegions(NodeRegion[] nodeRegions) {
    mNodeRegions = nodeRegions;
  }

  /**
   * Mount node regions in clipping.  See {@link DrawCommandManager} for more complete
   * documentation.
   *
   * @param nodeRegions The node regions to mount.
   * @param maxBottom See {@link DrawCommandManager}.
   * @param minTop See {@link DrawCommandManager}.
   */
  /* package */ void mountClippingNodeRegions(
      NodeRegion[] nodeRegions,
      float[] maxBottom,
      float[] minTop) {
    mNodeRegions = nodeRegions;
    Assertions.assertNotNull(mDrawCommandManager).mountNodeRegions(nodeRegions, maxBottom, minTop);
  }

  /**
   * Mount a list of views to add, and dismount a list of views to detach.  Ids will not appear in
   * both lists, aka:
   *   Set(viewsToAdd + viewsToDetach).size() == viewsToAdd.length + viewsToDetach.length
   *
   * Every time we get any change in the views in a FlatViewGroup, we detach all views first, then
   * reattach / remove them as needed.  viewsToAdd is odd in that the ids also specify whether
   * the view is new to us, or if we were already the parent.  If it is new to us, then the id has
   * a positive value, otherwise we are already the parent, but it was previously detached, since
   * we detach everything when anything changes.
   *
   * The reason we detach everything is that a single detach is on the order of O(n), as in the
   * average case we have to move half of the views one position to the right, and a single add is
   * the same.  Removing all views is also on the order of O(n), as you delete everything backward
   * from the end, while adding a new set of views is also on the order of O(n), as you just add
   * them all back in order.  ArrayLists are weird.
   *
   * @param viewResolver Resolves the views from their id.
   * @param viewsToAdd id of views to add if they weren't just attached to us, or -id if they are
   *     just being reattached.
   * @param viewsToDetach id of views that we don't own anymore.  They either moved to a new parent,
   *     or are being removed entirely.
   */
  /* package */ void mountViews(ViewResolver viewResolver, int[] viewsToAdd, int[] viewsToDetach) {
    if (mDrawCommandManager != null) {
      mDrawCommandManager.mountViews(viewResolver, viewsToAdd, viewsToDetach);
    } else {
      for (int viewToAdd : viewsToAdd) {
        if (viewToAdd > 0) {
          View view = viewResolver.getView(viewToAdd);
          ensureViewHasNoParent(view);
          addViewInLayout(view);
        } else {
          View view = viewResolver.getView(-viewToAdd);
          ensureViewHasNoParent(view);
          // We aren't clipping, so attach all the things, clipping is handled by the draw command
          // manager, if we have one.
          attachViewToParent(view);
        }
      }

      for (int viewToDetach : viewsToDetach) {
        View view = viewResolver.getView(viewToDetach);
        if (view.getParent() != null) {
          throw new RuntimeException("Trying to remove view not owned by FlatViewGroup");
        } else {
          removeDetachedView(view, false);
        }
      }
    }

    invalidate();
  }

  /**
   * Exposes the protected addViewInLayout call for the {@link DrawCommandManager}.
   *
   * @param view The view to add.
   */
  /* package */ void addViewInLayout(View view) {
    addViewInLayout(view, -1, ensureLayoutParams(view.getLayoutParams()), true);
  }

  /**
   * Exposes the protected addViewInLayout call for the {@link DrawCommandManager}.
   *
   * @param view The view to add.
   * @param index The index position at which to add this child.
   */
  /* package */ void addViewInLayout(View view, int index) {
    addViewInLayout(view, index, ensureLayoutParams(view.getLayoutParams()), true);
  }

  /**
   * Exposes the protected attachViewToParent call for the {@link DrawCommandManager}.
   *
   * @param view The view to attach.
   */
  /* package */ void attachViewToParent(View view) {
    attachViewToParent(view, -1, ensureLayoutParams(view.getLayoutParams()));
  }

  /**
   * Exposes the protected attachViewToParent call for the {@link DrawCommandManager}.
   *
   * @param view The view to attach.
   * @param index The index position at which to attach this child.
   */
  /* package */ void attachViewToParent(View view, int index) {
    attachViewToParent(view, index, ensureLayoutParams(view.getLayoutParams()));
  }

  private void processLayoutRequest() {
    mIsLayoutRequested = false;
    for (int i = 0, childCount = getChildCount(); i != childCount; ++i) {
      View child = getChildAt(i);
      if (!child.isLayoutRequested()) {
        continue;
      }

      child.measure(
        MeasureSpec.makeMeasureSpec(child.getWidth(), MeasureSpec.EXACTLY),
        MeasureSpec.makeMeasureSpec(child.getHeight(), MeasureSpec.EXACTLY));
      child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
    }
  }

  /**
   * Called after the view hierarchy is updated in {@link StateBuilder}, to process all the
   * FlatViewGroups that have requested layout.
   */
  /* package */ static void processLayoutRequests() {
    for (int i = 0, numLayoutRequests = LAYOUT_REQUESTS.size(); i != numLayoutRequests; ++i) {
      FlatViewGroup flatViewGroup = LAYOUT_REQUESTS.get(i);
      flatViewGroup.processLayoutRequest();
    }
    LAYOUT_REQUESTS.clear();
  }

  // Helper method for measure functionality provided by MeasuredViewGroup.
  @Override
  public Rect measureWithCommands() {
    int childCount = getChildCount();
    if (childCount == 0 && mDrawCommands.length == 0) {
      return new Rect(0, 0, 0, 0);
    }
    int left = Integer.MAX_VALUE;
    int top = Integer.MAX_VALUE;
    int right = Integer.MIN_VALUE;
    int bottom = Integer.MIN_VALUE;
    for (int i = 0; i < childCount; i++) {
      // This is technically a dupe, since the DrawView has its bounds, but leaving in to handle if
      // the View is animating or rebelling against the DrawView bounds for some reason.
      View child = getChildAt(i);
      left = Math.min(left, child.getLeft());
      top = Math.min(top, child.getTop());
      right = Math.max(right, child.getRight());
      bottom = Math.max(bottom, child.getBottom());
    }

    for (DrawCommand mDrawCommand : mDrawCommands) {
      if (!(mDrawCommand instanceof AbstractDrawCommand)) {
        continue;
      }
      AbstractDrawCommand drawCommand = (AbstractDrawCommand) mDrawCommand;
      left = Math.min(left, Math.round(drawCommand.getLeft()));
      top = Math.min(top, Math.round(drawCommand.getTop()));
      right = Math.max(right, Math.round(drawCommand.getRight()));
      bottom = Math.max(bottom, Math.round(drawCommand.getBottom()));
    }
    return new Rect(left, top, right, bottom);
  }

  /**
   * Searches for a virtual node region matching the specified x and y touch.  Virtual in this case
   * means simply that the node region represents a command, rather than a native view.
   *
   * @param touchX The touch x coordinate.
   * @param touchY The touch y coordinate.
   * @return A virtual node region matching the specified touch, or null if no regions match.
   */
  private @Nullable NodeRegion virtualNodeRegionWithinBounds(float touchX, float touchY) {
    if (mDrawCommandManager != null) {
      return mDrawCommandManager.virtualNodeRegionWithinBounds(touchX, touchY);
    }
    for (int i = mNodeRegions.length - 1; i >= 0; --i) {
      NodeRegion nodeRegion = mNodeRegions[i];
      if (!nodeRegion.mIsVirtual) {
        // only interested in virtual nodes
        continue;
      }
      if (nodeRegion.withinBounds(touchX, touchY)) {
        return nodeRegion;
      }
    }

    return null;
  }

  /**
   * Searches for a node region matching the specified x and y touch.  Will search regions which
   * representing both commands and native views.
   *
   * @param touchX The touch x coordinate.
   * @param touchY The touch y coordinate.
   * @return A node region matching the specified touch, or null if no regions match.
   */
  private @Nullable NodeRegion anyNodeRegionWithinBounds(float touchX, float touchY) {
    if (mDrawCommandManager != null) {
      return mDrawCommandManager.anyNodeRegionWithinBounds(touchX, touchY);
    }
    for (int i = mNodeRegions.length - 1; i >= 0; --i) {
      NodeRegion nodeRegion = mNodeRegions[i];
      if (nodeRegion.withinBounds(touchX, touchY)) {
        return nodeRegion;
      }
    }

    return null;
  }

  private static void ensureViewHasNoParent(View view) {
    ViewParent oldParent = view.getParent();
    if (oldParent != null) {
      throw new RuntimeException(
          "Cannot add view " + view + " to FlatViewGroup while it has a parent " + oldParent);
    }
  }

  /**
   * Propagate attach to a list of listeners, passing a callback by which they can invalidate.
   *
   * @param listeners List of listeners to attach.
   */
  private void dispatchOnAttached(AttachDetachListener[] listeners) {
    int numListeners = listeners.length;
    if (numListeners == 0) {
      return;
    }

    InvalidateCallback callback = getInvalidateCallback();
    for (AttachDetachListener listener : listeners) {
      listener.onAttached(callback);
    }
  }

  /**
   * Get an invalidate callback singleton for this view instance.
   *
   * @return Invalidate callback singleton.
   */
  private InvalidateCallback getInvalidateCallback() {
    if (mInvalidateCallback == null) {
      mInvalidateCallback = new InvalidateCallback(this);
    }
    return mInvalidateCallback;
  }

  /**
   * Propagate detach to a list of listeners.
   *
   * @param listeners List of listeners to detach.
   */
  private static void dispatchOnDetached(AttachDetachListener[] listeners) {
    for (AttachDetachListener listener : listeners) {
      listener.onDetached();
    }
  }

  private ViewGroup.LayoutParams ensureLayoutParams(ViewGroup.LayoutParams lp) {
    if (checkLayoutParams(lp)) {
      return lp;
    }
    return generateDefaultLayoutParams();
  }

  @Override
  public void updateClippingRect() {
    if (mDrawCommandManager == null) {
      // Don't update the clipping rect if we aren't clipping.
      return;
    }
    if (mDrawCommandManager.updateClippingRect()) {
      // Manager says something changed.
      invalidate();
    }
  }

  @Override
  public void getClippingRect(Rect outClippingRect) {
    if (mDrawCommandManager == null) {
      // We could call outClippingRect.set(null) here, but throw in case the underlying React Native
      // behaviour changes without us knowing.
      throw new RuntimeException(
          "Trying to get the clipping rect for a non-clipping FlatViewGroup");
    }
     mDrawCommandManager.getClippingRect(outClippingRect);
  }

  @Override
  public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
    boolean currentlyClipping = getRemoveClippedSubviews();
    if (removeClippedSubviews == currentlyClipping) {
      // We aren't changing state, so don't do anything.
      return;
    }
    if (currentlyClipping) {
      // Trying to go from a clipping to a non-clipping state, not currently supported by Nodes.
      // If this is an issue, let us know, but currently there does not seem to be a good case for
      // supporting this.
      throw new RuntimeException(
          "Trying to transition FlatViewGroup from clipping to non-clipping state");
    }
    mDrawCommandManager = DrawCommandManager.getVerticalClippingInstance(this, mDrawCommands);
    mDrawCommands = DrawCommand.EMPTY_ARRAY;
    // We don't need an invalidate here because this can't cause new views to come onscreen, since
    // everything was unclipped.
  }

  @Override
  public boolean getRemoveClippedSubviews() {
    return mDrawCommandManager != null;
  }

  @Override
  public @Nullable Rect getHitSlopRect() {
    return mHitSlopRect;
  }

  /* package */ void setHitSlopRect(@Nullable Rect rect) {
    mHitSlopRect = rect;
  }
}
