import groovy.xml.XmlSlurper
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.tasks.JavaExec
import org.gradle.internal.jvm.Jvm
import org.gradle.internal.os.OperatingSystem
import org.gradle.jvm.toolchain.JvmVendorSpec

plugins {
    id 'base'
    id 'org.jetbrains.kotlin.jvm' version '2.2.21' apply false
    id 'org.jetbrains.kotlin.plugin.compose' version '2.2.21' apply false
    id 'org.jetbrains.compose' version '1.9.1' apply false
}

def javaLanguageVersion = 21
def releaseAppName = 'WireShare'
def releaseAppId = 'org.teamhermes.WireShare'
def releaseVendor = 'Hermes'
def releaseAppVersion = '7.0'
def windowsUpgradeUuid = '5CAFE0D2-ABB3-4F01-8CFE-C564E00C09EC'
group = releaseAppId
version = releaseAppVersion

def hostOs = OperatingSystem.current()
def hostArch = (System.getProperty('os.arch') ?: '').toLowerCase(Locale.ROOT)
def isArm64 = hostArch == 'aarch64' || hostArch == 'arm64'
def isX86_64 = hostArch == 'amd64' || hostArch == 'x86_64'
def localJbrRoot = new File(System.getProperty('user.home'), '.gradle/jdks')
def macArm64RunJavaHomeOverride = providers.gradleProperty('wireShareMacArm64RunJavaHome').orNull
def macArm64RunJbrSelector = providers.gradleProperty('wireShareMacArm64RunJbrSelector').orNull
def macArm64RunDefaultJbrSelector = '21.0.10'
def macArm64RunJbrDirectories = localJbrRoot.isDirectory()
    ? (localJbrRoot.listFiles()
        ?.findAll { candidate ->
            candidate.isDirectory()
                && candidate.name.startsWith('jbr')
                && candidate.name.contains('21.')
                && candidate.name.contains('-osx-aarch64-')
        } ?: [])
    : []
def macArm64RunPreferredJbrDirectories = macArm64RunJbrDirectories.findAll { candidate ->
    candidate.name.contains(macArm64RunJbrSelector ?: macArm64RunDefaultJbrSelector)
}
def macArm64RunJbrJava = macArm64RunJavaHomeOverride != null
    ? new File(macArm64RunJavaHomeOverride, 'bin/java')
    : ((macArm64RunPreferredJbrDirectories.isEmpty() ? macArm64RunJbrDirectories : macArm64RunPreferredJbrDirectories)
            .sort { left, right -> left.name <=> right.name }
            ?.collect { candidate -> new File(candidate, 'Contents/Home/bin/java') }
            ?.findAll { candidate -> candidate.canExecute() }
            ?.last())
// Avoid the HotSpot AArch64 C1 OSR crash seen on older JDK 21 runtimes (for example JDK-8335662 backported in 21.0.8).
def arm64HotSpotOsrWorkaroundArg = '-XX:-UseOnStackReplacement'
def macArm64RunUsesTier1Jit = providers.gradleProperty('wireShareMacArm64RunTier1')
    .map { it.toBoolean() }
    .getOrElse(false)
def macArm64RunUsesEd2kCompileExclude = providers.gradleProperty('wireShareMacArm64RunEd2kCompileExclude')
    .map { it.toBoolean() }
    .getOrElse(false)
def macArm64RunEd2kCompileExcludePattern = 'org/limewire/ed2k/backend/Ed2kServiceImpl*.*()'
def appleExtensionsJarName = 'Apple' + 'JavaExtensions'
def factoryProviderToken = 'FactoryProvider' + '.newFactory'
def guiceInternalToken = ['com', 'google', 'inject', 'internal'].join('.')
def appleExtensionsToken = 'Apple' + 'JavaExtensions'
def torrentWrapperToken = 'torrent' + '-wrapper'
def libtorrentWrapperToken = 'libtorrent' + '-wrapper'
def addOpensToken = '--add' + '-opens'

def managedGuiceVersion = gradle.ext.has('managedGuiceVersion')
    ? gradle.ext.managedGuiceVersion
    : '6.0.0'

def macosCodesignIdentity = providers.gradleProperty('macosCodesignIdentity')
    .orElse(providers.environmentVariable('MACOS_CODESIGN_IDENTITY'))
def macosCodesignKeychain = providers.gradleProperty('macosCodesignKeychain')
    .orElse(providers.environmentVariable('MACOS_CODESIGN_KEYCHAIN'))
def macosNotaryProfile = providers.gradleProperty('macosNotaryProfile')
    .orElse(providers.environmentVariable('MACOS_NOTARY_PROFILE'))
def windowsSignToolPath = providers.gradleProperty('windowsSignToolPath')
    .orElse(providers.environmentVariable('WINDOWS_SIGNTOOL_PATH'))
def windowsCodesignCertificatePath = providers.gradleProperty('windowsCodesignCertificatePath')
    .orElse(providers.environmentVariable('WINDOWS_CODESIGN_CERTIFICATE_PATH'))
def windowsCodesignCertificatePassword = providers.gradleProperty('windowsCodesignCertificatePassword')
    .orElse(providers.environmentVariable('WINDOWS_CODESIGN_CERTIFICATE_PASSWORD'))
def windowsCodesignCertificateThumbprint = providers.gradleProperty('windowsCodesignCertificateThumbprint')
    .orElse(providers.environmentVariable('WINDOWS_CODESIGN_CERTIFICATE_THUMBPRINT'))
def windowsTimestampUrl = providers.gradleProperty('windowsTimestampUrl')
    .orElse(providers.environmentVariable('WINDOWS_TIMESTAMP_URL'))
    .orElse('http://timestamp.digicert.com')
def windowsSigningConfigured = windowsCodesignCertificatePath.isPresent() || windowsCodesignCertificateThumbprint.isPresent()

def legacySkippedDependencies = [
    'AnimatedTransitions',
    'MozillaGlue',
    'MozillaInterfaces',
    'aopalliance',
    'compile/' + appleExtensionsJarName,
    'daap',
    'gettext-commons',
    'guice',
    'guice-assistedinject',
    'htmlcleaner',
    'jdic',
    'jogg',
    'jorbis',
    'jna',
    'mozswing',
    'mozdom4java',
    'onion-common',
    'onion-fec',
    'smack',
    'smackx',
    'windows/iTunes',
] as Set

def legacyExplicitArtifacts = [:]

def ivyDependenciesForOrg = { Project project, String organization ->
    File ivyFile = project.file('ivy.xml')
    if (!ivyFile.exists()) {
        return []
    }

    def ivy = new XmlSlurper().parse(ivyFile)
    ivy.dependencies.dependency
        .findAll { dependency -> dependency.@org.text() == organization }
        .collect { dependency -> dependency.@name.text() }
        .findAll { dependencyName -> dependencyName }
        .unique()
}

def limewireProjectDependencies = { Project project ->
    ivyDependenciesForOrg(project, 'org.limewire')
        .findAll { dependencyName -> dependencyName && dependencyName != project.name }
}

