package org.thoughtcrime.securesms.components;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.Arrays;

import kotlin.jvm.functions.Function2;

/**
 * Drawable which renders a gradient at a specified angle. Note that this drawable does
 * not implement drawable state, and all the baggage that comes with a normal Drawable
 * override, so this may not work in every scenario.
 *
 * Essentially, this drawable creates a LinearGradient shader using the given colors and
 * positions, but makes it larger than the bounds, such that it can be rotated and still
 * fill the bounds with a gradient.
 *
 * If you wish to apply clipping to this drawable, it is recommended to either use it with
 * a CardView or utilize {@link org.thoughtcrime.securesms.util.CustomDrawWrapperKt#customizeOnDraw(Drawable, Function2)}
 */
public final class RotatableGradientDrawable extends Drawable {

  /**
   * From investigation into how Gradients are rendered vs how they are rendered in
   * designs, in order to match spec, we need to rotate gradients by 225 degrees. (180 + 45)
   *
   * This puts 0 at the bottom (0, -1) of the surface area.
   */
  private static final float DEGREE_OFFSET = 225f;

  private final float   degrees;
  private final int[]   colors;
  private final float[] positions;

  private final Rect  fillRect  = new Rect();
  private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

  /**
   * @param degrees    Gradient rotation in degrees, relative to a vector pointed from the center to bottom center
   * @param colors     The colors of the gradient
   * @param positions  The positions of the colors. Values should be between 0f and 1f and this array should be the
   *                   same length as colors.
   */
  public RotatableGradientDrawable(float degrees, int[] colors, @Nullable float[] positions) {
    this.degrees   = degrees + DEGREE_OFFSET;
    this.colors    = colors;
    this.positions = positions;
  }

  @Override
  public void setBounds(int left, int top, int right, int bottom) {
    super.setBounds(left, top, right, bottom);

    Point topLeft     = new Point(left, top);
    Point topRight    = new Point(right, top);
    Point bottomLeft  = new Point(left, bottom);
    Point bottomRight = new Point(right, bottom);
    Point origin      = new Point(getBounds().width() / 2, getBounds().height() / 2);

    Point rotationTopLeft     = cornerPrime(origin, topLeft, degrees);
    Point rotationTopRight    = cornerPrime(origin, topRight, degrees);
    Point rotationBottomLeft  = cornerPrime(origin, bottomLeft, degrees);
    Point rotationBottomRight = cornerPrime(origin, bottomRight, degrees);

    fillRect.left   = Integer.MAX_VALUE;
    fillRect.top    = Integer.MAX_VALUE;
    fillRect.right  = Integer.MIN_VALUE;
    fillRect.bottom = Integer.MIN_VALUE;

    for (Point point : Arrays.asList(topLeft, topRight, bottomLeft, bottomRight, rotationTopLeft, rotationTopRight, rotationBottomLeft, rotationBottomRight)) {
      if (point.x < fillRect.left) {
        fillRect.left = point.x;
      }

      if (point.x > fillRect.right) {
        fillRect.right = point.x;
      }

      if (point.y < fillRect.top) {
        fillRect.top = point.y;
      }

      if (point.y > fillRect.bottom) {
        fillRect.bottom = point.y;
      }
    }

    fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
  }

  public void setXfermode(@NonNull Xfermode xfermode) {
    fillPaint.setXfermode(xfermode);
  }

  private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
    return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
  }

  private static int xPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
    return (int) Math.ceil(((corner.x - origin.x) * Math.cos(theta)) - ((corner.y - origin.y) * Math.sin(theta)) + origin.x);
  }

  private static int yPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
    return (int) Math.ceil(((corner.x - origin.x) * Math.sin(theta)) + ((corner.y - origin.y) * Math.cos(theta)) + origin.y);
  }

  @Override
  public void draw(Canvas canvas) {
    int save = canvas.save();
    canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);

    int height = fillRect.height();
    int width = fillRect.width();
    canvas.drawRect(fillRect.left - width, fillRect.top - height, fillRect.right + width, fillRect.bottom + height, fillPaint);

    canvas.restoreToCount(save);
  }

  @Override
  public void setAlpha(int alpha) {
    // Not supported
  }

  @Override
  public void setColorFilter(@Nullable ColorFilter colorFilter) {
    // Not supported
  }

  @Override
  public int getOpacity() {
    return PixelFormat.OPAQUE;
  }
}
