/*
 * Copyright 2025 Dynatrace LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.diffplug.gradle.spotless.JavaExtension

plugins {
	id "java-library"
	id "jacoco"
	id "me.champeau.jmh" version "0.7.3"
	id "org.sonarqube" version "6.3.1.5724"
	id "com.diffplug.spotless" version "8.0.0"
	id "maven-publish"
	id "signing"
	id "net.ltgt.errorprone" version "4.3.0"
	id "me.champeau.gradle.japicmp" version "0.4.5"
}

repositories {
	mavenCentral()
	gradlePluginPortal()
}

ext {
	latestReleasedVersion = {
		def tempConfig = configurations.create("tempConfig_4e0253959df7d471")
		dependencies.add(tempConfig.getName(), "com.dynatrace.hash4j:hash4j:+")
		def moduleVersion = tempConfig.resolvedConfiguration.firstLevelModuleDependencies[0].moduleVersion
		configurations.remove(tempConfig)
		return moduleVersion
	}.call()
	archivesBaseName = "hash4j"
}

group = "com.dynatrace.hash4j"
version = "0.27.0-SNAPSHOT"

dependencies {
	testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.4") // upgrade to version 6 requires Java 17
	testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.13.4") // upgrade to version 6 requires Java 17
	testImplementation("org.junit.jupiter:junit-jupiter-params:5.13.4") // upgrade to version 6 requires Java 17
	testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.13.4") // upgrade to version 6 requires Java 17
	testImplementation("org.assertj:assertj-core:3.27.6")
	testImplementation("org.hipparchus:hipparchus-stat:4.0.2")
	testImplementation("org.hipparchus:hipparchus-optim:4.0.2")
	testImplementation("com.google.guava:guava:33.5.0-jre")
	testImplementation("commons-codec:commons-codec:1.19.0")
	testImplementation("net.openhft:zero-allocation-hashing:0.16")
	testImplementation("com.appmattus.crypto:cryptohash:1.0.2")
	testImplementation("org.greenrobot:essentials:3.1.0")
	testImplementation("com.sangupta:murmur:1.0.0")
	errorprone("com.google.errorprone:error_prone_core:2.42.0")
}

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

sourceSets {
	jmh
	main
	test
	java21 {
		java.srcDir "src/main/java21"
		compileClasspath += sourceSets.main.output
	}
	java21Test {
		java.srcDir "src/test/java21"
		compileClasspath += sourceSets.test.output + sourceSets.java21.output + sourceSets.main.output
		runtimeClasspath += sourceSets.test.output
	}
}


configurations {
	java21TestImplementation.extendsFrom testImplementation
	java21TestRuntimeOnly.extendsFrom testRuntimeOnly
}


File findArtifact(String version) {
	def existingGroup = project.group
	try {
		project.group = "tempGroup_ae16825cf1d0af8d"
		def depModule = "$existingGroup:$archivesBaseName:$version@jar"
		def depJar = "${archivesBaseName}-${version}.jar"
		def configuration = project.configurations.detachedConfiguration(
				project.dependencies.create(depModule)
				)
		return project.files(configuration.files).filter { it.name == depJar }.singleFile
	} finally {
		project.group = existingGroup
	}
}


tasks.register("jApiCmp", me.champeau.gradle.japicmp.JapicmpTask) {
	dependsOn "jar"

	def baselineVersion = project.hasProperty("apiCheckBaseVersion")?apiCheckBaseVersion:latestReleasedVersion
	def baselineArtifact = findArtifact(baselineVersion)
	oldClasspath = files(baselineArtifact)

	def newVersion = project.hasProperty("apiCheckNewVersion")?apiCheckNewVersion:version
	def newArtifact = project.hasProperty("apiCheckNewVersion")?findArtifact(newVersion):file(tasks.named("jar").get().archiveFile.get().asFile)
	newClasspath = files(newArtifact)

	onlyModified = true
	packageExcludes.addAll("*.internal", "*.internal.*")


	txtOutputFile = file("$rootDir/docs/apidiffs/v${baselineVersion}___v${newVersion}.txt")
}

jar {
	dependsOn "compileJava21Java"
	into("META-INF/versions/21") {
		from sourceSets.java21.output
	}
	manifest {
		attributes "Multi-Release": "true"
	}
}

tasks.compileJava.options.release.set(11)
tasks.compileTestJava.options.release.set(11)
tasks.compileJava21Java.options.release.set(21)
tasks.compileJava21TestJava.options.release.set(21)

tasks.test {
	javaLauncher = javaToolchains.launcherFor {
		languageVersion = JavaLanguageVersion.of(11)
	}
	finalizedBy jacocoTestReport
}

tasks.register("java21Test", Test) {
	group = LifecycleBasePlugin.VERIFICATION_GROUP
	javaLauncher = javaToolchains.launcherFor {
		languageVersion = JavaLanguageVersion.of(21)
	}
	testClassesDirs = sourceSets.java21Test.output + sourceSets.test.output
	classpath = files(jar) + sourceSets.java21Test.runtimeClasspath
	finalizedBy "jacocoTestReportJava21"
}

tasks.check {
	dependsOn "java21Test", "jacocoTestCoverageVerification", "jacocoTestCoverageVerificationJava21", "jApiCmp"
}

tasks.withType(Test).configureEach {
	useJUnitPlatform()
	maxHeapSize = "8g"
	maxParallelForks =  Runtime.runtime.availableProcessors()
}

tasks.withType(JavaCompile).configureEach {
	options.compilerArgs << "-Werror"
	options.errorprone.disableWarningsInGeneratedCode = false
	// options.errorprone.enabled = false
}


static def readJavaLicense(licenseName) {
	File licenseFile = new File("licenses/" + licenseName + ".txt")
	def line
	def s = "/*\n"
	licenseFile.withReader { reader ->
		while ((line = reader.readLine()) != null) {
			s += " *"
			if(!line.isEmpty()) {
				s += " "
				s += line
			}
			s += "\n"
		}
	}
	s += " */"
	return s
}