def legacyExternalDependencies = { Project project ->
    ivyDependenciesForOrg(project, 'none').collectMany { dependencyName ->
        if (legacySkippedDependencies.contains(dependencyName)) {
            return []
        }

        def definition = legacyExplicitArtifacts[dependencyName]
        if (definition == null) {
            throw new GradleException("No explicit replacement declared for legacy Ivy dependency '${dependencyName}' in ${project.path}")
        }

        if (definition instanceof Map) {
            return [definition + [name: dependencyName]]
        }

        return [[name: dependencyName, scope: 'implementation', notation: definition]]
    }
}

def hostJlibtorrentNativeTaskName = {
    if (hostOs.isMacOsX()) {
        return isArm64 ? 'nativeMacOSArm64Jar' : 'nativeMacOSX86_64Jar'
    }
    if (hostOs.isLinux()) {
        return isArm64 ? 'nativeLinuxArm64Jar' : 'nativeLinuxX86_64Jar'
    }
    if (hostOs.isWindows()) {
        return 'nativeWindowsX86_64Jar'
    }
    throw new GradleException("Unsupported host platform for jlibtorrent natives: ${hostOs.name()}/${hostArch}")
}

def managedJlibtorrentUsePublishedNatives = gradle.ext.has('managedJlibtorrentUsePublishedNatives') &&
    gradle.ext.managedJlibtorrentUsePublishedNatives

def hostJlibtorrentNativeArtifactName = { String version ->
    if (hostOs.isMacOsX()) {
        return isArm64
            ? "jlibtorrent-macosx-arm64-${version}.jar"
            : "jlibtorrent-macosx-x86_64-${version}.jar"
    }
    if (hostOs.isLinux()) {
        return isArm64
            ? "jlibtorrent-linux-arm64-${version}.jar"
            : "jlibtorrent-linux-x86_64-${version}.jar"
    }
    if (hostOs.isWindows()) {
        return "jlibtorrent-windows-${version}.jar"
    }
    throw new GradleException("Unsupported host platform for jlibtorrent native artifact: ${hostOs.name()}/${hostArch}")
}

def messageBundleSourceDir = rootDir.toPath().resolve('main/lib/messagebundles').toFile()
def generatedMessageBundleClassesDir = layout.buildDirectory.dir('generated/messagebundles/classes')
def generatedMessageBundleJarDir = layout.buildDirectory.dir('generated/messagebundles/lib')

String toJavaBundleLocaleName(String localeName) {
    if (!localeName.contains('@')) {
        return localeName
    }

    int atIndex = localeName.indexOf('@')
    def baseLocale = localeName.substring(0, atIndex)
    def variant = localeName.substring(atIndex + 1)
    def tokens = new ArrayList<String>(Arrays.asList(baseLocale.split('_', -1)))

    while (tokens.size() < 2) {
        tokens.add('')
    }

    if (tokens.size() == 2) {
        tokens.add(variant)
    } else {
        def existingVariant = tokens.get(tokens.size() - 1)
        tokens.set(tokens.size() - 1, existingVariant ? "${existingVariant}_${variant}" : variant)
    }

    return tokens.join('_')
}

tasks.register('generateMessageBundleClasses') {
    inputs.files(fileTree(messageBundleSourceDir) {
        include '*.po'
    })
    outputs.dir(generatedMessageBundleClassesDir)

    doLast {
        def outputDir = generatedMessageBundleClassesDir.get().asFile
        delete(outputDir)
        outputDir.mkdirs()

        fileTree(messageBundleSourceDir)
            .matching { include '*.po' }
            .files
            .findAll { file -> file.isFile() }
            .sort()
            .each { File poFile ->
                def sourceLocaleName = poFile.name[0..-4]
                def localeName = toJavaBundleLocaleName(sourceLocaleName)
                def command = [
                    'msgfmt',
                    '--java2',
                    '-d', outputDir.absolutePath,
                    '-r', 'org.limewire.i18n.Messages',
                    '-l', localeName,
                    poFile.name,
                ]
                def process = new ProcessBuilder(command)
                    .directory(messageBundleSourceDir)
                    .redirectErrorStream(true)
                    .start()
                def output = new StringBuilder()
                process.inputStream.eachLine { line ->
                    output.append(line).append(System.lineSeparator())
                }
                def exitCode = process.waitFor()
                if (exitCode != 0) {
                    throw new GradleException(
                        "msgfmt failed for ${poFile.name} [locale ${sourceLocaleName} -> ${localeName}] (${command.join(' ')}):\n${output.toString().trim()}"
                    )
                }
            }
    }
}

tasks.register('messagesJar', Jar) {
    dependsOn tasks.named('generateMessageBundleClasses')
    archiveBaseName = 'messages'
    archiveVersion = ''
    destinationDirectory = generatedMessageBundleJarDir
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from(generatedMessageBundleClassesDir)
}

def generatedMessagesJar = tasks.named('messagesJar').flatMap { archiveTask -> archiveTask.archiveFile }

legacyExplicitArtifacts.putAll([
    'AppFramework'       : 'org.jdesktop:appframework:1.0.3',
    'TimingFramework'    : 'net.java.dev.timingframework:timingframework:1.0',
    'clink'              : 'org.cybergarage.upnp:core:2.1.5',
    'commons-codec'      : 'commons-codec:commons-codec:1.17.1',
    'commons-logging'    : 'commons-logging:commons-logging:1.3.5',
    'dnsjava'            : 'dnsjava:dnsjava:3.6.3',
    'fmj'                : 'org.jitsi:fmj:1.0.2-jitsi',
    'foxtrot-core'       : 'foxtrot:foxtrot-core:4.0',
    'glazedlists'        : 'net.java.dev.glazedlists:glazedlists_java15:1.9.1',
    'google-collect'     : 'com.google.guava:guava:33.2.1-jre',
    'httpclient'         : 'org.apache.httpcomponents:httpclient:4.5.14',
    'httpcore'           : 'org.apache.httpcomponents:httpcore:4.4.16',
    'httpcore-nio'       : 'org.apache.httpcomponents:httpcore-nio:4.4.16',
    'hsqldb'             : 'hsqldb:hsqldb:1.8.0.10',
    'icu4j'              : 'com.ibm.icu:icu4j:76.1',
    'jaudiotagger'       : 'net.jthink:jaudiotagger:2.2.5',
    'jcip-annotations'   : 'net.jcip:jcip-annotations:1.0',
    'jmdns'              : 'org.jmdns:jmdns:3.5.10',
    'json'               : 'org.json:json:20240303',
    'jxlayer'            : 'org.swinglabs:jxlayer:3.0.4',
    'log4j'              : 'log4j:log4j:1.2.17',
    'messages'           : [
        scope   : 'runtimeOnly',
        notation: files(generatedMessagesJar) {
            builtBy tasks.named('messagesJar')
        }
    ],
    'miglayout'          : 'com.miglayout:miglayout-swing:5.3',
    'mp3spi'             : 'com.googlecode.soundlibs:mp3spi:1.9.5.4',
    'osx/asm'            : 'org.ow2.asm:asm:9.7',
    'osx/cglib'          : 'cglib:cglib:3.3.0',
    'osx/rococoa-cocoa'  : 'org.rococoa:rococoa-cocoa:0.5',
    'osx/rococoa-core'   : 'org.rococoa:rococoa-core:0.5',
    'osx/slf4j-api'      : 'org.slf4j:slf4j-api:1.7.36',
    'osx/slf4j-jdk14'    : 'org.slf4j:slf4j-jdk14:1.7.36',
    'swingx'             : 'org.swinglabs:swingx:1.0',
    'tritonus'           : 'com.googlecode.soundlibs:tritonus-share:0.3.7.4',
    'windows/jacob'      : 'io.github.osobolev:jacob:1.21',
])

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url = 'https://maven.pkg.jetbrains.space/public/p/compose/dev' }
    }
}

