/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This is the build configuration for the google-ads-java subproject. It
 * depends on the google-ads-annotation-processing subproject.
 */


import com.google.common.collect.Sets
import groovy.json.JsonSlurper

import java.util.stream.Collectors

import static nl.javadude.gradle.plugins.license.DownloadLicensesExtension.license

plugins {
    id 'com.google.api-ads.java-conventions'
    id 'com.google.protobuf' version '0.8.15'
    id "com.github.hierynomus.license-report" version "0.16.1"
}

description = 'Google Ads API client library for Java'

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.25.3'
    }
}

dependencies {
    api project(':google-ads-codegen')
    implementation 'org.slf4j:slf4j-api:1.7.25'
    annotationProcessor 'com.google.auto.value:auto-value:1.10.4'
    testImplementation 'org.hamcrest:hamcrest:2.2'
    testImplementation 'org.mockito:mockito-core:4.11.0'
    testImplementation 'com.google.api:gax-grpc::testlib'
    testImplementation 'io.grpc:grpc-context'
    testImplementation 'com.google.truth:truth:0.27'
    implementation 'com.google.auto.value:auto-value-annotations'
    testImplementation testFixtures(project(':google-ads-codegen'))
}

compileJava {
    dependsOn(":google-ads-codegen:generateCode")
}


// --------------------- third_party directory generation ----------------------

task generateThirdPartyDirectory() {
    description = "Generates the third_party directory (containing dependencies" +
            " + licenses)"

    ext {
        thirdPartyDir = file '../third_party'
    }
    dependsOn 'createThirdPartyDirectory'
    dependsOn 'copyThirdPartySources'
    dependsOn 'copyThirdPartyBinaries'
    dependsOn 'copyLicenses'
    dependsOn 'updateAllstarConfigFile'
}

task createThirdPartyDirectory() {
    doLast {
        def thirdPartyDir = tasks.generateThirdPartyDirectory.thirdPartyDir
        if (thirdPartyDir.exists() && !thirdPartyDir.deleteDir()) {
            throw new GradleException('third_party exists but failed to delete')
        }
        if (!thirdPartyDir.mkdir()) {
            throw new GradleException("Failed to create third_party directory")
        }
    }
}

task generateLicenses(type: nl.javadude.gradle.plugins.license.DownloadLicenses) {
    description = "Copies the license report into the third_party directory"

    includeProjectDependencies = true

    // Works around issue where the plugin looks at the deprecated 'compile'
    // configuration by default.
    dependencyConfiguration = 'runtimeClasspath'

    // Excludes dependencies on subprojects. These should not be in the license
    // report since they are part of this library. The license-report plugin
    // will accept regexes here as well, but explicitly listing projects ensures
    // the exclusion is precise.
    excludeDependencies = rootProject.allprojects
            .collect { it.group + ':' + it.name + ':' + it.version }

    // Sets an extension property that contains all allowed licenses and their
    // canonical names. Update this map if you need to add a new license.
    ext.allowedLicenses = [
            'apacheTwo'      : license('Apache License, Version 2.0', 'http://opensource.org/licenses/Apache-2.0'),
            'bsdThreeClause' : license('3-Clause BSD License', 'https://opensource.org/licenses/BSD-3-Clause'),
            'mit'            : license('MIT License', 'https://opensource.org/licenses/mit-license.php'),
            'cddlAndGplv2Cpe': license('CDDL + GPLv2 with classpath exception', 'https://github.com/javaee/javax.annotation/blob/master/LICENSE'),
            'go'             : license('Go License', 'https://go.dev/LICENSE'),
            'bouncyCastle'   : license('Bouncy Castle Licence', 'https://www.bouncycastle.org/licence.html')
    ]

    // Specifies aliases for the known licenses so the report will use the
    // canonical name of each license.
    aliases = [
            (ext.allowedLicenses['apacheTwo'])     : ['Apache License 2.0',
                                                      'The Apache License, Version 2.0',
                                                      'The Apache Software License, Version 2.0',
                                                      'Apache 2.0',
                                                      'Apache-2.0',
                                                      'Apache 2'],
            (ext.allowedLicenses['bsdThreeClause']): ['BSD 3-clause',
                                                      'BSD-3-Clause',
                                                      'BSD',
                                                      'BSD New license'],
            (ext.allowedLicenses['mit'])           : ['The MIT License', 'MIT license'],
    ]
}