static def readPythonLicense(licenseName) {
	File licenseFile = new File("licenses/" + licenseName + ".txt")
	def line
	def s = "#\n"
	licenseFile.withReader { reader ->
		while ((line = reader.readLine()) != null) {
			s += "#"
			if(!line.isEmpty()) {
				s += " "
				s += line
			}
			s += "\n"
		}
	}
	s += "#"
	return s
}

apply plugin: "groovy"
spotless {
	def googleJavaFormatVersion = "1.28.0"
	def eclipseCdtVersion = "11.6"
	def blackVersion = "25.1.0"
	def greclipseVersion = "4.32"
	def specialLicenseHeaders = [
		new Tuple3("javaImohash", "MIT_IMOHASH", [
			"src/main/java/com/dynatrace/hash4j/file/Imohash1_0_2.java"
		]),
		new Tuple3("javaKomihash", "MIT_KOMIHASH" , [
			"src/main/java/com/dynatrace/hash4j/hashing/Komihash4_3.java",
			"src/main/java/com/dynatrace/hash4j/hashing/Komihash5_0.java",
			"src/main/java/com/dynatrace/hash4j/hashing/AbstractKomihash.java"
		]),
		new Tuple3("javaRapidhash", "MIT_RAPIDHASH" , [
			"src/main/java/com/dynatrace/hash4j/hashing/Rapidhash3.java"
		]),
		new Tuple3("javaFarmHash", "MIT_APACHE_2_0_FARMHASH",[
			"src/main/java/com/dynatrace/hash4j/hashing/FarmHashNa.java",
			"src/main/java/com/dynatrace/hash4j/hashing/FarmHashUo.java"
		]),
		new Tuple3("javaPolymurHash", "ZLIB_POLYMURHASH",[
			"src/main/java/com/dynatrace/hash4j/hashing/PolymurHash2_0.java"
		]),
		new Tuple3("javaSplitMix64", "CREATIVE_COMMONS_SPLITMIX64",[
			"src/main/java/com/dynatrace/hash4j/random/SplitMix64V1.java"
		]),
		new Tuple3("javaExponential", "BOOST_EXPONENTIAL_RANDOM_GENERATION",[
			"src/main/java/com/dynatrace/hash4j/random/RandomExponentialUtil.java"
		]),
		new Tuple3("javaConsistentJumpHash", "APACHE_2_0_GUAVA",[
			"src/main/java/com/dynatrace/hash4j/consistent/ConsistentJumpBucketHasher.java"
		]),
		new Tuple3("javaXXH", "APACHE_2_0_XXH",[
			"src/main/java/com/dynatrace/hash4j/hashing/XXH3_64.java",
			"src/main/java/com/dynatrace/hash4j/hashing/XXH3_128.java",
			"src/main/java/com/dynatrace/hash4j/hashing/XXH3Base.java"
		]),
		new Tuple3("javaConsistentJumpBackAnchorBucketSetHasher", "MIT_ANCHOR_HASH",[
			"src/main/java/com/dynatrace/hash4j/consistent/ConsistentJumpBackAnchorBucketSetHasher.java"
		])
	]

	ratchetFrom "origin/main"
	groovy {
		importOrder()
		removeSemicolons()
		greclipse(greclipseVersion)
		licenseHeader readJavaLicense("APACHE_2_0_DYNATRACE"), "(package|import|public|class|module|plugins)"
		target("**/build.gradle")
	}
	python {
		target "python/**/*.py"
		black(blackVersion)
		licenseHeader readPythonLicense("APACHE_2_0_DYNATRACE"), "(import|from)"
	}
	cpp {
		target "reference-implementations/*/*.cpp", "reference-implementations/*/*.hpp", "reference-implementations/*.cpp", "reference-implementations/*.hpp"
		eclipseCdt(eclipseCdtVersion)
		licenseHeader readJavaLicense("APACHE_2_0_DYNATRACE")
	}
	java {
		importOrder()
		removeUnusedImports()
		cleanthat()
		googleJavaFormat(googleJavaFormatVersion)
		formatAnnotations()
		licenseHeader readJavaLicense("APACHE_2_0_DYNATRACE")
		targetExclude specialLicenseHeaders.collect {it.get(2)}.flatten()
	}
	specialLicenseHeaders.forEach {
		def formatName = it.get(0)
		def licenseName = it.get(1)
		def files = it.get(2)
		format formatName, JavaExtension, {
			importOrder()
			removeUnusedImports()
			cleanthat()
			googleJavaFormat(googleJavaFormatVersion)
			formatAnnotations()
			licenseHeader readJavaLicense("APACHE_2_0_DYNATRACE") + "\n\n" + readJavaLicense(licenseName)
			target files
		}
	}
}

