internal import ExpoModulesJSI_Cxx
import Foundation
internal import jsi

/// Represents any JS value (undefined, null, boolean, number, bigint, symbol, string, or object).
/// As opposed to other concrete types (e.g. `JavaScriptObject`, `JavaScriptFunction`),
/// this one is a reference type so can be safely captured in closures, passed to other isolation context,
/// and stored in containers that don't support non-copyable types etc.
public final class JavaScriptValue: JavaScriptType, Equatable, Escapable {
  internal weak let runtime: JavaScriptRuntime?
  internal let pointee: facebook.jsi.Value

  /// Takes ownership of the given JSI value. Only runtime-free values (undefined, null, booleans and
  /// numbers) pass `nil` here.
  internal init(_ runtime: JavaScriptRuntime?, _ pointee: consuming facebook.jsi.Value) {
    self.runtime = runtime
    self.pointee = pointee
  }

  /// Takes ownership of the given JSI value. Same as the optional-runtime overload, but the exact
  /// parameter type keeps the caller from promoting its runtime to an optional, which costs a
  /// retain/release pair around every call on the hot paths (property reads, function results).
  internal init(_ runtime: JavaScriptRuntime, _ pointee: consuming facebook.jsi.Value) {
    self.runtime = runtime
    self.pointee = pointee
  }

  /// Copies the given JSI value, which asks the engine to clone the handle for pointer values.
  /// The `copying:` label keeps this initializer from being picked for temporaries: with a plain
  /// second argument, a non-optional runtime used to win overload resolution over the consuming
  /// initializer above, cloning every freshly returned value instead of taking it over.
  internal init(_ runtime: JavaScriptRuntime, copying pointee: borrowing facebook.jsi.Value) {
    self.runtime = runtime
    self.pointee = facebook.jsi.Value(runtime.pointee, pointee)
  }

  /// Creates a boolean JS value.
  public init(_ runtime: JavaScriptRuntime, _ bool: Bool) {
    self.runtime = runtime
    self.pointee = facebook.jsi.Value(bool)
  }

  /// Creates a string JS value.
  public init(_ runtime: JavaScriptRuntime, _ string: String) {
    self.runtime = runtime
    self.pointee = string.toJSIValue(in: runtime.pointee)
  }

  /// Creates a BigInt JS value from an Int64.
  public init(_ runtime: JavaScriptRuntime, bigInt: Int64) {
    self.runtime = runtime
    self.pointee = facebook.jsi.Value(runtime.pointee, facebook.jsi.BigInt.fromInt64(runtime.pointee, bigInt))
  }

  /// Creates a BigInt JS value from a UInt64.
  public init(_ runtime: JavaScriptRuntime, bigInt: UInt64) {
    self.runtime = runtime
    self.pointee = facebook.jsi.Value(runtime.pointee, facebook.jsi.BigInt.fromUint64(runtime.pointee, bigInt))
  }

  /// Creates a JS value from a JS representable.
  public init(_ runtime: JavaScriptRuntime, _ value: JavaScriptRepresentable) {
    self.runtime = runtime
    self.pointee = value.toJavaScriptValue(in: runtime).toJSIValue(in: runtime.pointee)
  }

  /// Creates a JS value from a JSI representable.
  internal init(_ runtime: JavaScriptRuntime, _ value: JSIRepresentable) {
    self.runtime = runtime
    self.pointee = value.toJSIValue(in: runtime.pointee)
  }

  /// Copies the value.
  public func copy() -> JavaScriptValue {
    if let runtime {
      return JavaScriptValue(runtime, facebook.jsi.Value(runtime.pointee, pointee))
    }
    // Some simple value kinds do not require the runtime.
    switch kind {
    case .undefined:
      return .undefined
    case .null:
      return .null
    case .bool:
      return .init(nil, facebook.jsi.Value(getBool()))
    case .number:
      return .init(nil, facebook.jsi.Value(getDouble()))
    default:
      FatalError.runtimeLost()
    }
  }

