/**
 * Copyright (c) 2016-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.keyframes;

import android.annotation.TargetApi;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.Choreographer;

import com.facebook.keyframes.model.KFImage;

import java.lang.ref.WeakReference;

/**
 * A simple callback that when run, will call back indefinitely with progress updates until
 * cancelled.  This will continuously feed back progress data from [0, 1] calculated by millis
 * per loop.
 */
public abstract class KeyframesDrawableAnimationCallback {

  /**
   * And interface for a class which wants to listen to progress updates.
   */
  public interface FrameListener {
    void onProgressUpdate(float frameProgress);
    void onStop();
  }

  private final WeakReference<FrameListener> mListener;
  private final int mFrameCount;
  private final int mMillisPerLoop;

  private long mStartTimeMillis;
  private boolean mStopAtLoopEnd;
  private int mCurrentLoopNumber;

  private long mMinimumMillisBetweenProgressUpdates = -1;
  private long mPreviousProgressMillis = 0;

  /**
   * Creates a KeyframesDrawableAnimationCallback appropriate for the API level of the device.
   * @param listener The listener that will receive callbacks on updates to the value
   * @return A KeyframesDrawableAnimationCallback implementation
   */
  public static KeyframesDrawableAnimationCallback create(
      FrameListener listener,
      KFImage face) {
    if (hasChoreographer()) {
      return new FrameCallbackFaceAnimationCallback(
          listener,
          face.getFrameRate(),
          face.getFrameCount());
    } else {
      return new RunnableFaceAnimationCallback(
          listener,
          face.getFrameRate(),
          face.getFrameCount());
    }
  }

  private static boolean hasChoreographer() {
    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
  }

  private KeyframesDrawableAnimationCallback(FrameListener listener, int frameRate, int frameCount) {
    mListener = new WeakReference<>(listener);
    mFrameCount = frameCount;
    mMillisPerLoop = Math.round(1000 * ((float) frameCount / frameRate));
  }

  /**
   * Set the maximum frame rate for this animation.
   * Consider using this for low end devices.
   * @param maxFrameRate
   */
  public void setMaxFrameRate(int maxFrameRate) {
    mMinimumMillisBetweenProgressUpdates = 1000 / maxFrameRate;
  }

  protected abstract void postCallback();

  protected abstract void cancelCallback();

  /**
   * Starts this animation callback and resets the start time.
   *
   * !IMPORTANT! This animator will run indefinitely, so it must be cancelled via #stop()
   * or #pause() when no longer in use!
   */
  public void start() {
    mStopAtLoopEnd = false;
    mStartTimeMillis = 0;
    mCurrentLoopNumber = -1;
    cancelCallback();
    postCallback();
  }

  /**
   * Starts the animation and plays it once
   */
  public void playOnce() {
    mStopAtLoopEnd = true;
    mStartTimeMillis = 0;
    mCurrentLoopNumber = 0;
    cancelCallback();
    postCallback();
  }

  /**
   * Stops the callbacks animation and resets the start time.
   */
  public void stop() {
    cancelCallback();
    mStartTimeMillis = 0;
    mCurrentLoopNumber = -1;
    mListener.get().onStop();
  }

  /**
   * Pauses the callbacks animation and saves start time.
   */
  public void pause() {
    cancelCallback();
    mStartTimeMillis *= -1;
  }

  /**
   * Resumes this animation callback.
   *
   * !IMPORTANT! This animator will run indefinitely, so it must be cancelled via #stop()
   * or #pause() when no longer in use!
   */
  public void resume() {
    mStopAtLoopEnd = false;
    cancelCallback();
    postCallback();
  }


  /**
   * Stops looping the animation, but finishes the current animation.
   */
  public void stopAtLoopEnd() {
    mStopAtLoopEnd = true;
  }

  protected void advanceAnimation(final long frameTimeMillis) {
    if (mListener.get() == null) {
      cancelCallback();
      mStartTimeMillis = 0;
      mPreviousProgressMillis = 0;
      mCurrentLoopNumber = -1;
      return;
    }
    if (mStartTimeMillis == 0) {
      mStartTimeMillis = frameTimeMillis;
    } else if (mStartTimeMillis < 0) {
      long pausedTimeMillis = frameTimeMillis - mPreviousProgressMillis;
      mStartTimeMillis = mStartTimeMillis * -1 + pausedTimeMillis;
      mPreviousProgressMillis += pausedTimeMillis;
    }

    int currentLoopNumber = (int) (frameTimeMillis - mStartTimeMillis) / mMillisPerLoop;
    final boolean loopHasEnded = currentLoopNumber > mCurrentLoopNumber;
    if (mStopAtLoopEnd && loopHasEnded) {
      mListener.get().onProgressUpdate(mFrameCount);
      stop();
      return;
    }
    long currentProgressMillis = (frameTimeMillis - mStartTimeMillis) % mMillisPerLoop;

    boolean shouldUpdateProgress = true;
    if (frameTimeMillis - mPreviousProgressMillis < mMinimumMillisBetweenProgressUpdates) {
      shouldUpdateProgress = false;
    } else {
      mPreviousProgressMillis = frameTimeMillis;
    }

    if (shouldUpdateProgress) {
      mListener.get().onProgressUpdate((float) currentProgressMillis / mMillisPerLoop * mFrameCount);
    }
    mCurrentLoopNumber = (int) (frameTimeMillis - mStartTimeMillis) / mMillisPerLoop;
    postCallback();
  }

  @TargetApi(16)
  private static class FrameCallbackFaceAnimationCallback extends KeyframesDrawableAnimationCallback
      implements Choreographer.FrameCallback {

    private final Choreographer mChoreographer;

    private FrameCallbackFaceAnimationCallback(
        FrameListener listener,
        int frameRate,
        int frameCount) {
      super(listener, frameRate, frameCount);
      mChoreographer = Choreographer.getInstance();
    }

    @Override
    public void doFrame(long frameTimeNanos) {
      advanceAnimation(frameTimeNanos / 1000000); // nanoseconds per millisecond
    }

    @Override
    protected void postCallback() {
      mChoreographer.postFrameCallback(this);
    }

    @Override
    protected void cancelCallback() {
      mChoreographer.removeFrameCallback(this);
    }
  }

  private static class RunnableFaceAnimationCallback extends KeyframesDrawableAnimationCallback
      implements Runnable {
    private static final int ANIMATION_MIN_STEP_TIME_MS = 25; // 40 fps

    private final Handler mHandler;

    private RunnableFaceAnimationCallback(
        FrameListener listener,
        int frameRate,
        int frameCount) {
      super(listener, frameRate, frameCount);
      mHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void run() {
      advanceAnimation(SystemClock.uptimeMillis());
    }

    @Override
    protected void postCallback() {
      mHandler.postDelayed(this, ANIMATION_MIN_STEP_TIME_MS);
    }

    @Override
    protected void cancelCallback() {
      mHandler.removeCallbacks(this);
    }
  }
}
