/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 *
 *   http://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.
 *
 */

import com.github.spotbugs.SpotBugsPlugin
import com.github.spotbugs.SpotBugsTask
import com.github.vlsi.gradle.crlf.CrLfSpec
import com.github.vlsi.gradle.crlf.LineEndings
import com.github.vlsi.gradle.crlf.filter
import com.github.vlsi.gradle.git.FindGitAttributes
import com.github.vlsi.gradle.git.dsl.gitignore
import com.github.vlsi.gradle.release.RepositoryType
import org.ajoberstar.grgit.Grgit
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.sonarqube.gradle.SonarQubeProperties

plugins {
    java
    jacoco
    checkstyle
    id("org.jetbrains.gradle.plugin.idea-ext") apply false
    id("org.nosphere.apache.rat")
    id("com.diffplug.gradle.spotless")
    id("com.github.spotbugs")
    id("org.sonarqube")
    id("com.github.vlsi.crlf")
    id("com.github.vlsi.ide")
    id("com.github.vlsi.stage-vote-release")
    signing
    publishing
}

ide {
    copyrightToAsf()
    ideaInstructionsUri =
        uri("https://github.com/apache/jmeter/blob/master/CONTRIBUTING.md#intellij")
    doNotDetectFrameworks("android", "jruby")
}

fun Project.boolProp(name: String) =
    findProperty(name)
        // Project properties include tasks, extensions, etc, and we want only String properties
        // We don't want to use "task" as a boolean property
        ?.let { it as? String }
        ?.equals("false", ignoreCase = true)?.not()

// Release candidate index
val String.v: String get() = rootProject.extra["$this.version"] as String
version = "jmeter".v + releaseParams.snapshotSuffix

val displayVersion by extra {
    version.toString() +
            if (releaseParams.release.get()) {
                ""
            } else {
                // Append 7 characters of Git commit id for snapshot version
                val grgit: Grgit? by project
                grgit?.let { " " + it.head().abbreviatedId }
            }
}

println("Building JMeter $version")

fun reportsForHumans() = !(System.getenv()["CI"]?.toBoolean() ?: boolProp("CI") ?: false)

val lastEditYear by extra {
    file("$rootDir/NOTICE")
        .readLines()
        .first { it.contains("Copyright") }
        .let {
            """Copyright \d{4}-(\d{4})""".toRegex()
                .find(it)?.groupValues?.get(1)
                ?: throw IllegalStateException("Unable to identify copyright year from $rootDir/NOTICE")
        }
}

// This task scans the project for gitignore / gitattributes, and that is reused for building
// source/binary artifacts with the appropriate eol/executable file flags
// It enables to automatically exclude patterns from .gitignore
val gitProps by tasks.registering(FindGitAttributes::class) {
    // Scanning for .gitignore and .gitattributes files in a task avoids doing that
    // when distribution build is not required (e.g. code is just compiled)
    root.set(rootDir)
}

val rat by tasks.getting(org.nosphere.apache.rat.RatTask::class) {
    gitignore(gitProps)
    // Note: patterns are in non-standard syntax for RAT, so we use exclude(..) instead of excludeFile
    exclude(rootDir.resolve("rat-excludes.txt").readLines())
}

releaseArtifacts {
    fromProject(":src:dist")
    previewSite {
        into("rat")
        from(rat) {
            filteringCharset = "UTF-8"
            // XML is not really interesting for now
            exclude("rat-report.xml")
            // RAT reports have absolute paths, and we don't want to expose them
            filter { str: String -> str.replace(rootDir.absolutePath, "") }
        }
    }
}

releaseParams {
    tlp.set("JMeter")
    releaseTag.set("rel/v${project.version}")
    rcTag.set(rc.map { "v${project.version}-rc$it" })
    svnDist {
        // All the release versions are put under release/jmeter/{source,binary}
        releaseFolder.set("release/jmeter")
        releaseSubfolder.apply {
            put(Regex("_src\\."), "sources")
            put(Regex("."), "binaries")
        }
    }
    nexus {
        if (repositoryType.get() == RepositoryType.PROD) {
            // org.apache.jmeter at repository.apache.org
            stagingProfileId.set("4d29c092016673")
        }
    }
}