  /// Provides scoped access to a raw pointer to the underlying `facebook.jsi.Value`.
  /// The pointer is valid only for the duration of the closure and must not be stored or escaped.
  public func withUnsafePointee<R>(_ body: (UnsafeRawPointer) throws -> R) rethrows -> R {
    // Using `withUnsafePointer(to:)` crashes the SIL optimizer in release builds
    // due to a Swift compiler bug with C++ interop value types.
    return try withUnsafeBytes(of: pointee) { bytes in
      // Force-unwrap is safe — baseAddress is only nil for zero-length buffers,
      // which can't happen since facebook.jsi.Value has a non-zero size.
      return try body(bytes.baseAddress!)
    }
  }

  // MARK: - Type checks

  public func isUndefined() -> Bool {
    return pointee.isUndefined()
  }

  public func isNull() -> Bool {
    return pointee.isNull()
  }

  public func isBool() -> Bool {
    return pointee.isBool()
  }

  public func isNumber() -> Bool {
    return pointee.isNumber()
  }

  public func isString() -> Bool {
    return pointee.isString()
  }

  public func isSymbol() -> Bool {
    return pointee.isSymbol()
  }

  public func isBigInt() -> Bool {
    return pointee.isBigInt()
  }

  public func isObject() -> Bool {
    return pointee.isObject()
  }

  public func isArray() -> Bool {
    guard let jsiRuntime = runtime?.pointee else {
      FatalError.runtimeLost()
    }
    return pointee.isObject() && pointee.getObject(jsiRuntime).isArray(jsiRuntime)
  }

  public func isFunction() -> Bool {
    guard let jsiRuntime = runtime?.pointee else {
      FatalError.runtimeLost()
    }
    return pointee.isObject() && pointee.getObject(jsiRuntime).isFunction(jsiRuntime)
  }

  public func isTypedArray() -> Bool {
    guard let jsiRuntime = runtime?.pointee else {
      FatalError.runtimeLost()
    }
    return pointee.isObject() && expo.isTypedArray(jsiRuntime, pointee.getObject(jsiRuntime))
  }

  /// Checks whether the value is an `ArrayBuffer`.
  public func isArrayBuffer() -> Bool {
    guard let jsiRuntime = runtime?.pointee else {
      FatalError.runtimeLost()
    }
    return pointee.isObject() && pointee.getObject(jsiRuntime).isArrayBuffer(jsiRuntime)
  }

  /// Checks whether the value is an instance of a global class of the given name.
  /// For example `value.is("Promise")` checks whether the value is a promise.
  @JavaScriptActor
  public func `is`(_ typeName: String) -> Bool {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    // A primitive can't be an instance of anything, so bail out before touching globals — this keeps
    // the predicate side-effect free for non-objects (e.g. it won't trigger a replaced constructor's getter).
    guard isObject() else {
      return false
    }

    // Since the type names are limited to global constructors (Promise, Error, Array, etc.),
    // caching them to avoid creating new prop names on each call makes a lot of sense.
    let propName = JavaScriptPropNameID.cached(runtime, typeName)

    // A missing or non-callable global constructor means the value can't be its instance, so swallow
    // the lookup error and report `false` rather than propagating it out of this predicate.
    guard let constructor = try? runtime.global().getPropertyAsFunction(propName) else {
      return false
    }
    return getObject().instanceOf(constructor)
  }

  // MARK: - Asserting conversions ("get functions")

  @available(*, deprecated, message: "Use typed accessors (getBool, getInt, getString, getObject, getArray) instead")
  public func getAny() -> Any {
    if isUndefined() || isNull() {
      return NSNull()
    }
    if isBool() {
      return getBool()
    }
    if isNumber() {
      return getDouble()
    }
    if isString() {
      return getString()
    }
    if let object = isObject() ? getObject() : nil {
      if let array = object.isArray() ? object.getArray() : nil {
        let size = array.length
        var result = [Any]()

        result.reserveCapacity(size)

        for index in 0..<size {
          result.append((try? array.getValue(at: index).getAny()) as Any)
        }
        return result
      }
      if object.isFunction() {
        // Don't trap, callers convert speculatively under `try?`, which can't catch a `fatalError`.
        return NSNull()
      }
      var result = [String: Any]()

      for propertyName in object.getPropertyNames() {
        let property = object.getProperty(propertyName)
        // Undefined property values are dropped so the resulting dictionary only
        // carries keys that were actually assigned a value on the JS side.
        if !property.isUndefined() {
          result[propertyName] = property.getAny()
        }
      }
      return result
    }
    // Unrepresentable kind (e.g. symbol). Don't trap, for the same reason as above.
    return NSNull()
  }