task verifyLicenses() {
    description = "Verifies that the licenses used by 3P dependencies are allowed"
    dependsOn 'generateLicenses'

    doLast {
        // Reads the dependency-license.json file generated by generateLicenses.
        // The `outputs` from that task should consist of a single directory that
        // contains the JSON file (among other files).
        def inputFile = tasks.generateLicenses.outputs.files.singleFile.listFiles().find { it.path.endsWith(".json") }
        def json = new JsonSlurper().parseText(inputFile.text)
        // Gets the names of all the licenses except those for ourselves
        // (which aren't set yet).
        def licenseNames = json
                .dependencies
                .findAll { !it.name.startsWith("com.google.api-ads") }
                .licenses
                .name
                .flatten() as Set
        // Gets the canonical names of allowed licenses from the
        // generateLicenses task.
        def allowedLicenseNames = tasks.generateLicenses.ext.allowedLicenses
                .values()
                .stream()
                .map { it.licenseName }
                .collect(Collectors.toSet())
        // Computes which licenses are in use but not permitted.
        def notAllowed = Sets.difference(licenseNames, allowedLicenseNames)
        if (!notAllowed.empty) {
            throw new GradleException(
                    """Licenses found which were not allowed: ${notAllowed}
- Note that this script is not very smart. It doesn't attempt to do partial
string comparison on the license name. It is possible that the new license is
already allowed with a different spelling. If the new license is already allowed
just add the new spelling to the aliases in google-ads/build.gradle.
- Otherwise you need to consider if we should accept this new license. For
Googlers, there is a company policy for this available internally.""")
        }
    }
}

task copyLicenses(type: Copy) {
    description = "Copies licenses from build directory to third_party"
    dependsOn 'verifyLicenses'
    mustRunAfter 'createThirdPartyDirectory'

    from new File(tasks.generateLicenses.xmlDestination.getAbsolutePath(), "dependency-license.json"),
            new File(tasks.generateLicenses.xmlDestination.getAbsolutePath(), "dependency-license.html"),
            new File(tasks.generateLicenses.xmlDestination.getAbsolutePath(), "dependency-license.xml")
    into tasks.generateThirdPartyDirectory.thirdPartyDir
    eachFile {
        if (!path.contains("dependency-license")) {
            throw new GradleException(
                    "Expecting files named dependency-license.*, found: " + path)
        }
        path = path.replace("dependency-license", "LICENSES")
    }
    doLast {
        // Fails the build if we didn't copy everything. Gradle will fail silently if
        // it cannot copy one of the inputs which is dangerous when you're dealing
        // with legal obligations.
        //
        // We're expecting exactly 3 input files, a JSON, XML and HTML version of
        // the license report.
        if (3 != inputs.sourceFiles.size()) {
            throw new GradleException(
                    "Failed to locate files to copy from. Expected: 3 but was: "
                            + inputs.sourceFiles.toList())
        }
    }
}

task copyThirdPartySources(type: Copy) {
    description = "Copies the sources and binaries of dependencies to third_party"
    mustRunAfter 'createThirdPartyDirectory'

    // Requires that this task is always run since it's hard to detect when the
    // dependencies have changed.
    outputs.upToDateWhen { false }

    def componentIds = configurations.runtimeClasspath
            .incoming
            .resolutionResult
            .allDependencies
            .stream()
    // Avoids failing the build with cryptic errors when a bad dependency is
    // added.
            .filter { it.hasProperty("selected") != null }
    // Avoids attempting to download sources for projects in the build.
            .filter { !(it.selected.id instanceof ProjectComponentIdentifier) }
    // Avoids attempting to download sources for Maven BOMs.
            .filter { !it.getResolvedVariant().getDisplayName().equals("platform-runtime") }
            .collect { it.selected.id } as Set

    def resolvedComponents = project.dependencies.createArtifactResolutionQuery()
            .forComponents(componentIds)
            .withArtifacts(JvmLibrary, SourcesArtifact)
            .execute()
            .resolvedComponents

    // Allows a dependency to be excluded from having its sources cached.
    // There should be a _very_ good reason for anything being in here.
    final def exemptionEligible = [
            // Google owned dummy module which doens't need any code.
            "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava"
    ]

    // Grabs the source jars.
    def files = []
    for (componentId in componentIds) {
        def resolvedComponentsForId = resolvedComponents.findAll { (it.id == componentId) }
        if (resolvedComponentsForId.size() != 1) {
            throw new GradleException("Failed to resolve component for ${componentId}. Got: " + resolvedComponentsForId)
        }
        def sources = resolvedComponentsForId[0].getArtifacts(SourcesArtifact)
        if (sources.size() == 1) {
            files.add(sources[0].file)
        } else if (exemptionEligible.contains(componentId.toString())) {
            logger.debug("Allowing exemption for ${componentId}")
        } else {
            throw new RuntimeException("Unable to resolve sources for ${componentId}. Got: ${sources}")
        }
    }
    from files
    into tasks.generateThirdPartyDirectory.thirdPartyDir
    doLast {
        logger.info('Resolved third party sources')
        files.each {
            logger.debug(" - ${it.path}")
        }
    }
}

