package org.thoughtcrime.securesms.imageeditor.model;

import android.animation.ValueAnimator;
import android.graphics.Matrix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.animation.CycleInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

import org.thoughtcrime.securesms.imageeditor.CanvasMatrix;

/**
 * Animation Matrix provides a matrix that animates over time down to the identity matrix.
 */
final class AnimationMatrix {

  private final static float[]      iValues           = new float[9];
  private final static Interpolator interpolator      = new DecelerateInterpolator();
  private final static Interpolator pulseInterpolator = inverse(new CycleInterpolator(0.5f));

  static AnimationMatrix NULL = new AnimationMatrix();

  static {
    new Matrix().getValues(iValues);
  }

  private final Runnable invalidate;
  private final boolean  canAnimate;
  private final float[]  undoValues = new float[9];

  private final Matrix   temp       = new Matrix();
  private final float[]  tempValues = new float[9];

  private ValueAnimator animator;
  private float         animatedFraction;

  private AnimationMatrix(@NonNull Matrix undo, @NonNull Runnable invalidate) {
    this.invalidate = invalidate;
    this.canAnimate = true;
    undo.getValues(undoValues);
  }

  private AnimationMatrix() {
    canAnimate = false;
    invalidate = null;
  }

  static @NonNull AnimationMatrix animate(@NonNull Matrix from, @NonNull Matrix to, @Nullable Runnable invalidate) {
    if (invalidate == null) {
      return NULL;
    }

    Matrix undo = new Matrix();
    boolean inverted = to.invert(undo);
    if (inverted) {
      undo.preConcat(from);
    }
    if (inverted && !undo.isIdentity()) {
      AnimationMatrix animationMatrix = new AnimationMatrix(undo, invalidate);
      animationMatrix.start(interpolator);
      return animationMatrix;
    } else {
      return NULL;
    }
  }

  /**
   * Animate applying a matrix and then animate removing.
   */
  static @NonNull AnimationMatrix singlePulse(@NonNull Matrix pulse, @Nullable Runnable invalidate) {
    if (invalidate == null) {
      return NULL;
    }

    AnimationMatrix animationMatrix = new AnimationMatrix(pulse, invalidate);
    animationMatrix.start(pulseInterpolator);

    return animationMatrix;
  }

  private void start(@NonNull Interpolator interpolator) {
    if (canAnimate) {
      animator = ValueAnimator.ofFloat(1, 0);
      animator.setDuration(250);
      animator.setInterpolator(interpolator);
      animator.addUpdateListener(animation -> {
        animatedFraction = (float) animation.getAnimatedValue();
        invalidate.run();
      });
      animator.start();
    }
  }

  void stop() {
    ValueAnimator animator = this.animator;
    if (animator != null) animator.cancel();
  }

  /**
   * Append the current animation value.
   */
  void preConcatValueTo(@NonNull Matrix onTo) {
    if (!canAnimate) return;

    onTo.preConcat(buildTemp());
  }

  /**
   * Append the current animation value.
   */
  void preConcatValueTo(@NonNull CanvasMatrix canvasMatrix) {
    if (!canAnimate) return;

    canvasMatrix.concat(buildTemp());
  }

  private Matrix buildTemp() {
    if (!canAnimate) {
      temp.reset();
      return temp;
    }

    final float fractionCompliment = 1f - animatedFraction;
    for (int i = 0; i < 9; i++) {
      tempValues[i] = fractionCompliment * iValues[i] + animatedFraction * undoValues[i];
    }

    temp.setValues(tempValues);
    return temp;
  }

  private static Interpolator inverse(@NonNull Interpolator interpolator) {
    return input -> 1f - interpolator.getInterpolation(input);
  }
}