/*
 * 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.snom.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.properties.dsl.lastEditYear
import com.github.vlsi.gradle.properties.dsl.props
import com.github.vlsi.gradle.release.RepositoryType
import net.ltgt.gradle.errorprone.errorprone
import org.ajoberstar.grgit.Grgit
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.sonarqube.gradle.SonarQubeProperties

plugins {
    java
    kotlin("jvm") apply false
    jacoco
    checkstyle
    id("org.jetbrains.gradle.plugin.idea-ext") apply false
    id("org.nosphere.apache.rat")
    id("com.github.autostyle")
    id("com.github.spotbugs")
    id("net.ltgt.errorprone") apply false
    id("org.sonarqube")
    id("com.github.vlsi.crlf")
    id("com.github.vlsi.gradle-extensions")
    id("com.github.vlsi.ide")
    id("com.github.vlsi.stage-vote-release")
    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(lastEditYear().toString())

// 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)
    verbose.set(true)
    // Note: patterns are in non-standard syntax for RAT, so we use exclude(..) instead of excludeFile
    exclude(rootDir.resolve(".ratignore").readLines())
}

tasks.validateBeforeBuildingReleaseArtifacts {
    dependsOn(rat)
}

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\\."), "source")
            put(Regex("."), "binaries")
        }
        staleRemovalFilters {
            excludes.add(Regex("release/.*/HEADER\\.html"))
        }
    }
    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 = props.bool("spotbugs", default = false)
val enableErrorprone by props()
val ignoreSpotBugsFailures by props()
val skipCheckstyle by props()
val skipAutostyle by props()
val werror by props(true) // treat javac warnings as errors
// Allow to skip building source/binary distributions
val skipDist by extra {
    boolProp("skipDist") ?: false
}
// Inherited from stage-vote-release-plugin: skipSign, useGpgCmd

allprojects {
    apply(plugin = "com.github.vlsi.gradle-extensions")

    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, valueProvider: () -> String) {
    properties.getOrPut(name) { mutableSetOf<Any>() }
        .also {
            @Suppress("UNCHECKED_CAST")
            (it as MutableCollection<Any>).add(object {
                // SonarQube calls toString when converting properties to values
                // (see SonarQubeProperties), so we use that to emulate "lazy properties"
                override fun toString() = valueProvider()
            })
        }
}

if (jacocoEnabled) {
    val mergedCoverage = jacocoReport.get().reports.xml.outputLocation.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") {
                        // Note: report is created with lower-case xml, and then
                        // the created entry MUST be retrieved as upper-case XML
                        reports.named("XML").get().destination.toString()
                    }
                }
            }
        }
    }
}

fun com.github.autostyle.gradle.BaseFormatExtension.license() {
    licenseHeader(rootProject.ide.licenseHeader) {
        copyrightStyle("bat", com.github.autostyle.generic.DefaultCopyrightStyle.REM)
        copyrightStyle("cmd", com.github.autostyle.generic.DefaultCopyrightStyle.REM)
        addBlankLineAfter.set(true)
    }
    trimTrailingWhitespace()
    endWithNewline()
}

