/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**********************************************************************
  THIS FILE IS OBSOLETE AND UNMAINTAINED.
  The functionality in this file has moved to a proper Gradle plugin
  in https://github.com/mozilla/glean
  However, this file must remain on the master branch indefinitely so that
  Glean-using applications can continue to build historical revisions from
  prior to the existence of the plugin.
**********************************************************************/

/*
 * IMPORTANT DEVELOPER'S NOTE:
 *
 * Because of how Fenix and &-browser include this file, any time this file
 * is updated, we need to publish an android-components snapshot immediately
 * to avoid breaking the build.
 *
 * (This comment will become irrelevant when Fenix starts using tagged releases
 * of android-components rather than tracking snapshots.)
 */

import groovy.json.JsonOutput
import java.util.logging.Logger
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.internal.artifacts.ArtifactAttributes

Logger logger = Logger.getLogger("Glean sdk_generator.gradle")
logger.warning("The Glean sdk_generator.gradle script is deprecated. Use the Glean Gradle plugin instead")

// This installs a Miniconda3 environment into the gradle user home directory
// so that it will be shared between all libraries that use Glean.  This is
// important because it is approximately 300MB in installed size.

///////////////////////////////////////////////////////////////////////////
// NOTE: SINCE THIS FILE HAS BEEN DEPRECATED, THIS VERSION SHOULD NO LONGER BE
// UPDATED. THE CANONICAL SOURCE FOR THE glean_parser VERSION IS IN THE
// glean-gradle-plugin IN https://github.com/mozilla/glean
///////////////////////////////////////////////////////////////////////////
String GLEAN_PARSER_VERSION = "1.18.2"
// The version of Miniconda is explicitly specified.
// Miniconda3-4.5.12 is known to not work on Windows.
String MINICONDA_VERSION = "4.5.11"
File GLEAN_BOOTSTRAP_DIR = new File(
    project.getGradle().gradleUserHomeDir,
    "glean/bootstrap-${MINICONDA_VERSION}"
)
File GLEAN_MINICONDA_DIR = new File(
    GLEAN_BOOTSTRAP_DIR,
    "Miniconda3"
)

// Define the path to the Miniconda3 installation as a buildConfig variable so
// that tests that need to run JSON through our JSON schema can do so.
android {
    defaultConfig {
        buildConfigField(
            "String",
            "GLEAN_MINICONDA_DIR",
            // Carefully escape the string here so it will support `\` in
            // Windows paths correctly.
            JsonOutput.toJson(GLEAN_MINICONDA_DIR.path)
        )
    }
}

/* This script runs a given Python module as a "main" module, like
 * `python -m module`. However, it first checks that the installed
 * package is at the desired version, and if not, upgrades it using `pip`.
 */
String runPythonScript = """
import importlib
import subprocess
import sys

module_name = sys.argv[1]
expected_version = sys.argv[2]

try:
    module = importlib.import_module(module_name)
except ImportError:
    found_version = None
else:
    found_version = getattr(module, '__version__')

if found_version != expected_version:
    subprocess.check_call([
        sys.executable,
        '-m',
        'pip',
        'install',
        '--upgrade',
        f'{module_name}=={expected_version}'
    ])

subprocess.check_call([
    sys.executable,
    '-m',
    module_name
] + sys.argv[3:])
"""

// Even though we are installing the Miniconda environment to the gradle user
// home directory, the gradle-python-envs plugin is hardcoded to download the
// installer to the project's build directory. Doing so will fail if the
// project's build directory doesn't already exist. This task ensures that
// the project's build directory exists before downloading and installing the
// Miniconda environment.
// See https://github.com/JetBrains/gradle-python-envs/issues/26
// The fix in the above is not actually sufficient -- we need to add createBuildDir
// as a dependency of Bootstrap_CONDA (where conda is installed), as the preBuild
// task alone isn't early enough.
task createBuildDir {
    description = "Make sure the build dir exists before creating the Python Environments"
    onlyIf {
        !file(buildDir).exists()
    }
    doLast {
        project.logger.lifecycle("Creating build directory:" + buildDir.getPath())
        buildDir.mkdir()
    }
}

// Configure the Python environments
envs {
    bootstrapDirectory = GLEAN_BOOTSTRAP_DIR
    pipInstallOptions = "--trusted-host pypi.python.org --no-cache-dir"

    // Setup a miniconda environment. conda is used because it works
    // non-interactively on Windows, unlike the other options
    conda "Miniconda3", "Miniconda3-${MINICONDA_VERSION}", "64", ["glean_parser==${GLEAN_PARSER_VERSION}"]
}

