// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import Foundation

let packageDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path
let podsRoot = resolvePodsRoot()

// Header roots for ExpoModulesJSI and ExpoModulesJSI-Cxx. The
// Pods/Headers/Public paths cover no-frameworks and prebuilt-RN; the trailing
// entries fall back to canonical sources for the static + source-built RN
// combo, where each React-X / third-party-deps pod compiles as a static
// framework and its headers don't get mirrored to Pods/Headers/Public. Clang
// ignores missing `-I` paths, so they're no-ops elsewhere. `RN_ROOT` is
// forwarded from build-xcframework.sh (Node-resolved for hoisted monorepos).
// `REACT_NATIVE_PATH` is exported by Xcode for hosts that build RN from a
// non-npm location, e.g. Expo Go.
let publicHeaders = "\(podsRoot)/Headers/Public"
let reactNative = ProcessInfo.processInfo.environment["RN_ROOT"]
  ?? ProcessInfo.processInfo.environment["REACT_NATIVE_PATH"]
  ?? "\(podsRoot)/../../node_modules/react-native"
let headerSearchPaths = [
  publicHeaders,
  "\(publicHeaders)/React-jsi",
  "\(publicHeaders)/hermes-engine",
  "\(publicHeaders)/React-runtimescheduler",
  "\(publicHeaders)/React-rendererconsistency",
  "\(publicHeaders)/React-performancetimeline",
  "\(publicHeaders)/React-timing",
  "\(publicHeaders)/React-debug",
  "\(publicHeaders)/React-callinvoker",
  "\(publicHeaders)/React-runtimeexecutor",
  "\(publicHeaders)/RCT-Folly",
  "\(publicHeaders)/ReactNativeDependencies",
  "\(publicHeaders)/glog",
  "\(publicHeaders)/DoubleConversion",
  "\(publicHeaders)/fmt",
  "\(publicHeaders)/fast_float",
  "\(reactNative)/ReactCommon",
  "\(reactNative)/ReactCommon/jsi",
  "\(reactNative)/ReactCommon/runtimeexecutor",
  "\(reactNative)/ReactCommon/callinvoker",
  "\(podsRoot)/RCT-Folly",
  "\(podsRoot)/fmt/include",
  "\(podsRoot)/glog/src",
  "\(podsRoot)/DoubleConversion",
]

// Path to the generated module map for the `jsi` Clang module. The
// `scripts/generate-modulemap.sh` script writes this file at build time so
// the absolute header path can be resolved against the runtime PODS_ROOT.
// Lives outside `.build/` so SwiftPM state can be wiped without losing this
// file.
let generatedModuleMap = "\(packageDir)/.generated/module.modulemap"
let apiNotesPath = "\(packageDir)/APINotes"

let cxxIncludeFlags = headerSearchPaths.map({ "-I\($0)" })
let swiftIncludeFlags = headerSearchPaths.flatMap({ ["-Xcc", "-I\($0)"] })

let testFrameworks = resolveTestFrameworks()

let package = Package(
  name: "ExpoModulesJSI",
  platforms: [
    .iOS("16.4"),
    .tvOS("16.4"),
    .macOS("13.4"),
  ],
  products: [
    .library(
      name: "ExpoModulesJSI",
      type: .dynamic,
      targets: ["ExpoModulesJSI"],
    ),
  ],
  dependencies: [],
  targets: [
    // Swift target (public)
    .target(
      name: "ExpoModulesJSI",
      dependencies: [
        "ExpoModulesJSI-Cxx",
      ],
      swiftSettings: [
        .interoperabilityMode(.Cxx),

        // Enable some upcoming features that improve ergonomics and reduce executor hoppings
        // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md
        .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
        // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0470-isolated-conformances.md
        .enableUpcomingFeature("InferIsolatedConformances"),

        .unsafeFlags([
          "-enable-library-evolution",
          "-emit-module-interface",
          "-no-verify-emitted-module-interface",
          "-Xfrontend",
          "-clang-header-expose-decls=has-expose-attr",

          // Module map for the `jsi` Clang module, generated by build-xcframework.sh
          // before xcodebuild runs.
          "-Xcc", "-fmodule-map-file=\(generatedModuleMap)",

          // API Notes
          "-Xcc", "-iapinotes-modules",
          "-Xcc", apiNotesPath,
        ]),

        .unsafeFlags(swiftIncludeFlags),
      ],
      linkerSettings: [
        // React, ReactCommon, hermes, and JSI symbols are provided by the host
        // app at final link time. Defer their resolution so the xcframework
        // builds without those static libs being available here.
        .unsafeFlags([
          "-Xlinker", "-undefined", "-Xlinker", "dynamic_lookup",
        ]),
      ],
    ),

    // C++ target (internal)
    .target(
      name: "ExpoModulesJSI-Cxx",
      dependencies: [],
      cxxSettings: [
        .unsafeFlags(cxxIncludeFlags),
      ],
    ),

    // Tests
    .testTarget(
      name: "Tests",
      dependencies: testFrameworks.dependencies,
    ),
  ] + testFrameworks.binaryTargets,
  swiftLanguageModes: [.v6],
  cxxLanguageStandard: .cxx20
)

// Resolve PODS_ROOT from the environment. CocoaPods build phases always set
// it; otherwise fall back to bare-expo's Pods so headers resolve for
// indexing and direct script invocations. The manifest sandbox blocks
// `fileExists` outside the package — fail loudly later if Pods aren't there.
func resolvePodsRoot() -> String {
  let env = ProcessInfo.processInfo.environment
  if let explicit = env["PODS_ROOT"] {
    return explicit
  }
  let repoRoot = env["EXPO_ROOT_DIR"] ?? URL(fileURLWithPath: packageDir)
    .deletingLastPathComponent() // expo-modules-jsi
    .deletingLastPathComponent() // packages
    .deletingLastPathComponent() // repo root
    .path
  return "\(repoRoot)/apps/bare-expo/ios/Pods"
}

// Prebuilt xcframeworks the test bundle links against so JSI, Hermes, and
// React symbols resolve at load time. The production xcframework build leaves
// these unresolved (`-undefined dynamic_lookup`) because the host app provides
// them — but a unit-test bundle has no such host.
//
// SwiftPM requires `binaryTarget` paths to be relative to the package root,
// so the test wrapper script (`scripts/test.sh`) symlinks each xcframework
// from $PODS_ROOT into `.test-frameworks/` before invoking xcodebuild.
func resolveTestFrameworks() -> (binaryTargets: [Target], dependencies: [Target.Dependency]) {
  let names = ["React", "hermesvm", "ReactNativeDependencies"]
  let available = names.filter({
    FileManager.default.fileExists(atPath: "\(packageDir)/.test-frameworks/\($0).xcframework")
  })
  let binaryTargets: [Target] = available.map({
    .binaryTarget(name: $0, path: ".test-frameworks/\($0).xcframework")
  })
  let dependencies: [Target.Dependency] = ["ExpoModulesJSI"]
    + available.map({ .target(name: $0) })
  return (binaryTargets, dependencies)
}