// Copies the third party binaries to the third_party folder.
task copyThirdPartyBinaries(type: Copy) {
    from configurations.runtimeClasspath {
        // Excludes JARs for the root project and its subprojects.
        exclude(
                rootProject.allprojects.collect { it.name + "-" + it.version + ".jar" }
        )
    }
    into tasks.generateThirdPartyDirectory.thirdPartyDir
}

// Updates the Allstar configuration file to contain the current list of .jar
// files in the third_party directory.
task updateAllstarConfigFile {

    dependsOn 'copyThirdPartySources'
    dependsOn 'copyThirdPartyBinaries'

    // Registers this task's output file.
    outputs.file(
            new File(rootProject.rootDir, ".allstar/binary_artifacts.yaml"))

    doLast {
        File thirdPartyDir = tasks.generateThirdPartyDirectory.thirdPartyDir
        // Collects all .jar files in the third party directory into a sorted
        // set.
        Set<String> thirdPartyFiles = Arrays.stream(thirdPartyDir.listFiles())
                .filter { it.getPath().endsWith(".jar") }
                .map { it.getName() }
                .sorted()
                .collect(Collectors.toCollection { new TreeSet<String>() })
        // Gets the third party directory name, relative to the root project's
        // directory.
        String thirdPartyDirName = rootProject.rootDir.toPath()
                .relativize(thirdPartyDir.toPath())
                .toString()

        // Constructs the updated contents of the YAML file.
        StringBuilder allstarConfigBuilder = new StringBuilder()

        // Adds the header section with the ignore reason.
        allstarConfigBuilder.append(
'''# Ignore reason: These artifacts are required per the approved approach
# for publishing shaded libraries. See go/non-google-third-party-locations
# and launch 4072458 for more information.
ignorePaths:
''')
        // Adds an entry for each .jar file.
        thirdPartyFiles.stream().forEachOrdered {
            allstarConfigBuilder.append(
                    String.format("- %s/%s%n", thirdPartyDirName, it))
        }
        // Updates the contents of the Allstar configuration file.
        outputs.files.singleFile.text = allstarConfigBuilder.toString()
    }
}

publishing {
    publications {
        maven(MavenPublication) { publication ->
            configurePom(publication,
                    "Google Ads API client library for Java - main library",
                    "Main library for the Google Ads API client library for Java")
        }
    }
}

sourceSets {
    main {
        java {
            // Includes the generated classes from the google-ads-codegen module
            // to be compiled by google-ads. The generated sources depend on
            // imports from google-ads, so are not compiled with
            // google-ads-codegen. Confusingly we have an api dependency on
            // google-ads-codegen here, however this only exposes the stubs to
            // the main library.
            srcDir project.parent.findProject("google-ads-codegen").tasks.getByName("generateCode")
        }
    }
    test {
        java {
            // Adds the generated .java files from the protobuf plugin. This
            // isn't necessary strictly for Gradle, but IntelliJ doesn't
            // include the directory without this line.
            srcDir new File(buildDir, 'generated/source/proto/test')
        }
        proto {
            srcDir 'src/test/resources/protos/'
        }
    }
}
