import Foundation

/// Name of the JavaScript thread created by React Native. Copied from `RCTJSThreadManager.mm`.
private let jsThreadName = "com.facebook.react.runtime.JavaScript"

/// Global actor that is used to isolate the code that should only be executed from the JavaScript thread.
/// Theoretically it does not act as a real actor; it uses a serial executor that executes jobs **synchronously**
/// without hopping to the proper thread. Meaning that running these jobs on the JavaScript thread must be ensured
/// externally before switching to the isolated context, e.g. using `schedule` function on `JavaScriptRuntime`
/// or enforcing the isolation with `JavaScriptActor.assumeIsolated`.
@globalActor
public actor JavaScriptActor: GlobalActor {
  public static let shared = JavaScriptActor()

  private init() {}

  nonisolated private let executor = JavaScriptExecutor()

  nonisolated public var unownedExecutor: UnownedSerialExecutor {
    return executor.asUnownedSerialExecutor()
  }

  /// An equivalent of `MainActor.assumeIsolated`, but for the `JavaScriptActor`. Assumes that the currently executing
  /// synchronous function is actually executing on the JavaScript thread and invokes an isolated version of the operation,
  /// allowing synchronous access to JavaScript runtime state without hopping through asynchronous boundaries.
  ///
  /// See https://stackoverflow.com/a/79346971 for more information about the implementation.
  public static func assumeIsolated<T: ~Copyable>(_ operation: @JavaScriptActor () throws -> T) rethrows -> T {
    typealias YesActor = @JavaScriptActor () throws -> T
    typealias NoActor = () throws -> T

    // This will crash if the current context cannot be isolated.
    shared.executor.checkIsolated()

    // To do the unsafe cast, we have to pretend it's @escaping.
    return try withoutActuallyEscaping(operation) { (_ fn: @escaping YesActor) throws -> T in
      let rawFn = unsafeBitCast(fn, to: NoActor.self)
      return try rawFn()
    }
  }

  /// Stops program execution if the actor's executor is not isolating the current context.
  public static func checkIsolated() {
    shared.executor.checkIsolated()
  }
}

/// Executor for the `JavaScriptActor` that executes given jobs synchronously and immediately.
/// - Note: It does not ensure that given jobs are executed on the JavaScript thread; it must be done externally.
internal class JavaScriptExecutor: SerialExecutor, @unchecked Sendable {
  func enqueue(_ job: UnownedJob) {
    job.runSynchronously(on: self.asUnownedSerialExecutor())
  }

  /// Converts the executor to the optimized form of borrowed executor reference.
  func asUnownedSerialExecutor() -> UnownedSerialExecutor {
    return UnownedSerialExecutor(ordinary: self)
  }

  /// Stops program execution if the executor is not isolating the current context.
  func checkIsolated() {
    // Using `assert` instead of `precondition` because this check is a heuristic based on
    // thread name, not a precise isolation guarantee. Worklet runtimes legitimately run on
    // the UI thread, which would cause a false-positive crash with `precondition`.
    assert(isIsolatingCurrentContext() == true, "JavaScriptActor operations must be run on the JavaScript thread")
  }

  /// Checks whether the executor isolates the current context, i.e. the current thread is a JavaScript thread.
  /// The condition is also met when running tests and in single threaded environments.
  func isIsolatingCurrentContext() -> Bool? {
    // We must be careful as it relies on the thread name given by React Native.
    return Thread.current.name == jsThreadName || !Thread.isMultiThreaded()
      || ProcessInfo.processInfo.processName == "xctest"
  }
}

/// An actor that is dedicated for the specific runtime.
internal actor JavaScriptRuntimeActor {
  private weak let runtime: JavaScriptRuntime?
  nonisolated private let executor: JavaScriptExecutor

  init(runtime: JavaScriptRuntime) {
    self.runtime = runtime
    self.executor = JavaScriptRuntimeExecutor(runtime: runtime)
  }

  nonisolated var unownedExecutor: UnownedSerialExecutor {
    return executor.asUnownedSerialExecutor()
  }

  func execute<R: Sendable>(_ operation: @escaping @JavaScriptActor () async throws -> R) async rethrows -> sending R {
    return try await operation()
  }
}

/// Serial executor dedicated for the specific runtime.
internal final class JavaScriptRuntimeExecutor: JavaScriptExecutor, @unchecked Sendable {
  private weak let runtime: JavaScriptRuntime?

  init(runtime: JavaScriptRuntime) {
    self.runtime = runtime
  }

  override func enqueue(_ job: UnownedJob) {
    runtime?.schedule(priority: .immediate) {
      job.runSynchronously(on: self.asUnownedSerialExecutor())
    }
  }
}