allprojects {
    group = "org.apache.jmeter"
    version = rootProject.version

    repositories {
        // RAT and Autostyle dependencies
        mavenCentral()
    }

    // 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 (!skipAutostyle) {
        apply(plugin = "com.github.autostyle")
        autostyle {
            kotlinGradle {
                license()
                ktlint("ktlint".v)
            }
            format("configs") {
                filter {
                    include("**/*.sh", "**/*.bsh", "**/*.cmd", "**/*.bat")
                    include("**/*.properties", "**/*.yml")
                    include("**/*.xsd", "**/*.xsl", "**/*.xml")
                    // Autostyle does not support gitignore yet https://github.com/autostyle/autostyle/issues/13
                    exclude("out/**")
                    if (project == rootProject) {
                        exclude(rootDir.resolve(".ratignore").readLines())
                        exclude("gradlew*")
                        // Generated by batch tests. It ignores log4j2.xml, however it is not that important
                        // The configuration will be removed when Autostyle will use .gitignore
                        exclude("bin/*.xml")
                    } else {
                        exclude("bin/**")
                    }
                }
                license()
            }
            format("markdown") {
                filter.include("**/*.md")
                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
                configProperties = mapOf(
                    "cache_file" to buildDir.resolve("checkstyle/cacheFile")
                )
            }
            val sourceSets: SourceSetContainer by project
            if (sourceSets.isNotEmpty()) {
                val checkstyleTasks = tasks.withType<Checkstyle>()
                checkstyleTasks.configureEach {
                    // Checkstyle 8.26 does not need classpath, see https://github.com/gradle/gradle/issues/14227
                    classpath = files()
                }

                tasks.register("checkstyleAll") {
                    dependsOn(checkstyleTasks)
                }
                tasks.register("checkstyle") {
                    group = LifecycleBasePlugin.VERIFICATION_GROUP
                    description = "Executes Checkstyle verifications"
                    dependsOn("checkstyleAll")
                    dependsOn("autostyleCheck")
                }
                // Autostyle produces more meaningful error messages, so we ensure it is executed before Checkstyle
                if (!skipAutostyle) {
                    for (s in sourceSets.names) {
                        tasks.named("checkstyle" + s.capitalize()) {
                            mustRunAfter("autostyleApply")
                            mustRunAfter("autostyleCheck")
                        }
                    }
                }
            }
        }
        apply(plugin = "com.github.spotbugs")

        spotbugs {
            toolVersion.set("spotbugs".v)
            ignoreFailures.set(ignoreSpotBugsFailures)
        }

        if (!skipAutostyle) {
            autostyle {
                java {
                    license()
                    importOrder("static ", "java.", "javax", "org", "net", "com", "")
                    removeUnusedImports()
                    indentWithSpaces(4)
                }
            }
        }
        tasks.register("style") {
            group = LifecycleBasePlugin.VERIFICATION_GROUP
            description = "Formats code (license header, import order, whitespace at end of line, ...) and executes Checkstyle verifications"
            if (!skipAutostyle) {
                dependsOn("autostyleApply")
            }
            if (!skipCheckstyle) {
                dependsOn("checkstyleAll")
            }
        }

        if (enableErrorprone) {
            apply(plugin = "net.ltgt.errorprone")
            dependencies {
                "errorprone"("com.google.errorprone:error_prone_core:${"errorprone".v}")
                "annotationProcessor"("com.google.guava:guava-beta-checker:1.0")
            }
            tasks.withType<JavaCompile>().configureEach {
                options.compilerArgs.addAll(listOf("-Xmaxerrs", "10000", "-Xmaxwarns", "10000"))
                options.errorprone {
                    disableWarningsInGeneratedCode.set(true)
                    errorproneArgs.add("-XepExcludedPaths:.*/javacc/.*")
                    disable(
                        "ComplexBooleanConstant",
                        "EqualsGetClass",
                        "InlineMeSuggester",
                        "OperatorPrecedence",
                        "MutableConstantField",
                        // "ReferenceEquality",
                        "SameNameButDifferent",
                        "TypeParameterUnusedInFormals"
                    )
                    // Analyze issues, and enable the check
                    disable(
                        "MissingSummary",
                        "EmptyBlockTag",
                        "BigDecimalEquals",
                        "StringSplitter"
                    )
                }
            }
        }
    }
    plugins.withId("groovy") {
        if (!skipAutostyle) {
            autostyle {
                groovy {
                    license()
                    importOrder("static ", "java.", "javax", "org", "net", "com", "")
                    indentWithSpaces(4)
                }
            }
        }
    }

    plugins.withId("org.jetbrains.kotlin.jvm") {
        configure<KotlinJvmProjectExtension> {
            // Require explicit access modifiers and require explicit types for public APIs.
            // See https://kotlinlang.org/docs/whatsnew14.html#explicit-api-mode-for-library-authors
            if (props.bool("kotlin.explicitApi", default = true)) {
                explicitApi()
            }
        }
        tasks.withType<KotlinCompile>().configureEach {
            kotlinOptions {
                if (!name.startsWith("compileTest")) {
                    apiVersion = "kotlin.api".v
                }
            }
        }
        if (!skipAutostyle) {
            autostyle {
                kotlin {
                    license()
                    trimTrailingWhitespace()
                    ktlint("ktlint".v)
                    endWithNewline()
                }
            }
        }
    }

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

        val testTasks = tasks.withType<Test>()
        val javaExecTasks = tasks.withType<JavaExec>()
            .matching { it.name != "runGui" }
        // 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.required.set(reportsForHumans())
                xml.required.set(!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)
    }

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

        repositories {
            mavenCentral()
        }

        tasks {
            withType<JavaCompile>().configureEach {
                options.encoding = "UTF-8"
                options.compilerArgs.add("-Xlint:deprecation")
                if (werror) {
                    options.compilerArgs.add("-Werror")
                }
            }
            withType<ProcessResources>().configureEach {
                filteringCharset = "UTF-8"
                eachFile {
                    if (name.endsWith(".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)
                    } else if (name.endsWith(".dtd") || name.endsWith(".svg") ||
                        name.endsWith(".txt")
                    ) {
                        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
                }

                outputs.cacheIf("test outcomes sometimes depends on third-party systems, so we should not cache it for now") {
                    false
                }

                // Pass the property to tests
                fun passProperty(name: String, default: String? = null) {
                    val value = System.getProperty(name) ?: default
                    value?.let { systemProperty(name, it) }
                }
                System.getProperties().filter {
                    it.key.toString().startsWith("jmeter.properties.")
                }.forEach {
                    systemProperty(it.key.toString().substring("jmeter.properties.".length), it.value)
                }
                passProperty("java.awt.headless")
                passProperty("skip.test_TestDNSCacheManager.testWithCustomResolverAnd1Server")
                passProperty("junit.jupiter.execution.parallel.enabled", "true")
                passProperty("junit.jupiter.execution.timeout.default", "2 m")
            }
            withType<SpotBugsTask>().configureEach {
                group = LifecycleBasePlugin.VERIFICATION_GROUP
                if (enableSpotBugs) {
                    description = "$description (skipped by default, to enable it add -Pspotbugs)"
                }
                reports {
                    // xml goes for SonarQube, so we always create it just in case
                    create("xml")
                    if (reportsForHumans()) {
                        create("html")
                    }
                }
                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/")
                    }
                }
            }
        }
    }
}