evaluationDependsOn(':jlibtorrent')

def androidTaskRequested = gradle.startParameter.taskNames.any { taskName ->
    taskName == 'buildAndroid' ||
        taskName.startsWith(':android-app:') ||
        taskName.toLowerCase(Locale.ROOT).contains('android')
}

configure(subprojects.findAll { !['jlibtorrent', 'android-app', 'desktop-compose-app'].contains(it.name) }) {
    apply plugin: 'java-library'

    group = 'org.limewire'
    version = releaseAppVersion

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(javaLanguageVersion)
        }
        withSourcesJar()
    }

    sourceSets {
        main {
            java.setSrcDirs(['src/main/java'])
            resources.setSrcDirs(['src/main/resources'])
            if (project.name == 'core-glue') {
                java.exclude('org/limewire/core/impl/mozilla/**')
            }
            if (project.name == 'gnutella-core') {
                java.exclude('com/limegroup/gnutella/daap/**')
                java.exclude('com/limegroup/gnutella/util/FECUtilsImpl.java')
            }
        }
        test {
            java.setSrcDirs(['src/test/java'])
            resources.setSrcDirs(['src/test/resources'])
        }
    }

    tasks.withType(JavaCompile).configureEach {
        options.encoding = 'UTF-8'
        options.release = javaLanguageVersion
        options.compilerArgs.add('-Xlint:unchecked')
    }

    tasks.withType(Jar).configureEach {
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    }

    dependencies {
        implementation "com.google.inject:guice:${managedGuiceVersion}"
        implementation "com.google.inject.extensions:guice-assistedinject:${managedGuiceVersion}"
        implementation 'javax.inject:javax.inject:1'
        implementation 'aopalliance:aopalliance:1.0'
        implementation 'net.java.dev.jna:jna:5.14.0'
        testImplementation 'junit:junit:4.13.2'
    }

    afterEvaluate { Project componentProject ->
        limewireProjectDependencies(componentProject).each { dependencyName ->
            componentProject.dependencies.add('implementation', project(":${dependencyName}"))
        }
        legacyExternalDependencies(componentProject).each { dependency ->
            componentProject.dependencies.add(dependency.scope ?: 'implementation', dependency.notation)
        }
    }
}

project(':libtorrent') {
    dependencies {
        implementation project(':jlibtorrent')
    }
}

def javaHomePath = providers.environmentVariable('JAVA_HOME')
    .orElse(Jvm.current().javaHome.absolutePath)

def nativeSourceDir = rootDir.toPath().resolve('main/components/common/src/main/native/osx').toFile()
def nativeOutputDir = layout.buildDirectory.dir('native/osx')

def registerMacNativeCompile = { String taskName, String sourceName, String outputName, List<String> frameworks ->
    tasks.register(taskName, Exec) {
        onlyIf { hostOs.isMacOsX() }

        def sourceFile = new File(nativeSourceDir, sourceName)
        def outputFile = nativeOutputDir.map { dir -> new File(dir.asFile, outputName) }

        inputs.file(sourceFile)
        outputs.file(outputFile)

        doFirst {
            outputFile.get().parentFile.mkdirs()
        }

        commandLine(
            'clang',
            '-dynamiclib',
            '-o', outputFile.get().absolutePath,
            '-I', "${javaHomePath.get()}/include",
            '-I', "${javaHomePath.get()}/include/darwin",
            sourceFile.absolutePath,
            *frameworks.collectMany { framework -> ['-framework', framework] }
        )
    }
}

def compileMacSystemUtilities = registerMacNativeCompile(
    'compileMacSystemUtilities',
    'SystemUtilities.m',
    'libSystemUtilities.dylib',
    ['ApplicationServices']
)

def compileMacOSXUtils = registerMacNativeCompile(
    'compileMacOSXUtils',
    'MacOSXUtils.m',
    'libMacOSXUtils.dylib',
    ['Cocoa', 'CoreServices']
)

def compileMacGurlCompatibility = registerMacNativeCompile(
    'compileMacGurlCompatibility',
    'GURLjnilib.c',
    'libGURL.dylib',
    []
)

tasks.register('compileMacCompatibilityNatives') {
    if (hostOs.isMacOsX()) {
        dependsOn compileMacSystemUtilities
        dependsOn compileMacOSXUtils
        dependsOn compileMacGurlCompatibility
    }
}

def managedJlibtorrentReleaseAssets = gradle.ext.has('managedJlibtorrentReleaseAssets')
    ? (gradle.ext.managedJlibtorrentReleaseAssets as Map)
    : [:]
def managedJlibtorrentNativeAssetName = hostJlibtorrentNativeArtifactName(gradle.ext.managedJlibtorrentVersion.toString())
def managedJlibtorrentNativeAssetUrl = managedJlibtorrentReleaseAssets[managedJlibtorrentNativeAssetName]?.toString()
def managedJlibtorrentNativeAssetFile = layout.buildDirectory.file("managed-jlibtorrent/${managedJlibtorrentNativeAssetName}")

tasks.register('downloadManagedJlibtorrentHostNativeJar') {
    onlyIf { managedJlibtorrentUsePublishedNatives }
    outputs.file(managedJlibtorrentNativeAssetFile)

    doLast {
        def targetFile = managedJlibtorrentNativeAssetFile.get().asFile
        if (targetFile.exists() && !gradle.startParameter.refreshDependencies) {
            return
        }
        if (gradle.startParameter.isOffline()) {
            throw new GradleException(
                "Gradle is offline and ${targetFile.name} has not been cached yet. " +
                    "Run once online to download the managed frostwire-jlibtorrent native jar."
            )
        }
        if (!managedJlibtorrentNativeAssetUrl) {
            throw new GradleException(
                "No published frostwire-jlibtorrent native asset URL was resolved for ${managedJlibtorrentNativeAssetName}. " +
                    "Use -PfrostwireJlibtorrentSourceHead=true to opt into source-head mode if you need local native builds."
            )
        }

        targetFile.parentFile.mkdirs()
        def connection = new URL(managedJlibtorrentNativeAssetUrl).openConnection()
        connection.setRequestProperty('User-Agent', 'WireShare-Gradle')
        connection.setConnectTimeout(15000)
        connection.setReadTimeout(30000)
        connection.inputStream.withCloseable { stream ->
            targetFile.withOutputStream { output ->
                output << stream
            }
        }
    }
}

def sourceHostNativeJarTask = project(':jlibtorrent').tasks.named(hostJlibtorrentNativeTaskName())
def sourceHostNativeJarFile = sourceHostNativeJarTask.flatMap { archiveTask -> archiveTask.archiveFile }
def hostNativeJarTask = managedJlibtorrentUsePublishedNatives
    ? tasks.named('downloadManagedJlibtorrentHostNativeJar')
    : sourceHostNativeJarTask