tasks.whenTaskAdded { task ->
    if (task.name.startsWith('Bootstrap_CONDA')) {
        task.dependsOn(createBuildDir)
    }
}
preBuild.dependsOn(createBuildDir)
preBuild.finalizedBy("build_envs")

clean {
    delete GLEAN_BOOTSTRAP_DIR
}

// Support for extracting metrics.yaml from artifact files.

// This is how to extract `metrics.yaml` and `pings.yaml` from AAR files: an "artifact transform"
// identifies the files in an "exploded AAR" directory.  See
// https://docs.gradle.org/current/userguide/dependency_management_attribute_based_matching.html#sec:abm_artifact_transforms.
// This is exactly how elements of AAR files are consumed by the Android-Gradle plugin; see the
// transforms defined in
// https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/AarTransform.java
// and their usage at
// https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/VariantManager.java#592.
//
// Note that this mechanism only applies to `module` dependencies (i.e., AAR files downloaded from
// Maven) and not to `project` dependencies in the same root project or substituted as part of a
// Gradle composite build.
class GleanMetricsYamlTransform extends ArtifactTransform {
    List<File> transform(File file) {
        def f = new File(file, "metrics.yaml")
        if (f.exists()) {
            return [f]
        }
        return []
    }
}

if (project.ext.has("allowMetricsFromAAR")) {
    dependencies {
        registerTransform { reg ->
            // The type here should be
            // `com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.EXPLODED_AAR.getType())`,
            // but there's no good way to access the including script's classpath from `apply from:`
            // scripts. See https://stackoverflow.com/a/37060550. The 'android-exploded-aar' string is
            // very unlikely to change, so it's just hard-coded.
            reg.getFrom().attribute(
                    ArtifactAttributes.ARTIFACT_FORMAT,
                    'android-exploded-aar')
            reg.getTo().attribute(
                    ArtifactAttributes.ARTIFACT_FORMAT,
                    'glean-metrics-yaml')
            reg.artifactTransform(GleanMetricsYamlTransform.class)
        }
    }
}

def gleanTaskNamePrefix = "gleanGenerateMetrics"

// A shared function to get the Python command line executable.
ext.gleanGetPythonCommand = {
    // Note that the command line is OS dependant: on linux/mac is Miniconda3/bin/python.
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        return new File(GLEAN_MINICONDA_DIR, "python")
    }

    return new File(GLEAN_MINICONDA_DIR, "bin/python")
}

