package com.airbnb.lottie;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.graphics.Rect;
import androidx.collection.LongSparseArray;
import androidx.collection.SparseArrayCompat;
import com.airbnb.lottie.model.Font;
import com.airbnb.lottie.model.FontCharacter;
import com.airbnb.lottie.model.Marker;
import com.airbnb.lottie.model.layer.Layer;
import com.airbnb.lottie.utils.LottieValueAnimator;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;

public class LottieValueAnimatorUnitTest extends BaseTest {
  private interface VerifyListener {
    void verify(InOrder inOrder);
  }

  private LottieComposition composition;
  private LottieValueAnimator animator;
  private Animator.AnimatorListener spyListener;
  private InOrder inOrder;
  private AtomicBoolean isDone;

  @Before
  public void setup() {
    animator = createAnimator();
    composition = createComposition(0, 1000);

    animator.setComposition(composition);
    spyListener = Mockito.mock(Animator.AnimatorListener.class);
    isDone = new AtomicBoolean(false);
  }

  private LottieValueAnimator createAnimator() {
    // Choreographer#postFrameCallback hangs with robolectric.
    return new LottieValueAnimator() {
      @Override public void postFrameCallback() {
        running = true;
      }

      @Override public void removeFrameCallback() {
        running = false;
      }
    };
  }

  private LottieComposition createComposition(int startFrame, int endFrame) {
    LottieComposition composition = new LottieComposition();
    composition.init(new Rect(), startFrame, endFrame, 1000, new ArrayList<Layer>(),
            new LongSparseArray<Layer>(0), new HashMap<String, List<Layer>>(0),
            new HashMap<String, LottieImageAsset>(0), new SparseArrayCompat<FontCharacter>(0),
            new HashMap<String, Font>(0), new ArrayList<Marker>());
    return composition;
  }

  @Test
  public void testInitialState() {
    assertClose(0f, animator.getFrame());
  }

  @Test
  public void testResumingMaintainsValue() {
    animator.setFrame(500);
    animator.resumeAnimation();
    assertClose(500f, animator.getFrame());
  }

  @Test
  public void testFrameConvertsToAnimatedFraction() {
    animator.setFrame(500);
    animator.resumeAnimation();
    assertClose(0.5f, animator.getAnimatedFraction());
    assertClose(0.5f, animator.getAnimatedValueAbsolute());
  }

