/*
 * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.gradle.targets.js.ir

import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication.setUpResourcesVariant
import org.jetbrains.kotlin.gradle.targets.js.*
import org.jetbrains.kotlin.gradle.targets.js.dsl.*
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTargetConfigurator.Companion.configureJsDefaultOptions
import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsPlugin
import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.Companion.kotlinNodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.wasm.npm.WasmNpmResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolverPlugin
import org.jetbrains.kotlin.gradle.targets.js.typescript.TypeScriptValidationTask
import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec
import org.jetbrains.kotlin.gradle.tasks.registerTask
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
import org.jetbrains.kotlin.utils.addIfNotNull
import javax.inject.Inject
import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsRootPlugin.Companion.kotlinNodeJsRootExtension as wasmKotlinNodeJsRootExtension

internal fun ObjectFactory.KotlinJsIrTarget(
    project: Project,
    platformType: KotlinPlatformType,
    isMpp: Boolean,
): KotlinJsIrTarget = newInstance(project, platformType, isMpp)

abstract class KotlinJsIrTarget
@Inject
constructor(
    project: Project,
    platformType: KotlinPlatformType,
    internal val isMpp: Boolean
) :
    KotlinTargetWithBinaries<KotlinJsIrCompilation, KotlinJsBinaryContainer>(project, platformType),
    KotlinTargetWithTests<JsAggregatingExecutionSource, KotlinJsReportAggregatingTestRun>,
    KotlinJsTargetDsl,
    KotlinWasmJsTargetDsl,
    KotlinWasmWasiTargetDsl,
    KotlinJsSubTargetContainerDsl,
    KotlinWasmSubTargetContainerDsl {

    @Deprecated(
        message = "Internal Kotlin Gradle Plugin API. Scheduled for removal in Kotlin 2.4."
    )
    constructor(
        project: Project,
        platformType: KotlinPlatformType,
    ) : this(project, platformType, true)

    private val propertiesProvider = PropertiesProvider(project)
    internal val shouldGenerateTypeScriptDefinitions: Property<Boolean> = project.objects.property<Boolean>(false)

    override val subTargets: NamedDomainObjectContainer<KotlinJsIrSubTargetWithBinary> = project.container(
        KotlinJsIrSubTargetWithBinary::class.java
    )

    override val testRuns: NamedDomainObjectContainer<KotlinJsReportAggregatingTestRun> by lazy {
        project.container(KotlinJsReportAggregatingTestRun::class.java, KotlinJsTestRunFactory(this))
    }

    override var wasmTargetType: KotlinWasmTargetType? = null
        internal set

    @Deprecated("Use outputModuleName with Provider API instead. Scheduled for removal in Kotlin 2.3.", level = DeprecationLevel.ERROR)
    override var moduleName: String?
        get() = outputModuleName.get()
        set(value) {
            outputModuleName.set(value)
        }

    override val kotlinComponents: Set<KotlinTargetComponent> by lazy {
        val mainCompilation = compilations.getByName(MAIN_COMPILATION_NAME)
        val usageContexts = createUsageContexts(mainCompilation).toMutableSet()

        val componentName =
            if (project.kotlinExtension is KotlinMultiplatformExtension)
                targetName
            else PRIMARY_SINGLE_COMPONENT_NAME

        usageContexts.addIfNotNull(
            createSourcesJarAndUsageContextIfPublishable(
                producingCompilation = mainCompilation,
                componentName = componentName,
                artifactNameAppendix = wasmDecamelizedDefaultNameOrNull() ?: dashSeparatedName(targetName.toLowerCaseAsciiOnly())
            )
        )

        usageContexts.addIfNotNull(
            setUpResourcesVariant(
                mainCompilation
            )
        )

        val result = createKotlinVariant(componentName, mainCompilation, usageContexts)

        setOf(result)
    }

    override fun createKotlinVariant(
        componentName: String,
        compilation: KotlinCompilation<*>,
        usageContexts: Set<DefaultKotlinUsageContext>,
    ): KotlinVariant {
        return super.createKotlinVariant(componentName, compilation, usageContexts).apply {
            artifactTargetName = wasmDecamelizedDefaultNameOrNull() ?: componentName
        }
    }

    override fun createUsageContexts(producingCompilation: KotlinCompilation<*>): Set<DefaultKotlinUsageContext> {
        val usageContexts = super.createUsageContexts(producingCompilation)

        if (isMpp) return usageContexts

        return usageContexts +
                DefaultKotlinUsageContext(
                    compilation = compilations.getByName(MAIN_COMPILATION_NAME),
                    mavenScope = KotlinUsageContext.MavenScope.COMPILE,
                    dependencyConfigurationName = commonFakeApiElementsConfigurationName,
                    overrideConfigurationArtifacts = project.setProperty { emptyList() }
                )
    }

    internal val commonFakeApiElementsConfigurationName: String
        get() = lowerCamelCaseName(
            disambiguationClassifier,
            "commonFakeApiElements"
        )

    override val binaries: KotlinJsBinaryContainer
        get() = compilations.withType(KotlinJsIrCompilation::class.java)
            .named(MAIN_COMPILATION_NAME)
            .map { it.binaries }
            .get()

    internal val configureTestSideEffect: Unit by lazy {
        val mainCompilation = compilations.matching { it.isMain() }

        compilations.matching { it.isTest() }
            .all { testCompilation ->
                val testBinaries = testCompilation.binaries.executableIrInternal(testCompilation)

                if (wasmTargetType != KotlinWasmTargetType.WASI) {
                    testBinaries.forEach { binary ->
                        binary.linkSyncTask.configure { task ->
                            mainCompilation.all {
                                task.from.from(project.tasks.named(it.processResourcesTaskName))
                            }
                        }
                    }
                }
            }
    }

    private fun <T : KotlinJsIrSubTargetWithBinary> addSubTarget(type: Class<T>, configure: T.() -> Unit): T {
        val subTarget = project.objects.newInstance(type, this).also(configure)
        subTargets.add(subTarget)
        return subTarget
    }

    private val commonLazyDelegate = lazy {
        webTargetVariant(
            { NpmResolverPlugin.apply(project) },
            { WasmNpmResolverPlugin.apply(project) },
        )
        compilations.all { compilation ->
            compilation.binaries
                .withType(JsIrBinary::class.java)
                .all { binary ->
                    val syncTask = binary.linkSyncTask

                    binary.linkTask.configure {
                        it.finalizedBy(syncTask)
                    }
                }
        }
    }

    private val commonLazy by commonLazyDelegate

    private fun registerTypeScriptCheckTask(binary: JsIrBinary): TaskProvider<TypeScriptValidationTask> {
        val linkTask = binary.linkTask
        val compilation = binary.compilation
        return project.registerTask(binary.validateGeneratedTsTaskName, listOf(compilation)) {
            it.versions.value(
                compilation.webTargetVariant(
                    { project.rootProject.kotlinNodeJsRootExtension.versions },
                    { project.rootProject.wasmKotlinNodeJsRootExtension.versions },
                )
            ).disallowChanges()
            it.inputDir.set(linkTask.flatMap { it.destinationDirectory })
            it.validationStrategy.set(
                when (binary.mode) {
                    KotlinJsBinaryMode.DEVELOPMENT -> propertiesProvider.jsIrGeneratedTypeScriptValidationDevStrategy
                    KotlinJsBinaryMode.PRODUCTION -> propertiesProvider.jsIrGeneratedTypeScriptValidationProdStrategy
                }
            )
        }
    }

    @Deprecated(
        "Binaryen is enabled by default. This call is redundant. Scheduled for removal in Kotlin 2.3.",
        level = DeprecationLevel.ERROR
    )
    override fun applyBinaryen(body: BinaryenExec.() -> Unit) {
    }

    //Browser
    private val browserLazyDelegate = lazy {
        commonLazy
        addSubTarget(KotlinBrowserJsIr::class.java) {
            configureSubTarget()
            subTargetConfigurators.add(LibraryConfigurator(this))
            subTargetConfigurators.add(WebpackConfigurator(this))
        }
    }

    override val browser: KotlinJsBrowserDsl by browserLazyDelegate

    override fun browser(body: KotlinJsBrowserDsl.() -> Unit) {
        body(browser)
    }

    //node.js
    private val nodejsLazyDelegate = lazy {
        if (wasmTargetType != KotlinWasmTargetType.WASI) {
            commonLazy
        } else {
            WasmNodeJsPlugin.apply(project)
            WasmNodeJsRootPlugin.apply(project.rootProject)
        }

        addSubTarget(KotlinNodeJsIr::class.java) {
            configureSubTarget()
            subTargetConfigurators.add(LibraryConfigurator(this))
            subTargetConfigurators.add(NodeJsEnvironmentConfigurator(this))
        }
    }

    override val nodejs: KotlinJsNodeDsl by nodejsLazyDelegate

    override fun nodejs(body: KotlinJsNodeDsl.() -> Unit) {
        body(nodejs)
    }

    //d8
    @OptIn(ExperimentalWasmDsl::class)
    private val d8LazyDelegate = lazy {
        webTargetVariant(
            { NodeJsRootPlugin.apply(project.rootProject) },
            { WasmNodeJsRootPlugin.apply(project.rootProject) },
        )

        addSubTarget(KotlinD8Ir::class.java) {
            configureSubTarget()
            subTargetConfigurators.add(LibraryConfigurator(this))
            subTargetConfigurators.add(D8EnvironmentConfigurator(this))
        }
    }

    override val d8: KotlinWasmD8Dsl by d8LazyDelegate

    private fun KotlinJsIrSubTarget.configureSubTarget() {
        configure()
    }

    override fun d8(body: KotlinWasmD8Dsl.() -> Unit) {
        body(d8)
    }

    override fun useCommonJs() {
        compilations.configureEach { jsCompilation ->
            jsCompilation.compileTaskProvider.configure {
                compilerOptions.configureCommonJsOptions()
            }

            jsCompilation.binaries
                .withType(JsIrBinary::class.java)
                .configureEach {
                    it.linkTask.configure { linkTask ->
                        linkTask.compilerOptions.configureCommonJsOptions()
                    }
                }
        }
    }

    override fun useEsModules() {
        compilations.configureEach { jsCompilation ->
            // Here it is essential to configure compilation compiler options as npm queries
            // compilation fileExtension before any task configuration action is done
            @Suppress("DEPRECATION")
            jsCompilation.compilerOptions.options.configureEsModulesOptions()

            jsCompilation.binaries
                .withType(JsIrBinary::class.java)
                .configureEach {
                    it.linkTask.configure { linkTask ->
                        linkTask.compilerOptions.configureEsModulesOptions()
                    }
                }
        }

    }

    @ExperimentalMainFunctionArgumentsDsl
    override fun passAsArgumentToMainFunction(jsExpression: String) {
        compilations
            .all {
                it.binaries
                    .withType(JsIrBinary::class.java)
                    .all {
                        it.linkTask.configure { linkTask ->
                            linkTask.compilerOptions.freeCompilerArgs.add("-Xplatform-arguments-in-main-function=$jsExpression")
                        }
                    }
            }
    }

    private fun KotlinJsCompilerOptions.configureCommonJsOptions() {
        moduleKind.convention(JsModuleKind.MODULE_COMMONJS)
        sourceMap.convention(true)
        sourceMapEmbedSources.convention(JsSourceMapEmbedMode.SOURCE_MAP_SOURCE_CONTENT_NEVER)
    }

    private fun KotlinJsCompilerOptions.configureEsModulesOptions() {
        moduleKind.convention(JsModuleKind.MODULE_ES)
        sourceMap.convention(true)
        sourceMapEmbedSources.convention(JsSourceMapEmbedMode.SOURCE_MAP_SOURCE_CONTENT_NEVER)
    }

    override fun generateTypeScriptDefinitions() {
        shouldGenerateTypeScriptDefinitions.set(true)
        compilations
            .all {
                it.binaries
                    .withType(JsIrBinary::class.java)
                    .all { binary ->
                        val tsValidationTask = registerTypeScriptCheckTask(binary)

                        binary.linkTask.configure { linkTask ->
                            linkTask.compilerOptions.freeCompilerArgs.add(GENERATE_D_TS)
                            linkTask.finalizedBy(tsValidationTask)
                        }
                    }
            }
    }

    override val compilerOptions: KotlinJsCompilerOptions = project.objects
        .newInstance<KotlinJsCompilerOptionsDefault>()
        .apply {
            configureJsDefaultOptions()
        }

    internal companion object {
        private val DECAMELIZE_REGEX = "([A-Z])".toRegex()

        internal fun buildNpmProjectName(
            project: Project,
            targetName: String,
            defaultTargetName: String,
        ): String {
            val rootProjectName = project.rootProject.name

            val localName = if (project != project.rootProject) {
                (rootProjectName + project.path).replace(":", "-")
            } else rootProjectName

            val targetPartName = if (targetName.isNotEmpty() && targetName != defaultTargetName) {
                targetName
                    .replace(DECAMELIZE_REGEX) {
                        it.groupValues
                            .drop(1)
                            .joinToString(prefix = "-", separator = "-")
                    }
                    .toLowerCaseAsciiOnly()
            } else null

            return sequenceOf(
                localName,
                targetPartName
            )
                .filterNotNull()
                .joinToString("-")
        }
    }
}

fun KotlinJsIrTarget.wasmDecamelizedDefaultNameOrNull(): String? = if (platformType == KotlinPlatformType.wasm) {
    val defaultWasmTargetName = wasmTargetType?.let {
        KotlinWasmTargetPreset.WASM_PRESET_NAME + it.name.toLowerCaseAsciiOnly().capitalizeAsciiOnly()
    }

    defaultWasmTargetName
        ?.takeIf {
            targetName == defaultWasmTargetName
        }?.decamelize()
} else null
