// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package com.flutter.gradle

import com.android.build.api.dsl.ApplicationExtension
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.api.ApkVariant
import com.android.build.gradle.tasks.PackageAndroidArtifact
import com.android.build.gradle.tasks.ProcessAndroidResources
import com.flutter.gradle.FlutterPluginUtils.readPropertiesIfExist
import com.flutter.gradle.plugins.PluginHandler
import com.flutter.gradle.tasks.FlutterTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.file.Directory
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskInstantiationException
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
import org.gradle.internal.os.OperatingSystem
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.process.ExecOperations
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Paths
import java.util.Properties

class FlutterPlugin : Plugin<Project> {
    private var project: Project? = null
    private var flutterRoot: File? = null
    private var flutterExecutable: File? = null
    private var localEngine: String? = null
    private var localEngineHost: String? = null
    private var localEngineSrcPath: String? = null
    private var localProperties: Properties? = null
    private var engineVersion: String? = null
    private var engineRealm: String? = null
    private var pluginHandler: PluginHandler? = null

    override fun apply(project: Project) {
        this.project = project

        val rootProject = project.rootProject
        if (FlutterPluginUtils.isFlutterAppProject(project)) {
            addTaskForLockfileGeneration(rootProject)
        }

        val flutterRootSystemVal: String? = System.getenv("FLUTTER_ROOT")
        val flutterRootPath: String =
            resolveFlutterSdkProperty(flutterRootSystemVal)
                ?: throw GradleException(
                    "Flutter SDK not found. Define location with flutter.sdk in the " +
                        "local.properties file or with a FLUTTER_ROOT environment variable."
                )

        flutterRoot = project.file(flutterRootPath)
        if (!flutterRoot!!.isDirectory) {
            throw GradleException("flutter.sdk must point to the Flutter SDK directory")
        }

        engineVersion =
            if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
                "+" // Match any version since there's only one.
            } else {
                val engineStampPath =
                    Paths.get(flutterRoot!!.absolutePath, "bin", "cache", "engine.stamp")
                val engineStampContent = engineStampPath.toFile().readText().trim()
                "1.0.0-$engineStampContent"
            }

        engineRealm =
            Paths
                .get(flutterRoot!!.absolutePath, "bin", "cache", "engine.realm")
                .toFile()
                .readText()
                .trim()
        if (engineRealm!!.isNotEmpty()) {
            engineRealm += "/"
        }