  /// Returns the value as a boolean, or asserts if not a boolean.
  public func getBool() -> Bool {
    assert(isBool(), "Value is not a boolean")
    return pointee.getBool()
  }

  /// Returns the value as an integer, or asserts if not a number.
  public func getInt() -> Int {
    assert(isNumber(), "Value is not a number")
    return Int(pointee.getNumber())
  }

  /// Returns the value as a double, or asserts if not a number.
  public func getDouble() -> Double {
    assert(isNumber(), "Value is not a number")
    return pointee.getNumber()
  }

  /// Returns the value as a string, or asserts if not a string.
  public func getString() -> String {
    guard let jsiRuntime = runtime?.pointee else {
      FatalError.runtimeLost()
    }
    assert(isString(), "Value is not a string")
    return String(jsiString: pointee.getString(jsiRuntime), in: jsiRuntime)
  }

  /// Returns the value as a BigInt, or asserts if not a BigInt.
  public func getBigInt() -> JavaScriptBigInt {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(isBigInt(), "Value is not a BigInt")
    return JavaScriptBigInt(runtime, pointee.getBigInt(runtime.pointee))
  }

  /// Returns the value as an object, or asserts if not an object.
  public func getObject() -> JavaScriptObject {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(isObject(), "Value is not an object")
    return JavaScriptObject(runtime, pointee.getObject(runtime.pointee))
  }

  /// Returns the value as an array, or asserts if not an array.
  public func getArray() -> JavaScriptArray {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(isArray(), "Value is not an array")
    return JavaScriptArray(runtime, pointee.getObject(runtime.pointee).getArray(runtime.pointee))
  }

  /// Returns the value as a function, or asserts if not a function.
  public func getFunction() -> JavaScriptFunction {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(isFunction(), "Value is not a function")
    return JavaScriptFunction(runtime, pointee.getObject(runtime.pointee).getFunction(runtime.pointee))
  }

  /// Returns the value as a typed array, or asserts if it is not a typed array.
  public func getTypedArray() -> JavaScriptTypedArray {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(isTypedArray(), "Value is not a typed array")
    return JavaScriptTypedArray(runtime, pointee.getObject(runtime.pointee))
  }

  /// Returns the value as an array buffer, or asserts if it is not an array buffer.
  public func getArrayBuffer() -> JavaScriptArrayBuffer {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(isArrayBuffer(), "Value is not an array buffer")
    return JavaScriptArrayBuffer(runtime, pointee.getObject(runtime.pointee).getArrayBuffer(runtime.pointee))
  }

  /// Returns the value as a promise, or asserts if it is not a promise.
  @JavaScriptActor
  public func getPromise() throws -> JavaScriptPromise {
    guard let runtime else {
      FatalError.runtimeLost()
    }
    assert(self.is("Promise"), "Value is not a promise")
    return try JavaScriptPromise(runtime, getObject())
  }

  // MARK: - Throwing conversions ("as functions")

  @JavaScriptActor
  public func asBool() throws(TypeError) -> Bool {
    guard isBool() else {
      throw TypeError(type: Bool.self)
    }
    return getBool()
  }

  @JavaScriptActor
  public func asInt() throws(TypeError) -> Int {
    guard isNumber() else {
      throw TypeError(type: Int.self)
    }
    return getInt()
  }

  @JavaScriptActor
  public func asDouble() throws(TypeError) -> Double {
    guard isNumber() else {
      throw TypeError(type: Double.self)
    }
    return getDouble()
  }

  @JavaScriptActor
  public func asString() throws(TypeError) -> String {
    guard isString() else {
      throw TypeError(type: String.self)
    }
    return getString()
  }

  @JavaScriptActor
  public func asBigInt() throws(TypeError) -> JavaScriptBigInt {
    guard isBigInt() else {
      throw TypeError(type: JavaScriptBigInt.self)
    }
    return getBigInt()
  }