def hostNativeJarFile = managedJlibtorrentUsePublishedNatives
    ? managedJlibtorrentNativeAssetFile
    : sourceHostNativeJarFile
def macRuntimePayloadDir = layout.buildDirectory.dir('macos/runtime-payload')
def macAppInputDir = layout.buildDirectory.dir('macos/app-input')
def macAppImageDir = layout.buildDirectory.dir('macos/app-image')
def macDmgDir = layout.buildDirectory.dir('macos/dmg')
def macNotaryZipFile = layout.buildDirectory.file('macos/WireShare-mac.zip')
def windowsAppInputDir = layout.buildDirectory.dir('windows/app-input')
def windowsMsiDir = layout.buildDirectory.dir('windows/msi')
def packageAssociationsDir = layout.buildDirectory.dir('packaging/associations')
def torrentAssociationFile = packageAssociationsDir.map { dir -> new File(dir.asFile, 'torrent.properties') }
def flatpakRootDir = layout.buildDirectory.dir("flatpak/${releaseAppId}")
def flatpakFilesDir = flatpakRootDir.map { dir -> new File(dir.asFile, 'files') }
def flatpakRuntimeImageDir = layout.buildDirectory.dir('flatpak/jre')
def flatpakBuildDir = flatpakRootDir.map { dir -> new File(dir.asFile, 'build-dir') }
def flatpakRepoDir = flatpakRootDir.map { dir -> new File(dir.asFile, 'repo') }
def flatpakManifestFile = flatpakRootDir.map { dir -> new File(dir.asFile, "${releaseAppId}.yaml") }
def flatpakBundleFile = layout.buildDirectory.file("distributions/${releaseAppName}-${releaseAppVersion}-linux-${isArm64 ? 'arm64' : 'x64'}.flatpak")

project(':jlibtorrent') {
    tasks.withType(Test).configureEach {
        def sourceNativeTaskName = hostJlibtorrentNativeTaskName()
        def sourceNativeTask = managedJlibtorrentUsePublishedNatives ? null : tasks.named(sourceNativeTaskName)
        def sourceNativeJarFile = managedJlibtorrentUsePublishedNatives
            ? null
            : sourceNativeTask.flatMap { archiveTask -> archiveTask.archiveFile }
        def resolvedNativeTask = managedJlibtorrentUsePublishedNatives
            ? rootProject.tasks.named('downloadManagedJlibtorrentHostNativeJar')
            : sourceNativeTask
        def resolvedNativeJarFile = managedJlibtorrentUsePublishedNatives
            ? rootProject.layout.buildDirectory.file("managed-jlibtorrent/${hostJlibtorrentNativeArtifactName(gradle.ext.managedJlibtorrentVersion.toString())}")
            : sourceNativeJarFile

        dependsOn resolvedNativeTask
        onlyIf {
            if (managedJlibtorrentUsePublishedNatives) {
                logger.lifecycle(
                    "Skipping ${path} because managed frostwire-jlibtorrent release natives are used for packaging, not for running upstream JNI tests on this machine."
                )
                return false
            }

            def nativeJar = resolvedNativeJarFile.get().asFile
            if (nativeJar.exists()) {
                return true
            }

            logger.lifecycle(
                managedJlibtorrentUsePublishedNatives
                    ? "Skipping ${path} because the managed frostwire-jlibtorrent release asset ${nativeJar.name} is unavailable for this machine."
                    : "Skipping ${path} because ${nativeJar.name} was not produced by ${project.path}:${sourceNativeTaskName}. " +
                        "The managed frostwire-jlibtorrent checkout does not include host native binaries for this machine."
            )
            false
        }
    }
}

tasks.register('stageMacRuntimePayload', Sync) {
    onlyIf { hostOs.isMacOsX() }
    dependsOn tasks.named('compileMacCompatibilityNatives')
    dependsOn hostNativeJarTask

    into(macRuntimePayloadDir)
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    from(nativeOutputDir) {
        into 'lib/native'
    }
    from(rootDir.toPath().resolve('nativelibs/osx').toFile()) {
        into 'lib/native'
        include '*.dylib'
        include '*.jnilib'
    }
    from({
        def nativeJar = hostNativeJarFile.get().asFile
        if (!nativeJar.exists()) {
            return []
        }
        return zipTree(nativeJar).matching {
            include '**/*.dylib'
            include '**/*.jnilib'
        }
    })
}

tasks.register('signMacRuntimePayload') {
    onlyIf { hostOs.isMacOsX() && macosCodesignIdentity.isPresent() }
    dependsOn tasks.named('stageMacRuntimePayload')
    inputs.dir(macRuntimePayloadDir)
    outputs.dir(macRuntimePayloadDir)

    doLast {
        def identity = macosCodesignIdentity.get()
        def keychain = macosCodesignKeychain.orNull
        def binaries = fileTree(macRuntimePayloadDir.get().asFile).matching {
            include '**/*.dylib'
            include '**/*.jnilib'
        }.files.sort()

        binaries.each { File binary ->
            def command = ['codesign', '--force', '--sign', identity, '--timestamp']
            if (keychain) {
                command += ['--keychain', keychain]
            }
            command += [binary.absolutePath]
            exec {
                commandLine(command)
            }
        }
    }
}

tasks.register('prepareMacRuntimePayload') {
    onlyIf { hostOs.isMacOsX() }
    dependsOn(macosCodesignIdentity.isPresent()
        ? tasks.named('signMacRuntimePayload')
        : tasks.named('stageMacRuntimePayload'))
}

tasks.register('verifyMacRuntimePayloadSignatures') {
    onlyIf { hostOs.isMacOsX() && macosCodesignIdentity.isPresent() }
    dependsOn tasks.named('signMacRuntimePayload')

    doLast {
        fileTree(macRuntimePayloadDir.get().asFile).matching {
            include '**/*.dylib'
            include '**/*.jnilib'
        }.files.sort().each { File binary ->
            exec {
                commandLine('codesign', '--verify', '--strict', '--verbose=2', binary.absolutePath)
            }
        }
    }
}

tasks.register('stageMacAppInput', Sync) {
    onlyIf { hostOs.isMacOsX() }
    dependsOn tasks.named('wireShareJar')

    from(rootProject.layout.projectDirectory.file('WireShare.jar'))
    into(macAppInputDir)
}

tasks.register('writeJpackageFileAssociations') {
    def outputFile = torrentAssociationFile
    outputs.file(outputFile)

    doLast {
        def associationFile = outputFile.get()
        associationFile.parentFile.mkdirs()
        associationFile.text = """extension=torrent
mime-type=application/x-bittorrent
description=BitTorrent metadata
"""
    }
}

