import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform

import java.lang.module.ModuleFinder
import java.util.stream.Stream

plugins {
    id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
    id 'org.gradlex.extra-java-module-info' version '1.14' apply false
    id("com.diffplug.spotless") version "8.3.0" apply false
}

allprojects { subproject ->
    apply plugin: 'org.gradlex.extra-java-module-info'
    extraJavaModuleInfo {
        failOnMissingModuleInfo.set(false)
        skipLocalJars.set(true)
    }
    apply from: "$rootDir/gradle/gradle_scripts/modules.gradle"

    // https://docs.gradle.org/9.0.0/userguide/upgrading_major_version_9.html#reproducible_archives_by_default
    tasks.withType(AbstractArchiveTask).configureEach {
        reproducibleFileOrder = false
        preserveFileTimestamps = true
        useFileSystemPermissions()
    }
}

subprojects {subproject ->
    if (subproject.name == 'dist') {
        return
    }

    apply plugin: 'com.diffplug.spotless'
    spotless {
        java {
            palantirJavaFormat()
            trimTrailingWhitespace()
            endWithNewline()
            importOrder('io.xpipe', 'javafx', '', 'java', '\\#')
        }
    }
}

// Publishing

def sonatypeUser = project.hasProperty('sonatypeUsername') ? project.property('sonatypeUsername') : System.getenv('SONATYPE_USERNAME')
def sonatypePass = project.hasProperty('sonatypePassword') ? project.property('sonatypePassword') : System.getenv('SONATYPE_PASSWORD')

tasks.withType(GenerateModuleMetadata).configureEach {
    enabled = false
}

nexusPublishing  {
    repositories {
        sonatype  {
            nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/'))
            snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/'))
            username = sonatypeUser
            password = sonatypePass
        }
    }
    useStaging = true
}

// Developer config file setup

var devProps = file("$rootDir/app/dev.properties")
if (!devProps.exists()) {
    devProps.text = file("$rootDir/gradle/gradle_scripts/dev_default.properties").text
}

// Project variable functions

static def getArchName() {
    var arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT)
    if (arch == 'amd64' || arch == 'x86_64') {
        return 'x86_64'
    }

    if (arch == 'arm' || arch == 'aarch64') {
        return 'arm64'
    }

    if (arch == 'x86') {
        return 'x86'
    }

    return arch
}

static def getPlatformName() {
    def currentOS = DefaultNativePlatform.currentOperatingSystem
    def platform
    if (currentOS.isWindows()) {
        platform = 'windows'
    }  else if (currentOS.isMacOsX()) {
        platform = 'osx'
    } else {
        platform = 'linux'
    }
    return platform
}

def getJvmArgs() {
    def os = DefaultNativePlatform.currentOperatingSystem
    def jvmRunArgs = [
            "-Dfile.encoding=UTF-8",
            "-Dvisualvm.display.name=$productName",
            "-Djavafx.preloader=" + packageName("core.AppPreloader"),
            "-Djdk.virtualThreadScheduler.parallelism=8"
    ]

    // Virtual threads cause crashes on Windows ARM
    if (os.isWindows() && arch == "arm64") {
        jvmRunArgs += [
                "-D" + propertyName("useVirtualThreads") + "=false"
        ]
    }

    // Disable JDK24 warnings
    jvmRunArgs += [
            "--enable-native-access=com.sun.jna",
            "--enable-native-access=javafx.graphics",
            "--enable-native-access=javafx.web",
            "--sun-misc-unsafe-memory-access=allow",
    ]

    // Module access fixes
    def appPackage = packageName(null)
    jvmRunArgs += [
            "--add-opens", "java.base/java.lang=$appPackage",
            "--add-opens", "java.base/java.net=$appPackage",
            "--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=$appPackage",
            "--add-opens", "java.base/java.nio.file=$appPackage",
            "--add-exports", "javafx.graphics/com.sun.javafx.tk=$appPackage",
            "--add-exports", "jdk.zipfs/jdk.nio.zipfs=io.xpipe.modulefs",
            "--add-opens", "javafx.graphics/com.sun.glass.ui=$appPackage",
            "--add-opens", "javafx.graphics/javafx.stage=$appPackage",
            "--add-opens", "javafx.controls/javafx.scene.control.skin=$appPackage",
            "--add-opens", "javafx.graphics/com.sun.javafx.tk=$appPackage",
            "--add-opens", "javafx.graphics/com.sun.javafx.tk.quantum=$appPackage"
    ]

    if (fullVersion) {
        jvmRunArgs += [
                "--add-opens", "java.base/java.io=" + packageName("ext.proc", null),
                "--add-opens", "org.apache.commons.io/org.apache.commons.io.input=" + packageName("ext.proc", null),
        ]
    }

    // Use project liliput
    jvmRunArgs += ['-XX:+UseCompactObjectHeaders']

    // Reduce heap usage with deduplication
    jvmRunArgs += ['-XX:+UseStringDeduplication']

    // GC config
    jvmRunArgs += [
            '-XX:+UseG1GC',
            '-Xms200m',
            '-Xmx4G',
            '-XX:MinHeapFreeRatio=15',
            '-XX:MaxHeapFreeRatio=25',
            '-XX:GCTimeRatio=9',
            // The default makes GC pauses longer for some reason
            '-XX:G1HeapRegionSize=4m'
    ]

    // Why is this not on by default? ...
    // https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/net/doc-files/net-properties.html
    jvmRunArgs += [
            '-Djava.net.useSystemProxies=true'
    ]

    // Fix platform theme detection on macOS
    if (os.isMacOsX()) {
        jvmRunArgs += ["-Dapple.awt.application.appearance=system"]
    }

    // Use new metal pipeline on macOS
    if (os.isMacOsX()) {
        jvmRunArgs += ["-Dprism.order=mtl,es2,sw"]
    }

    if (os.isLinux()) {
        jvmRunArgs.addAll("--add-opens", "java.desktop/sun.awt.X11=" + packageName(null))
    }

    return jvmRunArgs
}

