/*
 * Scala (https://www.scala-lang.org)
 *
 * Copyright EPFL and Lightbend, Inc.
 *
 * Licensed under Apache License 2.0
 * (http://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package scala.tools.nsc

import java.io.File
import java.lang.Thread.UncaughtExceptionHandler
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.ConcurrentHashMap
import java.util.{Collections, Locale}

import javax.tools.Diagnostic.Kind
import javax.tools.{Diagnostic, DiagnosticListener, JavaFileObject, ToolProvider}

import scala.annotation.nowarn
import scala.collection.{immutable, mutable}
import scala.collection.immutable.ArraySeq.unsafeWrapArray
import scala.concurrent._
import scala.concurrent.duration.Duration
import scala.jdk.CollectionConverters._
import scala.math.Ordering.Double.TotalOrdering
import scala.reflect.internal.util.{BatchSourceFile, FakePos, NoPosition, Position}
import scala.reflect.io.PlainNioFile
import scala.tools.nsc.PipelineMain.{OutlineTypePipeline, Pipeline, Traditional}
import scala.tools.nsc.Reporting.WarningCategory
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.reporters.{ConsoleReporter, Reporter}
import scala.tools.nsc.util.ClassPath
import scala.util.{Failure, Success}

class PipelineMainClass(argFiles: Seq[Path], pipelineSettings: PipelineMain.PipelineSettings) {
  import pipelineSettings._
  private val pickleCache: Path = configuredPickleCache.getOrElse(Files.createTempDirectory("scala.picklecache"))
  private def cachePath(file: Path): Path = {
    val newExtension = if (useJars) ".jar" else ""
    val root = file.getRoot
    // An empty component on Unix, just the drive letter on Windows
    val validRootPathComponent = root.toString.replace("/", "").replace(":", "")
    val result = changeExtension(pickleCache.resolve(validRootPathComponent).resolve(root.relativize(file)).normalize(), newExtension)
    if (useJars) Files.createDirectories(result.getParent)
    strippedAndExportedClassPath.update(file.toRealPath().normalize(), result)
    result
  }

  private val strippedAndExportedClassPath = new ConcurrentHashMap[Path, Path]().asScala

  /** Forward errors to the (current) reporter. */
  protected def scalacError(msg: String): Unit = {
    reporterError(FakePos("scalac"), msg + "\n  scalac -help  gives more information")
  }

  private var reporter: Reporter = _
  private def reporterEcho(pos: Position, msg: String): Unit = synchronized {
    reporter.echo(pos, msg)
  }
  private def reporterEcho(msg: String): Unit = synchronized {
    reporter.echo(NoPosition, msg)
  }
  private def reporterError(pos: Position, msg: String): Unit = synchronized {
    reporter.error(pos, msg)
  }

  private object handler extends UncaughtExceptionHandler {
    override def uncaughtException(t: Thread, e: Throwable): Unit = {
      e.printStackTrace()
      System.exit(-1)
    }
  }

  implicit val executor: ExecutionContext = ExecutionContext.fromExecutor(new java.util.concurrent.ForkJoinPool(parallelism), t => handler.uncaughtException(Thread.currentThread(), t))
  def changeExtension(p: Path, newExtension: String): Path = {
    val fileName = p.getFileName.toString
    val changedFileName = fileName.lastIndexOf('.') match {
      case -1 => fileName + newExtension
      case n => fileName.substring(0, n) + newExtension
    }
    p.getParent.resolve(changedFileName)
  }

  @nowarn("cat=lint-inaccessible")
  def writeDotFile(logDir: Path, dependsOn: mutable.LinkedHashMap[Task, List[Dependency]]): Unit = {
    val builder = new java.lang.StringBuilder()
    builder.append("digraph projects {\n")
    for ((p, deps) <- dependsOn) {
      //builder.append("  node \"[]").append(p.label).append("\";\n")
      for (dep <- deps) {
        builder.append("   \"").append(p.label).append("\" -> \"").append(dep.t.label).append("\" [")
        if (dep.isMacro) builder.append("label=M")
        else if (dep.isPlugin) builder.append("label=P")
        builder.append("];\n")
      }
    }
    builder.append("}\n")
    val path = logDir.resolve("projects.dot")
    Files.write(path, builder.toString.getBytes(UTF_8))
    reporterEcho("Wrote project dependency graph to: " + path.toAbsolutePath)
  }

  private case class Dependency(t: Task, isMacro: Boolean, isPlugin: Boolean)

  def process(): Boolean = {
    reporter = createReporter(new Settings(scalacError))
    reporterEcho(s"parallelism = $parallelism, strategy = $strategy")

    def commandFor(argFileArg: Path): Task = {
      val ss = new Settings(scalacError)
      val command = new CompilerCommand(("@" + argFileArg) :: Nil, ss)
      Task(argFileArg, command, command.files)
    }

    val projects: List[Task] = argFiles.toList.map(commandFor)
    if (reporter.hasErrors) return false

    val produces = mutable.LinkedHashMap[Path, Task]()
    for (p <- projects) {
      produces(p.outputDir) = p
    }
    val dependsOn = mutable.LinkedHashMap[Task, List[Dependency]]()
    for (p <- projects) {
      val macroDeps = p.macroClassPath.flatMap(p => produces.get(p)).toList.filterNot(_ == p).map(t => Dependency(t, isMacro = true, isPlugin = false))
      val pluginDeps = p.pluginClassPath.flatMap(p => produces.get(p)).toList.filterNot(_ == p).map(t => Dependency(t, isMacro = false, isPlugin = true))
      val classPathDeps = p.classPath.flatMap(p => produces.get(p)).toList.filterNot(_ == p).filterNot(p => macroDeps.exists(_.t == p)).map(t => Dependency(t, isMacro = false, isPlugin = false))
      dependsOn(p) = classPathDeps ++ macroDeps ++ pluginDeps
    }
    val dependedOn: Set[Task] = dependsOn.valuesIterator.flatten.map(_.t).toSet
    val externalClassPath = projects.iterator.flatMap(_.classPath).filter(p => !produces.contains(p) && Files.exists(p)).toSet

    if (strategy != Traditional) {
      if (stripExternalClassPath) {
        val exportTimer = new Timer
        exportTimer.start()
        for (entry <- externalClassPath) {
          val extracted = cachePath(entry)
          val sourceTimeStamp = Files.getLastModifiedTime(entry)
          if (Files.exists(extracted) && Files.getLastModifiedTime(extracted) == sourceTimeStamp) {
            // println(s"Skipped export of pickles from $entry to $extracted (up to date)")
          } else {
            PickleExtractor.process(entry, extracted)
            Files.setLastModifiedTime(extracted, sourceTimeStamp)
            reporterEcho(s"Exported pickles from $entry to $extracted")
            Files.setLastModifiedTime(extracted, sourceTimeStamp)
          }
          strippedAndExportedClassPath(entry) = extracted
        }
        exportTimer.stop()
        reporterEcho(f"Exported external classpath in ${exportTimer.durationMs}%.0f ms")
      }
    }

    val timer = new Timer
    timer.start()

    def sequenceFailSlow[A](fs: Seq[Future[A]]): Future[Seq[A]] = {
      Future.traverse(fs)(_.transform(tr => Success(tr.toEither))).map { results =>
        val (failures, successes) = results.partitionMap(identity)
        failures.toList match {
          case head :: rest => rest.foreach(head.addSuppressed(_)); throw head
          case _            => successes
        }
      }
    }

    def awaitDone(): Unit = {
      val allFutures: immutable.Seq[Future[_]] = projects.flatMap(_.futures)
      val numAllFutures = allFutures.size
      val awaitAllFutures: Future[_] = sequenceFailSlow(allFutures)
      var lastNumCompleted = allFutures.count(_.isCompleted)
      while (true) try {
        Await.ready(awaitAllFutures, Duration(60, "s"))
        timer.stop()
        val numCompleted = allFutures.count(_.isCompleted)
        reporterEcho(s"PROGRESS: $numCompleted / $numAllFutures")
        return
      } catch {
        case _: TimeoutException =>
          val numCompleted = allFutures.count(_.isCompleted)
          if (numCompleted == lastNumCompleted) {
            reporterEcho(s"STALLED: $numCompleted / $numAllFutures")
            reporterEcho("Outline/Scala/Javac")
            projects.map {
              p =>
                def toX(b: Future[_]): String = b.value match { case None => "-"; case Some(Success(_)) => "x"; case Some(Failure(_)) => "!" }
                val s = List(p.outlineDoneFuture, p.groupsDoneFuture, p.javaDoneFuture).map(toX).mkString(" ")
                reporter.echo(s + " " + p.label)
            }
          } else {
            reporterEcho(s"PROGRESS: $numCompleted / $numAllFutures")
            lastNumCompleted = numCompleted
          }
      }
    }
    strategy match {
      case OutlineTypePipeline =>
        projects.foreach { p: Task =>
          val depsReady = Future.traverse(dependsOn.getOrElse(p, Nil))(task => p.dependencyReadyFuture(task))
          val f = for {
            _ <- depsReady
            _ <- {
              p.outlineCompile()
              p.outlineDone.future
            }
            _ <- {
              p.fullCompile()
              Future.traverse(p.groups)(_.done.future)
            }
            _ <- Future.traverse(dependsOn.getOrElse(p, Nil))(task => task.t.javaDone.future)
          } yield {
            p.javaCompile()
          }
          f.onComplete { _ => p.close() }
        }

        awaitDone()

        for (p <- projects) {
          val dependencies = dependsOn(p).map(_.t)

          def maxByOrZero[A](as: List[A])(f: A => Double): Double = if (as.isEmpty) 0d else as.map(f).max

          val maxOutlineCriticalPathMs = maxByOrZero(dependencies)(_.outlineCriticalPathMs)
          p.outlineCriticalPathMs = maxOutlineCriticalPathMs + p.outlineTimer.durationMs
          p.regularCriticalPathMs = maxOutlineCriticalPathMs + maxByOrZero(p.groups)(_.timer.durationMs)
          p.fullCriticalPathMs = maxByOrZero(dependencies)(_.fullCriticalPathMs) + p.groups.map(_.timer.durationMs).sum
        }

        if (parallelism == 1) {
          val criticalPath = projects.maxBy(_.regularCriticalPathMs)
          reporterEcho(f"Critical path: ${criticalPath.regularCriticalPathMs}%.0f ms. Wall Clock: ${timer.durationMs}%.0f ms")
        } else
          reporterEcho(f" Wall Clock: ${timer.durationMs}%.0f ms")
      case Pipeline =>
        projects.foreach { p =>
          val depsReady = Future.traverse(dependsOn.getOrElse(p, Nil))(task => p.dependencyReadyFuture(task))
          val f = for {
            _ <- depsReady
            _ <- {
              val isLeaf = !dependedOn.contains(p)
              if (isLeaf && useTraditionalForLeaf) {
                p.outlineDone.complete(Success(()))
                p.fullCompile()
              } else
                p.fullCompileExportPickles()
              // Start javac after scalac has completely finished
              Future.traverse(p.groups)(_.done.future)
            }
            _ <- Future.traverse(dependsOn.getOrElse(p, Nil))(task => task.t.javaDone.future)
          } yield {
            p.javaCompile()
          }
          f.onComplete { _ => p.close() }
        }
        awaitDone()

        for (p <- projects) {
          val dependencies = dependsOn(p).map(_.t)

          def maxByOrZero[A](as: List[A])(f: A => Double): Double = if (as.isEmpty) 0d else as.map(f).max

          val maxOutlineCriticalPathMs = maxByOrZero(dependencies)(_.outlineCriticalPathMs)
          p.outlineCriticalPathMs = maxOutlineCriticalPathMs + p.outlineTimer.durationMs
          p.regularCriticalPathMs = maxOutlineCriticalPathMs + maxByOrZero(p.groups)(_.timer.durationMs)
          p.fullCriticalPathMs = maxByOrZero(dependencies)(_.fullCriticalPathMs) + p.groups.map(_.timer.durationMs).sum
        }

        if (parallelism == 1) {
          val criticalPath = projects.maxBy(_.regularCriticalPathMs)
          reporterEcho(f"Critical path: ${criticalPath.regularCriticalPathMs}%.0f ms. Wall Clock: ${timer.durationMs}%.0f ms")
        } else
          reporterEcho(f" Wall Clock: ${timer.durationMs}%.0f ms")
      case Traditional =>
        projects.foreach { p =>
          val f1 = Future.traverse(dependsOn.getOrElse(p, Nil))(_.t.javaDone.future)
          val f2 = f1.flatMap { _ =>
            p.outlineDone.complete(Success(()))
            p.fullCompile()
            val eventualUnits: Future[List[Unit]] = Future.traverse(p.groups)(_.done.future)
            eventualUnits.map(_ => p.javaCompile())
          }
          f2.onComplete { _ => p.close() }
        }
        awaitDone()

        for (p <- projects) {
          val dependencies = dependsOn(p).map(_.t)

          def maxByOrZero[A](as: List[A])(f: A => Double): Double = if (as.isEmpty) 0d else as.map(f).max

          p.fullCriticalPathMs = maxByOrZero(dependencies)(_.fullCriticalPathMs) + p.groups.map(_.timer.durationMs).sum
        }
        if (parallelism == 1) {
          val maxFullCriticalPath: Double = projects.map(_.fullCriticalPathMs).max
          reporterEcho(f"Critical path: $maxFullCriticalPath%.0f ms. Wall Clock: ${timer.durationMs}%.0f ms")
        } else {
          reporterEcho(f"Wall Clock: ${timer.durationMs}%.0f ms")
        }
    }

    logDir.foreach { dir =>
      Files.createDirectories(dir)
      writeDotFile(dir, dependsOn)
      writeChromeTrace(dir, projects)
    }
    deleteTempPickleCache()
    !reporter.hasErrors
  }

  private def deleteTempPickleCache(): Unit = {
    if (configuredPickleCache.isEmpty) {
      AbstractFile.getDirectory(pickleCache.toFile).delete()
    }
  }

  private def writeChromeTrace(logDir: Path, projects: List[Task]) = {
    val trace = new java.lang.StringBuilder()
    trace.append("""{"traceEvents": [""")
    val sb = new mutable.StringBuilder(trace)

    @annotation.nowarn("cat=deprecation")
    def durationEvent(name: String, cat: String, t: Timer): String = {
      s"""{"name": "$name", "cat": "$cat", "ph": "X", "ts": ${(t.startMicros).toLong}, "dur": ${(t.durationMicros).toLong}, "pid": 0, "tid": ${t.thread.getId}}"""
    }

    def projectEvents(p: Task): List[String] = {
      val events = List.newBuilder[String]
      if (p.outlineTimer.durationMicros > 0d) {
        val desc = if (strategy == OutlineTypePipeline) "outline-type" else "parser-to-pickler"
        events += durationEvent(p.label, desc, p.outlineTimer)
      }
      for ((g, ix) <- p.groups.zipWithIndex) {
        if (g.timer.durationMicros > 0d)
          events += durationEvent(p.label, "compile-" + ix, g.timer)
      }
      if (p.javaTimer.durationMicros > 0d) {
        val desc = "javac"
        events += durationEvent(p.label, desc, p.javaTimer)
      }
      events.result()
    }

    projects.iterator.flatMap(projectEvents).addString(sb, ",\n")
    trace.append("]}")
    val traceFile = logDir.resolve(s"build-${label}.trace")
    Files.write(traceFile, trace.toString.getBytes(UTF_8))
    reporterEcho("Chrome trace written to " + traceFile.toAbsolutePath)
  }

  case class Group(files: List[String]) {
    val timer = new Timer
    val done = Promise[Unit]()
  }

  private case class Task(argsFile: Path, command: CompilerCommand, files: List[String]) {
    val label = argsFile.toString.replaceAll(".*/target/", "").replaceAll("""(.*)/(.*).args""", "$1:$2")
    override def toString: String = argsFile.toString
    def outputDir: Path = command.settings.outputDirs.getSingleOutput.get.file.toPath.toAbsolutePath.normalize()
    private def expand(s: command.settings.PathSetting): List[Path] = {
      ClassPath.expandPath(s.value, expandStar = true).map(s => Paths.get(s).toAbsolutePath.normalize())
    }
    lazy val classPath: Seq[Path] = expand(command.settings.classpath)
    lazy val macroClassPath: Seq[Path] = expand(command.settings.YmacroClasspath)
    lazy val macroClassPathSet: Set[Path] = macroClassPath.toSet
    lazy val pluginClassPath: Set[Path] = {
      def asPath(p: String) = ClassPath split p

      val paths = command.settings.plugin.value filter (_ != "") flatMap (s => asPath(s) map (s => Paths.get(s)))
      paths.toSet
    }
    def dependencyReadyFuture(dependency: Dependency) = if (dependency.isMacro) {
      log(s"dependency is on macro classpath, will wait for .class files: ${dependency.t.label}")
      dependency.t.javaDone.future
    } else if (dependency.isPlugin) {
      log(s"dependency is on plugin classpath, will wait for .class files: ${dependency.t.label}")
      dependency.t.javaDone.future
    } else
      dependency.t.outlineDone.future


    if (cacheMacro)
      command.settings.YcacheMacroClassLoader.value = "always"
    if (cachePlugin)
      command.settings.YcachePluginClassLoader.value = "always"

    if (strategy != Traditional) {
      command.settings.YpickleJava.value = true
    }

    val groupSize = Integer.getInteger("scala.pipeline.group.size", 128)

    val groups: List[Group] = {
      val isScalaLibrary = files.exists(_.endsWith("Predef.scala"))
      if (strategy != OutlineTypePipeline || isScalaLibrary) {
        Group(files) :: Nil
      } else {
        command.settings.classpath.value = command.settings.outputDirs.getSingleOutput.get.toString + File.pathSeparator + command.settings.classpath.value
        val length = files.length
        val groups = (length.toDouble / groupSize).toInt.max(1)
        files.grouped((length.toDouble / groups).ceil.toInt.max(1)).toList.map(Group(_))
      }
    }
    command.settings.outputDirs.getSingleOutput.get.file.mkdirs()

    val isGrouped = groups.size > 1

    val outlineTimer = new Timer()
    val javaTimer = new Timer()

    var outlineCriticalPathMs = 0d
    var regularCriticalPathMs = 0d
    var fullCriticalPathMs = 0d
    val outlineDone: Promise[Unit] = Promise[Unit]()
    val outlineDoneFuture = outlineDone.future
    val javaDone: Promise[Unit] = Promise[Unit]()
    val javaDoneFuture: Future[_] = javaDone.future
    val groupsDoneFuture: Future[List[Unit]] = Future.traverse(groups)(_.done.future)
    val futures: List[Future[_]] = {
      outlineDone.future :: javaDone.future :: groups.map(_.done.future)
    }

    val originalClassPath: String = command.settings.classpath.value

    private[this] var initCompiler: () => Global = () => try {
      val result = newCompiler(command.settings)
      val reporter = result.reporter
      if (reporter.hasErrors)
        reporter.flush()
      else if (command.shouldStopWithInfo)
        reporterEcho(command.getInfoMessage(result))
      result.reporter = createReporter(result.settings)
      result
    } catch {
      case t: Throwable =>
        t.printStackTrace()
        throw t
    }

    lazy val compiler: Global = { val res = initCompiler(); initCompiler = null; res }

    def close() = if (initCompiler == null) compiler.close()

    def outlineCompile(): Unit = {
      outlineTimer.start()
      try {
        log("scalac outline: start")
        command.settings.Youtline.value = true
        command.settings.stopAfter.value = List("pickler")
        command.settings.Ymacroexpand.value = command.settings.MacroExpand.None
        command.settings.YpickleWrite.value = cachePath(command.settings.outputDirs.getSingleOutput.get.file.toPath).toAbsolutePath.toString
        val run1 = new compiler.Run()
        run1 compile files
        outlineTimer.stop()
        log(f"scalac outline: done ${outlineTimer.durationMs}%.0f ms")
        compiler.reporter.finish()
        if (compiler.reporter.hasErrors) {
          log("scalac outline: failed")
          outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ")))
        } else {
          log(f"scalac outline: done ${outlineTimer.durationMs}%.0f ms")
          outlineDone.complete(Success(()))
        }
      } catch {
        case t: Throwable =>
          outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ", t)))
      }
    }

    def fullCompile(): Unit = {
      command.settings.Youtline.value = false
      command.settings.stopAfter.value = Nil
      command.settings.Ymacroexpand.value = command.settings.MacroExpand.Normal
      command.settings.YpickleWrite.value = ""

      val groupCount = groups.size
      for ((group, ix) <- groups.zipWithIndex) {
        group.done.completeWith {
          Future {
            log(s"scalac (${ix + 1}/$groupCount): start")
            group.timer.start()
            val compiler2 = newCompiler(command.settings)
            try {
              try {
                val run2 = new compiler2.Run()
                run2 compile group.files
                compiler2.reporter.finish()
              } finally {
                group.timer.stop()
                log(f"scalac (${ix + 1}/$groupCount): done ${group.timer.durationMs}%.0f ms")
              }
              if (compiler2.reporter.hasErrors) {
                throw new RuntimeException(label + ": compile failed: ")
              }
            } finally {
              compiler2.close()
            }
          }
        }
      }
    }

    def fullCompileExportPickles(): Unit = {
      assert(groups.size == 1)
      val group = groups.head
      log("scalac: start")
      command.settings.YpickleWrite.value = cachePath(command.settings.outputDirs.getSingleOutput.get.file.toPath).toString
      outlineTimer.start()
      try {
        val run2 = new compiler.Run() {
          override def advancePhase(): Unit = {
            if (compiler.phase == this.picklerPhase) {
              outlineTimer.stop()
              log(f"scalac outline: done ${outlineTimer.durationMs}%.0f ms")
              outlineDone.complete(Success(()))
              group.timer.start()
            }
            super.advancePhase()
          }
        }

        run2 compile group.files
        compiler.reporter.finish()
        group.timer.stop()
        if (compiler.reporter.hasErrors) {
          log("scalac: failed")
          if (!outlineDone.isCompleted)
            outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ")))
          group.done.complete(Failure(new RuntimeException(label + ": compile failed: ")))
        } else {
          log(f"scalac: done ${group.timer.durationMs}%.0f ms")
          group.done.complete(Success(()))
        }
      } catch {
        case t: Throwable =>
          t.printStackTrace()
          if (!outlineDone.isCompleted)
            outlineDone.complete(Failure(new RuntimeException(label + ": compile failed: ")))
          if (!group.done.isCompleted)
            group.done.complete(Failure(new RuntimeException(label + ": compile failed: ")))
      }
    }

    def javaCompile(): Unit = {
      val javaSources = files.filter(_.endsWith(".java"))
      if (javaSources.nonEmpty) {
        log("javac: start")
        javaTimer.start()
        javaDone.completeWith(Future {
          val opts: java.util.List[String] = java.util.Arrays.asList("-d", command.settings.outdir.value, "-cp", command.settings.outdir.value + File.pathSeparator + originalClassPath)
          val javaCompiler = ToolProvider.getSystemJavaCompiler()
          //If the running JRE isn't from a JDK distribution, getSystemJavaCompiler returns null
          if (javaCompiler == null) throw new UnsupportedOperationException("no java compiler found in current Java runtime")
          val listener = new DiagnosticListener[JavaFileObject] {
            override def report(diagnostic: Diagnostic[_ <: JavaFileObject]): Unit = {
              val msg = diagnostic.getMessage(Locale.getDefault)
              val source: JavaFileObject = diagnostic.getSource
              val path = Paths.get(source.toUri)
              val position = if (diagnostic.getPosition == Diagnostic.NOPOS) NoPosition else {
                val sourceFile = new BatchSourceFile(new PlainNioFile(path))
                Position.range(sourceFile, diagnostic.getStartPosition.toInt, diagnostic.getPosition.toInt, diagnostic.getEndPosition.toInt)
              }
              diagnostic.getKind match {
                case Kind.ERROR                            => reporterError(position, msg)
                case Kind.WARNING | Kind.MANDATORY_WARNING => Task.this.compiler.runReporting.warning(position, msg, WarningCategory.JavaSource, site = "")
                case Kind.NOTE | Kind.OTHER                => reporterEcho(position, msg)
              }
            }
          }

          val fileManager = javaCompiler.getStandardFileManager(null, null, null)
          val compileTask = javaCompiler.getTask(null, fileManager, listener, opts, null, fileManager.getJavaFileObjects(javaSources.toArray: _*))
          compileTask.setProcessors(Collections.emptyList())
          val success = compileTask.call()
          javaTimer.stop()
          if (success)
            log(f"javac: done ${javaTimer.durationMs}%.0f ms ")
          else
            throw new RuntimeException(f"javac: error ${javaTimer.durationMs}%.0f ms ")
        })
      } else {
        javaDone.complete(Success(()))
      }
    }
    def log(msg: String): Unit = reporterEcho(this.label + ": " + msg)
  }

  final class Timer() {
    private var startNanos: Long = 0
    private var endNanos: Long = 0
    def start(): Unit = {
      assert(startNanos == 0L)
      startNanos = System.nanoTime
    }
    var thread: Thread = Thread.currentThread()
    def stop(): Unit = {
      thread = Thread.currentThread()
      endNanos = System.nanoTime()
    }
    def startMs: Double = startNanos.toDouble / 1000 / 1000
    def durationMs: Double = {
      val result = (endNanos - startNanos).toDouble / 1000 / 1000
      if (result < 0)
        getClass
      result
    }
    def startMicros: Double = startNanos.toDouble / 1000d
    def durationMicros: Double = (endNanos - startNanos).toDouble / 1000d
  }

  protected def newCompiler(settings: Settings): Global = {
    if (strategy != Traditional) {
      val classPath = ClassPath.expandPath(settings.classpath.value, expandStar = true)
      val modifiedClassPath = classPath.map { entry =>
        val entryPath = Paths.get(entry)
        if (Files.exists(entryPath))
          strippedAndExportedClassPath.getOrElse(entryPath.toRealPath().normalize(), entryPath).toString
        else
          entryPath
      }
      settings.classpath.value = modifiedClassPath.mkString(File.pathSeparator)
    }
    Global(settings)
  }
}