tasks.register('packageMacAppImage', Exec) {
    onlyIf { hostOs.isMacOsX() }
    dependsOn tasks.named('stageMacAppInput')
    dependsOn tasks.named('writeJpackageFileAssociations')

    def outputDir = macAppImageDir.get().asFile
    def inputDir = macAppInputDir.get().asFile
    def jpackageBinary = new File(javaHomePath.get(), 'bin/jpackage').absolutePath
    def iconFile = rootDir.toPath().resolve('main/lib/resources/osx/wireshare.icns').toFile()
    def torrentAssociation = torrentAssociationFile.get()

    doFirst {
        delete(outputDir)
        outputDir.mkdirs()
        if (!macosCodesignIdentity.isPresent()) {
            logger.lifecycle("Building unsigned ${releaseAppName}.app because no macOS codesign identity is configured.")
        }
    }

    def command = [
        jpackageBinary,
        '--type', 'app-image',
        '--dest', outputDir.absolutePath,
        '--input', inputDir.absolutePath,
        '--name', releaseAppName,
        '--main-jar', 'WireShare.jar',
        '--app-version', releaseAppVersion,
        '--vendor', releaseVendor,
        '--mac-package-identifier', releaseAppId,
        '--icon', iconFile.absolutePath,
        '--file-associations', torrentAssociation.absolutePath,
    ]
    if (macosCodesignIdentity.isPresent()) {
        command += ['--mac-sign', '--mac-signing-key-user-name', macosCodesignIdentity.get()]
        if (macosCodesignKeychain.isPresent()) {
            command += ['--mac-signing-keychain', macosCodesignKeychain.get()]
        }
    }
    if (isArm64) {
        command += ['--java-options', arm64HotSpotOsrWorkaroundArg]
    }
    commandLine(command)
}

tasks.register('zipMacAppImageForNotarization', Exec) {
    onlyIf { hostOs.isMacOsX() }
    dependsOn tasks.named('packageMacAppImage')

    def appBundle = macAppImageDir.map { dir -> new File(dir.asFile, 'WireShare.app') }
    def zipFile = macNotaryZipFile.get().asFile

    doFirst {
        zipFile.parentFile.mkdirs()
        zipFile.delete()
    }

    commandLine(
        'ditto',
        '-c',
        '-k',
        '--keepParent',
        appBundle.get().absolutePath,
        zipFile.absolutePath
    )
}

tasks.register('notarizeMacAppImage') {
    onlyIf {
        hostOs.isMacOsX() &&
            macosCodesignIdentity.isPresent() &&
            macosNotaryProfile.isPresent()
    }
    dependsOn tasks.named('zipMacAppImageForNotarization')

    doLast {
        def zipFile = macNotaryZipFile.get().asFile
        def appBundle = macAppImageDir.get().asFile.toPath().resolve('WireShare.app').toFile()

        exec {
            commandLine(
                'xcrun',
                'notarytool',
                'submit',
                zipFile.absolutePath,
                '--wait',
                '--keychain-profile',
                macosNotaryProfile.get()
            )
        }
        exec {
            commandLine('xcrun', 'stapler', 'staple', appBundle.absolutePath)
        }
    }
}

tasks.register('packageMacDmg', Exec) {
    onlyIf { hostOs.isMacOsX() }
    dependsOn(macosCodesignIdentity.isPresent() && macosNotaryProfile.isPresent()
        ? tasks.named('notarizeMacAppImage')
        : tasks.named('packageMacAppImage'))

    def outputDir = macDmgDir.get().asFile
    def appBundle = macAppImageDir.map { dir -> new File(dir.asFile, "${releaseAppName}.app") }
    def jpackageBinary = new File(javaHomePath.get(), 'bin/jpackage').absolutePath

    doFirst {
        delete(outputDir)
        outputDir.mkdirs()
        if (!macosCodesignIdentity.isPresent()) {
            logger.lifecycle("Building unsigned ${releaseAppName}.dmg because no macOS codesign identity is configured.")
        } else if (!macosNotaryProfile.isPresent()) {
            logger.lifecycle("Building signed but unnotarized ${releaseAppName}.dmg because no macOS notary profile is configured.")
        }
    }

    commandLine(
        jpackageBinary,
        '--type', 'dmg',
        '--dest', outputDir.absolutePath,
        '--app-image', appBundle.get().absolutePath,
        '--name', releaseAppName,
        '--app-version', releaseAppVersion,
        '--vendor', releaseVendor,
        '--mac-package-identifier', releaseAppId
    )
}

tasks.register('stageWindowsAppInput', Sync) {
    onlyIf { hostOs.isWindows() && isX86_64 }
    dependsOn tasks.named('wireShareJar')

    from(rootProject.layout.projectDirectory.file('WireShare.jar'))
    into(windowsAppInputDir)
}

tasks.register('packageWindowsMsi', Exec) {
    onlyIf { hostOs.isWindows() && isX86_64 }
    dependsOn tasks.named('stageWindowsAppInput')
    dependsOn tasks.named('writeJpackageFileAssociations')

    def outputDir = windowsMsiDir.get().asFile
    def inputDir = windowsAppInputDir.get().asFile
    def jpackageBinary = new File(javaHomePath.get(), 'bin/jpackage').absolutePath
    def iconFile = rootDir.toPath().resolve('nativelibs/windows/WireShare.ico').toFile()
    def torrentAssociation = torrentAssociationFile.get()

    doFirst {
        delete(outputDir)
        outputDir.mkdirs()
        if (!windowsSigningConfigured) {
            logger.lifecycle("Building unsigned ${releaseAppName}.msi because no Windows signing certificate is configured.")
        }
    }

    commandLine(
        jpackageBinary,
        '--type', 'msi',
        '--dest', outputDir.absolutePath,
        '--input', inputDir.absolutePath,
        '--name', releaseAppName,
        '--main-jar', 'WireShare.jar',
        '--app-version', releaseAppVersion,
        '--vendor', releaseVendor,
        '--icon', iconFile.absolutePath,
        '--install-dir', releaseAppName,
        '--win-dir-chooser',
        '--win-menu',
        '--win-menu-group', releaseAppName,
        '--win-shortcut',
        '--win-upgrade-uuid', windowsUpgradeUuid,
        '--file-associations', torrentAssociation.absolutePath
    )
}

tasks.register('signWindowsMsi') {
    onlyIf { hostOs.isWindows() && isX86_64 && windowsSigningConfigured }
    dependsOn tasks.named('packageWindowsMsi')

    doLast {
        def msiFiles = fileTree(windowsMsiDir.get().asFile).matching {
            include '*.msi'
        }.files.sort()
        if (msiFiles.isEmpty()) {
            throw new GradleException("No MSI files were produced in ${windowsMsiDir.get().asFile}")
        }

        msiFiles.each { File msiFile ->
            def command = [
                windowsSignToolPath.orNull ?: 'signtool',
                'sign',
                '/fd', 'SHA256',
                '/tr', windowsTimestampUrl.get(),
                '/td', 'SHA256',
            ]
            if (windowsCodesignCertificatePath.isPresent()) {
                command += ['/f', windowsCodesignCertificatePath.get()]
                if (windowsCodesignCertificatePassword.isPresent()) {
                    command += ['/p', windowsCodesignCertificatePassword.get()]
                }
            } else {
                command += ['/sha1', windowsCodesignCertificateThumbprint.get()]
            }
            command += [msiFile.absolutePath]
            exec {
                commandLine(command)
            }
        }
    }
}

