version = getSubprojectVersion(project)

moduleDependencies(project, [
	'fabric-convention-tags-v2',
	'fabric-recipe-api-v1',
	'fabric-registry-sync-v0',
	'fabric-resource-conditions-api-v1',
	'fabric-tag-api-v1',
])

testDependencies(project, [
	':fabric-item-group-api-v1',
    ':fabric-object-builder-api-v1'
])

dependencies {
}

sourceSets {
	testmod {
		resources {
			srcDirs += [
				'src/testmod/generated'
			]
		}
	}
}

loom {
	accessWidenerPath = file("src/main/resources/fabric-data-generation-api-v1.accesswidener")

	runs {
		datagen {
			inherit testmodServer
			name "Data Generation"
			vmArg "-Dfabric-api.datagen"
			vmArg "-Dfabric-api.datagen.output-dir=${file("src/testmod/generated")}"
			vmArg "-Dfabric-api.datagen.strict-validation"

			ideConfigGenerated = true
			runDir "build/datagen"
		}
		datagenClient {
			client()
			name "Data Generation Client"
			vmArg "-Dfabric-api.datagen"
			vmArg "-Dfabric-api.datagen.output-dir=${file("src/testmod/generated")}"
			vmArg "-Dfabric-api.datagen.strict-validation"

			ideConfigGenerated = true
			runDir "build/datagen"
			source sourceSets.testmodClient
		}
	}
}

runDatagen {
	outputs.dir file("src/testmod/generated")
}

runDatagenClient {
	outputs.dir file("src/testmod/generated")
}

test.dependsOn runDatagenClient

tasks.register('datapackZip', Zip) {
	dependsOn runDatagenClient
	archiveFileName = "${base.archivesName.get()}-${project.version}-test-datapack.zip"
	destinationDirectory = layout.buildDirectory.dir('libs')

	from file("src/testmod/generated")
	from file("pack.mcmeta")
}

build.dependsOn datapackZip

import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode

import java.lang.reflect.Modifier
import java.util.zip.ZipEntry
import java.util.zip.ZipFile

tasks.register('generateAccessWidener') {
	inputs.files(loom.getNamedMinecraftJars())

	doLast {
		// Use parent provider to get the jar before the AWs are applied
		def minecraftProvider = loom.namedMinecraftProvider.parentMinecraftProvider
		def commonJar = minecraftProvider.commonJar.path.toFile()
		def clientJar = minecraftProvider.clientOnlyJar.path.toFile()
		def classes = getClasses([commonJar, clientJar])

		String accessWidener = "accessWidener\tv2\tnamed\n"
		accessWidener += "\n"
		accessWidener += "# DO NOT EDIT BY HAND! This file is generated automatically.\n"
		accessWidener += "# Edit \"template.accesswidener\" instead then run \"gradlew generateAccessWidener\".\n"
		accessWidener += "\n"
		accessWidener += file("template.accesswidener").text + "\n"

		visitMethods(classes["net/minecraft/data/recipe/RecipeGenerator"]) { name, desc, owner ->
			if (it.name == "generate")
				return

			accessWidener += "transitive-accessible\tmethod\t${owner}\t${name}\t${desc}\n"
		}

		visitMethods(classes["net/minecraft/client/data/BlockStateModelGenerator"]) { name, desc, owner ->
			if (desc == "()V")
				// Skip over methods that dont take any arguments, as they are specific to minecraft.
				return

			accessWidener += "transitive-accessible\tmethod\t${owner}\t${name}\t${desc}\n"
		}

		visitMethods(classes["net/minecraft/data/loottable/BlockLootTableGenerator"]) { name, desc, owner ->
			accessWidener += "transitive-accessible\tmethod\t${owner}\t${name}\t${desc}\n"
		}

		visitMethods(classes["net/minecraft/client/data/ItemModelGenerator"]) { name, desc, owner ->
			accessWidener += "transitive-accessible\tmethod\t${owner}\t${name}\t${desc}\n"
		}

		classes.values().forEach { classNode ->
			visitFinalMethods(classNode) { name, desc, owner ->
				if (name != "getName" || desc != "()Ljava/lang/String;") {
					// Not the method we are after
					return
				}

				if (!hasAncestor(classNode, classes, "net/minecraft/data/DataProvider")) {
					// Not a descendant of DataProvider
					return
				}

				accessWidener += "transitive-extendable\tmethod\t${owner}\t${name}\t${desc}\n"
			}
		}

		file("src/main/resources/fabric-data-generation-api-v1.accesswidener").text = accessWidener
	}
}

static def visitMethods(ClassNode classNode, closure) {
	classNode.methods.forEach {
		if ((it.access & Opcodes.ACC_SYNTHETIC) != 0 || (it.access & Opcodes.ACC_PUBLIC) != 0)
			return

		if (it.name.startsWith("<"))
			return

		closure(it.name, it.desc, classNode.name)
	}
}

static def visitFinalMethods(ClassNode classNode, closure) {
	classNode.methods.forEach {
		if (!Modifier.isFinal(it.access))
			return

		if (it.name.startsWith("<"))
			return

		closure(it.name, it.desc, classNode.name)
	}
}

// Return a map of all class names to classNodes
static def getClasses(List<File> inputs) {
	Map<String, ClassNode> classes = new TreeMap<>()

	for (File input : inputs) {
		new ZipFile(input).withCloseable { ZipFile zip  ->
			zip.entries().toList().forEach { ZipEntry entry ->
				if (!entry.name.endsWith(".class")) {
					return
				}

				zip.getInputStream(entry).withCloseable { is ->
					ClassReader reader = new ClassReader(is)
					ClassNode classNode = new ClassNode()
					reader.accept(classNode, ClassReader.SKIP_CODE)

					classes.put(classNode.name, classNode)
				}
			}
		}
	}

	return classes
}

def hasAncestor(ClassNode classNode, Map<String, ClassNode> classes, String ancestorName) {
	if (classNode.superName == ancestorName) {
		return true
	}

	// Recuse through the super classes
	def superClass = classes.get(classNode.superName)
	if (superClass != null && hasAncestor(superClass, classes, ancestorName)) {
		return true
	}

	for (def interfaceName : classNode.interfaces) {
		if (interfaceName == ancestorName) {
			return true
		}

		def ifaceClass = classes.get(interfaceName)
		if (ifaceClass != null && hasAncestor(ifaceClass, classes, ancestorName)) {
			return true
		}
	}
}

generateResources.dependsOn generateAccessWidener
generateResources.dependsOn runDatagenClient