        // Configure the Maven repository.
        val hostedRepository: String =
            System.getenv(FlutterPluginConstants.FLUTTER_STORAGE_BASE_URL)
                ?: FlutterPluginConstants.DEFAULT_MAVEN_HOST
        val repository: String? =
            if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
                project.property(PROP_LOCAL_ENGINE_REPO) as String?
            } else {
                "$hostedRepository/${engineRealm}download.flutter.io"
            }
        rootProject.allprojects {
            repositories.maven {
                url = uri(repository!!)
            }
            if (plugins.hasPlugin("com.android.application") && isInvokedFromAndroidStudio()) {
                dependencies.add("compileOnly", "io.flutter:flutter_embedding_debug:$engineVersion")
                dependencies.add("compileOnly", "io.flutter:armeabi_v7a_debug:$engineVersion")
                dependencies.add("compileOnly", "io.flutter:arm64_v8a_debug:$engineVersion")
                dependencies.add("compileOnly", "io.flutter:x86_64_debug:$engineVersion")
            }
        }

        project.apply {
            from(
                Paths.get(
                    flutterRoot!!.absolutePath,
                    "packages",
                    "flutter_tools",
                    "gradle",
                    "src",
                    "main",
                    "scripts",
                    "native_plugin_loader.gradle.kts"
                )
            )
        }

        val flutterExtension: FlutterExtension =
            project.extensions.create("flutter", FlutterExtension::class.java)

        // TODO(gmackall): is this actually a different properties file than the previous one?
        val rootProjectLocalProperties = Properties()
        val rootProjectLocalPropertiesFile = rootProject.file("local.properties")
        if (rootProjectLocalPropertiesFile.exists()) {
            rootProjectLocalPropertiesFile.reader(StandardCharsets.UTF_8).use { reader ->
                rootProjectLocalProperties.load(reader)
            }
        }
        flutterExtension.flutterVersionCode =
            rootProjectLocalProperties.getProperty("flutter.versionCode", "1")
        flutterExtension.flutterVersionName =
            rootProjectLocalProperties.getProperty("flutter.versionName", "1.0")

        this.addFlutterTasks(project)

        // By default, assembling APKs generates fat APKs if multiple platforms are passed.
        // Configuring split per ABI allows to generate separate APKs for each abi.
        // This is a noop when building a bundle.
        if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
            FlutterPluginUtils.getAndroidExtension(project).splits.abi {
                isEnable = true
                reset()
                isUniversalApk = false
            }
        } else {
            // When splits-per-abi is NOT enabled, configure abiFilters to control which
            // native libraries are included in the APK.
            //
            // This is crucial: If a project includes third-party dependencies with x86 native libraries,
            // without these abiFilters, Google Play would incorrectly identify the app as supporting x86.
            // When users with x86 devices install the app, it would crash at runtime because Flutter's
            // native libraries aren't available for x86. By filtering out x86 at build time, Google Play
            // correctly excludes x86 devices from the compatible device list.
            //
            // NOTE: This code does NOT affect "add-to-app" scenarios because:
            // 1. For 'flutter build aar': abiFilters have no effect since libflutter.so and libapp.so
            //    are not packaged into AAR artifacts - they are only added as dependencies
            //    in pom files.
            // 2. For project dependencies (implementation(project(":flutter"))): The Flutter
            //    Gradle Plugin is not applied to the main app subproject, so this apply()
            //    method is never called.
            //
            // abiFilters cannot be added to templates because it would break builds when
            // --splits-per-abi is used due to conflicting configuration. This approach
            // adds them programmatically only when splits are not configured.
            //
            // If the user has specified abiFilters in their build.gradle file, those
            // settings will take precedence over these defaults.
            FlutterPluginUtils.getAndroidExtension(project).buildTypes.forEach { buildType ->
                buildType.ndk.abiFilters.clear()
                FlutterPluginConstants.DEFAULT_PLATFORMS.forEach { platform ->
                    val abiValue: String =
                        FlutterPluginConstants.PLATFORM_ARCH_MAP[platform]
                            ?: throw GradleException("Invalid platform: $platform")
                    buildType.ndk.abiFilters.add(abiValue)
                }
            }
        }
        val propDeferredComponentNames = "deferred-component-names"
        val deferredComponentNamesValue: String? =
            project.findProperty(propDeferredComponentNames) as? String
        if (deferredComponentNamesValue != null) {
            val componentNames: Set<String> =
                deferredComponentNamesValue
                    .split(',')
                    .map { ":$it" }
                    .toSet()
            // TODO(gmackall): Unify the types we use for the android extension. This is yet
            //   another type we need unfortunately.
            val androidExtensionAsApplicationExtension =
                project.extensions.getByType(ApplicationExtension::class.java)
            // TODO(gmackall): Should we clear here? I think this is equivalent to what we used to
            //    do, but unsure. Can't use a closure.
            androidExtensionAsApplicationExtension.dynamicFeatures.clear()
            androidExtensionAsApplicationExtension.dynamicFeatures.addAll(componentNames)
        }

        FlutterPluginUtils.getTargetPlatforms(project).forEach { targetArch ->
            val abiValue: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch]
            val androidExtension: BaseExtension = FlutterPluginUtils.getAndroidExtension(project)
            androidExtension.splits.abi.include(abiValue!!)
        }

        val flutterExecutableName = getExecutableNameForPlatform("flutter")
        flutterExecutable =
            Paths.get(flutterRoot!!.absolutePath, "bin", flutterExecutableName).toFile()

        // Validate that the provided Gradle, Java, AGP, and KGP versions are all within our
        // supported range.
        val shouldSkipDependencyChecks: Boolean =
            project.hasProperty("skipDependencyChecks") &&
                (
                    project.properties["skipDependencyChecks"] as? Boolean
                        ?: false
                )
        if (!shouldSkipDependencyChecks) {
            try {
                DependencyVersionChecker.checkDependencyVersions(project)
            } catch (e: Exception) {
                if (!project.hasProperty("usesUnsupportedDependencyVersions") ||
                    !(project.properties["usesUnsupportedDependencyVersions"] as Boolean)
                ) {
                    // Possible bug in dependency checking code - warn and do not block build.
                    project.logger.error(
                        "Warning: Flutter was unable to detect project Gradle, Java, " +
                            "AGP, and KGP versions. Skipping dependency version checking. Error was: " +
                            e
                    )
                } else {
                    // If usesUnsupportedDependencyVersions is set, the exception was thrown by us
                    // in the dependency version checker plugin so re-throw it here.
                    throw e
                }
            }
        }

        BaseApplicationNameHandler.setBaseName(project)
        val flutterProguardRules: String =
            Paths
                .get(
                    flutterRoot!!.absolutePath,
                    "packages",
                    "flutter_tools",
                    "gradle",
                    "flutter_proguard_rules.pro"
                ).toString()
        // TODO(gmackall): reconsider getting the android extension every time
        FlutterPluginUtils.getAndroidExtension(project).buildTypes {
            // Add profile build type.
            create("profile") {
                initWith(getByName("debug"))
                // TODO(gmackall): do we need to clear?
                this.matchingFallbacks.clear()
                this.matchingFallbacks.addAll(listOf("debug", "release"))
            }

            // TODO(garyq): Shrinking is only false for multi apk split aot builds, where shrinking is not allowed yet.
            // This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove
            // this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see
            // increased app size due to this.
            if (FlutterPluginUtils.shouldShrinkResources(project)) {
                getByName("release") {
                    isMinifyEnabled = true
                    // Enables resource shrinking, which is performed by the Android Gradle plugin.
                    // The resource shrinker can't be used for libraries.
                    isShrinkResources = FlutterPluginUtils.isBuiltAsApp(project)
                    // Fallback to `android/app/proguard-rules.pro`.
                    // This way, custom Proguard rules can be configured as needed.
                    proguardFiles(
                        FlutterPluginUtils
                            .getAndroidExtension(project)
                            .getDefaultProguardFile("proguard-android-optimize.txt"),
                        flutterProguardRules,
                        "proguard-rules.pro"
                    )
                }
            }
        }

        FlutterPluginUtils.forceNdkDownload(project, flutterRootPath)

        if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
            // This is required to pass the local engine to flutter build aot.
            val engineOutPath: String = project.properties["local-engine-out"] as String
            val engineOut: File = project.file(engineOutPath)
            if (!engineOut.isDirectory) {
                throw GradleException("local-engine-out must point to a local engine build")
            }
            localEngine = engineOut.name
            localEngineSrcPath = engineOut.parentFile.parent

            val engineHostOutPath: String = project.properties["local-engine-host-out"] as String
            val engineHostOut: File = project.file(engineHostOutPath)
            if (!engineHostOut.isDirectory) {
                throw GradleException("local-engine-host-out must point to a local engine host build")
            }
            localEngineHost = engineHostOut.name
        }
        FlutterPluginUtils.getAndroidExtension(project).buildTypes.all {
            addFlutterDependencies(this)
        }
    }

    private fun addFlutterDependencies(buildType: com.android.builder.model.BuildType) {
        FlutterPluginUtils.addFlutterDependencies(
            project!!,
            buildType,
            getPluginHandler(project!!),
            engineVersion!!
        )
    }

    private fun getExecutableNameForPlatform(baseExecutableName: String): String =
        if (OperatingSystem.current().isWindows) "$baseExecutableName.bat" else baseExecutableName

    private fun resolveFlutterSdkProperty(defaultValue: String?): String? {
        val propertyName = "flutter.sdk"
        if (localProperties == null) {
            localProperties =
                readPropertiesIfExist(File(project!!.projectDir.parentFile, "local.properties"))
        }
        return project?.findProperty(propertyName) as? String ?: localProperties!!.getProperty(
            propertyName,
            defaultValue
        )
    }

    private fun addTaskForLockfileGeneration(rootProject: Project) {
        try {
            rootProject.tasks.register("generateLockfiles") {
                doLast {
                    rootProject.subprojects.forEach { subproject ->
                        val gradlew: String =
                            getExecutableNameForPlatform("${rootProject.projectDir}/gradlew")
                        val execOps = rootProject.serviceOf<ExecOperations>()
                        execOps.exec {
                            workingDir(rootProject.projectDir)
                            executable(gradlew)
                            args(":${subproject.name}:dependencies", "--write-locks")
                        }
                    }
                }
            }
        } catch (e: TaskInstantiationException) {
            // ignored
        }
    }

    private fun addFlutterTasks(projectToAddTasksTo: Project) {
        if (projectToAddTasksTo.state.failure != null) {
            return
        }

        FlutterPluginUtils.addTaskForJavaVersion(projectToAddTasksTo)
        FlutterPluginUtils.addTaskForKGPVersion(projectToAddTasksTo)
        if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) {
            FlutterPluginUtils.addTaskForPrintBuildVariants(projectToAddTasksTo)
            FlutterPluginUtils.addTasksForOutputsAppLinkSettings(projectToAddTasksTo)
        }

        val targetPlatforms: List<String> =
            FlutterPluginUtils.getTargetPlatforms(projectToAddTasksTo)

        val flutterPlugin = this

        if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) {
            // TODO(gmackall): I think this can be BaseExtension, with findByType.
            val android: AbstractAppExtension =
                projectToAddTasksTo.extensions.findByName("android") as AbstractAppExtension
            android.applicationVariants.configureEach {
                val variant = this
                val assembleTask = variant.assembleProvider.get()
                if (!FlutterPluginUtils.shouldConfigureFlutterTask(
                        projectToAddTasksTo,
                        assembleTask
                    )
                ) {
                    return@configureEach
                }
                val copyFlutterAssetsTask: Task =
                    addFlutterDeps(variant, flutterPlugin, targetPlatforms)

                // TODO(gmackall): Migrate to AGPs variant api.
                //    https://github.com/flutter/flutter/issues/166550
                @Suppress("DEPRECATION")
                val variantOutput: com.android.build.gradle.api.BaseVariantOutput = variant.outputs.first()
                val processResources: ProcessAndroidResources =
                    try {
                        variantOutput.processResourcesProvider.get()
                    } catch (e: UnknownTaskException) {
                        // TODO(gmackall): Migrate to AGPs variant api.
                        //    https://github.com/flutter/flutter/issues/166550
                        @Suppress("DEPRECATION")
                        variantOutput.processResources
                    }
                processResources.dependsOn(copyFlutterAssetsTask)

                // Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
                // can discover them. By default, this is `<app-dir>/build/app/outputs/flutter-apk/<filename>.apk`.
                //
                // The filename consists of `app<-abi>?<-flavor-name>?-<build-mode>.apk`.
                // Where:
                //   * `abi` can be `armeabi-v7a|arm64-v8a|x86_64` only if the flag `split-per-abi` is set.
                //   * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called.
                //   * `build-mode` can be `release|debug|profile`.
                variant.outputs.forEach { output ->
                    assembleTask.doLast {
                        // TODO(gmackall): Migrate to AGPs variant api.
                        //    https://github.com/flutter/flutter/issues/166550
                        @Suppress("DEPRECATION")
                        output as com.android.build.gradle.api.ApkVariantOutput
                        val packageApplicationProvider: PackageAndroidArtifact =
                            variant.packageApplicationProvider.get()
                        val outputDirectory: Directory =
                            packageApplicationProvider.outputDirectory.get()
                        val outputDirectoryStr: String = outputDirectory.toString()
                        var filename = "app"

                        // TODO(gmackall): Migrate to AGPs variant api.
                        //    https://github.com/flutter/flutter/issues/166550
                        @Suppress("DEPRECATION")
                        val abi = output.getFilter(com.android.build.VariantOutput.FilterType.ABI)
                        if (abi != null && abi.isNotEmpty()) {
                            filename += "-$abi"
                        }
                        if (variant.flavorName != null && variant.flavorName.isNotEmpty()) {
                            filename += "-${FlutterPluginUtils.lowercase(variant.flavorName)}"
                        }
                        filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}"
                        projectToAddTasksTo.copy {
                            from(File("$outputDirectoryStr/${output.outputFileName}"))
                            into(
                                File(
                                    "${
                                        projectToAddTasksTo.layout.buildDirectory.dir("outputs/flutter-apk")
                                            .get()
                                    }"
                                )
                            )
                            rename { "$filename.apk" }
                        }
                    }
                }
            }
            // Copy the native assets created by build.dart and placed here by flutter assemble.
            // This path is not flavor specific and must only be added once.
            // If support for flavors is added to native assets, then they must only be added
            // once per flavor; see https://github.com/dart-lang/native/issues/1359.
            val nativeAssetsDir =
                "${projectToAddTasksTo.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/"
            android.sourceSets
                .getByName("main")
                .jniLibs
                .srcDir(nativeAssetsDir)
            getPluginHandler(projectToAddTasksTo).configurePlugins(engineVersion!!)
            FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
                projectToAddTasksTo,
                getPluginHandler(projectToAddTasksTo).getPluginList()
            )
            return
        }
        // Flutter host module project (Add-to-app).
        val hostAppProjectName: String? =
            if (projectToAddTasksTo.rootProject.hasProperty("flutter.hostAppProjectName")) {
                projectToAddTasksTo.rootProject.property(
                    "flutter.hostAppProjectName"
                ) as? String
            } else {
                "app"
            }
        val appProject: Project? =
            projectToAddTasksTo.rootProject.findProject(":$hostAppProjectName")
        check(appProject != null) {
            "Project :$hostAppProjectName doesn't exist. To customize the host app project name, set `flutter.hostAppProjectName=<project-name>` in gradle.properties."
        }
        // Wait for the host app project configuration.
        appProject.afterEvaluate {
            val androidLibraryExtension =
                projectToAddTasksTo.extensions.findByType(LibraryExtension::class.java)
            check(androidLibraryExtension != null)
            androidLibraryExtension.libraryVariants.all libraryVariantAll@{
                val libraryVariant = this
                var copyFlutterAssetsTask: Task? = null
                val androidAppExtension =
                    appProject.extensions.findByName("android") as? AbstractAppExtension
                check(androidAppExtension != null)
                androidAppExtension.applicationVariants.all applicationVariantAll@{
                    val appProjectVariant = this
                    val appAssembleTask: Task = appProjectVariant.assembleProvider.get()
                    if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, appAssembleTask)) {
                        return@applicationVariantAll
                    }

                    // Find a compatible application variant in the host app.
                    //
                    // For example, consider a host app that defines the following variants:
                    // | ----------------- | ----------------------------- |
                    // |   Build Variant   |   Flutter Equivalent Variant  |
                    // | ----------------- | ----------------------------- |
                    // |   freeRelease     |   release                     |
                    // |   freeDebug       |   debug                       |
                    // |   freeDevelop     |   debug                       |
                    // |   profile         |   profile                     |
                    // | ----------------- | ----------------------------- |
                    //
                    // This mapping is based on the following rules:
                    // 1. If the host app build variant name is `profile` then the equivalent
                    //    Flutter variant is `profile`.
                    // 2. If the host app build variant is debuggable
                    //    (e.g. `buildType.debuggable = true`), then the equivalent Flutter
                    //    variant is `debug`.
                    // 3. Otherwise, the equivalent Flutter variant is `release`.
                    val variantBuildMode: String =
                        FlutterPluginUtils.buildModeFor(libraryVariant.buildType)
                    if (FlutterPluginUtils.buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
                        return@applicationVariantAll
                    }
                    copyFlutterAssetsTask = copyFlutterAssetsTask ?: addFlutterDeps(
                        libraryVariant,
                        flutterPlugin,
                        targetPlatforms
                    )
                    // TODO(gmackall): Migrate to AGPs variant api.
                    //    https://github.com/flutter/flutter/issues/166550
                    val mergeAssets =
                        projectToAddTasksTo
                            .tasks
                            .findByPath(":$hostAppProjectName:merge${FlutterPluginUtils.capitalize(appProjectVariant.name)}Assets")
                    check(mergeAssets != null)
                    mergeAssets.dependsOn(copyFlutterAssetsTask)
                }
            }
        }
        getPluginHandler(projectToAddTasksTo).configurePlugins(engineVersion!!)
        FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
            projectToAddTasksTo,
            getPluginHandler(projectToAddTasksTo).getPluginList()
        )
    }

    private fun getPluginHandler(project: Project): PluginHandler {
        if (this.pluginHandler == null) {
            this.pluginHandler = PluginHandler(project)
        }
        return this.pluginHandler!!
    }

    companion object {
        const val PROP_LOCAL_ENGINE_REPO: String = "local-engine-repo"

        /**
         * The name prefix for flutter builds. This is used to identify gradle tasks
         * where we expect the flutter tool to provide any error output, and skip the
         * standard Gradle error output in the FlutterEventLogger. If you change this,
         * be sure to change any instances of this string in symbols in the code below
         * to match.
         */
        private const val FLUTTER_BUILD_PREFIX: String = "flutterBuild"

        /**
         * Finds a task by name, returning null if the task does not exist.
         */
        private fun findTaskOrNull(
            project: Project,
            taskName: String
        ): Task? =
            try {
                project.tasks.named(taskName).get()
            } catch (ignored: UnknownTaskException) {
                null
            }

        // TODO(gmackall): Migrate to AGPs variant api.
        //    https://github.com/flutter/flutter/issues/166550
        private fun addFlutterDeps(
            @Suppress("DEPRECATION") variant: com.android.build.gradle.api.BaseVariant,
            flutterPlugin: FlutterPlugin,
            targetPlatforms: List<String>
        ): Task {
            // Shorthand
            val project: Project = flutterPlugin.project!!

            val fileSystemRootsValue: Array<String>? =
                project
                    .findProperty("filesystem-roots")
                    ?.toString()
                    ?.split("\\|")
                    ?.toTypedArray()
            val fileSystemSchemeValue: String? =
                project.findProperty("filesystem-scheme")?.toString()
            val trackWidgetCreationValue: Boolean =
                project.findProperty("track-widget-creation")?.toString()?.toBoolean() ?: true
            val frontendServerStarterPathValue: String? =
                project.findProperty("frontend-server-starter-path")?.toString()
            val extraFrontEndOptionsValue: String? =
                project.findProperty("extra-front-end-options")?.toString()
            val extraGenSnapshotOptionsValue: String? =
                project.findProperty("extra-gen-snapshot-options")?.toString()
            val splitDebugInfoValue: String? = project.findProperty("split-debug-info")?.toString()
            val dartObfuscationValue: Boolean =
                project.findProperty("dart-obfuscation")?.toString()?.toBoolean() ?: false
            val treeShakeIconsOptionsValue: Boolean =
                project.findProperty("tree-shake-icons")?.toString()?.toBoolean() ?: false
            val dartDefinesValue: String? = project.findProperty("dart-defines")?.toString()
            val performanceMeasurementFileValue: String? =
                project.findProperty("performance-measurement-file")?.toString()
            val codeSizeDirectoryValue: String? =
                project.findProperty("code-size-directory")?.toString()
            val deferredComponentsValue: Boolean =
                project.findProperty("deferred-components")?.toString()?.toBoolean() ?: false
            val validateDeferredComponentsValue: Boolean =
                project.findProperty("validate-deferred-components")?.toString()?.toBoolean() ?: true

            if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
                variant.outputs.forEach { output ->
                    // need to force this as the API does not return the right thing for our use.
                    // TODO(gmackall): Migrate to AGPs variant api.
                    //    https://github.com/flutter/flutter/issues/166550
                    @Suppress("DEPRECATION")
                    output as com.android.build.gradle.api.ApkVariantOutput
                    val versionCodeIfPresent: Int? = if (variant is ApkVariant) variant.versionCode else null

                    // TODO(gmackall): Migrate to AGPs variant api.
                    //    https://github.com/flutter/flutter/issues/166550
                    @Suppress("DEPRECATION")
                    val filterIdentifier: String? =
                        output.getFilter(com.android.build.VariantOutput.FilterType.ABI)
                    val abiVersionCode: Int? = FlutterPluginConstants.ABI_VERSION[filterIdentifier]
                    if (abiVersionCode != null) {
                        output.versionCodeOverride = abiVersionCode * 1000 + (
                            versionCodeIfPresent
                                ?: variant.mergedFlavor.versionCode as Int
                        )
                    }
                }
            }

            // Build an AAR when this property is defined.
            val isBuildingAar: Boolean = project.hasProperty("is-plugin")
            // In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
            // `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR.
            // TODO(gmackall): I think this is just always null? Which is great news! Consider removing.
            val packageAssets: Task? =
                findTaskOrNull(
                    project,
                    "package${FlutterPluginUtils.capitalize(variant.name)}Assets"
                )
            val cleanPackageAssets: Task? =
                findTaskOrNull(
                    project,
                    "cleanPackage${FlutterPluginUtils.capitalize(variant.name)}Assets"
                )

            val isUsedAsSubproject: Boolean =
                packageAssets != null && cleanPackageAssets != null && !isBuildingAar

            val variantBuildMode: String = FlutterPluginUtils.buildModeFor(variant.buildType)
            val flavorValue: String = variant.flavorName
            val taskName: String =
                FlutterPluginUtils.toCamelCase(
                    listOf(
                        "compile",
                        FLUTTER_BUILD_PREFIX,
                        variant.name
                    )
                )
            // The task provider below will shadow a lot of the variable names, so provide this reference
            // to access them within that scope.

            // Be careful when configuring task below, Groovy has bizarre
            // scoping rules: writing `verbose isVerbose()` means calling
            // `isVerbose` on the task itself - which would return `verbose`
            // original value. You either need to hoist the value
            // into a separate variable `verbose verboseValue` or prefix with
            // `this` (`verbose this.isVerbose()`).
            val compileTaskProvider: TaskProvider<FlutterTask> =
                project.tasks.register(taskName, FlutterTask::class.java) {
                    flutterRoot = flutterPlugin.flutterRoot
                    flutterExecutable = flutterPlugin.flutterExecutable
                    buildMode = variantBuildMode
                    minSdkVersion = variant.mergedFlavor.minSdkVersion!!.apiLevel
                    localEngine = flutterPlugin.localEngine
                    localEngineHost = flutterPlugin.localEngineHost
                    localEngineSrcPath = flutterPlugin.localEngineSrcPath
                    targetPath = FlutterPluginUtils.getFlutterTarget(project)
                    verbose = FlutterPluginUtils.isProjectVerbose(project)
                    fastStart = FlutterPluginUtils.isProjectFastStart(project)
                    fileSystemRoots = fileSystemRootsValue
                    fileSystemScheme = fileSystemSchemeValue
                    trackWidgetCreation = trackWidgetCreationValue
                    targetPlatformValues = targetPlatforms
                    sourceDir = FlutterPluginUtils.getFlutterSourceDirectory(project)
                    intermediateDir =
                        project.file(
                            project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/")
                        )
                    frontendServerStarterPath = frontendServerStarterPathValue
                    extraFrontEndOptions = extraFrontEndOptionsValue
                    extraGenSnapshotOptions = extraGenSnapshotOptionsValue
                    splitDebugInfo = splitDebugInfoValue
                    treeShakeIcons = treeShakeIconsOptionsValue
                    dartObfuscation = dartObfuscationValue
                    dartDefines = dartDefinesValue
                    performanceMeasurementFile = performanceMeasurementFileValue
                    codeSizeDirectory = codeSizeDirectoryValue
                    deferredComponents = deferredComponentsValue
                    validateDeferredComponents = validateDeferredComponentsValue
                    flavor = flavorValue
                }
            val compileTask: FlutterTask = compileTaskProvider.get()
            val libJar: File =
                project.file(
                    project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar")
                )
            val packJniLibsTaskProvider: TaskProvider<Jar> =
                project.tasks.register(
                    "packJniLibs${FLUTTER_BUILD_PREFIX}${FlutterPluginUtils.capitalize(variant.name)}",
                    Jar::class.java
                ) {
                    destinationDirectory.set(libJar.parentFile)
                    archiveFileName.set(libJar.name)
                    dependsOn(compileTask)
                    targetPlatforms.forEach { targetPlatform ->
                        val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
                        from("${compileTask.intermediateDir}/$abi") {
                            include("*.so")
                            // Move `app.so` to `lib/<abi>/libapp.so`
                            rename { filename: String -> "lib/$abi/lib$filename" }
                        }
                        // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble.
                        // The `$project.layout.buildDirectory` is '.android/Flutter/build/' instead of 'build/'.
                        val buildDir =
                            "${FlutterPluginUtils.getFlutterSourceDirectory(project)}/build"
                        val nativeAssetsDir =
                            "$buildDir/native_assets/android/jniLibs/lib"
                        from("$nativeAssetsDir/$abi") {
                            include("*.so")
                            rename { filename: String -> "lib/$abi/$filename" }
                        }
                    }
                }
            val packJniLibsTask: Task = packJniLibsTaskProvider.get()
            FlutterPluginUtils.addApiDependencies(
                project,
                variant.name,
                project.files({
                    packJniLibsTask
                })
            )
            val copyFlutterAssetsTaskProvider: TaskProvider<Copy> =
                project.tasks.register(
                    "copyFlutterAssets${FlutterPluginUtils.capitalize(variant.name)}",
                    Copy::class.java
                ) {
                    dependsOn(compileTask)
                    with(compileTask.assets)
                    filePermissions {
                        user {
                            read = true
                            write = true
                        }
                    }
                    if (isUsedAsSubproject) {
                        // TODO(gmackall): above is always false, can delete
                        dependsOn(packageAssets)
                        dependsOn(cleanPackageAssets)
                        into(packageAssets!!.outputs)
                    }
                    val mergeAssets =
                        try {
                            variant.mergeAssetsProvider.get()
                        } catch (e: IllegalStateException) {
                            // TODO(gmackall): Migrate to AGPs variant api.
                            //    https://github.com/flutter/flutter/issues/166550
                            @Suppress("DEPRECATION")
                            variant.mergeAssets
                        }
                    dependsOn(mergeAssets)
                    dependsOn("clean${FlutterPluginUtils.capitalize(mergeAssets.name)}")
                    mergeAssets.mustRunAfter("clean${FlutterPluginUtils.capitalize(mergeAssets.name)}")
                    into(mergeAssets.outputDir)
                }
            val copyFlutterAssetsTask: Task = copyFlutterAssetsTaskProvider.get()
            if (!isUsedAsSubproject) {
                // TODO(gmackall): Migrate to AGPs variant api.
                //    https://github.com/flutter/flutter/issues/166550
                @Suppress("DEPRECATION")
                val variantOutput: com.android.build.gradle.api.BaseVariantOutput = variant.outputs.first()
                val processResources =
                    try {
                        variantOutput.processResourcesProvider.get()
                    } catch (e: IllegalStateException) {
                        // TODO(gmackall): Migrate to AGPs variant api.
                        //    https://github.com/flutter/flutter/issues/166550
                        @Suppress("DEPRECATION")
                        variantOutput.processResources
                    }
                processResources.dependsOn(copyFlutterAssetsTask)
            }
            // The following tasks use the output of copyFlutterAssetsTask,
            // so it's necessary to declare it as an dependency since Gradle 8.
            // See https://docs.gradle.org/8.1/userguide/validation_problems.html#implicit_dependency.
            val tasksToCheck =
                listOf(
                    "compress${FlutterPluginUtils.capitalize(variant.name)}Assets",
                    "bundle${FlutterPluginUtils.capitalize(variant.name)}Aar",
                    "bundle${FlutterPluginUtils.capitalize(variant.name)}LocalLintAar"
                )
            tasksToCheck.forEach { taskTocheck ->
                try {
                    project.tasks.named(taskTocheck).configure {
                        dependsOn(copyFlutterAssetsTask)
                    }
                } catch (ignored: UnknownTaskException) {
                    // ignored
                }
            }
            return copyFlutterAssetsTask
        }
    }

    /**
     * Returns true if the Gradle task is invoked by Android Studio.
     *
     * This is true when the property `android.injected.invoked.from.ide` is passed to Gradle.
     * This property is set by Android Studio when it invokes a Gradle task.
     */
    private fun isInvokedFromAndroidStudio(): Boolean = project?.hasProperty("android.injected.invoked.from.ide") == true
}