tasks.register('buildLinuxFlatpakRuntime', Exec) {
    onlyIf { hostOs.isLinux() && (isX86_64 || isArm64) }

    def jlinkBinary = new File(javaHomePath.get(), 'bin/jlink').absolutePath
    def runtimeDir = flatpakRuntimeImageDir.get().asFile
    def runtimeModules = [
        'java.base',
        'java.compiler',
        'java.datatransfer',
        'java.desktop',
        'java.instrument',
        'java.logging',
        'java.management',
        'java.naming',
        'java.net.http',
        'java.prefs',
        'java.rmi',
        'java.scripting',
        'java.security.jgss',
        'java.security.sasl',
        'java.sql',
        'java.xml',
        'jdk.charsets',
        'jdk.crypto.ec',
        'jdk.localedata',
        'jdk.unsupported',
        'jdk.zipfs',
    ].join(',')

    outputs.dir(runtimeDir)

    doFirst {
        delete(runtimeDir)
        runtimeDir.parentFile.mkdirs()
    }

    commandLine(
        jlinkBinary,
        '--output', runtimeDir.absolutePath,
        '--add-modules', runtimeModules,
        '--strip-debug',
        '--no-header-files',
        '--no-man-pages'
    )
}

tasks.register('stageLinuxFlatpakFiles', Sync) {
    onlyIf { hostOs.isLinux() && (isX86_64 || isArm64) }
    dependsOn tasks.named('wireShareJar')
    dependsOn tasks.named('buildLinuxFlatpakRuntime')

    into(flatpakFilesDir)
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    from(rootProject.layout.projectDirectory.file('WireShare.jar'))
    from(flatpakRuntimeImageDir) {
        into 'jre'
    }
    from(rootDir.toPath().resolve('nativelibs/linux').toFile()) {
        into 'lib64'
        include '*.so'
        include '*.so.*'
    }

    [16, 20, 22, 24, 32, 36, 40, 48, 64, 72, 96, 128, 192, 256, 384, 512, 1024].each { size ->
        from(rootDir.toPath().resolve("main/lib/resources/linux/wsicon_${size}.png").toFile()) {
            into "hicolor/${size}x${size}/apps"
            rename { "${releaseAppId}.png" }
        }
        from(rootDir.toPath().resolve("main/lib/resources/linux/dload_${size}.png").toFile()) {
            into "hicolor/${size}x${size}/status"
            rename { "${releaseAppId}.XferDone.png" }
        }
    }

    doLast {
        def filesDir = flatpakFilesDir.get()
        def launcher = new File(filesDir, 'WireShare.sh')
        launcher.text = """#!/usr/bin/env bash
set -euo pipefail

JAVA_ARGS=()
case "\$(uname -m)" in
  arm64|aarch64)
    JAVA_ARGS+=(${arm64HotSpotOsrWorkaroundArg})
    ;;
esac

exec /app/jre/bin/java "\${JAVA_ARGS[@]}" -Djava.library.path=/app/lib -jar /app/share/${releaseAppName}/WireShare.jar "\$@"
"""
        launcher.setExecutable(true, false)

        def desktopFile = new File(filesDir, "applications/${releaseAppId}.desktop")
        desktopFile.parentFile.mkdirs()
        desktopFile.text = """[Desktop Entry]
Type=Application
Version=1.0
Name=${releaseAppName}
Exec=${releaseAppName} %U
Icon=${releaseAppId}
Terminal=false
Categories=Network;FileTransfer;P2P;
MimeType=application/x-bittorrent;application/torrent;application/x-torrent;x-scheme-handler/magnet;x-scheme-handler/ed2k;
"""
    }
}

tasks.register('writeLinuxFlatpakManifest') {
    onlyIf { hostOs.isLinux() && (isX86_64 || isArm64) }
    dependsOn tasks.named('stageLinuxFlatpakFiles')

    outputs.file(flatpakManifestFile)

    doLast {
        def manifestFile = flatpakManifestFile.get()
        manifestFile.parentFile.mkdirs()
        manifestFile.text = """id: ${releaseAppId}
branch: stable
runtime: org.freedesktop.Platform
runtime-version: '25.08'
sdk: org.freedesktop.Sdk
command: ${releaseAppName}
modules:
  - name: wireshare
    buildsystem: simple
    sources:
      - type: dir
        path: ./files
    build-commands:
      - install -Dm755 WireShare.sh /app/bin/${releaseAppName}
      - install -Dm644 WireShare.jar /app/share/${releaseAppName}/WireShare.jar
      - mkdir -p /app/jre
      - cp -a jre/. /app/jre/
      - mkdir -p /app/lib
      - if [ -d lib64 ]; then cp -a lib64/. /app/lib/; fi
      - mkdir -p /app/share/icons
      - cp -a hicolor /app/share/icons/
      - install -Dm644 applications/${releaseAppId}.desktop /app/share/applications/${releaseAppId}.desktop
finish-args:
  - --share=ipc
  - --share=network
  - --socket=pulseaudio
  - --socket=wayland
  - --socket=fallback-x11
  - --device=dri
  - --filesystem=home
  - --talk-name=org.freedesktop.Notifications
"""
    }
}

tasks.register('packageLinuxFlatpak') {
    onlyIf { hostOs.isLinux() && (isX86_64 || isArm64) }
    dependsOn tasks.named('writeLinuxFlatpakManifest')
    outputs.file(flatpakBundleFile)

    doLast {
        def manifestFile = flatpakManifestFile.get()
        def buildDir = flatpakBuildDir.get()
        def repoDir = flatpakRepoDir.get()
        def bundleFile = flatpakBundleFile.get().asFile

        delete(buildDir)
        delete(repoDir)
        bundleFile.parentFile.mkdirs()
        bundleFile.delete()

        exec {
            commandLine(
                'flatpak-builder',
                '--force-clean',
                '--repo', repoDir.absolutePath,
                buildDir.absolutePath,
                manifestFile.absolutePath
            )
        }
        exec {
            commandLine(
                'flatpak',
                'build-bundle',
                repoDir.absolutePath,
                bundleFile.absolutePath,
                releaseAppId,
                'stable'
            )
        }
    }
}

