// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

package net.realify.lib.androidimagecropper;

import android.graphics.RectF;

/** Handler from crop window stuff, moving and knowing possition. */
final class CropWindowHandler {

  // region: Fields and Consts

  /** The 4 edges of the crop window defining its coordinates and size */
  private final RectF mEdges = new RectF();

  /**
   * Rectangle used to return the edges rectangle without ability to change it and without creating
   * new all the time.
   */
  private final RectF mGetEdges = new RectF();

  /** Minimum width in pixels that the crop window can get. */
  private float mMinCropWindowWidth;

  /** Minimum height in pixels that the crop window can get. */
  private float mMinCropWindowHeight;

  /** Maximum width in pixels that the crop window can CURRENTLY get. */
  private float mMaxCropWindowWidth;

  /** Maximum height in pixels that the crop window can CURRENTLY get. */
  private float mMaxCropWindowHeight;

  /**
   * Minimum width in pixels that the result of cropping an image can get, affects crop window width
   * adjusted by width scale factor.
   */
  private float mMinCropResultWidth;

  /**
   * Minimum height in pixels that the result of cropping an image can get, affects crop window
   * height adjusted by height scale factor.
   */
  private float mMinCropResultHeight;

  /**
   * Maximum width in pixels that the result of cropping an image can get, affects crop window width
   * adjusted by width scale factor.
   */
  private float mMaxCropResultWidth;

  /**
   * Maximum height in pixels that the result of cropping an image can get, affects crop window
   * height adjusted by height scale factor.
   */
  private float mMaxCropResultHeight;

  /** The width scale factor of shown image and actual image */
  private float mScaleFactorWidth = 1;

  /** The height scale factor of shown image and actual image */
  private float mScaleFactorHeight = 1;
  // endregion

  /** Get the left/top/right/bottom coordinates of the crop window. */
  public RectF getRect() {
    mGetEdges.set(mEdges);
    return mGetEdges;
  }

  /** Minimum width in pixels that the crop window can get. */
  public float getMinCropWidth() {
    return Math.max(mMinCropWindowWidth, mMinCropResultWidth / mScaleFactorWidth);
  }

  /** Minimum height in pixels that the crop window can get. */
  public float getMinCropHeight() {
    return Math.max(mMinCropWindowHeight, mMinCropResultHeight / mScaleFactorHeight);
  }

  /** Maximum width in pixels that the crop window can get. */
  public float getMaxCropWidth() {
    return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
  }

  /** Maximum height in pixels that the crop window can get. */
  public float getMaxCropHeight() {
    return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
  }

  /** get the scale factor (on width) of the showen image to original image. */
  public float getScaleFactorWidth() {
    return mScaleFactorWidth;
  }

  /** get the scale factor (on height) of the showen image to original image. */
  public float getScaleFactorHeight() {
    return mScaleFactorHeight;
  }

