plugins {
    id 'conventions.common'
    id 'conventions.versionprops'
    id 'conventions.boringssl'
    id 'conventions.jvm'
    id 'conventions.cpp'
    alias libs.plugins.bnd
    alias libs.plugins.shadow
}

import aQute.bnd.gradle.BundleTaskConvention
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.codehaus.groovy.runtime.InvokerHelper

description = 'Conscrypt: OpenJdk'

// Gradle mostly uses Java os.arch names for architectures which feeds into default
// targetPlatform names.  Notable exception Gradle 6.9.x reports MacOS/ARM as
// arm-v8.
//
// The Maven osdetector plugin (which we recommend to developers) uses different
// arch names, so that's what we need for artifacts.
//
// This class encapsulates both naming schemes as well as other per-platform information
// about native builds, more of which will migrate in here over time.
enum NativeBuildInfo {
    WINDOWS_X86_64("windows", "x86_64"),
    LINUX_X86_64("linux", "x86_64"),
    MAC_X86_64("osx", "x86_64") {
        String libDir() {
            "build.x86"
        }
    },
    MAC_AARCH64("osx", "aarch_64") {
        String libDir() {
            "build.arm"
        }
    };

    static String buildDir = "FIXME" // See below

    public final String os
    public final String arch

    // Maps osdetector arch to Gradle equivalent.
    private static final gradleArchMap = [
            "aarch_64": "aarch64",
            "x86_64" : "x86-64",
    ]

    NativeBuildInfo(String os, String arch) {
        this.os = os
        this.arch = arch
    }

    // Classifier as generated by Maven osdetector.
    String mavenClassifier() {
        "${os}-${arch}"
    }

    // Gradle equivalent to Maven arch
    String gradleArch() {
        gradleArch(arch)
    }

    // Output directory for native resources
    String nativeResourcesDir() {
        "$buildDir/${mavenClassifier()}/native-resources"
    }

    // Directory for native resources inside final jar.
    String jarNativeResourcesDir() {
        nativeResourcesDir() + '/META-INF/native'
    }

    // Target platform identifier as used by Gradle
    String targetPlatform() {
        "${os}_${gradleArch()}"
    }

    String libDir() {
        "build64"
    }

    static String gradleArch(String arch) {
        gradleArchMap.get(arch)
    }

    static NativeBuildInfo findForGradle(String os, String arch) {
        values().find {
            it.os == os && it.gradleArch() == arch
        }
    }

    static NativeBuildInfo find(String os, String arch) {
        values().find {
            it.os == os && it.arch == arch
        }
    }

    static NativeBuildInfo find(NativePlatform targetPlatform) {
        String targetOS = targetPlatform.operatingSystem.name
        String targetArch = targetPlatform.architecture.name
        def result = findForGradle(targetOS, targetArch)
        assert result != null : "Unknown target platform: ${targetOS}-${targetArch}"
        result
    }

    static findAll(String os) {
        values().findAll {
            it.os == os
        }
    }
}

// TODO: There has to be a better way of accessing Gradle properties from Groovy code than this
NativeBuildInfo.buildDir = "$buildDir"

ext {
    jniSourceDir = "$rootDir/common/src/jni"
    assert file("$jniSourceDir").exists()

    // Decide which targets we should build and test
    nativeBuilds = NativeBuildInfo.findAll("${osdetector.os}")
    buildToTest = NativeBuildInfo.find("${osdetector.os}", "${osdetector.arch}")

    assert !nativeBuilds.isEmpty() : "No native builds selected."
    assert buildToTest != null : "No test build selected for os.arch = ${osdetector.arch}"

    // Compatibility with other sub-projects
    preferredSourceSet = buildToTest.mavenClassifier()
    preferredNativeFileDir = buildToTest.nativeResourcesDir()
}

// Since we're not taking a direct dependency on the constants module, we need to add an
// explicit task dependency to make sure the code is generated.
evaluationDependsOn(':conscrypt-constants')