def getWindowsSchemaCanonicalVersion() {
    def v = canonicalVersionString
    def last = isStage ? versionReleaseNumber : 0
    if (v.split("\\.").length == 2) {
        v = v + ".0"
    }
    return v + "." + last
}

// https://stackoverflow.com/questions/79720795/replacing-deprecated-projectexec-in-dofirst-dolast
interface InjectedExecOps {
    @javax.inject.Inject
    ExecOperations getExecOps()
}


// Project variables

project.ext {
    // Release pipeline config
    isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE'))
    isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE'))
    ci = System.getenv('CI') != null
    obfuscate = true
    fullVersion = file("$rootDir/private_files.txt").exists()
    bundleCds = ci && fullVersion

    // Names
    productName = isStage ? 'XPipe PTB' : 'XPipe'
    kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'
    flatcaseProductName = isStage ? 'xpipeptb' : 'xpipe'
    snakeProductName = isStage ? 'xpipe_ptb' : 'xpipe'
    artifactBaseName = "xpipe"

    // Info
    publisher = 'XPipe UG (haftungsbeschränkt)'
    shortDescription = isStage ? 'XPipe PTB Public Test Build' : 'Your entire server infrastructure at your fingertips'
    longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'
    website = 'https://xpipe.io'
    sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'
    authors = 'Christopher Schnick'

    // Version info
    rawVersion = file('version').text.strip()
    versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT')
    versionReleaseNumber = rawVersion.split('-').length == 2 ? Integer.parseInt(rawVersion.split('-')[1]) : 1
    canonicalVersionString = rawVersion.split('-').length == 2 ? rawVersion.split('-')[0] : rawVersion
    buildId = UUID.nameUUIDFromBytes(versionString.getBytes())
    windowsSchemaCanonicalVersion = getWindowsSchemaCanonicalVersion()

    // Changelog info
    changelog = file("dist/changelog/${canonicalVersionString}.md").exists() ? file("dist/changelog/${canonicalVersionString}.md").text.strip() + '\n' : ""
    changelogFile = file("$rootDir/dist/changelog/${versionString}.md").exists() ?
            file("$rootDir/dist/changelog/${versionString}.md") :
            file("$rootDir/dist/changelog/${canonicalVersionString}.md")
    incrementalChangelogFile = file("$rootDir/dist/changelog/${canonicalVersionString}_incremental.md")
    announce = System.getenv('SKIP_ANNOUNCEMENT') == null || !Boolean.parseBoolean(System.getenv('SKIP_ANNOUNCEMENT'))

    // Signing config
    signingKeyId = project.hasProperty('signingKeyId') ? project.property("signingKeyId") : (System.getenv('GPG_KEY_ID') != null ? System.getenv('GPG_KEY_ID') : "")
    signingKey = project.hasProperty('signingKeyFile') ? file(project.property("signingKeyFile")).text : (System.getenv('GPG_KEY') != null ? System.getenv('GPG_KEY') : "")
    signingPassword = project.hasProperty('signingKeyPassword') ? project.property("signingKeyPassword") : (System.getenv('GPG_KEY_PASSWORD') != null ? System.getenv('GPG_KEY_PASSWORD') : "")

    // Extension config
    allExtensions = Stream.concat(Stream.of(project(':base')), Arrays.stream(file("$rootDir/ext").list())
            .filter(s -> file("$rootDir/ext/$s/build.gradle").exists())
            .filter(s -> s != 'base')
            .map(l -> project(":$l"))).toList()
    privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : []

    // Build config
    os = org.gradle.internal.os.OperatingSystem.current()
    groupName = 'io.xpipe'
    artifactName = 'app'
    arch = getArchName()
    jvmRunArgs = getJvmArgs()
    useBundledJna = fullVersion
    sentryUrl = "https://fd5f67ff10764b7e8a704bec9558c8fe@o1084459.ingest.sentry.io/6094279"

    // JPackage config
    jpackageExecutableName = "xpiped"
    jpackageMacOsBundleName = isStage ? groupName + '.ptb-app' : groupName + '.app'
    jpackageReleaseArguments = jvmRunArgs + [
            "-D" + propertyName("version") + "=" + versionString,
            "-D" + propertyName("build") + "=$versionString/${new Date().format('yyyy-MM-dd-HH-mm')}",
            "-D" + propertyName("buildId") + "=" + buildId,
            "-D" + propertyName("fullVersion") + "=" + fullVersion,
            "-D" + propertyName("staging") + "=" + isStage,
            "-D" + propertyName("sentryUrl") + "=" + sentryUrl,
            '-Djna.nosys=false',
            '-Djna.nounpack=true',
            '-Djna.noclasspath=true'
    ]
    if (os.isMacOsX()) {
        jpackageReleaseArguments += "-Xdock:name=$productName"
    }
    if (isFullRelease || isStage) {
        jpackageReleaseArguments += "-XX:+DisableAttachMechanism"
    }

    // JavaFX config
    devJavafxVersion = '27-ea+4'
    platformName = getPlatformName()
    useBundledJavaFx = fullVersion
    bundledJdkJavaFx = ModuleFinder.ofSystem().find("javafx.base").isPresent()
    // Define a custom JavaFX SDK location
    customJavaFxLibsPath = null; // file("C:\\Projects\\jfx\\build\\sdk\\lib")
    customJavaFxJmodsPath = null; // file("C:\\Projects\\jfx\\build\\jmods")

    // Other
    deeplApiKey = findProperty('DEEPL_API_KEY') != null ? findProperty('DEEPL_API_KEY') : ""

    def injected = project.objects.newInstance(InjectedExecOps)
    execOps = injected.execOps
}