  @JavaScriptActor
  public func asObject() throws(TypeError) -> JavaScriptObject {
    guard isObject() else {
      throw TypeError(type: JavaScriptObject.self)
    }
    return getObject()
  }

  @JavaScriptActor
  public func asArray() throws(TypeError) -> JavaScriptArray {
    guard isArray() else {
      throw TypeError(type: JavaScriptArray.self)
    }
    return getArray()
  }

  @JavaScriptActor
  public func asFunction() throws(TypeError) -> JavaScriptFunction {
    guard isFunction() else {
      throw TypeError(type: JavaScriptFunction.self)
    }
    return getFunction()
  }

  @JavaScriptActor
  public func asTypedArray() throws(TypeError) -> JavaScriptTypedArray {
    guard isTypedArray() else {
      throw TypeError(type: JavaScriptTypedArray.self)
    }
    return getTypedArray()
  }

  @JavaScriptActor
  public func asPromise() throws -> JavaScriptPromise {
    guard self.is("Promise") else {
      throw TypeError(type: JavaScriptPromise.self)
    }
    return try getPromise()
  }

  /// Returns the value as a `JavaScriptArrayBuffer`.
  /// Throws `TypeError` if the value is not an object or not an `ArrayBuffer`.
  @JavaScriptActor
  public func asArrayBuffer() throws(TypeError) -> JavaScriptArrayBuffer {
    let object = try asObject()
    guard object.isArrayBuffer() else {
      throw TypeError(type: JavaScriptArrayBuffer.self)
    }
    return object.getArrayBuffer()
  }

  // MARK: - Serializing

  /// Returns a string representing the value. Same as calling `toString()` in JS.
  public func toString() -> String {
    guard let jsiRuntime = runtime?.pointee else {
      FatalError.runtimeLost()
    }
    return String(jsiString: pointee.toString(jsiRuntime), in: jsiRuntime)
  }

  /// Converts the JavaScript value to a JSON string representation.
  ///
  /// This method is equivalent to calling `JSON.stringify()` in JavaScript with this value
  /// as the first argument. It serializes the value into a JSON-formatted string, with
  /// optional control over the serialization process through the replacer and space parameters.
  ///
  /// - Parameters:
  ///   - replacer: An optional function or array that alters the behavior of the stringification process.
  ///     - If a function: Called for each property, receiving the key and value as arguments.
  ///     - If an array: Only properties whose names are in the array will be included in the result.
  ///     - If `nil`: All properties are included.
  ///   - space: An optional string or number that controls the indentation and spacing in the output.
  ///     - If a number: Indicates the number of spaces to use for indentation (clamped to 10).
  ///     - If a string: Used as the indentation string (truncated to 10 characters).
  ///     - If `nil`: No whitespace is added, resulting in compact output.
  ///
  /// - Returns: A JSON string representation of the value, or `nil` if the value cannot be
  ///   serialized (e.g., functions, symbols, or undefined values in object properties).
  ///
  /// - Throws: An error if the serialization fails (e.g., circular references, or if a
  ///   replacer function throws an error).
  ///
  /// ## Examples
  /// ```swift
  /// let runtime = JavaScriptRuntime()
  ///
  /// // Simple value
  /// let number = JavaScriptValue(runtime, 42)
  /// try number.jsonStringify() // "42"
  ///
  /// // Object with pretty printing
  /// let obj = runtime.eval("({ name: 'Alice', age: 30 })")
  /// try obj.jsonStringify(space: JavaScriptValue(runtime, 2))
  /// // Returns:
  /// // {
  /// //   "name": "Alice",
  /// //   "age": 30
  /// // }
  ///
  /// // Using a replacer to filter properties
  /// let replacer = runtime.eval("['name']") // Only include 'name' property
  /// try obj.jsonStringify(replacer: replacer) // {"name":"Alice"}
  ///
  /// // Undefined returns nil
  /// let undefined = JavaScriptValue.undefined
  /// try undefined.jsonStringify() // nil
  /// ```
  ///
  /// - Note: For simple values (undefined, null, boolean, number) that don't have an associated
  ///   runtime, this method can still produce a JSON representation without invoking JavaScript's
  ///   `JSON.stringify()`.
  ///
  /// - SeeAlso: [MDN: JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
  public func jsonStringify(replacer: JavaScriptValue? = nil, space: JavaScriptValue? = nil) throws -> String? {
    guard let runtime else {
      // Some types do not need the runtime. Handle them before crashing due to missing runtime.
      switch kind {
      case .undefined:
        return nil
      case .null:
        return "null"
      case .bool:
        return getBool() ? "true" : "false"
      case .number:
        return String(getDouble())
      default:
        FatalError.runtimeLost()
      }
    }
    let value =
      try runtime
      .global()
      .getPropertyAsObject("JSON")
      .getPropertyAsFunction("stringify")
      .call(arguments: self, replacer, space)
    return value.isString() ? value.getString() : nil
  }