val jacocoReport by tasks.registering(JacocoReport::class) {
    group = "Coverage reports"
    description = "Generates an aggregate report from all subprojects"
}

val jacocoEnabled by extra {
    (boolProp("coverage") ?: false) || gradle.startParameter.taskNames.any { it.contains("jacoco") }
}

// Do not enable spotbugs by default. Execute it only when -Pspotbugs is present
val enableSpotBugs by extra {
    boolProp("spotbugs") ?: false
}

val ignoreSpotBugsFailures by extra {
    boolProp("ignoreSpotBugsFailures") ?: false
}

val skipCheckstyle by extra {
    boolProp("skipCheckstyle") ?: false
}

val skipSpotless by extra {
    boolProp("skipSpotless") ?: false
}

// Allow to skip building source/binary distributions
val skipDist by extra {
    boolProp("skipDist") ?: false
}

// By default use Java implementation to sign artifacts
// When useGpgCmd=true, then gpg command line tool is used for signing
val useGpgCmd by extra {
    boolProp("useGpgCmd") ?: false
}

// Signing is required for RELEASE version
val skipSigning by extra {
    boolProp("skipSigning") ?: boolProp("skipSign") ?: false
}

allprojects {
    if (project.path != ":src") {
        tasks.register<DependencyInsightReportTask>("allDependencyInsight") {
            group = HelpTasksPlugin.HELP_GROUP
            description =
                "Shows insights where the dependency is used. For instance: allDependencyInsight --configuration compile --dependency org.jsoup:jsoup"
        }
    }
}

sonarqube {
    properties {
        // See https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner+for+Gradle#AnalyzingwithSonarQubeScannerforGradle-Configureanalysisproperties
        property("sonar.sourceEncoding", "UTF-8")
        val projectName = "JMeter"
        property("sonar.projectName", projectName)
        property("sonar.projectKey", System.getenv()["SONAR_PROJECT_KEY"] ?: projectName)
        property("sonar.organization", System.getenv()["SONAR_ORGANIZATION"] ?: "apache")
        property("sonar.projectVersion", project.version.toString())
        property("sonar.host.url", System.getenv()["SONAR_HOST_URL"] ?: "http://localhost:9000")
        property("sonar.login", System.getenv()["SONAR_LOGIN"] ?: "")
        property("sonar.password", System.getenv()["SONAR_PASSWORD"] ?: "")
        property("sonar.links.homepage", "https://jmeter.apache.org")
        property("sonar.links.ci", "https://builds.apache.org/job/JMeter-trunk/")
        property("sonar.links.scm", "https://jmeter.apache.org/svnindex.html")
        property("sonar.links.issue", "https://jmeter.apache.org/issues.html")
    }
}

fun SonarQubeProperties.add(name: String, value: String) {
    properties.getOrPut(name) { mutableSetOf<String>() }
        .also {
            @Suppress("UNCHECKED_CAST")
            (it as MutableCollection<String>).add(value)
        }
}

if (jacocoEnabled) {
    val mergedCoverage = jacocoReport.get().reports.xml.destination.toString()

    // For every module we pass merged coverage report
    // That enables to see ":src:core" lines covered even in case they are covered from
    // "batch tests"
    subprojects {
        if (File(projectDir, "src/main").exists()) {
            apply(plugin = "org.sonarqube")
            sonarqube {
                properties {
                    property("sonar.coverage.jacoco.xmlReportPaths", mergedCoverage)
                }
            }
        }
    }

    tasks.sonarqube {
        dependsOn(jacocoReport)
    }
}