  @Test
    public void testPlayingResetsValue() {
    animator.setFrame(500);
    animator.playAnimation();
    assertClose(0f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
  }

  @Test
  public void testReversingMaintainsValue() {
    animator.setFrame(250);
    animator.reverseAnimationSpeed();
    assertClose(250f, animator.getFrame());
    assertClose(0.75f, animator.getAnimatedFraction());
    assertClose(0.25f, animator.getAnimatedValueAbsolute());
  }

  @Test
    public void testReversingWithMinValueMaintainsValue() {
    animator.setMinFrame(100);
    animator.setFrame(1000);
    animator.reverseAnimationSpeed();
    assertClose(1000f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(1f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testReversingWithMaxValueMaintainsValue() {
    animator.setMaxFrame(900);
    animator.reverseAnimationSpeed();
    assertClose(0f, animator.getFrame());
    assertClose(1f, animator.getAnimatedFraction());
    assertClose(0f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testResumeReversingWithMinValueMaintainsValue() {
    animator.setMaxFrame(900);
    animator.reverseAnimationSpeed();
    animator.resumeAnimation();
    assertClose(900f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(0.9f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testPlayReversingWithMinValueMaintainsValue() {
    animator.setMaxFrame(900);
    animator.reverseAnimationSpeed();
    animator.playAnimation();
    assertClose(900f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(0.9f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testMinAndMaxBothSet() {
    animator.setMinFrame(200);
    animator.setMaxFrame(800);
    animator.setFrame(400);
    assertClose(0.33333f, animator.getAnimatedFraction());
    assertClose(0.4f, animator.getAnimatedValueAbsolute());
    animator.reverseAnimationSpeed();
    assertClose(400f, animator.getFrame());
    assertClose(0.66666f, animator.getAnimatedFraction());
    assertClose(0.4f, animator.getAnimatedValueAbsolute());
    animator.resumeAnimation();
    assertClose(400f, animator.getFrame());
    assertClose(0.66666f, animator.getAnimatedFraction());
    assertClose(0.4f, animator.getAnimatedValueAbsolute());
    animator.playAnimation();
    assertClose(800f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(0.8f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testSetFrameIntegrity() {
    animator.setMinAndMaxFrames(200, 800);

    // setFrame < minFrame should clamp to minFrame
    animator.setFrame(100);
    assertEquals(200f, animator.getFrame());

    animator.setFrame(900);
    assertEquals(800f, animator.getFrame());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testMinAndMaxFrameIntegrity() {
    animator.setMinAndMaxFrames(800, 200);
  }

  @Test
  public void testDefaultAnimator() {
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        inOrder.verify(spyListener, times(1)).onAnimationStart(animator, false);
        inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
        Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator);
      }
    });
  }

  @Test
  public void testReverseAnimator() {
    animator.reverseAnimationSpeed();
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        inOrder.verify(spyListener, times(1)).onAnimationStart(animator, true);
        inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, true);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
        Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorOnce() {
    animator.setRepeatCount(1);
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false);
        Mockito.verify(spyListener, times(1)).onAnimationRepeat(animator);
        Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorZeroTimes() {
    animator.setRepeatCount(0);
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator);
        Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorTwice() {
    animator.setRepeatCount(2);
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false);
        Mockito.verify(spyListener, times(2)).onAnimationRepeat(animator);
        Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorOnceReverse() {
    animator.setFrame(1000);
    animator.setRepeatCount(1);
    animator.reverseAnimationSpeed();
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        inOrder.verify(spyListener, times(1)).onAnimationStart(animator, true);
        inOrder.verify(spyListener, times(1)).onAnimationRepeat(animator);
        inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, true);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void setMinFrameSmallerThanComposition() {
    animator.setMinFrame(-9000);
    assertClose(animator.getMinFrame(), composition.getStartFrame());
  }

  @Test
  public void setMaxFrameLargerThanComposition() {
    animator.setMaxFrame(9000);
    assertClose(animator.getMaxFrame(), composition.getEndFrame());
  }

  @Test
  public void setMinFrameBeforeComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setMinFrame(100);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMinFrame());
  }

  @Test
  public void setMaxFrameBeforeComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setMaxFrame(100);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMaxFrame());
  }

  @Test
  public void setMinAndMaxFrameBeforeComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setMinAndMaxFrames(100, 900);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(900.0f, animator.getMaxFrame());
  }

  @Test
  public void setMinFrameAfterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    animator.setMinFrame(100);
    assertClose(100.0f, animator.getMinFrame());
  }

  @Test
  public void setMaxFrameAfterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    animator.setMaxFrame(100);
    assertEquals(100.0f, animator.getMaxFrame());
  }

  @Test
  public void setMinAndMaxFrameAfterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    animator.setMinAndMaxFrames(100, 900);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(900.0f, animator.getMaxFrame());
  }

  @Test
  public void maxFrameOfNewShorterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    LottieComposition composition2 = createComposition(0, 500);
    animator.setComposition(composition2);
    assertClose(500.0f, animator.getMaxFrame());
  }

  @Test
  public void maxFrameOfNewLongerComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    LottieComposition composition2 = createComposition(0, 1500);
    animator.setComposition(composition2);
    assertClose(1500.0f, animator.getMaxFrame());
  }

  @Test
  public void clearComposition() {
    animator.clearComposition();
    assertClose(0.0f, animator.getMaxFrame());
    assertClose(0.0f, animator.getMinFrame());
  }

  @Test
  public void resetComposition() {
    animator.clearComposition();
    animator.setComposition(composition);
    assertClose(0.0f, animator.getMinFrame());
    assertClose(1000.0f, animator.getMaxFrame());
  }

  @Test
  public void resetAndSetMinBeforeComposition() {
    animator.clearComposition();
    animator.setMinFrame(100);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(1000.0f, animator.getMaxFrame());
  }

  @Test
  public void resetAndSetMinAterComposition() {
    animator.clearComposition();
    animator.setComposition(composition);
    animator.setMinFrame(100);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(1000.0f, animator.getMaxFrame());
  }

  private void testAnimator(final VerifyListener verifyListener) {
    spyListener = Mockito.spy(new AnimatorListenerAdapter() {
      @Override public void onAnimationEnd(Animator animation) {
        verifyListener.verify(inOrder);
        isDone.set(true);
      }
    });
    inOrder = inOrder(spyListener);
    animator.addListener(spyListener);

    animator.playAnimation();
    while (!isDone.get()) {
      animator.doFrame(System.nanoTime());
    }
  }

  /**
   * Animations don't render on the out frame so if an animation is 1000 frames, the actual end will be 999.99. This causes
   * actual fractions to be something like .74999 when you might expect 75.
   */
  private static void assertClose(float expected, float actual) {
    assertEquals(expected, actual, expected * 0.01f);
  }
}