/**
 * 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.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;

/**
 * Base class for all DrawCommands. Becomes immutable once it has its bounds set. Until then, a
 * subclass is able to mutate any of its properties (e.g. updating Layout in DrawTextLayout).
 *
 * The idea is to be able to reuse unmodified objects when we build up DrawCommands before we ship
 * them to UI thread, but we can only do that if DrawCommands are immutable.
 */
/* package */ abstract class AbstractDrawCommand extends DrawCommand implements Cloneable {

  private float mLeft;
  private float mTop;
  private float mRight;
  private float mBottom;
  private boolean mFrozen;

  protected boolean mNeedsClipping;
  private float mClipLeft;
  private float mClipTop;
  private float mClipRight;
  private float mClipBottom;

  // Used to draw highlights in debug draw.
  private static Paint sDebugHighlightRed;
  private static Paint sDebugHighlightYellow;
  private static Paint sDebugHighlightOverlayText;

  public final boolean clipBoundsMatch(
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    return mClipLeft == clipLeft && mClipTop == clipTop
        && mClipRight == clipRight && mClipBottom == clipBottom;
  }

  protected final void setClipBounds(
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    mClipLeft = clipLeft;
    mClipTop = clipTop;
    mClipRight = clipRight;
    mClipBottom = clipBottom;
    // We put this check here to not clip when we have the default [-infinity, infinity] bounds,
    // since clipRect in those cases is essentially no-op anyway. This is needed to fix a bug that
    // shows up during screenshot testing. Note that checking one side is enough, since if one side
    // is infinite, all sides will be infinite, since we only set infinite for all sides at the
    // same time - conversely, if one side is finite, all sides will be finite.
    mNeedsClipping = mClipLeft != Float.NEGATIVE_INFINITY;
  }

  public final float getClipLeft() {
    return mClipLeft;
  }

  public final float getClipTop() {
    return mClipTop;
  }

  public final float getClipRight() {
    return mClipRight;
  }

  public final float getClipBottom() {
    return mClipBottom;
  }

  protected void applyClipping(Canvas canvas) {
    canvas.clipRect(mClipLeft, mClipTop, mClipRight, mClipBottom);
  }

  /**
   * Don't override this unless you need to do custom clipping in a draw command.  Otherwise just
   * override onPreDraw and onDraw.
   */
  @Override
  public void draw(FlatViewGroup parent, Canvas canvas) {
    onPreDraw(parent, canvas);
    if (mNeedsClipping && shouldClip()) {
      canvas.save(Canvas.CLIP_SAVE_FLAG);
      applyClipping(canvas);
      onDraw(canvas);
      canvas.restore();
    } else {
      onDraw(canvas);
    }
  }

  protected static int getDebugBorderColor() {
    return Color.CYAN;
  }

  protected String getDebugName() {
    return getClass().getSimpleName().substring(4);
  }

  private void initDebugHighlightResources(FlatViewGroup parent) {
    if (sDebugHighlightRed == null) {
      sDebugHighlightRed = new Paint();
      sDebugHighlightRed.setARGB(75, 255, 0, 0);
    }
    if (sDebugHighlightYellow == null) {
      sDebugHighlightYellow = new Paint();
      sDebugHighlightYellow.setARGB(100, 255, 204, 0);
    }
    if (sDebugHighlightOverlayText == null) {
      sDebugHighlightOverlayText = new Paint();
      sDebugHighlightOverlayText.setAntiAlias(true);
      sDebugHighlightOverlayText.setARGB(200, 50, 50, 50);
      sDebugHighlightOverlayText.setTextAlign(Paint.Align.RIGHT);
      sDebugHighlightOverlayText.setTypeface(Typeface.MONOSPACE);
      sDebugHighlightOverlayText.setTextSize(parent.dipsToPixels(9));
    }
  }

  private void debugDrawHighlightRect(Canvas canvas, Paint paint, String text) {
    canvas.drawRect(getLeft(), getTop(), getRight(), getBottom(), paint);
    canvas.drawText(text, getRight() - 5, getBottom() - 5, sDebugHighlightOverlayText);
  }

  protected void debugDrawWarningHighlight(Canvas canvas, String text) {
    debugDrawHighlightRect(canvas, sDebugHighlightRed, text);
  }

  protected void debugDrawCautionHighlight(Canvas canvas, String text) {
    debugDrawHighlightRect(canvas, sDebugHighlightYellow, text);
  }

  @Override
  public final void debugDraw(FlatViewGroup parent, Canvas canvas) {
    onDebugDraw(parent, canvas);
    if (FlatViewGroup.DEBUG_HIGHLIGHT_PERFORMANCE_ISSUES) {
      initDebugHighlightResources(parent);
      onDebugDrawHighlight(canvas);
    }
  }

  protected void onDebugDraw(FlatViewGroup parent, Canvas canvas) {
    parent.debugDrawNamedRect(
        canvas,
        getDebugBorderColor(),
        getDebugName(),
        mLeft,
        mTop,
        mRight,
        mBottom);
  }

  protected void onDebugDrawHighlight(Canvas canvas) {
  }

  protected void onPreDraw(FlatViewGroup parent, Canvas canvas) {
  }

  /**
   * Updates boundaries of the AbstractDrawCommand and freezes it.
   * Will return a frozen copy if the current AbstractDrawCommand cannot be mutated.
   *
   * This should not be called on a DrawView, as the DrawView is modified on UI thread.  Use
   * DrawView.collectDrawView instead to avoid race conditions.
   */
  public AbstractDrawCommand updateBoundsAndFreeze(
      float left,
      float top,
      float right,
      float bottom,
      float clipLeft,
      float clipTop,
      float clipRight,
      float clipBottom) {
    if (mFrozen) {
      // see if we can reuse it
      boolean boundsMatch = boundsMatch(left, top, right, bottom);
      boolean clipBoundsMatch = clipBoundsMatch(clipLeft, clipTop, clipRight, clipBottom);
      if (boundsMatch && clipBoundsMatch) {
        return this;
      }

      try {
        AbstractDrawCommand copy = (AbstractDrawCommand) clone();
        if (!boundsMatch) {
          copy.setBounds(left, top, right, bottom);
        }
        if (!clipBoundsMatch) {
          copy.setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
        }
        return copy;
      } catch (CloneNotSupportedException e) {
        // This should not happen since AbstractDrawCommand implements Cloneable
        throw new RuntimeException(e);
      }
    }

    setBounds(left, top, right, bottom);
    setClipBounds(clipLeft, clipTop, clipRight, clipBottom);
    mFrozen = true;
    return this;
  }

  /**
   * Returns a non-frozen shallow copy of AbstractDrawCommand as defined by {@link Object#clone()}.
   */
  public final AbstractDrawCommand mutableCopy() {
    try {
      AbstractDrawCommand copy = (AbstractDrawCommand) super.clone();
      copy.mFrozen = false;
      return copy;
    } catch (CloneNotSupportedException e) {
      // should not happen since we implement Cloneable
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns whether this object was frozen and thus cannot be mutated.
   */
  public final boolean isFrozen() {
    return mFrozen;
  }

  /**
   * Mark this object as frozen, indicating that it should not be mutated.
   */
  public final void freeze() {
    mFrozen = true;
  }

  /**
   * Left position of this DrawCommand relative to the hosting View.
   */
  public final float getLeft() {
    return mLeft;
  }

  /**
   * Top position of this DrawCommand relative to the hosting View.
   */
  public final float getTop() {
    return mTop;
  }

  /**
   * Right position of this DrawCommand relative to the hosting View.
   */
  public final float getRight() {
    return mRight;
  }

  /**
   * Bottom position of this DrawCommand relative to the hosting View.
   */
  public final float getBottom() {
    return mBottom;
  }

  protected abstract void onDraw(Canvas canvas);

  protected boolean shouldClip() {
    return mLeft < getClipLeft() || mTop < getClipTop() ||
        mRight > getClipRight() || mBottom > getClipBottom();
  }

  protected void onBoundsChanged() {
  }

  /**
   * Updates boundaries of this DrawCommand.
   */
  protected final void setBounds(float left, float top, float right, float bottom) {
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    onBoundsChanged();
  }

  /**
   * Returns true if boundaries match and don't need to be updated. False otherwise.
   */
  protected final boolean boundsMatch(float left, float top, float right, float bottom) {
    return mLeft == left && mTop == top && mRight == right && mBottom == bottom;
  }
}
