package com.beust.kobalt.misc

import com.beust.kobalt.*
import com.beust.kobalt.api.Kobalt
import com.beust.kobalt.api.Project
import com.beust.kobalt.internal.build.BuildFile
import com.beust.kobalt.maven.Md5
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

class KFiles {
    /**
     * This actually returns a list of strings because in development mode, we are not pointing to a single
     * jar file but to a set of /classes directories.
     */
    val kobaltJar : List<String>
        get() {
            val envJar = System.getenv("KOBALT_JAR")
            if (envJar != null) {
                debug("Using kobalt jar $envJar")
                return listOf(File(envJar).absolutePath)
            } else {
                val jar = joinDir(distributionsDir, "kobalt-" + Kobalt.version,
                        "kobalt/wrapper/kobalt-" + Kobalt.version + ".jar")
                val jarFile = File(jar)
                if (jarFile.exists()) {
                    return listOf(jarFile.absolutePath)
                } else {
                    // Will only happen when building kobalt itself: the jar file might not be in the dist/ directory
                    // yet since we're currently building it. Instead, use the classes directly
                    val result = listOf("kobalt", "kobalt-plugin-api", "kobalt-wrapper").map {
                        File(homeDir(KFiles.joinDir("kotlin", "kobalt", "out", "production", it))).absolutePath
                    }
                    debug("Couldn't find ${jarFile.absolutePath}, using\n  " + result.joinToString(" "))
                    return result
                }
            }
        }

    init {
        File(KOBALT_DOT_DIR).mkdirs()
    }

