plugins {
	id "java-library"
	id "eclipse"
	id "maven-publish"
	id "fabric-loom" version "1.10.1" apply false
	id "com.diffplug.spotless" version "7.0.2"
	id "me.modmuss50.remotesign" version "0.5.0" apply false
	id "me.modmuss50.mod-publish-plugin" version "0.4.5"
}

def branchProvider = providers.of(GitBranchValueSource.class) {}
version = project.version + "+" + (providers.environmentVariable("CI").present ? branchProvider.get() : "local")
logger.lifecycle("Building Fabric: " + version)

def metaProjects = [
	'deprecated',
	'fabric-api-bom',
	'fabric-api-catalog'
]

def debugArgs = [
	"-enableassertions",
	"-Dmixin.debug.verify=true",
	//"-Dmixin.debug.strict=true",
	"-Dmixin.debug.countInjections=true",
]

import groovy.json.JsonSlurper
import org.apache.commons.codec.digest.DigestUtils
import net.fabricmc.fabric.impl.build.CommitHashValueSource
import net.fabricmc.fabric.impl.build.GitBranchValueSource
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

def getSubprojectVersion(Project project) {
	// Get the version from the gradle.properties file
	def version = project.properties["${project.name}-version"]

	if (!version) {
		throw new NullPointerException("Could not find version for " + project.name)
	}

	if (!project.providers.environmentVariable("CI").present) {
		return version + "+local"
	}

	def hashProvider = project.providers.of(CommitHashValueSource.class) {
		parameters.directory = project.name
	}
	return version + "+" + hashProvider.get().substring(0, 8) + DigestUtils.sha256Hex(project.rootProject.minecraft_version).substring(0, 2)
}

def moduleDependencies(project, List<String> depNames) {
	def deps = depNames.iterator().collect { project.dependencies.project(path: ":$it", configuration: 'namedElements') }
	def clientOutputs = depNames.iterator().collect { findProject(":$it").sourceSets.client.output }

	project.dependencies {
		deps.each {
			api it
		}

		clientOutputs.each {
			clientImplementation it
		}
	}

	def depNodes = deps.collect {
		[
			groupId: it.group,
			artifactId: it.name,
			version: getSubprojectVersion(project.project(":" + it.name)),
			scope: "compile"
		]
	}

	// As we manually handle the maven artifacts, we need to also manually specify the deps.
	project.publishing {
		publications {
			mavenJava(MavenPublication) {
				pom.withXml {
					def depsNode = asNode().appendNode("dependencies")

					for (def depNode in depNodes) {
						def node = depsNode.appendNode("dependency")

						for (def entry in depNode) {
							node.appendNode(entry.key, entry.value)
						}
					}
				}
			}
		}
	}
}

def testDependencies(project, List<String> depNames) {
	def deps = depNames.iterator().collect { project.dependencies.project(path: ":$it", configuration: 'namedElements') }
	def clientOutputs = depNames.iterator().collect { findProject(":$it").sourceSets.client.output }

	project.dependencies {
		deps.each {
			testmodImplementation it
		}

		clientOutputs.each {
			testmodClientImplementation it
		}
	}
}