sourceSets {
    main {
        java {
            srcDirs += "${rootDir}/common/src/main/java"
            srcDirs += project(':conscrypt-constants').sourceSets.main.java.srcDirs
        }
        resources {
            srcDirs += "build/generated/resources"
        }
    }

    platform {
        java {
            srcDirs = [ "src/main/java" ]
            includes = [ "org/conscrypt/Platform.java" ]
        }
    }

    test {
        java {
            srcDirs += "${rootDir}/common/src/test/java"
        }
        resources {
            srcDirs += "${rootDir}/common/src/test/resources"
            // This shouldn't be needed but seems to help IntelliJ locate the native artifact.
            // srcDirs += preferredNativeFileDir
            srcDirs += buildToTest.nativeResourcesDir()
        }
    }

    // Add the source sets for each of the native builds
    nativeBuilds.each { nativeBuild ->
        String sourceSetName = nativeBuild.mavenClassifier()
        String nativeDir = nativeBuild.nativeResourcesDir()

        // Main sources for the native build
        "$sourceSetName" {
            output.dir(nativeDir, builtBy: "copyNativeLib${sourceSetName}")
        }
    }
}

compileJava {
    dependsOn generateProperties
}

processResources {
    dependsOn generateProperties
}

sourcesJar {
    dependsOn generateProperties
    dependsOn ':conscrypt-constants:runGen'
}

tasks.register("platformJar", Jar) {
    from sourceSets.platform.output
}

tasks.register("testJar", ShadowJar) {
    archiveClassifier = 'tests'
    configurations = [project.configurations.testRuntimeClasspath]
    from sourceSets.test.output
}

if (isExecutableOnPath('cpplint')) {
    def cpplint = tasks.register("cpplint", Exec) {
        executable = 'cpplint'

        // TODO(nmittler): Is there a better way of getting the JNI sources?
        def pattern = ['**/*.cc', '**/*.h']
        def sourceFiles = fileTree(dir: jniSourceDir, includes: pattern).asPath.tokenize(':')
        // Adding roots so that class #ifdefs don't require full path from the project root.
        args = sourceFiles

        // Capture stderr from the process
        errorOutput = new ByteArrayOutputStream()

        // Need to ignore exit value so that doLast will execute.
        ignoreExitValue = true

        doLast {
            // Create the report file.
            def reportDir = file("${buildDir}/cpplint")
            reportDir.mkdirs()
            def reportFile = new File(reportDir, "report.txt")
            def reportStream = new FileOutputStream(reportFile)

            try {
                // Check for failure
                if (execResult != null) {
                    execResult.assertNormalExitValue()
                }
            } catch (Exception e) {
                // The process failed - get the error report from the stderr.
                String report = errorOutput.toString()

                // Write the report to the console.
                System.err.println(report)

                // Also write the report file.
                reportStream.write(report.bytes)

                // Extension method cpplint.output() can be used to obtain the report
                ext.output = {
                    return report
                }

                // Rethrow the exception to terminate the build.
                throw e
            } finally {
                reportStream.close()
            }
        }
    }
    check.dependsOn cpplint
}

configurations {
    publicApiDocs
    platform
}

artifacts {
    platform platformJar
}

apply from: "$rootDir/gradle/publishing.gradle"
publishing.publications.maven {
    artifact sourcesJar
    artifact javadocJar
}

jar.manifest {
    attributes ('BoringSSL-Version' : boringSslVersion,
                'Automatic-Module-Name' : 'org.conscrypt',
                'Bundle-SymbolicName': 'org.conscrypt',
                '-exportcontents': 'org.conscrypt.*')
}

dependencies {
    // This is used for the @Internal annotation processing in JavaDoc
    publicApiDocs project(':conscrypt-api-doclet')

    // This is listed as compile-only, but we absorb its contents.
    compileOnly project(':conscrypt-constants')

    testImplementation project(':conscrypt-constants'),
            project(path: ':conscrypt-testing', configuration: 'shadow'),
            libs.junit,
            libs.mockito

    testRuntimeOnly sourceSets["$preferredSourceSet"].output

    platformCompileOnly sourceSets.main.output
}

nativeBuilds.each { nativeBuild ->
    // Create the JAR task and add it's output to the published archives for this project
    addNativeJar(nativeBuild)

    // Build the classes as part of the standard build.
    classes.dependsOn sourceSets[nativeBuild.mavenClassifier()].classesTaskName
}

