/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.fabric

import com.facebook.proguard.annotations.DoNotStripAny
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.modules.core.ReactChoreographer
import com.facebook.react.uimanager.GuardedFrameCallback
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.synchronized

internal fun interface AnimationFrameCallback {
  fun onAnimationFrame(frameTimeMs: Double)
}

@DoNotStripAny
internal class AnimationBackendChoreographer(
    reactApplicationContext: ReactApplicationContext,
) {

  var frameCallback: AnimationFrameCallback? = null
  private var lastFrameTimeMs: Double = 0.0
  private val reactChoreographer: ReactChoreographer = ReactChoreographer.getInstance()
  private val choreographerCallback: GuardedFrameCallback =
      object : GuardedFrameCallback(reactApplicationContext) {
        override fun doFrameGuarded(frameTimeNanos: Long) {
          executeFrameCallback(frameTimeNanos)
        }
      }
  private val callbackPosted: AtomicBoolean = AtomicBoolean()
  private val paused: AtomicBoolean = AtomicBoolean(true)

  /*
   * resume() and pause() should be called with the same lock to avoid race conditions.
   */

  fun resume() {
    if (paused.getAndSet(false)) {
      scheduleCallback()
    }
  }

  fun pause() {
    synchronized(paused) {
      if (!paused.getAndSet(true) && callbackPosted.getAndSet(false)) {
        reactChoreographer.removeFrameCallback(
            ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
            choreographerCallback,
        )
      }
    }
  }

  private fun scheduleCallback() {
    synchronized(paused) {
      if (!paused.get() && !callbackPosted.getAndSet(true)) {
        reactChoreographer.postFrameCallback(
            ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
            choreographerCallback,
        )
      }
    }
  }

  private fun executeFrameCallback(frameTimeNanos: Long) {
    callbackPosted.set(false)
    val currentFrameTimeMs = calculateTimestamp(frameTimeNanos)
    // It is possible for ChoreographerCallback to be executed twice within the same frame
    // due to frame drops. If this occurs, the additional callback execution should be ignored.
    if (currentFrameTimeMs > lastFrameTimeMs) {
      frameCallback?.onAnimationFrame(currentFrameTimeMs)
    }

    lastFrameTimeMs = currentFrameTimeMs
    scheduleCallback()
  }

  private fun calculateTimestamp(frameTimeNanos: Long): Double {
    val nanosecondsInMilliseconds = 1000000.0
    return frameTimeNanos / nanosecondsInMilliseconds
  }
}