allprojects {
	group = "net.fabricmc.fabric-api"

	apply plugin: "maven-publish"
	apply plugin: "me.modmuss50.remotesign"

	tasks.withType(GenerateModuleMetadata) {
		enabled = false
	}

	remoteSign {
		requestUrl = providers.environmentVariable("SIGNING_SERVER")
		pgpAuthKey = providers.environmentVariable("SIGNING_PGP_KEY")
		jarAuthKey = providers.environmentVariable("SIGNING_JAR_KEY")

		useDummyForTesting = !providers.environmentVariable("SIGNING_SERVER").present

		afterEvaluate {
			// PGP sign all maven publications.
			sign publishing.publications.mavenJava
		}
	}

	publishing {
		setupRepositories(repositories)
	}

	if (metaProjects.contains(it.name)) {
		return
	}

	apply plugin: "java-library"
	apply plugin: "checkstyle"
	apply plugin: "fabric-loom"
	apply plugin: "com.diffplug.spotless"

	tasks.withType(JavaCompile).configureEach {
		it.options.release = 21
	}

	java {
		// Must be added before the split source sets are setup.
		withSourcesJar()
	}

	loom {
		splitEnvironmentSourceSets()
		mixin {
			useLegacyMixinAp = false
		}
	}

	sourceSets {
		testmod {
			compileClasspath += main.compileClasspath
			runtimeClasspath += main.runtimeClasspath
		}

		testmodClient {
			compileClasspath += main.compileClasspath
			runtimeClasspath += main.runtimeClasspath
			compileClasspath += client.compileClasspath
			runtimeClasspath += client.runtimeClasspath

			compileClasspath += testmod.compileClasspath
			runtimeClasspath += testmod.runtimeClasspath
		}

		test {
			compileClasspath += testmodClient.compileClasspath
			runtimeClasspath += testmodClient.runtimeClasspath
		}
	}

	loom {
		runtimeOnlyLog4j = true

		runs {
			testmodClient {
				client()
				ideConfigGenerated project.rootProject == project
				name = "Testmod Client"
				source sourceSets.testmodClient
			}
			testmodServer {
				server()
				ideConfigGenerated project.rootProject == project
				name = "Testmod Server"
				source sourceSets.testmod
			}
		}
	}

	loom.runs.configureEach {
		vmArgs(debugArgs)
	}

	allprojects.each { p ->
		if (metaProjects.contains(p.name)) {
			return
		}

		loom.mods.register(p.name) {
			sourceSet p.sourceSets.main
			sourceSet p.sourceSets.client
		}

		loom.mods.register(p.name + "-testmod") {
			sourceSet p.sourceSets.testmod
			sourceSet p.sourceSets.testmodClient
		}
	}

	dependencies {
		minecraft "com.mojang:minecraft:$rootProject.minecraft_version"
		mappings "net.fabricmc:yarn:${rootProject.minecraft_version}${project.yarn_version}:v2"
		modApi "net.fabricmc:fabric-loader:${project.loader_version}"

		testmodImplementation sourceSets.main.output
		testmodClientImplementation sourceSets.main.output
		testmodClientImplementation sourceSets.client.output
		testmodClientImplementation sourceSets.testmod.output

		testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
		testImplementation sourceSets.testmodClient.output
		testImplementation 'org.mockito:mockito-core:5.13.0'
	}

	test {
		useJUnitPlatform()
	}

	tasks.withType(ProcessResources).configureEach {
		inputs.property "version", project.version

		filesMatching("fabric.mod.json") {
			expand "version": inputs.properties.version
		}
	}

	spotless {
		lineEndings = com.diffplug.spotless.LineEnding.UNIX

		java {
			licenseHeaderFile(rootProject.file("HEADER"))
			removeUnusedImports()
			importOrder('java', 'javax', '', 'net.minecraft', 'net.fabricmc')
			leadingSpacesToTabs()
			trimTrailingWhitespace()
		}

		// Sort the en_us translation files
		// The other languages are handled by Crowdin
		json {
			target 'src/**/lang/en_us.json'
			targetExclude 'src/**/generated/**'
			gson().indentWithSpaces(2).sortByKeys()
		}
	}

	checkstyle {
		configFile = rootProject.file("checkstyle.xml")
		toolVersion = "10.20.2"
	}

	tasks.withType(AbstractArchiveTask).configureEach {
		preserveFileTimestamps = false
		reproducibleFileOrder = true
	}

	remoteSign {
		sign remapJar
	}

	// Run this task after updating minecraft to regenerate any required resources
	tasks.register('generateResources') {
		group = "fabric"
	}

	tasks.register('testmodJar', Jar) {
		from sourceSets.testmod.output
		from sourceSets.testmodClient.output
		destinationDirectory = new File(project.buildDir, "devlibs")
		archiveClassifier = "testmod"
	}

	String archivesName = project.base.archivesName.get()
	[jar, sourcesJar].each {
		it.from(rootProject.file("LICENSE")) {
			rename { "${it}-${archivesName}"}
		}
	}

	if (file("src/client").exists() && !file("src/main").exists()) {
		tasks.named("remapJar", net.fabricmc.loom.task.AbstractRemapJarTask) {
			additionalClientOnlyEntries.add("LICENSE-${archivesName}".toString())
		}

		remapSourcesJar {
			additionalClientOnlyEntries.add("LICENSE-${archivesName}".toString())
		}
	}

	tasks.register('remapTestmodJar', net.fabricmc.loom.task.RemapJarTask) {
		dependsOn testmodJar
		input = testmodJar.archiveFile
		archiveClassifier = "testmod"
		addNestedDependencies = false
		includesClientOnlyClasses = true
		clientOnlySourceSetName = sourceSets.testmodClient.name
		classpath.from(sourceSets.testmodClient.compileClasspath)
	}
	build.dependsOn remapTestmodJar

	tasks.register('validateMixinNames', net.fabricmc.loom.task.ValidateMixinNameTask) {
		source(sourceSets.main.output)
		source(sourceSets.client.output)
		source(sourceSets.testmod.output)
	}

	// Apply to each valid subproject.
	apply from: rootProject.file('gradle/package-info.gradle')
	apply from: rootProject.file('gradle/validate-annotations.gradle')
}