group = groupName
version = versionString

// Global helper functions

def propertyName(String s) {
    return groupName + "." + artifactName + "." + s
}

def propertyName(String module, String s) {
    return groupName + "." + module + "." + s
}

def packageName(String s) {
    return groupName + "." + artifactName + (s != null ? "." + s : "")
}

def packageName(String module, String s) {
    return groupName + "." + module + (s != null ? "." + s : "")
}

def replaceVariablesInFileAsString(String f, Map<String, String> replacements) {
    def text = file(f).text
    def replaced = text.replace(replacements)
    return replaced
}

def replaceVariablesInFile(String f, Map<String, String> replacements) {
    def fileName = file(f).getName()
    def text = file(f).text
    def replaced = text.replace(replacements)
    def build = "${project.layout.buildDirectory.get()}/${UUID.randomUUID()}"
    file(build).mkdirs()
    def temp = "$build/$fileName"
    file(temp).text = replaced
    return file(temp)
}

// Test results

def testTasks = [
        project(':core').getTasksByName('test', true),
        project(':app').getTasksByName('test', true),
        project(':base').getTasksByName('localTest', true),
        project(':proc').getTasksByName('localTest', true),
]


if (file("cli").exists()) {
    testTasks += [project(':cli').getTasksByName('remoteTest', true)]
}

tasks.register('testReport', TestReport) {
    getDestinationDirectory().set(file("$rootProject.buildDir/reports/all"))
    getTestResults().from(testTasks.stream().filter {!it.isEmpty()}.map {
        file("${it.project.buildDir.get(0)}/test-results/${it.name.get(0)}/binary")
    }.toList())
}

tasks.register('testAll', DefaultTask) {
    for (final def t in testTasks) {
        t.forEach { dependsOn(it.getTaskDependencies()) }
    }
    doFirst {
        for (final def t in testTasks) {
            t.forEach { it.executeTests() }
        }
    }
    finalizedBy(testReport)
}

// Checks

if (System.getProperty("java.home").contains(" ")) {
    throw new IllegalArgumentException("Your JDK home path contains spaces. This will break several gradle plugins")
}

if (isFullRelease && rawVersion.contains("-")) {
    throw new IllegalArgumentException("Releases must have canonical versions")
}

if (isStage && !rawVersion.contains("-")) {
    throw new IllegalArgumentException("Stage releases must have release numbers")
}

