/*
 * Copyright (c) 2014 Contributor. All rights reserved.
 */

import java.io.{File => JFile, FileInputStream, FileOutputStream}
import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.reflect.io.{Directory, File}
import scala.tools.nsc.classpath.FlatClassPath.RootPackage
import scala.tools.nsc.classpath.PackageNameUtils
import scala.tools.nsc.io.Jar

/**
 * Generates directories, jars and zip files containing sources and classes
 * (the result of a compilation which is executed here)
 * and use them as a class- and sourcepath during compilation and running
 * created application. At the end everything is cleaned up.
 *
 * It can test also current, recursive classpath. Just right now we force
 * flat classpath to test it also when the recursive one would be set as a default.
 */
object Test {

  private implicit class JFileOps(file: JFile) {

    def createDir(newDirName: String) = {
      val newDir = new JFile(file, newDirName)
      newDir.mkdir()
      newDir
    }

    def createSrcFile(newFileName: String) = createFile(newFileName + ".scala")

    def createFile(fullFileName: String) = {
      val newFile = new JFile(file, fullFileName)
      newFile.createNewFile()
      newFile
    }

    def writeAll(text: String): Unit = File(file) writeAll text

    def moveContentToZip(zipName: String): Unit = {
      val newZip = zipsDir createFile s"$zipName.zip"
      val outputStream = new ZipOutputStream(new FileOutputStream(newZip))

      def addFileToZip(dirPrefix: String = "")(fileToAdd: JFile): Unit =
        if (fileToAdd.isDirectory) {
          val dirEntryName = fileToAdd.getName + "/"
          outputStream.putNextEntry(new ZipEntry(dirEntryName))
          fileToAdd.listFiles() foreach addFileToZip(dirEntryName)
        } else {
          val inputStream = new FileInputStream(fileToAdd)
          outputStream.putNextEntry(new ZipEntry(dirPrefix + fileToAdd.getName))

          val buffer = new Array[Byte](1024)
          var count = inputStream.read(buffer)
          while (count > 0) {
            outputStream.write(buffer, 0, count)
            count = inputStream.read(buffer)
          }

          inputStream.close()
        }

      file.listFiles() foreach addFileToZip()
      outputStream.close()

      cleanDir(file)
    }

    def moveContentToJar(jarName: String): Unit = {
      val newJar = jarsDir createFile s"$jarName.jar"
      Jar.create(file = File(newJar), sourceDir = Directory(file), mainClass = "won't be used")
      cleanDir(file)
    }

    def path: String = file.getAbsolutePath
  }

  private case class DirRep(name: String, nestedDirs: Seq[DirRep] = Nil, sourceFiles: Seq[String] = Nil)

  private val compiler = new scala.tools.nsc.MainClass
  private val appRunner = new scala.tools.nsc.MainGenericRunner
  private val classPathImplFlag = "-YclasspathImpl:flat"
  private val javaClassPath = sys.props("java.class.path")

  // creates a test dir in a temporary dir containing compiled files of this test
  // root dir will be automatically deleted after the end of test
  private val rootDir = new JFile(sys.props("partest.output"))
  private val testDir = rootDir createDir s"cp-tests-${System.currentTimeMillis()}"

  private val jarsDir = testDir createDir "jars"
  private val zipsDir = testDir createDir "zips"
  private val srcDir = testDir createDir "src"
  private val binDir = testDir createDir "bin"
  private val outDir = testDir createDir "out"

  def main(args: Array[String]): Unit = {
    createClassesZipInZipsDir()
    createClassesJarInJarsDir()
    createClassesInBinDir()
    createSourcesZipInZipsDir()
    createSourcesJarInJarsDir()
    createSourcesInSrcDir()
    compileFinalApp()
    runApp()
    // at the end all created files will be deleted automatically
  }

  private def createClassesZipInZipsDir(): Unit = {
    val baseFileName = "ZipBin"
    createStandardSrcHierarchy(baseFileName)
    compileSrc(baseFileName)
    outDir moveContentToZip "Bin"
    cleanDir(srcDir)
  }

