import groovy.json.JsonSlurper
import expo.modules.plugin.gradle.ExpoModuleExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

buildscript {
  // List of features that are required by linked modules
  def coreFeatures = project.findProperty("coreFeatures") ?: []
  ext.shouldIncludeCompose = coreFeatures.contains("compose")

  repositories {
    mavenCentral()
  }

  dependencies {
    if (shouldIncludeCompose) {
      classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${kotlinVersion}")
    }

  }
}

apply plugin: 'com.android.library'
apply plugin: 'expo-module-gradle-plugin'

if (shouldIncludeCompose) {
  apply plugin: 'org.jetbrains.kotlin.plugin.compose'
}

group = 'host.exp.exponent'
version = '56.0.14'

def isExpoModulesCoreTests = {
  Gradle gradle = getGradle()
  String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
  if (tskReqStr =~ /:expo-modules-core:connected\w*AndroidTest/) {
    def androidTests = project.file("src/androidTest")
    return androidTests.exists() && androidTests.isDirectory()
  }
  return false
}.call()

def isTests = {
  Gradle gradle = getGradle()
  String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
  return tskReqStr =~ /connected\w*AndroidTest/
}.call()

def expoModuleExtension = project.extensions.getByType(ExpoModuleExtension)

def reactNativeArchitectures() {
  def value = project.getProperties().get("reactNativeArchitectures")
  return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

// HERMES
def USE_HERMES = findProperty("hermesEnabled") ?: true

// Currently the needs for hermes/jsc are only for androidTest, so we turn on this flag only when `isExpoModulesCoreTests` is true
USE_HERMES = USE_HERMES && isExpoModulesCoreTests
// END HERMES

def shouldTurnWarningsIntoErrors = findProperty("EXPO_TURN_WARNINGS_INTO_ERRORS") == "true"

def enableWorkletsIntegration = !isTests
def workletsProject = enableWorkletsIntegration ? findProject(":react-native-worklets") : null

if (workletsProject != null) {
  evaluationDependsOn(workletsProject.path)

  afterEvaluate {
    println("Linking react-native-worklets native libs into expo-modules-core build tasks")
    println(workletsProject.tasks.getByName("mergeDebugNativeLibs"))
    println(workletsProject.tasks.getByName("mergeReleaseNativeLibs"))
    tasks.getByName("buildCMakeDebug").dependsOn(workletsProject.tasks.getByName("mergeDebugNativeLibs"))
    tasks.getByName("buildCMakeRelWithDebInfo").dependsOn(workletsProject.tasks.getByName("mergeReleaseNativeLibs"))
  }
}

def reactNativeWorkletsRootDir = workletsProject?.projectDir?.parentFile

expoModule {
  canBePublished false
}

android {
  if (rootProject.hasProperty("ndkPath")) {
    ndkPath rootProject.ext.ndkPath
  }
  if (rootProject.hasProperty("ndkVersion")) {
    ndkVersion rootProject.ext.ndkVersion
  }

  namespace "expo.modules"
  defaultConfig {
    consumerProguardFiles 'proguard-rules.pro'
    versionCode 1
    versionName "56.0.14"
    buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
    buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"

    testInstrumentationRunner "expo.modules.TestRunner"

    externalNativeBuild {
      cmake {
        def cppArguments = [
          "-DANDROID_STL=c++_shared",
          "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
          "-DREACT_NATIVE_DIR=${expoModuleExtension.reactNativeDir}",
          "-DREACT_NATIVE_TARGET_VERSION=${expoModuleExtension.reactNativeVersion.minor}",
          "-DUSE_HERMES=${USE_HERMES}",
          "-DUNIT_TEST=${isExpoModulesCoreTests}"
        ]

        if (reactNativeWorkletsRootDir != null) {
          cppArguments += "-DREACT_NATIVE_WORKLETS_DIR=${reactNativeWorkletsRootDir.absolutePath}"
        }

        abiFilters(*reactNativeArchitectures())
        arguments(*cppArguments)
      }
    }
  }

  externalNativeBuild {
    cmake {
      path "CMakeLists.txt"
    }
  }

  buildFeatures {
    buildConfig true
    prefab true
    compose shouldIncludeCompose
  }

  packagingOptions {
    // Gradle will add cmake target dependencies into packaging.
    // Theses files are intermediated linking files to build modules-core and should not be in final package.
    def sharedLibraries = [
      "**/libc++_shared.so",
      "**/libfabricjni.so",
      "**/libfbjni.so",
      "**/libfolly_json.so",
      "**/libfolly_runtime.so",
      "**/libglog.so",
      "**/libhermesvm.so",
      "**/libjscexecutor.so",
      "**/libjsi.so",
      "**/libreactnative.so",
      "**/libreactnativejni.so",
      "**/libreact_debug.so",
      "**/libreact_nativemodule_core.so",
      "**/libreact_utils.so",
      "**/libreact_render_debug.so",
      "**/libreact_render_graphics.so",
      "**/libreact_render_core.so",
      "**/libreact_render_componentregistry.so",
      "**/libreact_render_mapbuffer.so",
      "**/librrc_view.so",
      "**/libruntimeexecutor.so",
      "**/libyoga.so",
    ]

    // Required or mockk will crash
    resources {
      excludes += [
        "META-INF/LICENSE.md",
        "META-INF/LICENSE-notice.md"
      ]
    }

    // In android (instrumental) tests, we want to package all so files to enable our JSI functionality.
    // Otherwise, those files should be excluded, because will be loaded by the application.
    if (isExpoModulesCoreTests) {
      pickFirsts += sharedLibraries
    } else {
      excludes += sharedLibraries
    }
  }

  sourceSets {
    main {
      java {
        if (shouldIncludeCompose) {
          srcDirs += 'src/compose'
        } else {
          srcDirs += 'src/withoutCompose'
        }
      }
    }
  }

  testOptions {
    unitTests.includeAndroidResources = true

    unitTests.all { test ->
      testLogging {
        outputs.upToDateWhen { false }
        events "passed", "failed", "skipped", "standardError"
        showCauses true
        showExceptions true
        showStandardStreams true
      }
    }
  }
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}"
  implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}"
  implementation 'androidx.annotation:annotation:1.9.1'

  api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
  api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
  api "androidx.core:core-ktx:1.17.0"

  if (shouldIncludeCompose) {
    implementation 'androidx.compose.foundation:foundation-android:1.7.6'
  }

  implementation("androidx.tracing:tracing-ktx:1.2.0")

  implementation 'com.facebook.react:react-android'

  compileOnly 'com.facebook.fbjni:fbjni:0.5.1'

  if (workletsProject != null) {
    implementation workletsProject
  }

  implementation("io.github.lukmccall.pika:pika-api:0.3.2")

  testImplementation 'androidx.test:core:1.7.0'
  testImplementation 'junit:junit:4.13.2'
  testImplementation 'io.mockk:mockk:1.14.9'
  testImplementation "com.google.truth:truth:1.4.5"
  testImplementation "org.robolectric:robolectric:4.16.1"
  testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
  testImplementation "org.json:json:20250517"

  androidTestImplementation 'androidx.test:runner:1.7.0'
  androidTestImplementation 'androidx.test:core:1.7.0'
  androidTestImplementation 'androidx.test:rules:1.7.0'
  androidTestImplementation "io.mockk:mockk-android:1.14.9"
  androidTestImplementation "com.google.truth:truth:1.4.5"
  androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"

  if (isExpoModulesCoreTests) {
    if (USE_HERMES) {
      compileOnly "com.facebook.react:hermes-android"
    } else {
      compileOnly "org.webkit:android-jsc:+"
    }
  }
}