// Adds a JAR task for the native library.
def addNativeJar(NativeBuildInfo nativeBuild) {
    // Create a JAR for this configuration and add it to the output archives.
    SourceSet sourceSet = sourceSets[nativeBuild.mavenClassifier()]
    def jarTask = tasks.register(sourceSet.jarTaskName, Jar) { Jar t ->
        // Depend on the regular classes task
        dependsOn classes
        manifest = jar.manifest
        archiveClassifier = nativeBuild.mavenClassifier()

        from sourceSet.output + sourceSets.main.output

        // add OSGI headers
        t.convention.plugins.bundle = new BundleTaskConvention(t)
        t.doLast {
            t.buildBundle()
        }
    }

    // Add the jar task to the standard build.
    jar.dependsOn jarTask

    // Add it to the publishing archives list.
    publishing.publications.maven.artifact jarTask.get()
}

test {
    include "org/conscrypt/ConscryptOpenJdkSuite.class"
}

def testFdSocket = tasks.register("testFdSocket", Test) {
    include "org/conscrypt/ConscryptOpenJdkSuite.class"
    InvokerHelper.setProperties(testLogging, test.testLogging.properties)
    systemProperties = test.systemProperties
    systemProperty "org.conscrypt.useEngineSocketByDefault", false
}

// Tests that involve interoperation with the OpenJDK TLS provider (generally to
// test renegotiation, since we don't support initiating renegotiation but do
// support peer-initiated renegotiation).  The JDK TLS provider doesn't work
// if Conscrypt is installed as the default provider, so these need to live in
// a different task than the other tests, most of which need Conscrypt to be
// installed to function.
def testInterop = tasks.register("testInterop", Test) {
    include "org/conscrypt/ConscryptEngineTest.class"
    include "org/conscrypt/RenegotiationTest.class"
}
check.dependsOn testInterop

jacocoTestReport {
    additionalSourceDirs.from files("$rootDir/openjdk/src/test/java", "$rootDir/common/src/main/java")
    executionData tasks.withType(Test)
    dependsOn test
}

javadoc {
    dependsOn configurations.publicApiDocs
    options {
        showFromPublic()
        encoding = 'UTF-8'
        doclet = 'org.conscrypt.doclet.FilterDoclet'
        links = ['https://docs.oracle.com/en/java/javase/21/docs/api/java.base/']
        docletpath = configurations.publicApiDocs.files as List
    }
    failOnError false

    doLast {
        copy {
            from "$rootDir/api-doclet/src/main/resources/styles.css"
            into "$buildDir/docs/javadoc"
        }
    }
}

def jniIncludeDir() {
    def result = ""
    java {
        def jdkHome = javaToolchains.compilerFor(toolchain).get().metadata.getInstallationPath()
        result = jdkHome.file("include").toString()
    }
    result
}