    companion object {
        internal const val KOBALT_DOT_DIR : String = ".kobalt"
        val KOBALT_DIR : String = "kobalt"
        val HOME_KOBALT_DIR = makeDir(homeDir(".config", KOBALT_DIR))
        val KOBALT_BUILD_DIR = "kobaltBuild"

        /** Where all the .zip files are extracted */
        val distributionsDir = homeDir(KOBALT_DOT_DIR, "wrapper", "dist")

        // Directories under ./.kobalt
        val SCRIPT_BUILD_DIR : String = "build"
        val CLASSES_DIR : String = "classes"

        /** Where build file and support source files go, under KOBALT_DIR */
        private val SRC = "src"

        val TEST_CLASSES_DIR : String = "test-classes"

        private fun generatedDir(project: Project, variant: Variant)
                = KFiles.joinDir(project.directory, project.buildDirectory, "generated", variant.toIntermediateDir())

        fun generatedSourceDir(project: Project, variant: Variant, name: String) =
                KFiles.joinDir(project.directory, project.buildDirectory, "generated", "source", name,
                        variant.toIntermediateDir())

        fun buildDir(project: Project) = KFiles.makeDir(project.directory, project.buildDirectory)

        /**
         * Join the paths elements with the file separator.
         */
        fun joinDir(paths: List<String>): String = paths.joinToString(File.separator)

        /**
         * Join the paths elements with the file separator.
         */
        fun joinDir(vararg ts: String): String = ts.toMutableList().joinToString(File.separator)

        /**
         * Where assemblies get generated ("kobaltBuild/libs")
         */
        fun libsDir(project: Project) = KFiles.makeDir(KFiles.buildDir(project).path, "libs").path

        /**
         * The paths elements are expected to be a directory. Make that directory and join the
         * elements with the file separator.
         */
        fun joinAndMakeDir(paths: List<String>) = joinDir(paths).apply { File(this).mkdirs() }

        /**
         * The paths elements are expected to be a directory. Make that directory and join the
         * elements with the file separator.
         */
        fun joinAndMakeDir(vararg ts: String) = joinAndMakeDir(ts.toList())

        /**
         * The paths elements are expected to be a file. Make that parent directory of that file and join the
         * elements with the file separator.
         */
        fun joinFileAndMakeDir(vararg ts: String) = joinDir(joinAndMakeDir(ts.slice(0..ts.size - 2)), ts[ts.size - 1])

        fun makeDir(dir: String, s: String? = null) =
                (if (s != null) File(dir, s) else File(dir)).apply { mkdirs() }

        fun findRecursively(rootDir: File) : List<String> =
                findRecursively(rootDir, arrayListOf(), { s -> true })

        fun findRecursively(rootDir: File, directories: List<File>,
                function: Function1<String, Boolean>): List<String> {
            var result = arrayListOf<String>()

            val allDirs = arrayListOf<File>()
            if (directories.isEmpty()) {
                allDirs.add(rootDir)
            } else {
                allDirs.addAll(directories.map { File(rootDir, it.path) })
            }

            val seen = hashSetOf<java.nio.file.Path>()
            allDirs.forEach { dir ->
                if (! dir.exists()) {
                    log(2, "Couldn't find directory $dir")
                } else {
                    val files = findRecursively(dir, function)
                    files.map { Paths.get(it) }.forEach {
                        val rel = Paths.get(dir.path).relativize(it)
                        if (! seen.contains(rel)) {
                            result.add(File(dir, rel.toFile().path).path)
                            seen.add(rel)
                        } else {
                            log(2, "Skipped file already seen in previous flavor: $rel")
                        }
                    }
                }
            }
            // Return files relative to rootDir
            val r = result.map { it.substring(rootDir.path.length + 1)}
            return r
        }

        fun findRecursively(directory: File, function: Function1<String, Boolean>): List<String> {
            var result = arrayListOf<String>()
            directory.listFiles().forEach {
                if (it.isFile && function(it.path)) {
                    result.add(it.path)
                } else if (it.isDirectory) {
                    result.addAll(findRecursively(it, function))
                }
            }
            return result
        }

        fun copyRecursively(from: File, to: File, replaceExisting: Boolean = true, deleteFirst: Boolean = false,
                onError: (File, IOException) -> OnErrorAction = { file, exception -> throw exception }) {
            // Need to wait until copyRecursively supports an overwrite: Boolean = false parameter
            // Until then, wipe everything first
            if (deleteFirst) to.deleteRecursively()
//            to.mkdirs()
            hackCopyRecursively(from, to, replaceExisting = replaceExisting, onError = onError)
        }

        /** Private exception class, used to terminate recursive copying */
        private class TerminateException(file: File) : FileSystemException(file) {}

        /**
         * Copy/pasted from kotlin/io/Utils.kt to add support for overwriting.
         */
        private fun hackCopyRecursively(from: File, dst: File,
                replaceExisting: Boolean,
                checkTimestamp: Boolean = false,
                onError: (File, IOException) -> OnErrorAction =
                { file, exception -> throw exception }
        ): Boolean {
            if (!from.exists()) {
                return onError(from, NoSuchFileException(file = from, reason = "The source file doesn't exist")) !=
                        OnErrorAction.TERMINATE
            }
            try {
                // We cannot break for loop from inside a lambda, so we have to use an exception here
                for (src in from.walkTopDown().onFail { f, e ->
                    if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f)
                }) {
                    if (!src.exists()) {
                        if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) ==
                                OnErrorAction.TERMINATE)
                            return false
                    } else {
                        val relPath = src.relativeTo(from)
                        val dstFile = File(KFiles.joinDir(dst.path, relPath.path))
                        if (dstFile.exists() && !replaceExisting && !(src.isDirectory && dstFile.isDirectory)) {
                            if (onError(dstFile, FileAlreadyExistsException(file = src,
                                    other = dstFile,
                                    reason = "The destination file already exists")) == OnErrorAction.TERMINATE)
                                return false
                        } else if (src.isDirectory) {
                            dstFile.mkdirs()
                        } else {
                            if (Features.USE_TIMESTAMPS && dstFile.exists() && Md5.toMd5(src) == Md5.toMd5(dstFile)) {
                                log(2, "  Identical files, not copying $src to $dstFile")
                            } else {
                                val target = src.copyTo(dstFile, true)
                                if (target.length() != src.length()) {
                                    if (onError(src,
                                            IOException("src.length() != dst.length()")) == OnErrorAction.TERMINATE)
                                        return false
                                }
                            }
                        }
                    }
                }
                return true
            } catch (e: TerminateException) {
                return false
            }
        }

        fun findDotDir(startDir: File) : File {
            var result = startDir
            while (result != null && result.parentFile != null && ! File(result, KOBALT_DOT_DIR).exists()) {
                result = result.parentFile
            }
            if (result == null) {
                throw IllegalArgumentException("Couldn't locate $KOBALT_DOT_DIR in $startDir")
            }
            return File(result, KOBALT_DOT_DIR)
        }

        /**
         * The build location for build scripts is .kobalt/build
         */
        fun findBuildScriptLocation(buildFile: BuildFile, jarFile: String) : String {
            val result = joinDir(findDotDir(buildFile.directory).absolutePath, KFiles.SCRIPT_BUILD_DIR, jarFile)
            log(2, "Script jar file: $result")
            return result
        }

        fun saveFile(file: File, text: String) {
            file.absoluteFile.parentFile.mkdirs()
            file.writeText(text)
            log(2, "Created $file")
        }

        private fun isWindows() = System.getProperty("os.name").contains("Windows");

        fun copy(from: Path?, to: Path?, option: StandardCopyOption = StandardCopyOption.REPLACE_EXISTING) {
            if (isWindows() && to!!.toFile().exists()) {
                log(2, "Windows detected, not overwriting $to")
            } else {
                try {
                    if (from != null && to != null) {
                        if (!Files.exists(to) || Md5.toMd5(from.toFile()) != Md5.toMd5(to.toFile())) {
                            log(2, "Copy from $from to ${to}")
                            Files.copy(from, to, option)
                        } else {
                            log(2, "  Not copying, indentical files: $from $to")
                        }
                    }
                } catch(ex: IOException) {
                    // Windows is anal about this
                    log(1, "Couldn't copy $from to $to: ${ex.message}")
                }
            }
        }

        fun copy(from: InputStream, to: OutputStream) {
            var read = from.read()
            while (read != -1) {
                to.write(read)
                read = from.read()
            }
        }

        fun createTempFile(suffix : String = "", deleteOnExit: Boolean = false) : File =
            File.createTempFile("kobalt", suffix, File(SystemProperties.tmpDir)).let {
                if (deleteOnExit) it.deleteOnExit()
                return it
            }

        fun src(filePath: String): String = KFiles.joinDir(KOBALT_DIR, SRC, filePath)

        fun makeDir(project: Project, suffix: String) : File {
            return File(project.directory, project.buildDirectory + File.separator + suffix).apply { mkdirs() }
        }

        fun makeOutputDir(project: Project) : File = makeDir(project, KFiles.CLASSES_DIR)

        fun makeOutputTestDir(project: Project) : File = makeDir(project, KFiles.TEST_CLASSES_DIR)

        fun isExcluded(file: String, excludes: Glob) = isExcluded(file, listOf(excludes))

        fun isExcluded(file: File, excludes: Glob) = isExcluded(file.path, listOf(excludes))

        fun isExcluded(file: File, excludes: List<Glob>) = isExcluded(file.path, excludes)

        fun isExcluded(file: String, excludes: List<Glob>): Boolean = excludes.any { it.matches(file) }

        /**
         * TODO: cache these per project so we don't do it more than once.
         */
        fun findSourceFiles(projectDirectory: String, sourceDirectories: Collection<String>,
                suffixes: List<String>) : Set<String> {
            val result = hashSetOf<String>()
            sourceDirectories.forEach { source ->
                val sourceDir = File(KFiles.joinDir(projectDirectory, source))
                if (sourceDir.exists()) {
                    KFiles.findRecursively(sourceDir, { file ->
                        val ind = file.lastIndexOf(".")
                        if (ind >= 0) {
                            val suffix = file.substring(ind + 1)
                            if (suffixes.contains(suffix)) {
                                result.add(file)
                            }
                        }
                        false
                    })
                } else {
                    log(3, "Skipping nonexistent source directory $sourceDir")
                }
            }
            return result
        }

        fun isResource(name: String) = name.contains("res") || name.contains("resources")
    }

    fun findRecursively(directory: File, function: Function1<String, Boolean>): List<String> {
        return KFiles.findRecursively(directory, function)
    }

    fun findRecursively(rootDir: File, directories: List<File>,
            function: Function1<String, Boolean>): List<String> {
        return KFiles.findRecursively(rootDir, directories, function)
    }
}
