package net.corda.demobench.profile

import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import javafx.stage.FileChooser
import javafx.stage.FileChooser.ExtensionFilter
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.demobench.model.*
import net.corda.demobench.plugin.CordappController
import net.corda.demobench.plugin.inCordappsDir
import net.corda.demobench.plugin.isCordapp
import tornadofx.*
import java.io.File
import java.io.IOException
import java.net.URI
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.function.BiPredicate
import java.util.logging.Level
import java.util.stream.StreamSupport

class ProfileController : Controller() {

    private val jvm by inject<JVMConfig>()
    private val baseDir: Path = jvm.dataHome
    private val nodeController by inject<NodeController>()
    private val cordappController by inject<CordappController>()
    private val installFactory by inject<InstallFactory>()
    private val chooser = FileChooser()

    init {
        chooser.title = "DemoBench Profiles"
        chooser.initialDirectory = baseDir.toFile()
        chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.profile)", "*.profile", "*.PROFILE"))
    }

    /**
     * Saves the active node configurations into a ZIP file, along with their Cordapps.
     */
    @Throws(IOException::class)
    fun saveProfile(): Boolean {
        val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".profile")
        log.info("Saving profile as: $target")

        val configs = nodeController.activeNodes

        // Delete the profile, if it already exists. The save
        // dialogue has already confirmed that this is OK.
        target.delete()

        // Write the profile as a ZIP file.
        try {
            FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs ->
                configs.forEach { config ->
                    // Write the configuration file.
                    val nodeDir = fs.getPath(config.key).createDirectories()
                    val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8))
                    log.info("Wrote: $file")

                    // Write all of the non-built-in cordapps.
                    val cordappDir = Files.createDirectory(nodeDir.resolve(NodeConfig.cordappDirName))
                    cordappController.useCordappsFor(config).forEach {
                        val cordapp = Files.copy(it, cordappDir.resolve(it.fileName.toString()))
                        log.info("Wrote: $cordapp")
                    }
                }
            }

            log.info("Profile saved.")
        } catch (e: IOException) {
            log.log(Level.SEVERE, "Failed to save profile '$target': '${e.message}'", e)
            target.delete()
            throw e
        }

        return true
    }

    private fun forceExtension(target: File, ext: String): File {
        return if (target.extension.isEmpty()) File(target.parent, target.name + ext) else target
    }

    /**
     * Parses a profile (ZIP) file.
     */
    @Throws(IOException::class)
    fun openProfile(): List<InstallConfig>? {
        val chosen = chooser.showOpenDialog(null) ?: return null
        log.info("Selected profile: $chosen")

        val configs = LinkedList<InstallConfig>()

        FileSystems.newFileSystem(chosen.toPath(), null).use { fs ->
            // Identify the nodes first...
            StreamSupport.stream(fs.rootDirectories.spliterator(), false)
                    .flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() && attr.isRegularFile }) }
                    .map { file ->
                        try {
                            val config = installFactory.toInstallConfig(parse(file), baseDir)
                            log.info("Loaded: $file")
                            config
                        } catch (e: Exception) {
                            log.log(Level.SEVERE, "Failed to parse '$file': ${e.message}", e)
                            throw e
                        }
                        // Java seems to "walk" through the ZIP file backwards.
                        // So add new config to the front of the list, so that
                        // our final list is ordered to match the file.
                    }.forEach { configs.addFirst(it) }

            val nodeIndex = configs.map { it.key to it }.toMap()

            // Now extract all of the plugins from the ZIP file,
            // and copy them to a temporary location.
            StreamSupport.stream(fs.rootDirectories.spliterator(), false)
                    .flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inCordappsDir() && p.isCordapp() && attr.isRegularFile }) }
                    .forEach { cordapp ->
                        val config = nodeIndex[cordapp.getName(0).toString()] ?: return@forEach

                        try {
                            val cordappDir = Files.createDirectories(config.cordappsDir)
                            Files.copy(cordapp, cordappDir.resolve(cordapp.fileName.toString()))
                            log.info("Loaded: $cordapp")
                        } catch (e: Exception) {
                            log.log(Level.SEVERE, "Failed to extract '$cordapp': ${e.message}", e)
                            configs.forEach { c -> c.deleteBaseDir() }
                            throw e
                        }
                    }
        }

        return configs
    }

    private fun parse(path: Path): Config = Files.newBufferedReader(path).use {
        return ConfigFactory.parseReader(it)
    }

}
