/*
 * Copyright 2010-2018 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.
 */

@file:JvmName("KaptCli")
package org.jetbrains.kotlin.kapt.cli

import com.intellij.util.PathUtil
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.ArgumentParseErrors
import org.jetbrains.kotlin.cli.common.arguments.preprocessCommandLineArguments
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.cli.jvm.modules.isAtLeastJava9
import org.jetbrains.kotlin.kapt.cli.CliToolOption.Format.*
import java.io.File
import kotlin.system.exitProcess

fun main(args: Array<String>) {
    val messageCollector = PrintingMessageCollector(System.err, MessageRenderer.PLAIN_RELATIVE_PATHS, false)

    if (args.isEmpty() || args.contains("-help")) {
        printHelp()
        return
    }

    val kaptTransformed = transformArgs(args.asList(), messageCollector, false)

    if (messageCollector.hasErrors()) {
        exitProcess(ExitCode.COMPILATION_ERROR.code)
    }

    K2JVMCompiler.main(kaptTransformed.toTypedArray())
}

@TestOnly
internal fun transformArgs(args: List<String>, messageCollector: MessageCollector, isTest: Boolean): List<String> {
    val parseErrors = ArgumentParseErrors()
    val kotlincTransformed = preprocessCommandLineArguments(args, lazy { parseErrors })

    val errorMessage = validateArguments(parseErrors)
    if (errorMessage != null) {
        messageCollector.report(CompilerMessageSeverity.ERROR, errorMessage)
        return emptyList()
    }

    return try {
        transformKaptToolArgs(kotlincTransformed, messageCollector, isTest)
    } catch (e: IllegalArgumentException) {
        messageCollector.report(CompilerMessageSeverity.ERROR, e.localizedMessage)
        emptyList()
    }
}

private const val KAPT_COMPILER_PLUGIN_JAR_NAME = "kotlin-annotation-processing.jar"

private fun transformKaptToolArgs(args: List<String>, messageCollector: MessageCollector, isTest: Boolean): List<String> {
    val transformed = mutableListOf<String>()

    if (!isTest) {
        val kaptCompilerPluginFile = findKaptCompilerPlugin()
            ?: throw IllegalStateException("Can't find $KAPT_COMPILER_PLUGIN_JAR_NAME")

        transformed += "-Xplugin=${kaptCompilerPluginFile.absolutePath}"
    }

    var toolsJarPassed = false
    var aptModePassed = false
    var kaptVerboseModePassed = false

    data class Option(val cliToolOption: CliToolOption, val pluginOption: KaptCliOption)

    val cliOptions = KaptCliOption.values().mapNotNull { Option(it.cliToolOption ?: return@mapNotNull null, it) }

    val iterator = args.asIterable().iterator()
    loop@ while (iterator.hasNext()) {
        val arg = iterator.next()
        if (arg == "--") {
            transformed += arg
            iterator.forEach { transformed += it }
            break
        }

        if (arg == "-help") {
            throw IllegalStateException("-help option should be already processed")
        }

        val option = cliOptions.firstOrNull { it.cliToolOption.matches(arg) }
        if (option == null) {
            transformed += arg
            continue
        }

        val transformedOption = option.cliToolOption.transform(arg)

        when (option.pluginOption) {
            KaptCliOption.TOOLS_JAR_OPTION -> {
                // TOOLS_JAR option is not passed as other compiler plugin options.
                // It is only used in kapt-cli, and we add a -Xplugin compiler option instead.

                toolsJarPassed = true
                transformed.add(0, "-Xplugin=$transformedOption")
                continue@loop
            }
            KaptCliOption.APT_MODE_OPTION -> aptModePassed = true
            KaptCliOption.VERBOSE_MODE_OPTION -> kaptVerboseModePassed = true
            KaptCliOption.USE_K2 -> transformed.add("-Xuse-k2-kapt")
            else -> {}
        }

        transformed += kaptArg(option.pluginOption, transformedOption)
    }

    if (!aptModePassed) {
        val isK2 = "-Xuse-k2-kapt=false" !in transformed && transformed.none { it.startsWith("-language-version=1") } &&
                transformed.lastIndexOf("-language-version").takeIf { it >= 0 }
                    ?.let { transformed.getOrNull(it + 1)?.startsWith('1') } != true

        transformed.addAll(0, kaptArg(KaptCliOption.APT_MODE_OPTION, if (isK2) "stubsAndApt" else "compile"))
    }

    if (!isTest && !isAtLeastJava9() && !areJavacComponentsAvailable() && !toolsJarPassed) {
        val toolsJarFile = findToolsJar()
            ?: argError("'tools.jar' location should be specified (${KaptCliOption.TOOLS_JAR_OPTION.cliToolOption!!.name}=<path>)")
        transformed.add(0, "-Xplugin=" + toolsJarFile.absolutePath)
    }

    if (kaptVerboseModePassed) {
        messageCollector.report(CompilerMessageSeverity.INFO, "Options passed to kotlinc: " + transformed.joinToString(" "))
    }

    return transformed
}

private fun CliToolOption.matches(arg: String) = when (format) {
    FLAG, VALUE -> arg.startsWith(name + "=")
    KEY_VALUE -> arg.startsWith(name + ":")
}

private fun CliToolOption.transform(arg: String): String {
    val optionName = name

    return when (format) {
        FLAG -> {
            fun err(): Nothing = argError("Invalid option format, should be $optionName=true/false")

            if (arg.length < (optionName.length + 2)) err()
            arg.drop(optionName.length + 1).takeIf { it == "true" || it == "false" } ?: err()
        }
        VALUE -> {
            fun err(): Nothing = argError("Invalid option format, should be $optionName=<value>")

            if (arg.length < (optionName.length + 2)) err()
            arg.drop(optionName.length + 1)
        }
        KEY_VALUE -> {
            fun err(): Nothing = argError("Invalid option format, should be $optionName:<key>=<value>")

            if (arg.length < (optionName.length + 3) || arg[optionName.length] != ':') err()
            arg.drop(optionName.length + 1).takeIf { it.contains('=') } ?: err()
        }
    }
}

private fun kaptArg(option: KaptCliOption, value: String): List<String> {
    return listOf("-P", "plugin:" + KaptCliOption.ANNOTATION_PROCESSING_COMPILER_PLUGIN_ID + ":" + option.optionName + "=" + value)
}

private fun argError(text: String): Nothing {
    throw IllegalArgumentException(text)
}

private fun findKaptCompilerPlugin(): File? {
    val pathToThisJar = File(PathUtil.getJarPathForClass(CliToolOption::class.java))
    if (pathToThisJar.extension.lowercase() != "jar") {
        return null
    }

    return File(pathToThisJar.parentFile, KAPT_COMPILER_PLUGIN_JAR_NAME).takeIf { it.exists() }
}