  // MARK: - JavaScriptType

  public func asValue() -> JavaScriptValue {
    // We need to copy the value as `self` would be borrowed
    return copy()
  }

  /// Writes this value into a host callback's result slot. Undefined, null, booleans and numbers are
  /// emplaced with the engine's inline constructors, so the common results skip the out-of-line
  /// `jsi::Value` move and destroy that assigning to the slot would cost; everything else is copied
  /// through ``asJSIValue()``. The slot must hold a value with nothing to release (see `emplaceUndefined`).
  internal func writeJSIValue(to slot: UnsafeMutablePointer<facebook.jsi.Value>) {
    if pointee.isUndefined() {
      expo.emplaceUndefined(slot)
    } else if pointee.isNumber() {
      expo.emplaceNumber(slot, pointee.getNumber())
    } else if pointee.isBool() {
      expo.emplaceBool(slot, pointee.getBool())
    } else if pointee.isNull() {
      expo.emplaceNull(slot)
    } else {
      slot.pointee = asJSIValue()
    }
  }

  internal func asJSIValue() -> facebook.jsi.Value {
    if let runtime {
      return facebook.jsi.Value(runtime.pointee, pointee)
    }
    // Some simple value kinds do not require the runtime.
    switch kind {
    case .undefined:
      return facebook.jsi.Value.undefined()
    case .null:
      return facebook.jsi.Value.null()
    case .bool:
      return facebook.jsi.Value(getBool())
    case .number:
      return facebook.jsi.Value(getDouble())
    default:
      FatalError.runtimeLost()
    }
  }

  // MARK: - Kind

  public enum Kind: String {
    case undefined
    case null
    case bool
    case number
    case bigint
    case symbol
    case string
    case function
    case object
  }

  public var kind: Kind {
    // TODO: Make it a stored property, but computed on demand.
    // This feels like a better way to check value's type.
    switch true {
    case isUndefined():
      return .undefined
    case isNull():
      return .null
    case isBool():
      return .bool
    case isNumber():
      return .number
    case isBigInt():
      return .bigint
    case isSymbol():
      return .symbol
    case isString():
      return .string
    case isFunction():
      return .function
    default:
      return .object
    }
  }

  // MARK: - Equality

  /// Tests whether two values are strictly equal, according to https://262.ecma-international.org/11.0/#sec-strict-equality-comparison
  public func isEqual(to another: JavaScriptValue) -> Bool {
    if let jsiRuntime = runtime?.pointee ?? another.runtime?.pointee {
      return facebook.jsi.Value.strictEquals(jsiRuntime, pointee, another.pointee)
    }
    // Some types don't have to be tied to any runtime. Since `strictEquals` needs a runtime, we need to handle this case ourselves.
    let thisKind = kind
    if thisKind != another.kind {
      return false
    }
    switch thisKind {
    case .undefined, .null:
      return true
    case .number:
      return getDouble() == another.getDouble()
    case .bool:
      return getBool() == another.getBool()
    default:
      return false
    }
  }

  /// Same as `isEqual(to:)`.
  public static func == (lhs: JavaScriptValue, rhs: JavaScriptValue) -> Bool {
    return lhs.isEqual(to: rhs)
  }

  /// Negates the result of `isEqual(to:)`.
  public static func != (lhs: JavaScriptValue, rhs: JavaScriptValue) -> Bool {
    return !lhs.isEqual(to: rhs)
  }