if (shouldTurnWarningsIntoErrors) {
  tasks.withType(JavaCompile) configureEach {
    options.compilerArgs << "-Werror" << "-Xlint:all" << '-Xlint:-serial' << '-Xlint:-rawtypes'
  }
  tasks.withType(KotlinCompile) configureEach {
    compilerOptions.allWarningsAsErrors = true
  }
}

// Generates minimal stub PCH files from an empty header so the IDE's C++ engine
// doesn't fail during sync. Near-instant unlike the full ExpoHeader.pch.
// The actual build regenerates proper PCH files via ninja.
def cxxDir = project.file(".cxx")
def generateStubPCHTask = tasks.register("generateStubPCH") {
  dependsOn("configureCMakeDebug")

  doLast {
    if (!cxxDir.exists()) {
      return
    }

    cxxDir.eachFileRecurse { file ->
      if (file.name != "compile_commands.json") {
        return
      }

      new JsonSlurper().parseText(file.text).each { entry ->
        if (!entry.file.endsWith("cmake_pch.hxx.cxx")) {
          return
        }

        def pchFile = new File(entry.file.substring(0, entry.file.length() - ".cxx".length()) + ".pch")

        if (!pchFile.exists() || pchFile.length() == 0) {
          def stubHeader = new File(entry.directory, "stub_pch.hxx")
          stubHeader.text = ""

          def stubHeaderPath = stubHeader.absolutePath.replace('\\', '/')

          def cmd = entry.command
            // Replace the forced-include path: `-Xclang -include -Xclang <path>/cmake_pch.hxx`
            .replaceAll(/-Xclang -include -Xclang [^\s]+cmake_pch\.hxx(?=\s)/, java.util.regex.Matcher.quoteReplacement("-Xclang -include -Xclang ${stubHeaderPath}"))
            // Replace the source file operand: `<path>/cmake_pch.hxx.cxx`
            .replaceAll(/[^\s]+cmake_pch\.hxx\.cxx/, java.util.regex.Matcher.quoteReplacement(stubHeaderPath))

          def process = new ProcessBuilder(cmd.split(" ").toList())
            .directory(new File(entry.directory))
            .redirectErrorStream(true)
            .start()
          if (process.waitFor() != 0) {
            throw new GradleException("Stub PCH generation failed: ${process.inputStream.text}")
          }
        }

        // Ensure PCH is older than source so ninja rebuilds the real one during build
        pchFile.setLastModified(new File(entry.file).lastModified() - 1)
      }
    }
  }
}

tasks.register("prepareKotlinBuildScriptModel") {
  dependsOn(generateStubPCHTask)
}