sonarqube {
	properties {
		property "sonar.projectKey", "dynatrace-oss_hash4j"
		property "sonar.organization", "dynatrace-oss"
	}
}


jmh {
	fork = 1
	timeUnit = "us"
	failOnError = false
	timeOnIteration = "1s"
	warmupForks = 0
	warmupIterations = 5
	warmupBatchSize = 1
	warmup = "1s"
	iterations = 20
	resultFormat = "JSON"
	if (project.hasProperty("benchmarkIncludes")) {
		includes = [benchmarkIncludes.trim()]
	}
	jvmArgs = ["-Xmx8g"]
}

tasks.register("evaluateBenchmarks", Exec) {
	group = "evaluation"
	workingDir = "."
	commandLine "python", "python/benchmark_evaluation.py"
}

tasks.register("evaluateEstimationErrors", Exec) {
	group = "evaluation"
	workingDir = "."
	commandLine "python", "python/estimation_error_evaluation.py"
}

def sketches = ["UltraLogLog", "HyperLogLog"]
def pValues = [
	3,
	4,
	5,
	6,
	7,
	8,
	9,
	10,
	11,
	12,
	13,
	14,
	15,
	16
]
def evaluationTasks = []
for (sketch in sketches) {
	def sketchTasks = []
	for (p in pValues) {
		def sketchTaskName = "simulate" + sketch + "EstimationErrorsP" + String.format( "%02d", p )
		def outputFileName = "test-results/" + sketch.toLowerCase() + "-estimation-error-p" + String.format( "%02d", p )  + ".csv"
		tasks.register("${sketchTaskName}", JavaExec) {
			outputs.files outputFileName
			group = "evaluation"
			classpath = sourceSets.test.runtimeClasspath
			mainClass = "com.dynatrace.hash4j.distinctcount." + sketch + "EstimationErrorSimulation"
			args = [p.toString(), outputFileName]
			jvmArgs = ["-Xmx16g"]
		}
		sketchTasks.add(sketchTaskName)
	}
	def evaluationTaskName = "simulate" + sketch + "EstimationErrors"
	tasks.register("${evaluationTaskName}") {
		group = "evaluation"
		dependsOn sketchTasks
	}
	evaluationTasks.add(evaluationTaskName)
}
tasks.register("simulateEstimationErrors") {
	group = "evaluation"
	dependsOn evaluationTasks
}