object PipelineMain {
  sealed abstract class BuildStrategy

  /** Outline type check sources to compute type signatures an input to downstream compilation. Compile sources (optionally */
  case object OutlineTypePipeline extends BuildStrategy
  /** Transport pickles as an input to downstream compilation. */
  case object Pipeline extends BuildStrategy

  /** Emit class files before triggering downstream compilation */
  case object Traditional extends BuildStrategy

  case class PipelineSettings(label: String, parallelism: Int, strategy: BuildStrategy, useJars: Boolean,
                              configuredPickleCache: Option[Path], cacheMacro: Boolean, cachePlugin: Boolean,
                              stripExternalClassPath: Boolean, useTraditionalForLeaf: Boolean, logDir: Option[Path],
                              createReporter: (Settings => Reporter))
  def defaultSettings: PipelineSettings = {
    val strategies = List(OutlineTypePipeline, Pipeline, Traditional)
    val strategy = strategies.find(_.productPrefix.equalsIgnoreCase(System.getProperty("scala.pipeline.strategy", "pipeline"))).get
    val parallelism = java.lang.Integer.getInteger("scala.pipeline.parallelism", java.lang.Runtime.getRuntime.availableProcessors())
    val useJars = java.lang.Boolean.getBoolean("scala.pipeline.use.jar")
    val cacheMacro = java.lang.Boolean.getBoolean("scala.pipeline.cache.macro.classloader")
    val cachePlugin = java.lang.Boolean.getBoolean("scala.pipeline.cache.plugin.classloader")
    val stripExternalClassPath = java.lang.Boolean.getBoolean("scala.pipeline.strip.external.classpath")
    val useTraditionalForLeaf = java.lang.Boolean.getBoolean("scala.pipeline.use.traditional.for.leaf")
    val configuredPickleCache = Option(System.getProperty("scala.pipeline.picklecache")).map(Paths.get(_))
    val logDir = Paths.get(".")
    new PipelineSettings("1", parallelism, strategy, useJars, configuredPickleCache,
      cacheMacro, cachePlugin, stripExternalClassPath, useTraditionalForLeaf, Some(logDir), new ConsoleReporter(_))
  }