project(':desktop-compose-app') {
    pluginManager.withPlugin('application') {
        dependencies {
            runtimeOnly(files(hostNativeJarFile) {
                builtBy hostNativeJarTask
            })
        }

        tasks.named('run', JavaExec).configure {
            dependsOn hostNativeJarTask

            if (hostOs.isMacOsX()) {
                if (isArm64) {
                    if (macArm64RunJbrJava != null) {
                        // Prefer a locally installed JetBrains Runtime for desktop `run` on Apple Silicon.
                        // Compose Desktop is exercised most heavily against JBR, and Temurin 21.0.11 still
                        // produced intermittent HotSpot crashes across multiple UI/background threads here.
                        executable = macArm64RunJbrJava.absolutePath
                    } else {
                        // Fall back to a maintained Java 21 toolchain instead of inheriting an older host JVM
                        // (for example Oracle 21+35), which has already hit multiple AArch64 HotSpot crashes.
                        javaLauncher = javaToolchains.launcherFor {
                            languageVersion = JavaLanguageVersion.of(javaLanguageVersion)
                            vendor = JvmVendorSpec.ADOPTIUM
                        }
                    }
                }
                dependsOn rootProject.tasks.named('prepareMacRuntimePayload')
                def macRunJvmArgs = [
                    "-Djava.library.path=${macRuntimePayloadDir.get().asFile.toPath().resolve('lib/native').toFile().absolutePath}"
                ]
                if (isArm64) {
                    macRunJvmArgs += arm64HotSpotOsrWorkaroundArg
                    if (macArm64RunUsesTier1Jit) {
                        // Fall back to the simpler C1 tier if the selected runtime still crashes on Apple Silicon.
                        macRunJvmArgs += '-XX:TieredStopAtLevel=1'
                    }
                    if (macArm64RunUsesEd2kCompileExclude) {
                        // Keep the ED2K polling and adapter-refresh path interpreted as an opt-in fallback.
                        macRunJvmArgs += '-XX:CompileCommand=quiet'
                        macRunJvmArgs += "-XX:CompileCommand=exclude,${macArm64RunEd2kCompileExcludePattern}"
                    }
                }
                jvmArgs(macRunJvmArgs)
            }
        }

        if (hostOs.isMacOsX()) {
            tasks.named('startScripts').configure {
                dependsOn rootProject.tasks.named('prepareMacRuntimePayload')
                doLast {
                    def defaultJvmOpts = isArm64
                        ? 'DEFAULT_JVM_OPTS="-Djava.library.path=$APP_HOME/lib/native -XX:-UseOnStackReplacement"'
                        : 'DEFAULT_JVM_OPTS="-Djava.library.path=$APP_HOME/lib/native"'
                    unixScript.text = unixScript.text.replace(
                        'DEFAULT_JVM_OPTS=""',
                        defaultJvmOpts
                    )
                }
            }

            ['distTar', 'distZip', 'installDist'].each { taskName ->
                tasks.named(taskName).configure {
                    dependsOn rootProject.tasks.named('compileMacCompatibilityNatives')
                    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
                }
            }
        }

        distributions {
            main {
                contents {
                    from(hostNativeJarFile) {
                        into 'lib'
                    }
                    if (hostOs.isMacOsX()) {
                        from(macRuntimePayloadDir)
                    }
                    from(rootDir.toPath().resolve('main/lib/resources/linux').toFile()) {
                        into 'resources/linux'
                    }
                    from(rootDir.toPath().resolve('main/lib/resources/osx').toFile()) {
                        into 'resources/osx'
                    }
                    from(rootDir.toPath().resolve('main/lib/resources/windows').toFile()) {
                        into 'resources/windows'
                    }
                }
            }
        }
    }
}

def desktopComposeAppProject = project(':desktop-compose-app')
def desktopComposeRuntimeClasspath = desktopComposeAppProject.provider {
    desktopComposeAppProject.configurations.runtimeClasspath
}
def desktopComposeMainOutput = desktopComposeAppProject.provider {
    desktopComposeAppProject.sourceSets.main.output
}

tasks.register('wireShareJar', Jar) {
    dependsOn desktopComposeAppProject.tasks.named('classes')
    dependsOn desktopComposeRuntimeClasspath
    dependsOn hostNativeJarTask
    if (hostOs.isMacOsX()) {
        dependsOn tasks.named('prepareMacRuntimePayload')
    }

    archiveFileName = 'WireShare.jar'
    destinationDirectory = layout.projectDirectory.asFile
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    zip64 = true

    manifest {
        attributes(
            'Main-Class': 'org.limewire.ui.compose.Main',
            'Multi-Release': 'true'
        )
    }

    from({ desktopComposeMainOutput.get() })
    from({
        desktopComposeRuntimeClasspath.get()
            .filter { file -> file.exists() }
            .findAll { file -> !hostOs.isMacOsX() || file.name != hostNativeJarFile.get().asFile.name }
            .collect { file -> file.isDirectory() ? file : rootProject.zipTree(file) }
    })
    if (hostOs.isMacOsX()) {
        from(macRuntimePayloadDir)
    }
    from(rootDir.toPath().resolve('main/lib/resources/linux').toFile())
    from(rootDir.toPath().resolve('main/lib/resources/osx').toFile())
    from(rootDir.toPath().resolve('main/lib/resources/windows').toFile())
}

tasks.register('packageDesktopRelease') {
    group = 'distribution'
    description = 'Builds the host-specific WireShare desktop release package: DMG on macOS, MSI on Windows, Flatpak on Linux.'

    if (hostOs.isMacOsX()) {
        dependsOn tasks.named('packageMacDmg')
    } else if (hostOs.isWindows() && isX86_64) {
        dependsOn(windowsSigningConfigured ? tasks.named('signWindowsMsi') : tasks.named('packageWindowsMsi'))
    } else if (hostOs.isLinux() && (isX86_64 || isArm64)) {
        dependsOn tasks.named('packageLinuxFlatpak')
    } else {
        doFirst {
            throw new GradleException("Unsupported desktop release host: ${hostOs.name()}/${hostArch}")
        }
    }
}

tasks.register('buildAndroid') {
    group = 'build'
    description = 'Builds the WireShare Android debug APK.'
    dependsOn ':android-app:assembleDebug'
}

tasks.named('build') {
    dependsOn tasks.named('wireShareJar')
}

def hygienePatterns = [
    [label: 'legacy factory provider API', token: factoryProviderToken],
    [label: 'Guice internals namespace', token: guiceInternalToken],
    [label: 'Apple desktop extensions jar', token: appleExtensionsToken],
    [label: 'legacy torrent wrapper name', token: torrentWrapperToken],
    [label: 'legacy libtorrent wrapper name', token: libtorrentWrapperToken],
    [label: 'JPMS opens workaround', token: addOpensToken],
]

def vendoredJarDir = rootProject.file('main/lib/jars')
def retiredVendoredJarNames = [
    'MozillaGlue-1.9.jar',
    'MozillaInterfaces-1.9.jar',
    'mozswing.jar',
    'mozdom4java.jar',
    'htmlcleaner-2.1.jar',
    'jdic.jar',
]
def classpathConfigurationsToAudit = [
    'compileClasspath',
    'runtimeClasspath',
    'testCompileClasspath',
    'testRuntimeClasspath',
    'annotationProcessor',
    'testAnnotationProcessor',
]

def hygieneFiles = files(
    fileTree(rootDir) {
        include '**/*.java'
        include '**/*.gradle'
        include '**/*.groovy'
        include '**/*.xml'
        include '**/*.properties'
        include '**/*.sh'
        include '**/*.bat'
        include '**/*.md'
        include '**/*.txt'
        exclude '**/build/**'
        exclude '**/.gradle/**'
        exclude '**/.git/**'
        exclude '**/.ivy2/**'
        exclude '**/frostwire-master/**'
        exclude '**/*.orig'
    },
    rootProject.file('gradlew'),
    rootProject.file('gradlew.bat')
)

tasks.register('verifyDependencyHygiene') {
    group = 'verification'
    description = 'Fails the build if legacy Guice, Apple Java extensions, old libtorrent wrappers, or JPMS opens workarounds are referenced.'
    inputs.files(hygieneFiles)

    doLast {
        def violations = []

        hygieneFiles.files.findAll { it.exists() && it.isFile() }.sort().each { file ->
            String text = file.getText('UTF-8')
            hygienePatterns.each { rule ->
                if (text.contains(rule.token)) {
                    violations << "${rule.label}: ${rootProject.relativePath(file)}"
                }
            }
        }

        if (!violations.isEmpty()) {
            throw new GradleException(
                "Dependency hygiene check failed:\n - " + violations.unique().sort().join('\n - ')
            )
        }
    }
}