  // MARK: - Runtime-free initializers

  // Shared immortal instances for the runtime-free `undefined`/`null` kinds. The class is fully
  // immutable and these carry no runtime and a trivial `jsi::Value`, so one instance can be safely
  // handed out from any isolation context. Every void-returning `@JS` function returns `.undefined`,
  // so a computed getter here would allocate on every host call.
  //
  // The instances live in the module-level globals below rather than in static properties: the
  // `undefined` and `null` accessors are `@inlinable`, and an inlined body reaches a global through its
  // addressor alone, whereas a class static, even a `@usableFromInline` one, goes through a getter
  // that takes the class metatype, which costs the caller a metadata accessor call (`objc_opt_self`)
  // on every access. Every void host function call accesses `undefined`.

  /// This is a lightweight way to create an undefined value that can be used in contexts
  /// where a runtime is not available or needed. The resulting value can be passed to
  /// JavaScript functions or used in comparisons.
  @inlinable
  public static var undefined: JavaScriptValue {
    return sharedUndefinedValue
  }

  /// This is a lightweight way to create a null value that can be used in contexts
  /// where a runtime is not available or needed. The resulting value represents
  /// JavaScript's `null`, which is distinct from `undefined`.
  @inlinable
  public static var null: JavaScriptValue {
    return sharedNullValue
  }

  /// This is a lightweight way to create a boolean true value that can be used in contexts
  /// where a runtime is not available or needed.
  public static func `true`() -> JavaScriptValue {
    return JavaScriptValue(nil, facebook.jsi.Value(true))
  }

  /// This is a lightweight way to create a boolean false value that can be used in contexts
  /// where a runtime is not available or needed.
  public static func `false`() -> JavaScriptValue {
    return JavaScriptValue(nil, facebook.jsi.Value(false))
  }

  /// This is a lightweight way to create a numeric value that can be used in contexts
  /// where a runtime is not available or needed. JavaScript numbers are represented
  /// as double-precision floating-point values following the IEEE 754 standard.
  public static func number(_ number: Double) -> JavaScriptValue {
    return JavaScriptValue(nil, facebook.jsi.Value(number))
  }

  public static func representing(value: JavaScriptRepresentable, in runtime: JavaScriptRuntime) -> JavaScriptValue {
    return value.toJavaScriptValue(in: runtime)
  }

  @available(*, deprecated, renamed: "representing(value:in:)")
  public static func from(_ value: Any, runtime: JavaScriptRuntime) -> JavaScriptValue {
    if let value = value as? JavaScriptRepresentable {
      return value.toJavaScriptValue(in: runtime)
    }
    return .undefined
  }
}

extension JavaScriptValue: JavaScriptRepresentable {
  public static func fromJavaScriptValue(_ value: JavaScriptValue) -> JavaScriptValue {
    return value.copy()
  }

  public func toJavaScriptValue(in runtime: JavaScriptRuntime) -> JavaScriptValue {
    // `self` is borrowed, so we need to do a copy here
    return self.copy()
  }
}

extension JavaScriptValue: JSIRepresentable {
  static func fromJSIValue(_ value: borrowing facebook.jsi.Value, in runtime: facebook.jsi.IRuntime) -> JavaScriptValue
  {
    FatalError.unimplemented()
  }

  func toJSIValue(in runtime: facebook.jsi.IRuntime) -> facebook.jsi.Value {
    return facebook.jsi.Value(runtime, pointee)
  }
}

// MARK: - Errors

extension JavaScriptValue {
  public struct TypeError: Error, CustomStringConvertible {
    let type: any ~Copyable.Type

    public var description: String {
      return "TypeError: Value cannot be represented as \(type)"
    }
  }
}

// Shared instances behind `JavaScriptValue.undefined` and `JavaScriptValue.null`; see the comment on
// those accessors for why these are globals.
@usableFromInline
internal let sharedUndefinedValue = JavaScriptValue(nil, facebook.jsi.Value.undefined())
@usableFromInline
internal let sharedNullValue = JavaScriptValue(nil, facebook.jsi.Value.null())