// Generate the various Glean SDK artifacts.
ext.gleanAttachGeneratingTasks = {
    variant ->

    def sourceOutputDir =  "$buildDir/generated/source/glean/${variant.dirName}/kotlin"
    // Get the name of the package as if it were to be used in the R or BuildConfig
    // files. This is required since applications can define different application ids
    // depending on the variant type: the generated API definitions don't need to be
    // different due to that.
    TaskProvider buildConfigProvider = variant.getGenerateBuildConfigProvider()
    def originalPackageName = buildConfigProvider.get().getBuildConfigPackageName()

    def fullNamespace = "${originalPackageName}.GleanMetrics"
    def generateKotlinAPI = task("${gleanTaskNamePrefix}SourceFor${variant.name.capitalize()}", type: Exec) {
        description = "Generate the Kotlin code for the Metrics API"

        if (project.ext.has("allowMetricsFromAAR")) {
            // This is sufficiently lazy to be valid at configuration time.  See the model at
            // https://github.com/google/protobuf-gradle-plugin/blob/6d99a421c8d15710045e4e8d31a3af6cb0cc3b70/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy#L270-L277
            inputs.files variant.compileConfiguration.incoming.artifactView {
                attributes {
                    it.attribute(ArtifactAttributes.ARTIFACT_FORMAT, 'glean-metrics-yaml')
                }
            }.files
        }

        outputs.dir sourceOutputDir

        workingDir  rootDir
        commandLine gleanGetPythonCommand()

        def gleanNamespace = "mozilla.components.service.glean"
        if (project.ext.has("gleanNamespace")) {
            gleanNamespace = project.ext.get("gleanNamespace")
        }

        args "-c"
        args runPythonScript
        args "glean_parser"
        args GLEAN_PARSER_VERSION
        args "translate"
        args "-f"
        args "kotlin"
        args "-o"
        args "$sourceOutputDir"
        args "-s"
        args "namespace=$fullNamespace"
        args "-s"
        args "glean_namespace=$gleanNamespace"
        // We intentionally don't let out the local metrics.yaml, even if another one will
        // be extracted from the AAR file. The parser will just ignore this one, if not present.
        args "$projectDir/metrics.yaml"
        args "$projectDir/pings.yaml"

        // If we're building the Glean library itself (rather than an
        // application using Glean) pass the --allow-reserved flag so we can
        // use metrics in the "glean..." category
        if (project.name == 'service-glean' || project.ext.has("allowGleanInternal")) {
            args "--allow-reserved"
        }

        doFirst {
            // Add the potential 'metrics.yaml' files at evaluation-time, rather than
            // configuration-time. Otherwise the Gradle build will fail.
            if (project.ext.has("allowMetricsFromAAR")) {
                inputs.files.forEach{ file ->
                    project.logger.lifecycle("Glean SDK - generating API from ${file.path}")
                    args file.path
                }
            }
        }

        // Only show the output if something went wrong.
        ignoreExitValue = true
        standardOutput = new ByteArrayOutputStream()
        errorOutput = standardOutput
        doLast {
            if (execResult.exitValue != 0) {
                throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
            }
        }
    }

    // Create the task for generating Glean SDK docs.
    def generateDocsTask = task("${gleanTaskNamePrefix}DocsFor${variant.name.capitalize()}", type: Exec) {
        description = "Generate the Markdown docs for the collected metrics"

        def gleanDocsDirectory = "$projectDir/docs"
        if (project.ext.has("gleanDocsDirectory")) {
            gleanDocsDirectory = project.ext.get("gleanDocsDirectory")
        }

        if (project.ext.has("allowMetricsFromAAR")) {
            // This is sufficiently lazy to be valid at configuration time.  See the model at
            // https://github.com/google/protobuf-gradle-plugin/blob/6d99a421c8d15710045e4e8d31a3af6cb0cc3b70/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy#L270-L277
            inputs.files variant.compileConfiguration.incoming.artifactView {
                attributes {
                    it.attribute(ArtifactAttributes.ARTIFACT_FORMAT, 'glean-metrics-yaml')
                }
            }.files
        }

        outputs.dir gleanDocsDirectory
        workingDir rootDir
        commandLine gleanGetPythonCommand()

        args "-c"
        args runPythonScript
        args "glean_parser"
        args GLEAN_PARSER_VERSION
        args "translate"
        args "-f"
        args "markdown"
        args "-o"
        args gleanDocsDirectory
        args "$projectDir/metrics.yaml"
        args "$projectDir/pings.yaml"

        // If we're building the Glean library itself (rather than an
        // application using Glean) pass the --allow-reserved flag so we can
        // use metrics in the "glean..." category
        if (project.name == 'service-glean' || project.ext.has("allowGleanInternal")) {
            args "--allow-reserved"
        }

        doFirst {
            // Add the potential 'metrics.yaml' files at evaluation-time, rather than
            // configuration-time. Otherwise the Gradle build will fail.
            if (project.ext.has("allowMetricsFromAAR")) {
                inputs.files.forEach{ file ->
                    project.logger.lifecycle("Glean SDK - generating docs for ${file.path} in $gleanDocsDirectory")
                    args file.path
                }
            }
        }

        // Only show the output if something went wrong.
        ignoreExitValue = true
        standardOutput = new ByteArrayOutputStream()
        errorOutput = standardOutput
        doLast {
            if (execResult.exitValue != 0) {
                throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
            }
        }
    }

    // Only attach the generation task if the metrics file is available or we're requested
    // to fetch them from AAR files. We don't need to fail hard otherwise, as some 3rd party
    // project might just want metrics included in Glean and nothing more.
    if (file("$projectDir/metrics.yaml").exists()
        || file("$projectDir/pings.yaml").exists()
        || project.ext.has("allowMetricsFromAAR")) {
        // Generate the Metrics docs, if requested.
        if (project.ext.has("gleanGenerateMarkdownDocs")) {
            // Attach the docs generation task to the code generation task, to make sure
            // we generate docs, if we're asked for it.
            generateKotlinAPI.dependsOn(generateDocsTask)
        }

        // This is an Android-Gradle plugin 3+-ism.  Culted from reading the source,
        // searching for "registerJavaGeneratingTask", and finding
        // https://github.com/GoogleCloudPlatform/endpoints-framework-gradle-plugin/commit/2f2b91476fb1c6647791e2c6fe531a47615a1e85.
        // The added directory doesn't appear in the paths listed by the
        // `sourceSets` task, for reasons unknown.
        variant.registerJavaGeneratingTask(generateKotlinAPI, new File(sourceOutputDir))
    }
}

if (android.hasProperty('applicationVariants')) {
    android.applicationVariants.all(ext.gleanAttachGeneratingTasks)
} else {
    android.libraryVariants.all(ext.gleanAttachGeneratingTasks)
}