tasks.register('verifyNoVendoredJarUsage') {
    group = 'verification'
    description = 'Fails the build if normal module classpaths resolve jars from main/lib/jars or if retired Mozilla/JDIC/HtmlCleaner jars reappear.'

    doLast {
        def violations = []
        def vendoredJarPath = vendoredJarDir.toPath().toAbsolutePath().normalize()

        retiredVendoredJarNames.collect { new File(vendoredJarDir, it) }
            .findAll { it.exists() }
            .each { file ->
                violations << "retired vendored jar still present: ${rootProject.relativePath(file)}"
            }

        subprojects.sort { it.path }.each { proj ->
            classpathConfigurationsToAudit.each { configurationName ->
                def configuration = proj.configurations.findByName(configurationName)
                if (configuration == null || !configuration.canBeResolved) {
                    return
                }

                configuration.resolve().sort { it.absolutePath }.each { file ->
                    def resolvedPath = file.toPath().toAbsolutePath().normalize()
                    if (resolvedPath.startsWith(vendoredJarPath)) {
                        violations << "${proj.path}:${configurationName} -> ${rootProject.relativePath(file)}"
                    }
                }
            }
        }

        if (!violations.isEmpty()) {
            throw new GradleException(
                "Vendored jar usage check failed:\n - " + violations.unique().sort().join('\n - ')
            )
        }
    }
}

def legacySwingBuildFiles = files(
    rootProject.file('settings.gradle'),
    rootProject.file('build.gradle')
)

tasks.register('verifyNoLegacySwingBuildWiring') {
    group = 'verification'
    description = 'Fails when removed Swing build wiring is reintroduced.'
    inputs.files(legacySwingBuildFiles)

    doLast {
        def violations = []
        def forbiddenPatterns = [
            [label: ':swingui module', pattern: /'swingui'\s*:/],
            [label: ':integrated-ui module', pattern: /'integrated-ui'\s*:/],
            [label: 'runSwing task', pattern: /tasks\.register\(['"]runSwing['"]/],
            [label: 'legacy Swing module block', pattern: /project\(':integrated-ui'\)\s*\{/],
            [label: 'legacy Swing main class', pattern: /mainClass\s*=\s*['"]org\.limewire\.ui\.swing\.Main['"]/],
        ]

        legacySwingBuildFiles.files.findAll { it.exists() && it.isFile() }.sort().each { file ->
            String text = file.getText('UTF-8')
            forbiddenPatterns.each { entry ->
                if ((text =~ entry.pattern).find()) {
                    violations << "${entry.label}: ${rootProject.relativePath(file)}"
                }
            }
        }

        if (!violations.isEmpty()) {
            throw new GradleException(
                "Legacy Swing build wiring check failed:\n - " + violations.unique().sort().join('\n - ')
            )
        }
    }
}

tasks.register('verifyNoJmuleBuildDependency') {
    group = 'verification'
    description = 'Fails if the active build still depends on the donor jmule source tree.'

    def trackedFiles = files(
        rootProject.file('settings.gradle'),
        rootProject.file('build.gradle'),
        rootProject.file('desktop-compose-app/build.gradle'),
        rootProject.file('ed2k-api/build.gradle'),
        rootProject.file('ed2k-backend/build.gradle')
    )
    inputs.files(trackedFiles)

    doLast {
        def violations = []
        def forbiddenProjectToken = "project(':j" + "mule')"
        trackedFiles.files.findAll { it.exists() && it.isFile() }.sort().each { file ->
            String text = file.getText('UTF-8')
            if (text.contains(forbiddenProjectToken)) {
                violations << "${rootProject.relativePath(file)} still references :jmule"
            }
        }

        if (rootProject.file('jmule/build.gradle').exists()) {
            violations << 'jmule/build.gradle exists, but jmule must remain a donor-only folder'
        }

        if (!violations.isEmpty()) {
            throw new GradleException(
                "JMule donor boundary check failed:\n - " + violations.unique().sort().join('\n - ')
            )
        }
    }
}

tasks.register('verifyNoJed2kBuildDependency') {
    group = 'verification'
    description = 'Fails if the active build still depends on the donor jed2k source tree.'

    def trackedFiles = files(
        rootProject.file('settings.gradle'),
        rootProject.file('build.gradle'),
        rootProject.file('desktop-compose-app/build.gradle'),
        rootProject.file('ed2k-api/build.gradle'),
        rootProject.file('ed2k-backend/build.gradle')
    )
    inputs.files(trackedFiles)

    doLast {
        def violations = []
        def forbiddenProjectToken = "project(':jed2k" + "-master')"
        def forbiddenSourceTokenUnix = 'jed2k' + '-master/src'
        def forbiddenSourceTokenWindows = 'jed2k' + '-master\\src'
        trackedFiles.files.findAll { it.exists() && it.isFile() }.sort().each { file ->
            String text = file.getText('UTF-8')
            if (text.contains(forbiddenProjectToken) || text.contains(forbiddenSourceTokenUnix) || text.contains(forbiddenSourceTokenWindows)) {
                violations << "${rootProject.relativePath(file)} still references jed2k-master"
            }
        }

        if (rootProject.file('jed2k-master/build.gradle').exists()) {
            violations << 'jed2k-master/build.gradle exists, but jed2k-master must remain a donor-only folder'
        }

        if (!violations.isEmpty()) {
            throw new GradleException(
                "jed2k donor boundary check failed:\n - " + violations.unique().sort().join('\n - ')
            )
        }
    }
}

tasks.register('verifyNoFrostWireSearchBuildDependency') {
    group = 'verification'
    description = 'Fails if the active build still depends on the donor FrostWire source tree.'

    def trackedFiles = files(
        rootProject.file('settings.gradle'),
        rootProject.file('build.gradle'),
        rootProject.file('desktop-compose-app/build.gradle'),
        rootProject.file('main/components/core-glue/ivy.xml'),
        rootProject.file('frostwire-search-backend/build.gradle')
    )
    inputs.files(trackedFiles)

    doLast {
        def violations = []
        def forbiddenProjectToken = "project(':frostwire" + "-master')"
        def forbiddenSourceTokenUnix = 'frostwire' + '-master/src'
        def forbiddenSourceTokenWindows = 'frostwire' + '-master\\src'
        trackedFiles.files.findAll { it.exists() && it.isFile() }.sort().each { file ->
            String text = file.getText('UTF-8')
            if (text.contains(forbiddenProjectToken) || text.contains(forbiddenSourceTokenUnix) || text.contains(forbiddenSourceTokenWindows)) {
                violations << "${rootProject.relativePath(file)} still references frostwire-master"
            }
        }

        if (!violations.isEmpty()) {
            throw new GradleException(
                "FrostWire donor boundary check failed:\n - " + violations.unique().sort().join('\n - ')
            )
        }
    }
}

tasks.named('check') {
    dependsOn tasks.named('verifyDependencyHygiene')
    dependsOn tasks.named('verifyNoVendoredJarUsage')
    dependsOn tasks.named('verifyNoLegacySwingBuildWiring')
    dependsOn tasks.named('verifyNoJmuleBuildDependency')
    dependsOn tasks.named('verifyNoJed2kBuildDependency')
    dependsOn tasks.named('verifyNoFrostWireSearchBuildDependency')
}

tasks.register('run') {
    dependsOn project(':desktop-compose-app').tasks.named('run')
}