  private def createClassesJarInJarsDir(): Unit = {
    val baseFileName = "JarBin"
    createStandardSrcHierarchy(baseFileName)
    compileSrc(baseFileName)
    outDir moveContentToJar "Bin"
    cleanDir(srcDir)
  }

  private def createClassesInBinDir(): Unit = {
    val baseFileName = "DirBin"
    createStandardSrcHierarchy(baseFileName)
    compileSrc(baseFileName, destination = binDir)
    cleanDir(srcDir)
  }

  private def createSourcesZipInZipsDir(): Unit = {
    createStandardSrcHierarchy(baseFileName = "ZipSrc")
    srcDir moveContentToZip "Src"
  }

  private def createSourcesJarInJarsDir(): Unit = {
    createStandardSrcHierarchy(baseFileName = "JarSrc")
    srcDir moveContentToJar "Src"
  }

  private def createSourcesInSrcDir(): Unit = {
    createStandardSrcHierarchy(baseFileName = "DirSrc")

    val appFile = srcDir createSrcFile "Main"
    appFile writeAll s"""import nested._
         | object Main extends App {
         |   println(new ZipBin)
         |   println(new JarBin)
         |   println(new DirBin)
         |   println(new ZipSrc)
         |   println(new JarSrc)
         |   println(new DirSrc)
         |
         |   println(new NestedZipBin)
         |   println(new NestedJarBin)
         |   println(new NestedDirBin)
         |   println(new NestedZipSrc)
         |   println(new NestedJarSrc)
         |   println(new NestedDirSrc)
         | }
       """.stripMargin
  }

  private def compileFinalApp(): Unit = {
    val classPath = mkPath(javaClassPath, binDir.path, zipsDir.path + "/Bin.zip", jarsDir.path + "/Bin.jar")
    val sourcePath = mkPath(srcDir.path, zipsDir.path + "/Src.zip", jarsDir.path + "/Src.jar")

    compiler.process(Array(classPathImplFlag, "-cp", classPath, "-sourcepath", sourcePath,
      "-d", outDir.path, s"${srcDir.path}/Main.scala"))
  }

  private def runApp(): Unit = {
    val classPath = mkPath(javaClassPath, outDir.path, binDir.path, zipsDir.path + "/Bin.zip", jarsDir.path + "/Bin.jar")
    appRunner.process(Array(classPathImplFlag, "-cp", classPath, "Main"))
  }

  private def createStandardSrcHierarchy(baseFileName: String): Unit =
    createSources(RootPackage, srcDir,
      DirRep("",
        nestedDirs = Seq(DirRep("nested", sourceFiles = Seq("Nested" + baseFileName))),
        sourceFiles = Seq(baseFileName)
      )
    )

  private def createSources(pkg: String, dirFile: JFile, dirRep: DirRep): Unit = {
    dirRep.nestedDirs foreach { rep =>
      val nestedDir = dirFile createDir rep.name
      val nestedPkg = PackageNameUtils.packagePrefix(pkg) + rep.name
      createSources(nestedPkg, nestedDir, rep)
    }

    val pkgHeader = if (pkg == RootPackage) "" else s"package $pkg\n\n"
    dirRep.sourceFiles foreach { srcName =>
      val text = s"""${pkgHeader}case class $srcName(x: String = "")"""
      val srcFile = dirFile createSrcFile srcName
      srcFile writeAll text
    }
  }

  private def compileSrc(baseFileName: String, destination: JFile = outDir): Unit = {
    val srcDirPath = srcDir.path
    compiler.process(Array(classPathImplFlag, "-cp", javaClassPath, "-d", destination.path,
      s"$srcDirPath/$baseFileName.scala", s"$srcDirPath/nested/Nested$baseFileName.scala"))
  }

  private def cleanDir(dir: JFile): Unit =
    dir.listFiles().foreach { file =>
      if (file.isDirectory) cleanDir(file)
      file.delete()
    }

  private def mkPath(pathEntries: String*) = pathEntries.mkString(File.pathSeparator)
}
