package expo.modules.plugin

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.withColor
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.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import java.io.File
import java.nio.file.Paths

const val generatedPackageListNamespace = "expo.modules"
const val generatedPackageListFilename = "ExpoModulesPackageList.java"
const val generatedFilesSrcDir = "generated/expo/src/main/java"

open class ExpoAutolinkingPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    val gradleExtension = project.gradle.extensions.findByType(ExpoGradleExtension::class.java)
      ?: throw IllegalStateException("`ExpoGradleExtension` not found. Please, make sure that `useExpoModules` was called in `settings.gradle`.")
    val config = gradleExtension.config

    project.logger.quiet("")
    project.logger.quiet("Using expo modules")

    val appProject = findAppProject(project.rootProject)
    appProject?.let { copyAppDimensionsAndFlavorsToProject(project, it) }

    val (prebuiltProjects, projects) = config.allProjects.partition { project ->
      project.usePublication
    }

    project.withSubprojects(projects) { subproject ->
      // Ensures that dependencies are resolved before the project is evaluated.
      project.evaluationDependsOn(subproject.path)
      // Adds the subproject as a dependency to the current project (expo package).
      project.dependencies.add("api", subproject)

      project.logger.quiet("  - ${subproject.name.withColor(Colors.GREEN)} (${subproject.version})")
    }

    prebuiltProjects.forEach { prebuiltProject ->
      val publication = requireNotNull(prebuiltProject.publication)
      project.dependencies.add("api", "${publication.groupId}:${publication.artifactId}:${publication.version}")

      project.logger.quiet("  - ${"[\uD83D\uDCE6]".withColor(Colors.YELLOW)} ${prebuiltProject.name.withColor(Colors.GREEN)} (${publication.version})")
    }

    project.logger.quiet("")

    // Creates a task that generates a list of expo modules.
    val generatePackagesList = createGeneratePackagesListTask(project, gradleExtension.options, gradleExtension.hash, gradleExtension.projectRoot)

    // Ensures that the task is executed before the build.
    project.tasks
      .named("preBuild", Task::class.java)
      .dependsOn(generatePackagesList)

    // Adds the generated file to the source set.
    project.extensions.getByType(AndroidComponentsExtension::class.java).finalizeDsl { ext ->
      ext
        .sourceSets
        .getByName("main")
        .java
        .srcDir(getPackageListDir(project))
    }
  }

  fun getPackageListDir(project: Project): Provider<Directory> {
    return project.layout.buildDirectory.dir(generatedFilesSrcDir)
  }

  fun getPackageListFile(project: Project): Provider<RegularFile> {
    val packageListRelativePath = Paths.get(
      generatedFilesSrcDir,
      generatedPackageListNamespace.replace('.', '/'),
      generatedPackageListFilename
    ).toString()
    return project.layout.buildDirectory.file(packageListRelativePath)
  }

  fun createGeneratePackagesListTask(project: Project, options: AutolinkingOptions, hash: String, projectRoot: File): TaskProvider<GeneratePackagesListTask> {
    return project.tasks.register("generatePackagesList", GeneratePackagesListTask::class.java) {
      it.hash.set(hash)
      it.namespace.set(generatedPackageListNamespace)
      it.outputFile.set(getPackageListFile(project))
      it.workingDir = projectRoot
      // Serializes the autolinking options to JSON to pass them to the task.
      // The types supported as a task input are limited to primitives, strings, and files.
      it.options.set(options.toJson())
    }
  }

  private fun findAppProject(root: Project): Project? {
    return root.allprojects.firstOrNull { it.plugins.hasPlugin("com.android.application") }
  }

  private fun copyAppDimensionsAndFlavorsToProject(
    project: Project,
    appProject: Project
  ) {
    val appAndroid = appProject.extensions.findByName("android") as? BaseExtension ?: run {
      return
    }
    val consumerAndroid = project.extensions.findByName("android") as? BaseExtension ?: run {
      return
    }

    val appDimensions = syncFlavorDimensions(project, consumerAndroid, appAndroid)
    copyMissingProductFlavors(project, consumerAndroid, appAndroid, appDimensions)
  }

  private fun syncFlavorDimensions(
    project: Project,
    consumerAndroid: BaseExtension,
    appAndroid: BaseExtension
  ): List<String> {
    val appDimensions = appAndroid
      .flavorDimensionList
      .takeIf { it.isNotEmpty() }
      ?: return emptyList()

    val consumerDimensions = (consumerAndroid.flavorDimensionList).toMutableList()
    val dimensionsAdded = appDimensions.any { dimension ->
      if (dimension !in consumerDimensions) {
        consumerDimensions.add(dimension)
        true
      } else {
        false
      }
    }

    if (dimensionsAdded) {
      consumerAndroid.flavorDimensions(*consumerDimensions.toTypedArray())
      project.logger.quiet("  -> Copied/merged flavorDimensions: ${consumerDimensions.joinToString()}")
    }

    return appDimensions
  }

  private fun copyMissingProductFlavors(
    project: Project,
    consumerAndroid: BaseExtension,
    appAndroid: BaseExtension,
    appDimensions: List<String>
  ) {
    val appFlavors = appAndroid.productFlavors
    val consumerFlavors = consumerAndroid.productFlavors
    val existingFlavorNames = consumerFlavors.map { it.name }.toSet()

    appFlavors.forEach { appFlavor ->
      if (appFlavor.name !in existingFlavorNames) {
        val dimension = appFlavor.dimension ?: appDimensions.singleOrNull()

        consumerFlavors.create(appFlavor.name).apply {
          this.dimension = dimension
        }

        project.logger.quiet("  -> Created flavor '${appFlavor.name}' (dimension='$dimension') in :${project.path}")
      }
    }
  }
}
