package com.beust.kobalt.app

import com.beust.kobalt.Args
import com.beust.kobalt.Plugins
import com.beust.kobalt.TaskResult
import com.beust.kobalt.api.Kobalt
import com.beust.kobalt.api.KobaltContext
import com.beust.kobalt.api.PluginProperties
import com.beust.kobalt.api.Project
import com.beust.kobalt.internal.IncrementalManager
import com.beust.kobalt.internal.KobaltSettings
import com.beust.kobalt.internal.ParallelLogger
import com.beust.kobalt.internal.PluginInfo
import com.beust.kobalt.internal.build.IBuildSources
import com.beust.kobalt.internal.build.VersionFile
import com.beust.kobalt.maven.DependencyManager
import com.beust.kobalt.maven.PomGenerator
import com.beust.kobalt.maven.aether.KobaltMavenResolver
import com.beust.kobalt.misc.KFiles
import com.beust.kobalt.misc.KobaltExecutors
import com.beust.kobalt.misc.kobaltLog
import com.beust.kobalt.plugin.kotlin.kotlinCompilePrivate
import com.google.inject.assistedinject.Assisted
import java.io.File
import java.net.URL
import javax.inject.Inject

/**
 * Manage the compilation of Build.kt. There are two passes for this processing:
 * 1) Extract the repos() and plugins() statements in a separate .kt and compile it into preBuildScript.jar.
 * 2) Actually build the whole Build.kt file after adding to the classpath whatever phase 1 found (plugins, repos)
 */
class BuildFileCompiler @Inject constructor(@Assisted("buildSources") val buildSources: IBuildSources,
        @Assisted val pluginInfo: PluginInfo, val files: KFiles, val plugins: Plugins,
        val dependencyManager: DependencyManager, val pluginProperties: PluginProperties,
        val executors: KobaltExecutors, val buildScriptUtil: BuildScriptUtil, val settings: KobaltSettings,
        val incrementalManagerFactory: IncrementalManager.IFactory, val args: Args,
        val resolver: KobaltMavenResolver, val pomGeneratorFactory: PomGenerator.IFactory,
        val parallelLogger: ParallelLogger, val buildFiles: BuildFiles) {

    interface IFactory {
        fun create(@Assisted("buildSources") buildSources: IBuildSources, pluginInfo: PluginInfo) : BuildFileCompiler
    }

    private val SCRIPT_JAR = "buildScript.jar"

    fun compileBuildFiles(args: Args, forceRecompile: Boolean = false): FindProjectResult {
        //
        // Create the KobaltContext
        // Note: can't use apply{} here or each field will refer to itself instead of the constructor field
        //
        val context = KobaltContext(args)
        context.pluginInfo = pluginInfo
        context.pluginProperties = pluginProperties
        context.dependencyManager = dependencyManager
        context.executors = executors
        context.settings = settings
        context.incrementalManager = incrementalManagerFactory.create()
        context.resolver = resolver
        context.pomGeneratorFactory = pomGeneratorFactory
        context.logger = parallelLogger
        context.internalContext.forceRecompile = forceRecompile
        Kobalt.context = context

        // The list of dynamic plug-ins has to be a companion since it's modified directly from
        // the build file, so make sure to reset it so that dynamic plug-ins don't bleed from
        // one build compilation to the next
        Plugins.dynamicPlugins.clear()

        //
        // Find all the projects in the build file, possibly compiling them
        //
        val projectResult = findProjects(context)

        return projectResult
    }

    class FindProjectResult(val context: KobaltContext, val projects: List<Project>, val pluginUrls: List<URL>,
            val buildContentRoots: List<String>, val taskResult: TaskResult)

    private fun findProjects(context: KobaltContext): FindProjectResult {
        val root = buildSources.root
        var errorTaskResult: TaskResult? = null
        val projects = arrayListOf<Project>()

        // If buildScript.jar was generated by a different version, wipe our temporary build directory
        val buildScriptJarDir = KFiles.findBuildScriptDir(root.absolutePath)
        buildScriptJarDir.let { dir ->
            if (! VersionFile.isSameVersionFile(dir)) {
                kobaltLog(1, "Detected new installation, wiping $dir")
                dir.listFiles().map(File::delete)
            }
        }

        // Parse the build files in kobalt/src/*.kt, which will analyze all the buildScriptInfo{} sections
        // and possibly add new source build directories. The output of this process is a new Build.kt
        // file that contains the aggregation of all the build files with the profiles applied and with
        // the included build files inserted at the correct line.
        val parseResult = buildFiles.parseBuildFiles(root.absolutePath, context)
        val newBuildKt = parseResult.buildKt

        //
        // Save the current build script absolute directory
        //
        context.internalContext.absoluteDir = buildSources.root

        val buildScriptJarFile = File(KFiles.findBuildScriptDir(root.absolutePath), SCRIPT_JAR)

        //
        // Compile the newly generated Build.kt file
        //
        val pluginUrls = Plugins.dynamicPlugins.map { it.jarFile.get().toURI().toURL() }
        val containsProfiles = false
        val taskResult = maybeCompileBuildFile(context, listOf(newBuildKt.absolutePath),
                buildScriptJarFile, pluginUrls, context.internalContext.forceRecompile,
                containsProfiles)

        //
        // Run the new Build.kt
        //
        if (taskResult.success) {
            projects.addAll(buildScriptUtil.runBuildScriptJarFile(buildScriptJarFile, pluginUrls, context))
        } else {
            if (errorTaskResult == null) {
                errorTaskResult = taskResult
            }
        }

        // Clear the absolute dir
        context.internalContext.absoluteDir = null

        return FindProjectResult(context, projects, pluginUrls, parseResult.buildSourceDirectories,
                if (errorTaskResult != null) errorTaskResult else TaskResult())
    }

    fun maybeCompileBuildFile(context: KobaltContext, sourceFiles: List<String>, buildScriptJarFile: File,
            pluginUrls: List<URL>, forceRecompile: Boolean, containsProfiles: Boolean) : TaskResult {
        kobaltLog(2, "Compiling into $buildScriptJarFile")

        // If the user specifed --profiles, always recompile the build file since we don't know if
        // the current buildScript.jar we have contains the correct value for these profiles
        // There is still a potential bug if the user builds with a profile and then without any:
        // in this case, we won't recompile the build file. A potential solution for this would be
        // to have a side file that describes which profiles the current buildScript.jar was
        // compiled with.
        if (! containsProfiles && !forceRecompile && buildScriptUtil.isUpToDate(buildSources, buildScriptJarFile)) {
            kobaltLog(2, "  Build file $buildScriptJarFile is up to date")
            return TaskResult()
        } else {
            val reason =
                if (containsProfiles) "profiles were found"
                else if (forceRecompile) "forceRecompile is true"
                else "it's been modified"
            kobaltLog(2, "  Need to recompile $buildSources because $reason")

            buildScriptJarFile.deleteRecursively()
            val buildFileClasspath = Kobalt.buildFileClasspath.map { it.jarFile.get() }.map { it.absolutePath }
            val result = kotlinCompilePrivate {
                classpath(files.kobaltJar)
                classpath(pluginUrls.map { it.file })
                classpath(buildFileClasspath)
                sourceFiles(sourceFiles)
                output = buildScriptJarFile
                noIncrementalKotlin = true
            }.compile(context = context)

            //
            // Generate the file that contains the list of active profiles for this build file
            //
            BuildScriptJarFile(buildScriptJarFile).saveProfiles(args.profiles)

            return result
        }
    }
}