if (enableSpotBugs) {
    // By default sonarqube does not depend on spotbugs
    val sonarqubeTask = tasks.sonarqube

    // See https://jira.sonarsource.com/browse/SONARGRADL-59
    // Unfortunately, report paths must be specified manually for now
    allprojects {
        if (!File(projectDir, "src/main").exists()) {
            return@allprojects
        }
        val spotBugTasks = tasks.withType<SpotBugsTask>().matching {
            // We don't send spotbugs for test classes
            !it.name.endsWith("Test")
        }
        sonarqubeTask {
            dependsOn(spotBugTasks)
        }
        apply(plugin = "org.sonarqube")
        sonarqube {
            properties {
                spotBugTasks.configureEach {
                    add("sonar.java.spotbugs.reportPaths", reports.xml.destination.toString())
                }
            }
        }
    }
}

val licenseHeaderFile = file("config/license.header.java")
allprojects {
    group = "org.apache.jmeter"
    // JMeter ClassFinder parses "class.path" and tries to find jar names there,
    // so we should produce jars without versions names for now
    // version = rootProject.version
    if (!skipSpotless) {
        apply(plugin = "com.diffplug.gradle.spotless")
        spotless {
            kotlinGradle {
                ktlint()
                trimTrailingWhitespace()
                endWithNewline()
            }
            if (project == rootProject) {
                // Spotless does not exclude subprojects when using target(...)
                // So **/*.md is enough to scan all the md files in JMeter codebase
                // See https://github.com/diffplug/spotless/issues/468
                format("markdown") {
                    target("**/*.md")
                    // Flot is known to have trailing whitespace, so the files
                    // are kept in their original format (e.g. to simplify diff on library upgrade)
                    targetExclude("bin/report-template/**/flot*/*.md")
                    trimTrailingWhitespace()
                    endWithNewline()
                }
            }
        }
    }
    plugins.withType<JavaPlugin> {
        // We don't intend to resolve that configuration
        // It is in line with further Gradle versions: https://github.com/gradle/gradle/issues/8585
        dependencies {
            configurations {
                compileOnly(platform(project(":src:bom")))
            }
        }

        apply<IdeaPlugin>()
        apply<EclipsePlugin>()
        if (!skipCheckstyle) {
            apply<CheckstylePlugin>()
            checkstyle {
                toolVersion = "checkstyle".v
            }
            val sourceSets: SourceSetContainer by project
            if (sourceSets.isNotEmpty()) {
                tasks.register("checkstyleAll") {
                    dependsOn(sourceSets.names.map { "checkstyle" + it.capitalize() })
                }
                tasks.register("checkstyle") {
                    group = LifecycleBasePlugin.VERIFICATION_GROUP
                    description = "Executes Checkstyle verifications"
                    dependsOn("checkstyleAll")
                    dependsOn("spotlessCheck")
                }
                // Spotless produces more meaningful error messages, so we ensure it is executed before Checkstyle
                if (!skipSpotless) {
                    for (s in sourceSets.names) {
                        tasks.named("checkstyle" + s.capitalize()) {
                            mustRunAfter("spotlessApply")
                            mustRunAfter("spotlessCheck")
                        }
                    }
                }
            }
        }
        apply<SpotBugsPlugin>()

        spotbugs {
            toolVersion = "spotbugs".v
            isIgnoreFailures = ignoreSpotBugsFailures
        }

        if (!skipSpotless) {
            spotless {
                java {
                    licenseHeaderFile(licenseHeaderFile)
                    importOrder("static ", "java.", "javax", "org", "net", "com", "")
                    removeUnusedImports()
                    trimTrailingWhitespace()
                    indentWithSpaces(4)
                    endWithNewline()
                }
            }
        }
        tasks.register("style") {
            group = LifecycleBasePlugin.VERIFICATION_GROUP
            description = "Formats code (license header, import order, whitespace at end of line, ...) and executes Checkstyle verifications"
            if (!skipSpotless) {
                dependsOn("spotlessApply")
            }
            if (!skipCheckstyle) {
                dependsOn("checkstyleAll")
            }
        }
    }
    plugins.withId("groovy") {
        if (!skipSpotless) {
            spotless {
                groovy {
                    licenseHeaderFile(licenseHeaderFile)
                    importOrder("static ", "java.", "javax", "org", "net", "com", "")
                    trimTrailingWhitespace()
                    indentWithSpaces(4)
                    endWithNewline()
                }
            }
        }
    }

    plugins.withType<JacocoPlugin> {
        the<JacocoPluginExtension>().toolVersion = "jacoco".v

        val testTasks = tasks.withType<Test>()
        val javaExecTasks = tasks.withType<JavaExec>()
        // This configuration must be postponed since JacocoTaskExtension might be added inside
        // configure block of a task (== before this code is run). See :src:dist-check:createBatchTask
        afterEvaluate {
            for (t in arrayOf(testTasks, javaExecTasks)) {
                t.configureEach {
                    extensions.findByType<JacocoTaskExtension>()?.apply {
                        // Do not collect coverage when not asked (e.g. via jacocoReport or -Pcoverage)
                        isEnabled = jacocoEnabled
                        // We don't want to collect coverage for third-party classes
                        includes?.add("org.apache.jmeter.*")
                        includes?.add("org.apache.jorphan.*")
                        includes?.add("org.apache.commons.cli.*")
                    }
                }
            }
        }

        jacocoReport {
            // Note: this creates a lazy collection
            // Some of the projects might fail to create a file (e.g. no tests or no coverage),
            // So we check for file existence. Otherwise JacocoMerge would fail
            val execFiles =
                files(testTasks, javaExecTasks).filter { it.exists() && it.name.endsWith(".exec") }
            executionData(execFiles)
        }

        tasks.withType<JacocoReport>().configureEach {
            reports {
                html.isEnabled = reportsForHumans()
                xml.isEnabled = !reportsForHumans()
            }
        }
        // Add each project to combined report
        configure<SourceSetContainer> {
            val mainCode = main.get()
            jacocoReport.configure {
                additionalSourceDirs.from(mainCode.allJava.srcDirs)
                sourceDirectories.from(mainCode.allSource.srcDirs)
                // IllegalStateException: Can't add different class with same name: module-info
                // https://github.com/jacoco/jacoco/issues/858
                classDirectories.from(mainCode.output.asFileTree.matching {
                    exclude("module-info.class")
                })
            }
        }
    }

    tasks.withType<AbstractArchiveTask>().configureEach {
        // Ensure builds are reproducible
        isPreserveFileTimestamps = false
        isReproducibleFileOrder = true
        dirMode = "775".toInt(8)
        fileMode = "664".toInt(8)
    }

    // Not all the modules use publishing plugin
    plugins.withType<PublishingPlugin> {
        apply<SigningPlugin>()
        // Sign all the published artifacts
        signing {
            sign(publishing.publications)
        }
    }

    plugins.withType<SigningPlugin> {
        if (useGpgCmd) {
            configure<SigningExtension> {
                useGpgCmd()
            }
        }
        afterEvaluate {
            configure<SigningExtension> {
                val release = rootProject.releaseParams.release.get()
                // Note it would still try to sign the artifacts,
                // however it would fail only when signing a RELEASE version fails
                isRequired = release && !skipSigning
            }
        }
    }

    plugins.withType<JavaPlugin> {
        // This block is executed right after `java` plugin is added to a project
        java {
            sourceCompatibility = JavaVersion.VERSION_1_8
        }

        repositories {
            jcenter()
            ivy {
                url = uri("https://github.com/bulenkov/Darcula/raw/")
                content {
                    includeModule("com.github.bulenkov.darcula", "darcula")
                }
                patternLayout {
                    artifact("[revision]/build/[module].[ext]")
                }
                metadataSources {
                    artifact() // == don't try downloading .pom file from the repository
                }
            }
        }

        tasks {
            withType<JavaCompile>().configureEach {
                options.encoding = "UTF-8"
            }
            withType<ProcessResources>().configureEach {
                from(source) {
                    include("**/*.properties")
                    filteringCharset = "UTF-8"
                    // apply native2ascii conversion since Java 8 expects properties to have ascii symbols only
                    filter(org.apache.tools.ant.filters.EscapeUnicode::class)
                    filter(LineEndings.LF)
                }
                // Text-like resources are normalized to LF (just for consistency purposes)
                // This makes to produce exactly the same jar files no matter which OS is used for the build
                from(source) {
                    include("**/*.dtd")
                    include("**/*.svg")
                    include("**/*.txt")
                    filteringCharset = "UTF-8"
                    filter(LineEndings.LF)
                }
            }
            afterEvaluate {
                // Add default license/notice when missing (e.g. see :src:config that overrides LICENSE)
                withType<Jar>().configureEach {
                    CrLfSpec(LineEndings.LF).run {
                        into("META-INF") {
                            filteringCharset = "UTF-8"
                            duplicatesStrategy = DuplicatesStrategy.EXCLUDE
                            // Note: we need "generic Apache-2.0" text without third-party items
                            // So we use the text from $rootDir/config/ since source distribution
                            // contains altered text at $rootDir/LICENSE
                            textFrom("$rootDir/config/LICENSE")
                            textFrom("$rootDir/NOTICE")
                        }
                    }
                }
            }
            withType<Jar>().configureEach {
                manifest {
                    attributes["Bundle-License"] = "Apache-2.0"
                    attributes["Specification-Title"] = "Apache JMeter"
                    attributes["Specification-Vendor"] = "Apache Software Foundation"
                    attributes["Implementation-Vendor"] = "Apache Software Foundation"
                    attributes["Implementation-Vendor-Id"] = "org.apache"
                    attributes["Implementation-Version"] = rootProject.version
                }
            }
            withType<Test>().configureEach {
                useJUnitPlatform()
                testLogging {
                    exceptionFormat = TestExceptionFormat.FULL
                    showStandardStreams = true
                }
                // Pass the property to tests
                fun passProperty(name: String, default: String? = null) {
                    val value = System.getProperty(name) ?: default
                    value?.let { systemProperty(name, it) }
                }
                passProperty("java.awt.headless")
                passProperty("skip.test_TestDNSCacheManager.testWithCustomResolverAnd1Server")
                passProperty("junit.jupiter.execution.parallel.enabled", "true")
                passProperty("junit.jupiter.execution.timeout.default", "2 m")
                // https://github.com/junit-team/junit5/issues/2041
                // Gradle does not print parameterized test names yet :(
                afterTest(KotlinClosure2<TestDescriptor, TestResult, Any>({ descriptor, result ->
                    if (result.resultType != TestResult.ResultType.SUCCESS) {
                        val test = descriptor as org.gradle.api.internal.tasks.testing.TestDescriptorInternal
                        val classDisplayName = test.className?.let {
                            if (it.endsWith(test.classDisplayName)) it else "${test.className} [${test.classDisplayName}]"
                        } ?: test.classDisplayName
                        val testDisplayName = if (test.name == test.displayName) test.displayName else "${test.name} [${test.displayName}]"
                        println("\n$classDisplayName > $testDisplayName: ${result.resultType}")
                    }
                }))
            }
            withType<SpotBugsTask>().configureEach {
                group = LifecycleBasePlugin.VERIFICATION_GROUP
                if (enableSpotBugs) {
                    description = "$description (skipped by default, to enable it add -Dspotbugs)"
                }
                reports {
                    html.isEnabled = reportsForHumans()
                    xml.isEnabled = !reportsForHumans()
                    // This is for Sonar
                    xml.isWithMessages = true
                }
                enabled = enableSpotBugs
            }
            withType<Javadoc>().configureEach {
                (options as StandardJavadocDocletOptions).apply {
                    noTimestamp.value = true
                    showFromProtected()
                    locale = "en"
                    docEncoding = "UTF-8"
                    charSet = "UTF-8"
                    encoding = "UTF-8"
                    docTitle = "Apache JMeter ${project.name} API"
                    windowTitle = "Apache JMeter ${project.name} API"
                    header = "<b>Apache JMeter</b>"
                    addStringOption("source", "8")
                    bottom =
                        "Copyright © 1998-$lastEditYear Apache Software Foundation. All Rights Reserved."
                    if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
                        addBooleanOption("html5", true)
                        links("https://docs.oracle.com/javase/9/docs/api/")
                    } else {
                        links("https://docs.oracle.com/javase/8/docs/api/")
                    }
                }
            }
        }
    }
}