  def main(args: Array[String]): Unit = {
    val argFiles: Seq[Path] = args match {
      case Array(path) if Files.isDirectory(Paths.get(path)) =>
        Files.walk(Paths.get(path)).iterator().asScala.filter(_.getFileName.toString.endsWith(".args")).toList
      case _ =>
        unsafeWrapArray(args.map(Paths.get(_)))
    }
    val main = new PipelineMainClass(argFiles, defaultSettings)
    val result = main.process()
    if (!result)
      System.exit(1)
    else
      System.exit(0)
  }
}

//object PipelineMainTest {
//  def main(args: Array[String]): Unit = {
//    var i = 0
////    val argsFiles = Files.walk(Paths.get("/code/guardian-frontend")).iterator().asScala.filter(_.getFileName.toString.endsWith(".args")).toList
//    val argsFiles = List(Paths.get("/Users/jz/code/guardian-frontend/common/target/compile.args"))
//    val useJars = java.lang.Boolean.getBoolean("scala.pipeline.use.jar")
//    for (_ <- 1 to 20; n <- List(parallel.availableProcessors); start <- List(OutlineTypePipeline)) {
//      i += 1
//      val main = new PipelineMainClass(start + "-" + i, n, start, argsFiles, useJars)
//      println(s"====== ITERATION $i=======")
//      val result = main.process()
//      if (!result)
//        System.exit(1)
//    }
//    System.exit(0)
//  }
//}
