/*
 * 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

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.facebook.react.internal.PrivateReactExtension
import com.facebook.react.tasks.GenerateAutolinkingNewArchitecturesFileTask
import com.facebook.react.tasks.GenerateCodegenArtifactsTask
import com.facebook.react.tasks.GenerateCodegenSchemaTask
import com.facebook.react.tasks.GeneratePackageListTask
import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForApp
import com.facebook.react.utils.AgpConfiguratorUtils.configureBuildConfigFieldsForLibraries
import com.facebook.react.utils.AgpConfiguratorUtils.configureDevPorts
import com.facebook.react.utils.AgpConfiguratorUtils.configureNamespaceForLibraries
import com.facebook.react.utils.BackwardCompatUtils.configureBackwardCompatibilityReactMap
import com.facebook.react.utils.DependencyUtils.configureDependencies
import com.facebook.react.utils.DependencyUtils.configureRepositories
import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings
import com.facebook.react.utils.JdkConfiguratorUtils.configureJavaToolChains
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk
import com.facebook.react.utils.ProjectUtils.isNewArchEnabled
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
import com.facebook.react.utils.ProjectUtils.shouldWarnIfNewArchFlagIsSetInPrealpha
import com.facebook.react.utils.findPackageJsonFile
import java.io.File
import kotlin.system.exitProcess
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
import org.gradle.internal.jvm.Jvm

class ReactPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    checkJvmVersion(project)
    val extension = project.extensions.create("react", ReactExtension::class.java, project)
    checkIfNewArchFlagIsSet(project, extension)

    // We register a private extension on the rootProject so that project wide configs
    // like codegen config can be propagated from app project to libraries.
    val rootExtension =
        project.rootProject.extensions.findByType(PrivateReactExtension::class.java)
            ?: project.rootProject.extensions.create(
                "privateReact", PrivateReactExtension::class.java, project)

    // App Only Configuration
    project.pluginManager.withPlugin("com.android.application") {
      // We wire the root extension with the values coming from the app (either user populated or
      // defaults).
      rootExtension.root.set(extension.root)
      rootExtension.reactNativeDir.set(extension.reactNativeDir)
      rootExtension.codegenDir.set(extension.codegenDir)
      rootExtension.nodeExecutableAndArgs.set(extension.nodeExecutableAndArgs)

      project.afterEvaluate {
        val reactNativeDir = extension.reactNativeDir.get().asFile
        val propertiesFile = File(reactNativeDir, "ReactAndroid/gradle.properties")
        val versionAndGroupStrings = readVersionAndGroupStrings(propertiesFile)
        val versionString = versionAndGroupStrings.first
        val groupString = versionAndGroupStrings.second
        configureDependencies(project, versionString, groupString)
        configureRepositories(project, reactNativeDir)
      }

      configureReactNativeNdk(project, extension)
      configureBuildConfigFieldsForApp(project, extension)
      configureDevPorts(project)
      configureBackwardCompatibilityReactMap(project)
      configureJavaToolChains(project)

      project.extensions.getByType(AndroidComponentsExtension::class.java).apply {
        onVariants(selector().all()) { variant ->
          project.configureReactTasks(variant = variant, config = extension)
        }
      }
      configureAutolinking(project, extension)
      configureCodegen(project, extension, rootExtension, isLibrary = false)
    }

    // Library Only Configuration
    configureBuildConfigFieldsForLibraries(project)
    configureNamespaceForLibraries(project)
    project.pluginManager.withPlugin("com.android.library") {
      configureCodegen(project, extension, rootExtension, isLibrary = true)
    }
  }

  private fun checkJvmVersion(project: Project) {
    val jvmVersion = Jvm.current().javaVersion?.majorVersion
    if ((jvmVersion?.toIntOrNull() ?: 0) <= 16) {
      project.logger.error(
          """

      ********************************************************************************

      ERROR: requires JDK17 or higher.
      Incompatible major version detected: '$jvmVersion'

      ********************************************************************************

      """
              .trimIndent())
      exitProcess(1)
    }
  }

  private fun checkIfNewArchFlagIsSet(project: Project, extension: ReactExtension) {
    if (project.shouldWarnIfNewArchFlagIsSetInPrealpha(extension)) {
      project.logger.warn(
          """

      ********************************************************************************

      WARNING: This version of React Native is ignoring the `newArchEnabled` flag you set. Please set it to true or remove it to suppress this warning.


      ********************************************************************************

      """
              .trimIndent())
    }
  }

  /** This function sets up `react-native-codegen` in our Gradle plugin. */
  @Suppress("UnstableApiUsage")
  private fun configureCodegen(
      project: Project,
      localExtension: ReactExtension,
      rootExtension: PrivateReactExtension,
      isLibrary: Boolean
  ) {
    // First, we set up the output dir for the codegen.
    val generatedSrcDir: Provider<Directory> =
        project.layout.buildDirectory.dir("generated/source/codegen")

    // We specify the default value (convention) for jsRootDir.
    // It's the root folder for apps (so ../../ from the Gradle project)
    // and the package folder for library (so ../ from the Gradle project)
    if (isLibrary) {
      localExtension.jsRootDir.convention(project.layout.projectDirectory.dir("../"))
    } else {
      localExtension.jsRootDir.convention(localExtension.root)
    }

    // We create the task to produce schema from JS files.
    val generateCodegenSchemaTask =
        project.tasks.register(
            "generateCodegenSchemaFromJavaScript", GenerateCodegenSchemaTask::class.java) { it ->
              it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
              it.codegenDir.set(rootExtension.codegenDir)
              it.generatedSrcDir.set(generatedSrcDir)

              // We're reading the package.json at configuration time to properly feed
              // the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the
              // parsePackageJson should be invoked inside this lambda.
              val packageJson = findPackageJsonFile(project, rootExtension.root)
              val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }

              val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
              val includesGeneratedCode =
                  parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
              if (jsSrcsDirInPackageJson != null) {
                it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
              } else {
                it.jsRootDir.set(localExtension.jsRootDir)
              }
              val needsCodegenFromPackageJson =
                  project.needsCodegenFromPackageJson(rootExtension.root)
              it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
            }

    // We create the task to generate Java code from schema.
    val generateCodegenArtifactsTask =
        project.tasks.register(
            "generateCodegenArtifactsFromSchema", GenerateCodegenArtifactsTask::class.java) {
              it.dependsOn(generateCodegenSchemaTask)
              it.reactNativeDir.set(rootExtension.reactNativeDir)
              it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
              it.generatedSrcDir.set(generatedSrcDir)
              it.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root))
              it.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
              it.libraryName.set(localExtension.libraryName)

              // Please note that appNeedsCodegen is triggering a read of the package.json at
              // configuration time as we need to feed the onlyIf condition of this task.
              // Therefore, the appNeedsCodegen needs to be invoked inside this lambda.
              val needsCodegenFromPackageJson =
                  project.needsCodegenFromPackageJson(rootExtension.root)
              val packageJson = findPackageJsonFile(project, rootExtension.root)
              val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
              val includesGeneratedCode =
                  parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
              it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
            }

    // We update the android configuration to include the generated sources.
    // This equivalent to this DSL:
    //
    // android { sourceSets { main { java { srcDirs += "$generatedSrcDir/java" } } } }
    project.extensions.getByType(AndroidComponentsExtension::class.java).finalizeDsl { ext ->
      ext.sourceSets.getByName("main").java.srcDir(generatedSrcDir.get().dir("java").asFile)
    }

    // `preBuild` is one of the base tasks automatically registered by AGP.
    // This will invoke the codegen before compiling the entire project.
    project.tasks.named("preBuild", Task::class.java).dependsOn(generateCodegenArtifactsTask)
  }

  /** This function sets up Autolinking for App users */
  private fun configureAutolinking(
      project: Project,
      extension: ReactExtension,
  ) {
    val generatedAutolinkingJavaDir: Provider<Directory> =
        project.layout.buildDirectory.dir("generated/autolinking/src/main/java")
    val generatedAutolinkingJniDir: Provider<Directory> =
        project.layout.buildDirectory.dir("generated/autolinking/src/main/jni")

    // The autolinking.json file is available in the root build folder as it's generated
    // by ReactSettingsPlugin.kt
    val rootGeneratedAutolinkingFile =
        project.rootProject.layout.buildDirectory.file("generated/autolinking/autolinking.json")

    // We add a task called generateAutolinkingPackageList to do not clash with the existing task
    // called generatePackageList. This can to be renamed once we unlink the rn <-> cli
    // dependency.
    val generatePackageListTask =
        project.tasks.register(
            "generateAutolinkingPackageList", GeneratePackageListTask::class.java) { task ->
              task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
              task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
            }

    if (project.isNewArchEnabled(extension)) {
      // For New Arch, we also need to generate code for C++ Autolinking
      val generateAutolinkingNewArchitectureFilesTask =
          project.tasks.register(
              "generateAutolinkingNewArchitectureFiles",
              GenerateAutolinkingNewArchitecturesFileTask::class.java) { task ->
                task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
                task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
              }
      project.tasks
          .named("preBuild", Task::class.java)
          .dependsOn(generateAutolinkingNewArchitectureFilesTask)
    }

    // We let generateAutolinkingPackageList depend on the preBuild task so it's executed before
    // everything else.
    project.tasks.named("preBuild", Task::class.java).dependsOn(generatePackageListTask)

    // We tell Android Gradle Plugin that inside /build/generated/autolinking/src/main/java there
    // are sources to be compiled as well.
    project.extensions.getByType(AndroidComponentsExtension::class.java).apply {
      onVariants(selector().all()) { variant ->
        variant.sources.java?.addStaticSourceDirectory(
            generatedAutolinkingJavaDir.get().asFile.absolutePath)
      }
    }
  }
}