remapTestmodJar {
	def testModJarTasks = []

	subprojects {
		if (metaProjects.contains(it.name) || !(it.file("src/testmod").exists() || it.file("src/testmodClient").exists())) {
			return
		}

		testModJarTasks += it.tasks.remapTestmodJar
	}

	nestedJars.setFrom(testModJarTasks)
	addNestedDependencies = true
	clientOnlySourceSetName = sourceSets.testmodClient.name
}

// Apply auxiliary buildscripts to submodules
// This must be done after all plugins are applied to subprojects
apply from: "gradle/module-validation.gradle"
apply from: "gradle/module-versioning.gradle"

loom {
	accessWidenerPath = file("gradle/javadoc.accesswidener")
}

javadoc {
	options {
		source = "21"
		encoding = "UTF-8"
		charSet = "UTF-8"
		memberLevel = JavadocMemberLevel.PACKAGE
		links(
				"https://maven.fabricmc.net/docs/yarn-${rootProject.minecraft_version}${project.yarn_version}/"
				)
		// Disable the crazy super-strict doclint tool in Java 8
		addStringOption("Xdoclint:none", "-quiet")

		tags(
				'apiNote:a:API Note:',
				'implSpec:a:Implementation Requirements:',
				'implNote:a:Implementation Note:'
				)
	}

	allprojects.each {
		if (metaProjects.contains(it.name)) {
			return
		}

		source(it.sourceSets.main.allJava)
		source(it.sourceSets.client.allJava)
	}

	classpath = files(sourceSets.main.compileClasspath, sourceSets.client.compileClasspath)
	include("**/api/**")
	failOnError = true
}

tasks.register('javadocJar', Jar) {
	dependsOn javadoc
	from javadoc.destinationDir
	//Set as `fatjavadoc` to prevent an ide form trying to use this javadoc, over using the modules javadoc
	archiveClassifier = "fatjavadoc"
}

build.dependsOn javadocJar

loom {
	runs {
		gametest {
			inherit testmodServer

			name "Game Test"

			// Enable the gametest runner
			vmArg "-Dfabric-api.gametest"
			vmArg "-Dfabric-api.gametest.report-file=${project.layout.buildDirectory.file("junit.xml").get().getAsFile()}"
			runDir "build/gametest"
		}
		autoTestServer {
			inherit testmodServer
			name  "Auto Test Server"
			vmArg "-Dfabric.autoTest"
		}
		clientGametest {
			inherit testmodClient
			name "Client Game Test"
			vmArg "-Dfabric.client.gametest"
			vmArg "-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail"
			vmArg "-Dfabric-tag-conventions-v1.legacyTagWarning=fail"
		}
	}
}

runGametest {
	outputs.file project.layout.buildDirectory.file("junit.xml")
}

test.dependsOn runGametest

configurations {
	productionMods {
		transitive = false
	}
}

dependencies {
	productionMods project(':fabric-client-gametest-api-v1')
}

def productionMods = project.files(configurations.productionMods, remapJar.archiveFile, remapTestmodJar.archiveFile)

tasks.register('runProductionClientGametest', net.fabricmc.loom.task.prod.ClientProductionRunTask) {
	mods.setFrom productionMods
	jvmArgs.addAll([
		"-Dfabric.client.gametest",
		"-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail",
		"-Dfabric-tag-conventions-v1.legacyTagWarning=fail"
	])
	jvmArgs.addAll(debugArgs)

	if (providers.environmentVariable("ENABLE_TRACY").present) {
		tracy {
			tracyCapture = file("tracy-capture")
			output = file("profile.tracy")
		}
	}
}

tasks.register('runProductionAutoTestServer', net.fabricmc.loom.task.prod.ServerProductionRunTask) {
	mods.setFrom productionMods
	jvmArgs.add("-Dfabric.autoTest")
	jvmArgs.addAll(debugArgs)
	programArgs.add("nogui")
}

// Format all the gradle files
spotless {
	groovyGradle {
		target 'src/**/*.gradle', '*.gradle', 'gradle/*.gradle'
		greclipse()
	}
}

