/**
 * 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.modules.debug;

import android.view.Choreographer;

import com.facebook.react.bridge.ReactBridge;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
import com.facebook.react.common.LongArray;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;

/**
 * Debug object that listens to bridge busy/idle events and UiManagerModule dispatches and uses it
 * to calculate whether JS was able to update the UI during a given frame. After being installed
 * on a {@link ReactBridge} and a {@link UIManagerModule},
 * {@link #getDidJSHitFrameAndCleanup} should be called once per frame via a
 * {@link Choreographer.FrameCallback}.
 */
public class DidJSUpdateUiDuringFrameDetector implements NotThreadSafeBridgeIdleDebugListener,
    NotThreadSafeViewHierarchyUpdateDebugListener {

  private final LongArray mTransitionToIdleEvents = LongArray.createWithInitialCapacity(20);
  private final LongArray mTransitionToBusyEvents = LongArray.createWithInitialCapacity(20);
  private final LongArray mViewHierarchyUpdateEnqueuedEvents =
      LongArray.createWithInitialCapacity(20);
  private final LongArray mViewHierarchyUpdateFinishedEvents =
      LongArray.createWithInitialCapacity(20);
  private volatile boolean mWasIdleAtEndOfLastFrame = true;

  @Override
  public synchronized void onTransitionToBridgeIdle() {
    mTransitionToIdleEvents.add(System.nanoTime());
  }

  @Override
  public synchronized void onTransitionToBridgeBusy() {
    mTransitionToBusyEvents.add(System.nanoTime());
  }

  @Override
  public synchronized void onViewHierarchyUpdateEnqueued() {
    mViewHierarchyUpdateEnqueuedEvents.add(System.nanoTime());
  }

  @Override
  public synchronized void onViewHierarchyUpdateFinished() {
    mViewHierarchyUpdateFinishedEvents.add(System.nanoTime());
  }

  /**
   * Designed to be called from a {@link Choreographer.FrameCallback#doFrame} call.
   *
   * There are two 'success' cases that will cause {@link #getDidJSHitFrameAndCleanup} to
   * return true for a given frame:
   *
   * 1) UIManagerModule finished dispatching a batched UI update on the UI thread during the frame.
   *    This means that during the next hierarchy traversal, new UI will be drawn if needed (good).
   * 2) The bridge ended the frame idle (meaning there were no JS nor native module calls still in
   *    flight) AND there was no UiManagerModule update enqueued that didn't also finish. NB: if
   *    there was one enqueued that actually finished, we'd have case 1), so effectively we just
   *    look for whether one was enqueued.
   *
   * NB: This call can only be called once for a given frame time range because it cleans up
   * events it recorded for that frame.
   *
   * NB2: This makes the assumption that onViewHierarchyUpdateEnqueued is called from the
   * {@link UIManagerModule#onBatchComplete()}, e.g. while the bridge is still considered busy,
   * which means there is no race condition where the bridge has gone idle but a hierarchy update is
   * waiting to be enqueued.
   *
   * @param frameStartTimeNanos the time in nanos that the last frame started
   * @param frameEndTimeNanos the time in nanos that the last frame ended
   */
  public synchronized boolean getDidJSHitFrameAndCleanup(
      long frameStartTimeNanos,
      long frameEndTimeNanos) {
    // Case 1: We dispatched a UI update
    boolean finishedUiUpdate = hasEventBetweenTimestamps(
        mViewHierarchyUpdateFinishedEvents,
        frameStartTimeNanos,
        frameEndTimeNanos);
    boolean didEndFrameIdle = didEndFrameIdle(frameStartTimeNanos, frameEndTimeNanos);

    boolean hitFrame;
    if (finishedUiUpdate) {
      hitFrame = true;
    } else {
      // Case 2: Ended idle but no UI was enqueued during that frame
      hitFrame = didEndFrameIdle && !hasEventBetweenTimestamps(
          mViewHierarchyUpdateEnqueuedEvents,
          frameStartTimeNanos,
          frameEndTimeNanos);
    }

    cleanUp(mTransitionToIdleEvents, frameEndTimeNanos);
    cleanUp(mTransitionToBusyEvents, frameEndTimeNanos);
    cleanUp(mViewHierarchyUpdateEnqueuedEvents, frameEndTimeNanos);
    cleanUp(mViewHierarchyUpdateFinishedEvents, frameEndTimeNanos);

    mWasIdleAtEndOfLastFrame = didEndFrameIdle;

    return hitFrame;
  }

  private static boolean hasEventBetweenTimestamps(
      LongArray eventArray,
      long startTime,
      long endTime) {
    for (int i = 0; i < eventArray.size(); i++) {
      long time = eventArray.get(i);
      if (time >= startTime && time < endTime) {
        return true;
      }
    }
    return false;
  }

  private static long getLastEventBetweenTimestamps(
      LongArray eventArray,
      long startTime,
      long endTime) {
    long lastEvent = -1;
    for (int i = 0; i < eventArray.size(); i++) {
      long time = eventArray.get(i);
      if (time >= startTime && time < endTime) {
        lastEvent = time;
      } else if (time >= endTime) {
        break;
      }
    }
    return lastEvent;
  }

  private boolean didEndFrameIdle(long startTime, long endTime) {
    long lastIdleTransition = getLastEventBetweenTimestamps(
        mTransitionToIdleEvents,
        startTime,
        endTime);
    long lastBusyTransition = getLastEventBetweenTimestamps(
        mTransitionToBusyEvents,
        startTime,
        endTime);

    if (lastIdleTransition == -1 && lastBusyTransition == -1) {
      return mWasIdleAtEndOfLastFrame;
    }

    return lastIdleTransition > lastBusyTransition;
  }

  private static void cleanUp(LongArray eventArray, long endTime) {
    int size = eventArray.size();
    int indicesToRemove = 0;
    for (int i = 0; i < size; i++) {
      if (eventArray.get(i) < endTime) {
        indicesToRemove++;
      }
    }

    if (indicesToRemove > 0) {
      for (int i = 0; i < size - indicesToRemove; i++) {
        eventArray.set(i, eventArray.get(i + indicesToRemove));
      }
      eventArray.dropTail(indicesToRemove);
    }
  }
}
