/**
 * 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 android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;

/* package */ final class DrawView extends AbstractDrawCommand {
  public static final DrawView[] EMPTY_ARRAY = new DrawView[0];
  // the minimum rounded clipping value before we actually do rounded clipping
  /* package */ static final float MINIMUM_ROUNDED_CLIPPING_VALUE = 0.5f;
  private final RectF TMP_RECT = new RectF();

  /* package */ final int reactTag;
  // Indicates whether this DrawView has been previously mounted to a clipping FlatViewGroup.  This
  // lets us know that the bounds haven't changed, as a bounds change would trigger a new DrawView,
  // which will set this to false for the new DrawView.  This is safe, despite the dual access with
  // FlatViewGroup, because the FlatViewGroup copy is only ever modified by the FlatViewGroup.
  // Changing how this boolean is used should be handled with caution, as race conditions are the
  // quickest way to create unreproducible super bugs.
  /* package */ boolean mWasMounted;

  // the clipping radius - if this is greater than MINIMUM_ROUNDED_CLIPPING_VALUE, we clip using
  // a rounded path, otherwise we clip in a rectangular fashion.
  private float mClipRadius;

  // the path to clip against if we're doing path clipping for rounded borders.
  @Nullable private Path mPath;

  // These should only ever be set from within the DrawView, they serve to provide clipping bounds
  // for FlatViewGroups, which have strange clipping when it comes to overflow: visible.  They are
  // left package protected to speed up direct access.  For overflow visible, these are the adjusted
  // bounds while taking overflowing elements into account, other wise they are just the regular
  // bounds of the view.
  /* package */ float mLogicalLeft;
  /* package */ float mLogicalTop;
  /* package */ float mLogicalRight;
  /* package */ float mLogicalBottom;

  public DrawView(int reactTag) {
    this.reactTag = reactTag;
  }

  /**
   * Similar to updateBoundsAndFreeze, but thread safe as the mounting flag is modified on the UI
   * thread.
   *
   * @return A DrawView with the passed bounds and clipping bounds.  If we can use the same
   *     DrawView, it will just be this, otherwise it will be a frozen copy.
   */
  public DrawView collectDrawView(
      float left,
      float top,
      float right,
      float bottom,
      float logicalLeft,
      float logicalTop,
      float logicalRight,
      float logicalBottom,
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom,
      float clipRadius) {
    if (!isFrozen()) {
      // We haven't collected this draw view yet, so we can just set everything.
      setBounds(left, top, right, bottom);
      setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
      setClipRadius(clipRadius);
      setLogicalBounds(logicalLeft, logicalTop, logicalRight, logicalBottom);
      freeze();
      return this;
    }

    boolean boundsMatch = boundsMatch(left, top, right, bottom);
    boolean clipBoundsMatch = clipBoundsMatch(clipLeft, clipTop, clipRight, clipBottom);
    boolean clipRadiusMatch = mClipRadius == clipRadius;
    boolean logicalBoundsMatch =
        logicalBoundsMatch(logicalLeft, logicalTop, logicalRight, logicalBottom);

    // See if we can reuse the draw view.
    if (boundsMatch && clipBoundsMatch && clipRadiusMatch && logicalBoundsMatch) {
      return this;
    }

    DrawView drawView = (DrawView) mutableCopy();

    if (!boundsMatch) {
      drawView.setBounds(left, top, right, bottom);
    }

    if (!clipBoundsMatch) {
      drawView.setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
    }

    if (!logicalBoundsMatch) {
      drawView.setLogicalBounds(logicalLeft, logicalTop, logicalRight, logicalBottom);
    }

    if (!clipRadiusMatch || !boundsMatch) {
      // If the bounds change, we need to update the clip path.
      drawView.setClipRadius(clipRadius);
    }

    // It is very important that we unset this, as our spec is that newly created DrawViews
    // are handled differently by the FlatViewGroup.  This is needed because clone() maintains
    // the previous state.
    drawView.mWasMounted = false;

    drawView.freeze();

    return drawView;
  }

  private boolean logicalBoundsMatch(float left, float top, float right, float bottom) {
    return left == mLogicalLeft && top == mLogicalTop &&
        right == mLogicalRight && bottom == mLogicalBottom;
  }

  private void setLogicalBounds(float left, float top, float right, float bottom) {
    // Do rounding up front and off of the UI thread.
    mLogicalLeft = left;
    mLogicalTop = top;
    mLogicalRight = right;
    mLogicalBottom = bottom;
  }

  @Override
  public void draw(FlatViewGroup parent, Canvas canvas) {
    onPreDraw(parent, canvas);
    if (mNeedsClipping || mClipRadius > MINIMUM_ROUNDED_CLIPPING_VALUE) {
      canvas.save(Canvas.CLIP_SAVE_FLAG);
      applyClipping(canvas);
      parent.drawNextChild(canvas);
      canvas.restore();
    } else {
      parent.drawNextChild(canvas);
    }
  }

  /**
   * Set the clip radius.  Should only be called when the clip radius is first set or when it
   * changes, in order to avoid extra work.
   *
   * @param clipRadius The new clip radius.
   */
  void setClipRadius(float clipRadius) {
    mClipRadius = clipRadius;
    if (clipRadius > MINIMUM_ROUNDED_CLIPPING_VALUE) {
      // update the path that we'll clip based on
      updateClipPath();
    } else {
      mPath = null;
    }
  }

  /**
   * Update the path with which we'll clip this view
   */
  private void updateClipPath() {
    mPath = new Path();

    TMP_RECT.set(
        getLeft(),
        getTop(),
        getRight(),
        getBottom());

    // set the path
    mPath.addRoundRect(
        TMP_RECT,
        mClipRadius,
        mClipRadius,
        Path.Direction.CW);
  }

  @Override
  protected void applyClipping(Canvas canvas) {
    // only clip using a path if our radius is greater than some minimum threshold, because
    // clipPath is more expensive than clipRect.
    if (mClipRadius > MINIMUM_ROUNDED_CLIPPING_VALUE) {
      canvas.clipPath(mPath);
    } else {
      super.applyClipping(canvas);
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    // no op as we override draw.
  }

  @Override
  protected void onDebugDraw(FlatViewGroup parent, Canvas canvas) {
    parent.debugDrawNextChild(canvas);
  }

  @Override
  protected void onDebugDrawHighlight(Canvas canvas) {
    if (mPath != null) {
      debugDrawWarningHighlight(canvas, "borderRadius: " + mClipRadius);
    } else if (!boundsMatch(mLogicalLeft, mLogicalTop, mLogicalRight, mLogicalBottom)) {
      StringBuilder warn = new StringBuilder("Overflow: { ");
      String[] names = { "left: ", "top: ", "right: ", "bottom: "};
      int i = 0;
      float[] offsets = new float[4];
      offsets[i++] = getLeft() - mLogicalLeft;
      offsets[i++] = getTop() - mLogicalTop;
      offsets[i++] = mLogicalRight - getRight();
      offsets[i++] = mLogicalBottom - getBottom();

      for (i = 0; i < 4; i++) {
        if (offsets[i] != 0f) {
          warn.append(names[i]);
          warn.append(offsets[i]);
          warn.append(", ");
        }
      }

      warn.append("}");

      debugDrawCautionHighlight(canvas, warn.toString());
    }
  }
}