model {
    buildTypes {
        release
    }

    components {
        // Builds the JNI library.
        conscrypt_openjdk_jni(NativeLibrarySpec) {
            nativeBuilds.each { nativeBuild ->
                targetPlatform nativeBuild.targetPlatform()
            }

            sources {
                cpp {
                    source {
                        srcDirs "$jniSourceDir/main/cpp"
                        include "**/*.cc"
                    }
                }
            }

            binaries {
                // Build the JNI lib as a shared library.
                withType (SharedLibraryBinarySpec) {
                    cppCompiler.define "CONSCRYPT_OPENJDK"
                    def jdkIncludeDir = jniIncludeDir()
                    def nativeBuild = NativeBuildInfo.find(targetPlatform)
                    String libPath = "$boringsslHome/${nativeBuild.libDir()}"

                    if (toolChain in Clang || toolChain in Gcc) {
                        cppCompiler.args "-Wall",
                                "-Werror",
                                "-fPIC",
                                "-O3",
                                "-std=c++17",
                                "-I$jniSourceDir/main/include",
                                "-I$jniSourceDir/unbundled/include",
                                "-I$boringsslIncludeDir",
                                "-I$jdkIncludeDir",
                                "-I$jdkIncludeDir/linux",
                                "-I$jdkIncludeDir/darwin",
                                "-I$jdkIncludeDir/win32"
                        if (rootProject.hasProperty('checkErrorQueue')) {
                            System.out.println("Compiling with error queue checking enabled")
                            cppCompiler.define "CONSCRYPT_CHECK_ERROR_QUEUE"
                        }

                        // Static link to BoringSSL
                        linker.args "-O3",
                                "-fvisibility=hidden",
                                "-lpthread",
                                libPath + "/libssl.a",
                                libPath + "/libcrypto.a"
                        if (targetPlatform.operatingSystem.isLinux()) {
                            // Static link libstdc++ and libgcc because
                            // they are not available in some restrictive Linux
                            // environments.
                            linker.args "-static-libstdc++",
                                    "-static-libgcc"
                        } else {
                            linker.args "-lstdc++"
                        }
                    } else if (toolChain in VisualCpp) {
                        cppCompiler.define "DLL_EXPORT"
                        cppCompiler.define "WIN32_LEAN_AND_MEAN"
                        cppCompiler.define "NOMINMAX"
                        cppCompiler.define "WIN64"
                        cppCompiler.define "_WINDOWS"
                        cppCompiler.define "UNICODE"
                        cppCompiler.define "_UNICODE"
                        cppCompiler.define "NDEBUG"

                        cppCompiler.args "/nologo",
                                "/MT",
                                "/WX-",
                                "/Wall",
                                "/O2",
                                "/Oi",
                                "/Ot",
                                "/GL",
                                "/GS",
                                "/Gy",
                                "/fp:precise",
                                "/std:c++17",
                                "-wd4514", // Unreferenced inline function removed
                                "-wd4548", // Expression before comma has no effect
                                "-wd4625", // Copy constructor was implicitly defined as deleted
                                "-wd4626", // Assignment operator was implicitly defined as deleted
                                "-wd4710", // function not inlined
                                "-wd4711", // function inlined
                                "-wd4820", // Extra padding added to struct
                                "-wd4946", // reinterpret_cast used between related classes:
                                "-wd4996", // Thread safety for strerror
                                "-wd5027", // Move assignment operator was implicitly defined as deleted
                                "-I$jniSourceDir/main/include",
                                "-I$jniSourceDir/unbundled/include",
                                "-I$boringsslIncludeDir",
                                "-I$jdkIncludeDir",
                                "-I$jdkIncludeDir/win32"

                        // Static link to BoringSSL
                        linker.args "-WX",
                                "ws2_32.lib",
                                "advapi32.lib",
                                "${libPath}\\ssl.lib",
                                "${libPath}\\crypto.lib"
                    }
                }

                // Never build a static library.
                withType(StaticLibraryBinarySpec) {
                    buildable = false
                }
            }
        }
    }

    tasks { t ->
        $.binaries.withType(SharedLibraryBinarySpec).each { binary ->
            def nativeBuild = NativeBuildInfo.find(binary.targetPlatform)
            def classifier = nativeBuild.mavenClassifier()
            def source = binary.sharedLibraryFile

            // Copies the native library to a resource location that will be included in the jar.
            def copyTask = project.tasks.register("copyNativeLib${classifier}", Copy) {
                dependsOn binary.tasks.link
                from source
                // Rename the artifact to include the generated classifier
                rename '(.+)(\\.[^\\.]+)', "\$1-$classifier\$2"
                // Everything under will be included in the native jar.
                into nativeBuild.jarNativeResourcesDir()
            }
            processResources {
                dependsOn copyTask
            }
            processTestResources {
                dependsOn copyTask
            }

            // Now define a task to strip the release binary (linux only)
            if (osdetector.os == 'linux' && (!rootProject.hasProperty('nostrip') ||
                    !rootProject.nostrip.toBoolean())) {
                def stripTask = binary.tasks.taskName("strip")
                project.tasks.register(stripTask as String, Exec) {
                    dependsOn binary.tasks.link
                    executable "strip"
                    args binary.tasks.link.linkedFile.asFile.get()
                }
                copyTask.configure {
                    dependsOn stripTask
                }
            }
        }
    }
}

boolean isExecutableOnPath(executable) {
    FilenameFilter filter = new FilenameFilter() {
        @Override
        boolean accept(File dir, String name) {
            return executable == name
        }
    }
    for(String folder : System.getenv('PATH').split("" + File.pathSeparatorChar)) {
        File[] files = file(folder).listFiles(filter)
        if (files != null && files.size() > 0) {
            return true
        }
    }
    return false
}
