/**
 * Copyright (c) 2015-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.react.uimanager;

import java.util.ArrayDeque;

import android.view.Choreographer;

import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.ReactConstants;

/**
 * A simple wrapper around Choreographer that allows us to control the order certain callbacks
 * are executed within a given frame. The main difference is that we enforce this is accessed from
 * the UI thread: this is because this ordering cannot be guaranteed across multiple threads.
 */
public class ReactChoreographer {

  public static enum CallbackType {

    /**
     * For use by perf markers that need to happen immediately after draw
     */
    PERF_MARKERS(0),

    /**
     * For use by {@link com.facebook.react.uimanager.UIManagerModule}
     */
    DISPATCH_UI(1),

    /**
     * For use by {@link com.facebook.react.animated.NativeAnimatedModule}
     */
    NATIVE_ANIMATED_MODULE(2),

    /**
     * Events that make JS do things.
     */
    TIMERS_EVENTS(3),

    /**
     * Event used to trigger the idle callback. Called after all UI work has been
     * dispatched to JS.
     */
    IDLE_EVENT(4),
    ;

    private final int mOrder;

    private CallbackType(int order) {
      mOrder = order;
    }

    /*package*/ int getOrder() {
      return mOrder;
    }
  }

  private static ReactChoreographer sInstance;

  public static ReactChoreographer getInstance() {
    UiThreadUtil.assertOnUiThread();
    if (sInstance == null) {
      sInstance = new ReactChoreographer();
    }
    return sInstance;
  }

  private final Choreographer mChoreographer;
  private final ReactChoreographerDispatcher mReactChoreographerDispatcher;
  private final ArrayDeque<Choreographer.FrameCallback>[] mCallbackQueues;

  private int mTotalCallbacks = 0;
  private boolean mHasPostedCallback = false;

  private ReactChoreographer() {
    mChoreographer = Choreographer.getInstance();
    mReactChoreographerDispatcher = new ReactChoreographerDispatcher();
    mCallbackQueues = new ArrayDeque[CallbackType.values().length];
    for (int i = 0; i < mCallbackQueues.length; i++) {
      mCallbackQueues[i] = new ArrayDeque<>();
    }
  }

  public void postFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) {
    UiThreadUtil.assertOnUiThread();
    mCallbackQueues[type.getOrder()].addLast(frameCallback);
    mTotalCallbacks++;
    Assertions.assertCondition(mTotalCallbacks > 0);
    if (!mHasPostedCallback) {
      mChoreographer.postFrameCallback(mReactChoreographerDispatcher);
      mHasPostedCallback = true;
    }
  }

  public void removeFrameCallback(CallbackType type, Choreographer.FrameCallback frameCallback) {
    UiThreadUtil.assertOnUiThread();
    if (mCallbackQueues[type.getOrder()].removeFirstOccurrence(frameCallback)) {
      mTotalCallbacks--;
      maybeRemoveFrameCallback();
    } else {
      FLog.e(ReactConstants.TAG, "Tried to remove non-existent frame callback");
    }
  }

  private void maybeRemoveFrameCallback() {
    Assertions.assertCondition(mTotalCallbacks >= 0);
    if (mTotalCallbacks == 0 && mHasPostedCallback) {
      mChoreographer.removeFrameCallback(mReactChoreographerDispatcher);
      mHasPostedCallback = false;
    }
  }

  private class ReactChoreographerDispatcher implements Choreographer.FrameCallback {

    @Override
    public void doFrame(long frameTimeNanos) {
      mHasPostedCallback = false;
      for (int i = 0; i < mCallbackQueues.length; i++) {
        int initialLength = mCallbackQueues[i].size();
        for (int callback = 0; callback < initialLength; callback++) {
          mCallbackQueues[i].removeFirst().doFrame(frameTimeNanos);
          mTotalCallbacks--;
        }
      }
      maybeRemoveFrameCallback();
    }
  }
}