def addPomMetadataInformation(Project project, MavenPom pom) {
	def modJsonFile = project.file("src/main/resources/fabric.mod.json")

	if (!modJsonFile.exists()) {
		modJsonFile = project.file("src/client/resources/fabric.mod.json")
	}

	def modJson = new JsonSlurper().parse(modJsonFile)
	pom.name = modJson.name
	pom.url = "https://github.com/FabricMC/fabric/tree/HEAD/${project.rootDir.relativePath(project.projectDir)}"
	pom.description = modJson.description
	pom.licenses {
		license {
			name = "Apache-2.0"
			url = "https://github.com/FabricMC/fabric/blob/HEAD/LICENSE"
		}
	}
	pom.developers {
		developer {
			name = "FabricMC"
			url = "https://fabricmc.net/"
		}
	}
	pom.scm {
		connection = "scm:git:https://github.com/FabricMC/fabric.git"
		url = "https://github.com/FabricMC/fabric"
		developerConnection = "scm:git:git@github.com:FabricMC/fabric.git"
	}
	pom.issueManagement {
		system = "GitHub"
		url = "https://github.com/FabricMC/fabric/issues"
	}
}

subprojects {
	if (metaProjects.contains(it.name)) {
		return
	}

	base {
		archivesName = project.name
	}

	dependencies {
		testmodImplementation sourceSets.main.output

		// Make all modules depend on the gametest api (and thus res loader) to try and promote its usage.
		if (project.name != "fabric-gametest-api-v1") {
			testmodImplementation project(path: ':fabric-gametest-api-v1', configuration: 'namedElements')
			testmodClientImplementation project(":fabric-gametest-api-v1").sourceSets.client.output
			testmodImplementation project(path: ':fabric-resource-loader-v0', configuration: 'namedElements')
			testmodClientImplementation project(":fabric-resource-loader-v0").sourceSets.client.output
		}

		// Make all testmods run with registry-sync-v0 as it is required to register new objects.
		if (project.name != "fabric-registry-sync-v0") {
			testmodRuntimeOnly project(path: ':fabric-registry-sync-v0', configuration: 'namedElements')
			testmodClientImplementation project(":fabric-registry-sync-v0").sourceSets.client.output
		}
	}

	publishing {
		publications {
			mavenJava(MavenPublication) {
				pom {
					addPomMetadataInformation(project, pom)
				}
				artifact(signRemapJar.output) {
					builtBy(signRemapJar)
				}

				artifact(remapSourcesJar) {
					builtBy remapSourcesJar
				}
			}
		}
	}

	def projectName = it.name
	def projectVersion = getSubprojectVersion(it)

	// Skip publishing if the artifact already exists on the maven server
	tasks.withType(PublishToMavenRepository).configureEach {
		onlyIf {
			if (!providers.environmentVariable("MAVEN_URL").present) {
				// Always try to publish if the maven url is not set (e.g locally)
				return true
			}

			def artifactPath = "https://maven.fabricmc.net/net/fabricmc/fabric-api/${projectName}/${projectVersion}/${projectName}-${projectVersion}.pom"

			boolean exists = HttpClient.newHttpClient().withCloseable { client ->
				def request = HttpRequest.newBuilder()
						.uri(URI.create(artifactPath))
						.method("HEAD", HttpRequest.BodyPublishers.noBody())
						.build()

				def response = client.send(request, HttpResponse.BodyHandlers.discarding())
				response.statusCode() == 200
			}

			if (exists) {
				logger.lifecycle("${projectName}-${projectVersion}.pom has already been published")
			} else {
				logger.lifecycle("${projectName}-${projectVersion}.pom does not exist, publishing")
			}

			return !exists
		}
	}

	// We manually handle the pom generation
	loom.disableDeprecatedPomGeneration(publishing.publications.mavenJava)

	javadoc.enabled = false
}

publishing {
	publications {
		mavenJava(MavenPublication) {
			artifact(signRemapJar.output) {
				builtBy(signRemapJar)
			}

			artifact(sourcesJar) {
				builtBy remapSourcesJar
			}

			artifact javadocJar
			artifact remapTestmodJar

			pom {
				addPomMetadataInformation(rootProject, pom)
			}

			List<Map<String, String>> dependencies = []

			subprojects.each {
				// The maven BOM containing all of the deprecated modules is added manually below.
				if (it.path.startsWith(":deprecated") || metaProjects.contains(it.name)) {
					return
				}

				dependencies.add([
					'groupId': it.group,
					'artifactId': it.name,
					'version': getSubprojectVersion(it),
					'scope': 'compile'
				])
			}

			def thisGroup = group
			def thisVersion = version

			pom.withXml {
				def depsNode = asNode().appendNode("dependencies")
				for (dep in dependencies) {
					def depNode = depsNode.appendNode("dependency")
					depNode.appendNode("groupId", dep['groupId'])
					depNode.appendNode("artifactId", dep['artifactId'])
					depNode.appendNode("version", dep['version'])
					depNode.appendNode("scope", dep['scope'])
				}

				// Depend on the deprecated BOM to allow opting out of deprecated modules.
				def depNode = depsNode.appendNode("dependency")
				depNode.appendNode("groupId", thisGroup)
				depNode.appendNode("artifactId", "fabric-api-deprecated")
				depNode.appendNode("version", thisVersion)
				depNode.appendNode("scope", "compile")
			}
		}
	}
}

