// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0

package cc.tweaked.gradle

import cc.tweaked.vanillaextract.configurations.Capabilities
import cc.tweaked.vanillaextract.configurations.MinecraftSetup
import org.gradle.api.Project
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.kotlin.dsl.get

/**
 * This sets up a separate client-only source set, and extends that and the main/common source set with additional
 * metadata, to make it easier to consume jars downstream.
 */
class MinecraftConfigurations private constructor(private val project: Project) {
    private val java = project.extensions.getByType(JavaPluginExtension::class.java)
    private val sourceSets = java.sourceSets
    private val configurations = project.configurations

    private val main = sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
    private val test = sourceSets[SourceSet.TEST_SOURCE_SET_NAME]

    /**
     * Performs the initial setup of our configurations.
     */
    private fun setup() {
        // Define a client source set.
        val client = sourceSets.maybeCreate("client")

        // Ensure the client classpaths behave the same as the main ones.
        consistentWithMain(client)

        // Set up an API configuration for clients (to ensure it's consistent with the main source set).
        val clientApi = configurations.maybeCreate(client.apiConfigurationName).apply {
            isCanBeConsumed = false
            isCanBeResolved = false
        }
        configurations.named(client.implementationConfigurationName) { extendsFrom(clientApi) }

        project.tasks.register(client.jarTaskName, Jar::class.java) {
            description = "An empty jar standing in for the client classes."
            group = BasePlugin.BUILD_GROUP
            archiveClassifier.set("client")
        }

        MinecraftSetup(project).setupOutgoingConfigurations()

        // Reset the client classpath (Loom configures it slightly differently to this) and add a main -> client
        // dependency. Here we /can/ use source set outputs as we add transitive deps by patching the classpath. Nasty,
        // but avoids accidentally pulling in Forge's obfuscated jar.
        client.compileClasspath = client.compileClasspath + main.compileClasspath
        client.runtimeClasspath = client.runtimeClasspath + main.runtimeClasspath
        project.dependencies.add(client.apiConfigurationName, main.output)

        // Also add client classes to the test classpath. We do the same nasty tricks as needed for main -> client.
        test.compileClasspath += client.compileClasspath
        test.runtimeClasspath += client.runtimeClasspath
        project.dependencies.add(test.implementationConfigurationName, client.output)

        // Configure some tasks to include our additional files.
        project.tasks.named("javadoc", Javadoc::class.java) {
            source(client.allJava)
            classpath = main.compileClasspath + main.output + client.compileClasspath + client.output
        }
        // This are already done by Fabric, but we need it for Forge and vanilla. It shouldn't conflict at all.
        project.tasks.named("jar", Jar::class.java) { from(client.output) }
        project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) }

        setupBasic()
    }

    private fun consistentWithMain(sourceSet: SourceSet) {
        configurations.named(sourceSet.compileClasspathConfigurationName) {
            shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
        }

        configurations.named(sourceSet.runtimeClasspathConfigurationName) {
            shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
        }
    }

    private fun setupBasic() {
        val client = sourceSets["client"]

        project.extensions.configure(CCTweakedExtension::class.java) {
            sourceDirectories.add(SourceSetReference.internal(client))
        }

        // Register a task to check there are no conflicts with the core project.
        val checkDependencyConsistency =
            project.tasks.register("checkDependencyConsistency", DependencyCheck::class.java) {
                // We need to check both the main and client classpath *configurations*, as the actual configuration
                configuration(configurations.named(main.runtimeClasspathConfigurationName))
                configuration(configurations.named(client.runtimeClasspathConfigurationName))
            }
        project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
    }

    /**
     * Create a new configuration that pulls in the main and client classes from the mod.
     */
    private fun createDerivedConfiguration(name: String) {
        val client = sourceSets["client"]
        val sourceSet = sourceSets.create(name)
        sourceSet.compileClasspath += main.compileClasspath + client.compileClasspath
        sourceSet.runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
        consistentWithMain(sourceSet)
        project.dependencies.add(sourceSet.implementationConfigurationName, main.output)
        project.dependencies.add(sourceSet.implementationConfigurationName, client.output)
    }

    companion object {
        const val DATAGEN = "datagen"
        const val EXAMPLES = "examples"
        const val TEST_MOD = "testMod"

        fun setupBasic(project: Project) {
            MinecraftConfigurations(project).setupBasic()
        }

        fun setup(project: Project) {
            MinecraftConfigurations(project).setup()
        }

        fun createDerivedConfiguration(project: Project, name: String) {
            MinecraftConfigurations(project).createDerivedConfiguration(name)
        }
    }
}

fun DependencyHandler.clientClasses(notation: Any): ModuleDependency =
    Capabilities.clientClasses(create(notation) as ModuleDependency)

fun DependencyHandler.commonClasses(notation: Any): ModuleDependency =
    Capabilities.commonClasses(create(notation) as ModuleDependency)