  /**
   * the min size the resulting cropping image is allowed to be, affects the cropping window limits
   * (in pixels).<br>
   */
  public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
    mMinCropResultWidth = minCropResultWidth;
    mMinCropResultHeight = minCropResultHeight;
  }

  /**
   * the max size the resulting cropping image is allowed to be, affects the cropping window limits
   * (in pixels).<br>
   */
  public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
    mMaxCropResultWidth = maxCropResultWidth;
    mMaxCropResultHeight = maxCropResultHeight;
  }

  /**
   * set the max width/height and scale factor of the showen image to original image to scale the
   * limits appropriately.
   */
  public void setCropWindowLimits(
      float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
    mMaxCropWindowWidth = maxWidth;
    mMaxCropWindowHeight = maxHeight;
    mScaleFactorWidth = scaleFactorWidth;
    mScaleFactorHeight = scaleFactorHeight;
  }

  /** Set the variables to be used during crop window handling. */
  public void setInitialAttributeValues(CropImageOptions options) {
    mMinCropWindowWidth = options.minCropWindowWidth;
    mMinCropWindowHeight = options.minCropWindowHeight;
    mMinCropResultWidth = options.minCropResultWidth;
    mMinCropResultHeight = options.minCropResultHeight;
    mMaxCropResultWidth = options.maxCropResultWidth;
    mMaxCropResultHeight = options.maxCropResultHeight;
  }

  /** Set the left/top/right/bottom coordinates of the crop window. */
  public void setRect(RectF rect) {
    mEdges.set(rect);
  }

  /**
   * Indicates whether the crop window is small enough that the guidelines should be shown. Public
   * because this function is also used to determine if the center handle should be focused.
   *
   * @return boolean Whether the guidelines should be shown or not
   */
  public boolean showGuidelines() {
    return !(mEdges.width() < 100 || mEdges.height() < 100);
  }

  /**
   * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
   * box, and the touch radius.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @param targetRadius the target radius in pixels
   * @return the Handle that was pressed; null if no Handle was pressed
   */
  public CropWindowMoveHandler getMoveHandler(
      float x, float y, float targetRadius, CropImageView.CropShape cropShape) {
    CropWindowMoveHandler.Type type =
        cropShape == CropImageView.CropShape.OVAL
            ? getOvalPressedMoveType(x, y)
            : getRectanglePressedMoveType(x, y, targetRadius);
    return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
  }

  // region: Private methods

  /**
   * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
   * box, and the touch radius.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @param targetRadius the target radius in pixels
   * @return the Handle that was pressed; null if no Handle was pressed
   */
  private CropWindowMoveHandler.Type getRectanglePressedMoveType(
      float x, float y, float targetRadius) {
    CropWindowMoveHandler.Type moveType = null;

    // Note: corner-handles take precedence, then side-handles, then center.
    if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.TOP_LEFT;
    } else if (CropWindowHandler.isInCornerTargetZone(
        x, y, mEdges.right, mEdges.top, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
    } else if (CropWindowHandler.isInCornerTargetZone(
        x, y, mEdges.left, mEdges.bottom, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
    } else if (CropWindowHandler.isInCornerTargetZone(
        x, y, mEdges.right, mEdges.bottom, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
    } else if (CropWindowHandler.isInCenterTargetZone(
            x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
        && focusCenter()) {
      moveType = CropWindowMoveHandler.Type.CENTER;
    } else if (CropWindowHandler.isInHorizontalTargetZone(
        x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.TOP;
    } else if (CropWindowHandler.isInHorizontalTargetZone(
        x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.BOTTOM;
    } else if (CropWindowHandler.isInVerticalTargetZone(
        x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.LEFT;
    } else if (CropWindowHandler.isInVerticalTargetZone(
        x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
      moveType = CropWindowMoveHandler.Type.RIGHT;
    } else if (CropWindowHandler.isInCenterTargetZone(
            x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
        && !focusCenter()) {
      moveType = CropWindowMoveHandler.Type.CENTER;
    }

    return moveType;
  }

  /**
   * Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
   * box/oval, and the touch radius.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @return the Handle that was pressed; null if no Handle was pressed
   */
  private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) {

    /*
       Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While
       this is not perfect, it's a good quick-to-ship approach.

       TL T T T T TR
        L C C C C R
        L C C C C R
        L C C C C R
        L C C C C R
       BL B B B B BR
    */

    float cellLength = mEdges.width() / 6;
    float leftCenter = mEdges.left + cellLength;
    float rightCenter = mEdges.left + (5 * cellLength);

    float cellHeight = mEdges.height() / 6;
    float topCenter = mEdges.top + cellHeight;
    float bottomCenter = mEdges.top + 5 * cellHeight;

    CropWindowMoveHandler.Type moveType;
    if (x < leftCenter) {
      if (y < topCenter) {
        moveType = CropWindowMoveHandler.Type.TOP_LEFT;
      } else if (y < bottomCenter) {
        moveType = CropWindowMoveHandler.Type.LEFT;
      } else {
        moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
      }
    } else if (x < rightCenter) {
      if (y < topCenter) {
        moveType = CropWindowMoveHandler.Type.TOP;
      } else if (y < bottomCenter) {
        moveType = CropWindowMoveHandler.Type.CENTER;
      } else {
        moveType = CropWindowMoveHandler.Type.BOTTOM;
      }
    } else {
      if (y < topCenter) {
        moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
      } else if (y < bottomCenter) {
        moveType = CropWindowMoveHandler.Type.RIGHT;
      } else {
        moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
      }
    }

    return moveType;
  }

  /**
   * Determines if the specified coordinate is in the target touch zone for a corner handle.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @param handleX the x-coordinate of the corner handle
   * @param handleY the y-coordinate of the corner handle
   * @param targetRadius the target radius in pixels
   * @return true if the touch point is in the target touch zone; false otherwise
   */
  private static boolean isInCornerTargetZone(
      float x, float y, float handleX, float handleY, float targetRadius) {
    return Math.abs(x - handleX) <= targetRadius && Math.abs(y - handleY) <= targetRadius;
  }

  /**
   * Determines if the specified coordinate is in the target touch zone for a horizontal bar handle.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @param handleXStart the left x-coordinate of the horizontal bar handle
   * @param handleXEnd the right x-coordinate of the horizontal bar handle
   * @param handleY the y-coordinate of the horizontal bar handle
   * @param targetRadius the target radius in pixels
   * @return true if the touch point is in the target touch zone; false otherwise
   */
  private static boolean isInHorizontalTargetZone(
      float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) {
    return x > handleXStart && x < handleXEnd && Math.abs(y - handleY) <= targetRadius;
  }

  /**
   * Determines if the specified coordinate is in the target touch zone for a vertical bar handle.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @param handleX the x-coordinate of the vertical bar handle
   * @param handleYStart the top y-coordinate of the vertical bar handle
   * @param handleYEnd the bottom y-coordinate of the vertical bar handle
   * @param targetRadius the target radius in pixels
   * @return true if the touch point is in the target touch zone; false otherwise
   */
  private static boolean isInVerticalTargetZone(
      float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) {
    return Math.abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd;
  }

  /**
   * Determines if the specified coordinate falls anywhere inside the given bounds.
   *
   * @param x the x-coordinate of the touch point
   * @param y the y-coordinate of the touch point
   * @param left the x-coordinate of the left bound
   * @param top the y-coordinate of the top bound
   * @param right the x-coordinate of the right bound
   * @param bottom the y-coordinate of the bottom bound
   * @return true if the touch point is inside the bounding rectangle; false otherwise
   */
  private static boolean isInCenterTargetZone(
      float x, float y, float left, float top, float right, float bottom) {
    return x > left && x < right && y > top && y < bottom;
  }

  /**
   * Determines if the cropper should focus on the center handle or the side handles. If it is a
   * small image, focus on the center handle so the user can move it. If it is a large image, focus
   * on the side handles so user can grab them. Corresponds to the appearance of the
   * RuleOfThirdsGuidelines.
   *
   * @return true if it is small enough such that it should focus on the center; less than
   *     show_guidelines limit
   */
  private boolean focusCenter() {
    return !showGuidelines();
  }
  // endregion
}