// Required until the deprecation is removed. Fabric API's main jar that is published to maven does not contain sub modules.
loom.disableDeprecatedPomGeneration(publishing.publications.mavenJava)

void setupRepositories(RepositoryHandler repositories) {
	//repositories.mavenLocal() // uncomment for testing
	if (providers.environmentVariable("MAVEN_URL").present) {
		repositories.maven {
			url = providers.environmentVariable("MAVEN_URL")
			credentials {
				username = providers.environmentVariable("MAVEN_USERNAME").get()
				password = providers.environmentVariable("MAVEN_PASSWORD").get()
			}
		}
	}
}

subprojects.each {
	if (metaProjects.contains(it.name)) {
		return
	}

	remapJar.dependsOn("${it.path}:remapJar")
}

// These modules are not included in the fat jar, maven will resolve them via the pom.
def devOnlyModules = [
	"fabric-client-gametest-api-v1",
	"fabric-gametest-api-v1",
]

dependencies {
	afterEvaluate {
		subprojects.each {
			if (metaProjects.contains(it.name)) {
				return
			}

			api project(path: "${it.path}", configuration: "namedElements")
			clientImplementation project("${it.path}:").sourceSets.client.output

			testmodImplementation project("${it.path}:").sourceSets.testmod.output
			testmodClientImplementation project("${it.path}:").sourceSets.testmodClient.output
		}
	}
}

configurations {
	nestedJars {
		transitive = false
	}
}

dependencies {
	subprojects.each {
		if (it.name in devOnlyModules || metaProjects.contains(it.name)) {
			return
		}

		nestedJars project("${it.path}")
	}
}

remapJar {
	nestedJars.from configurations.nestedJars
}

// Attempt to create a single jar with all files from all nested jars, this will fail if there are duplicate files.
tasks.register("checkNoDuplicateFiles", Zip) {
	inputs.files configurations.nestedJars
	destinationDirectory = layout.buildDirectory.dir("test")

	from {
		configurations.nestedJars.files.collect { zipTree(it) }
	}

	// We expect these files to be duplicated, so exclude them.
	exclude 'META-INF/**'
	exclude 'fabric.mod.json'
}

check.dependsOn "checkNoDuplicateFiles"

publishMods {
	file = signRemapJar.output
	changelog = providers.environmentVariable("CHANGELOG").getOrElse("No changelog provided")
	type = project.prerelease == "true" ? BETA : STABLE
	displayName = "[${project.minecraft_version}] Fabric API $project.version"
	modLoaders.add("fabric")
	dryRun = providers.environmentVariable("CURSEFORGE_API_KEY").getOrNull() == null

	curseforge {
		accessToken = providers.environmentVariable("CURSEFORGE_API_KEY")
		projectId = "306612"
		minecraftVersions.add(project.curseforge_minecraft_version)
	}
	modrinth {
		accessToken = providers.environmentVariable("MODRINTH_TOKEN")
		projectId = "P7dR8mSH"
		minecraftVersions.add(project.minecraft_version)
	}
	github {
		accessToken = providers.environmentVariable("GITHUB_TOKEN")
		repository = providers.environmentVariable("GITHUB_REPOSITORY").getOrElse("FabricMC/dryrun")
		commitish = providers.environmentVariable("GITHUB_REF_NAME").getOrElse("dryrun")
	}
}

assemble.dependsOn signRemapJar

import java.util.stream.Collectors

// A task to ensure that the version being released has not already been released.
tasks.register('checkVersion') {
	doFirst {
		def xml = new URL("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/maven-metadata.xml").text
		def metadata = new XmlSlurper().parseText(xml)
		def versions = metadata.versioning.versions.version*.text();
		if (versions.contains(version)) {
			throw new RuntimeException("${version} has already been released!")
		}
	}
}

tasks.publishMods.dependsOn checkVersion
publish.mustRunAfter checkVersion