tasks.register("checkStatusForBenchmarks") {
	outputs.upToDateWhen { false }
	doLast {
		def status_text = "git status --porcelain".execute().text
		if(status_text?.trim() && !(project.hasProperty("benchmarkIgnoreGitChanges") && benchmarkIgnoreGitChanges.trim().equals("true"))) {
			throw new GradleException("There are uncommitted changes:\n" + status_text)
		}
	}
}

tasks.register("copyBenchmarkReport", Copy) {
	def proc = "git rev-parse HEAD".execute()
	def revision = proc.text.trim()
	from("build/results/jmh/") {
		include "results.*"
		if (project.hasProperty("benchmarkPrefix") && !benchmarkPrefix.trim().isEmpty()) {
			rename "results", benchmarkPrefix.trim() + "-" + new Date().format("yyyy-MM-dd-HH-mm-ss") + " " + revision
		} else {
			rename "results", new Date().format("yyyy-MM-dd-HH-mm-ss") + " " + revision
		}
	}
	into "benchmark-results"
}

tasks.register("deleteBenchmarkReport", Delete) {
	delete "build/results/jmh/results.json"
}

tasks.jmh.finalizedBy copyBenchmarkReport
tasks.copyBenchmarkReport.finalizedBy deleteBenchmarkReport
tasks.jmh.dependsOn checkStatusForBenchmarks
tasks.copyBenchmarkReport.dependsOn checkStatusForBenchmarks
tasks.simulateHyperLogLogEstimationErrors.finalizedBy evaluateEstimationErrors
tasks.simulateUltraLogLogEstimationErrors.finalizedBy evaluateEstimationErrors

javadoc {
	failOnError = true
	title = archivesBaseName + " " + project.version + " API"
}

jacoco {
	toolVersion = "0.8.13"
}

jacocoTestReport {
	dependsOn test
}

tasks.register("jacocoTestReportJava21", JacocoReport) {
	dependsOn java21Test
	executionData java21Test
	sourceSets sourceSets.java21
	reports.html.outputLocation = layout.buildDirectory.dir("reports/jacoco/java21Test/html")
	reports.csv.outputLocation = layout.buildDirectory.file("reports/jacoco/java21Test/jacocoTestReport.csv")
	reports.xml.outputLocation = layout.buildDirectory.file("reports/jacoco/java21Test/jacocoTestReport.xml")
}

tasks.withType(JacocoReport).configureEach {
	reports {
		xml.required = true // needed for sonarqube
		csv.required = true // needed for sonarqube
	}
}

tasks.register("jacocoTestCoverageVerificationJava21", JacocoCoverageVerification) {
	dependsOn jacocoTestReportJava21
	executionData jacocoTestReportJava21.executionData
	sourceSets sourceSets.java21
}

tasks.withType(JacocoCoverageVerification).configureEach {
	violationRules {
		rule {
			limit {
				minimum = 1.0
				counter = "LINE"
			}
			limit {
				minimum = 1.0
				counter = "BRANCH"
			}
		}
	}
}

publishing {
	publications {
		mavenJava(MavenPublication) {
			from components.java
			pom {
				name = "com.dynatrace.hash4j:hash4j"
				description = "hash4j: A Dynatrace hash library for Java"
				url = "https://github.com/dynatrace-oss/hash4j"
				licenses {
					license {
						name = "The Apache License, Version 2.0"
						url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
					}
				}
				developers {
					developer {
						id = "Dynatrace"
						name = "Dynatrace LLC"
						email = "opensource@dynatrace.com"
					}
				}
				scm {
					connection = "scm:git:git://github.com/dynatrace-oss/hash4j.git"
					developerConnection = "scm:git:ssh://github.com/dynatrace-oss/hash4j.git"
					url = "https://github.com/dynatrace-oss/hash4j"
				}
			}
		}
	}
	repositories {
		maven {
			url = "$projectDir/build/mavencentral/repo"
		}
	}
}

signing {
	useInMemoryPgpKeys(System.getenv("GPG_PRIVATE_KEY"), System.getenv("GPG_PASSPHRASE"))
	sign publishing.publications.mavenJava
}

tasks.register("createRepoForUploadToMavenCentral", Zip) {
	dependsOn build, publish
	from "$projectDir/build/mavencentral/repo"
	destinationDirectory = file("$projectDir/build/mavencentral")
	archiveFileName = "repo.zip"
}
