/* sbt -- Simple Build Tool
 * Copyright 2008, 2009, 2010, 2011  Mark Harrah
 */
package sbt

import complete.{ DefaultParsers, Parser }
import compiler.{ CompilerCache, EvalImports }
import Types.{ const, idFun }
import Aggregation.AnyKeys
import Project.LoadAction

import scala.annotation.tailrec
import Path._
import StandardMain._

import java.io.{ File, IOException }
import java.net.URI
import java.util.Locale
import scala.util.control.NonFatal

import BasicCommandStrings.Shell
import CommandStrings.BootCommand

/** This class is the entry point for sbt. */
final class xMain extends xsbti.AppMain {
  def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
    {
      import BasicCommands.early
      import BasicCommandStrings.runEarly
      import BuiltinCommands.{ initialize, defaults }
      import CommandStrings.{ BootCommand, DefaultsCommand, InitCommand }
      if (!java.lang.Boolean.getBoolean("sbt.skip.version.write"))
        setSbtVersion(configuration.baseDirectory(), configuration.provider().id().version())
      val state = initialState(configuration,
        Seq(defaults, early),
        runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil)
      notifyUsersAboutShell(state)
      runManaged(state)
    }

  private val sbtVersionRegex = """sbt\.version\s*=.*""".r
  private def isSbtVersionLine(s: String) = sbtVersionRegex.pattern matcher s matches ()

  private def isSbtProject(baseDir: File, projectDir: File) =
    projectDir.exists() || (baseDir * "*.sbt").get.nonEmpty

  private def setSbtVersion(baseDir: File, sbtVersion: String) = {
    val projectDir = baseDir / "project"
    val buildProps = projectDir / "build.properties"

    val buildPropsLines = if (buildProps.canRead) IO.readLines(buildProps) else Nil

    val sbtVersionAbsent = buildPropsLines forall (!isSbtVersionLine(_))

    if (sbtVersionAbsent) {
      val errorMessage = s"WARN: No sbt.version set in project/build.properties, base directory: $baseDir"
      try {
        if (isSbtProject(baseDir, projectDir)) {
          val line = s"sbt.version=$sbtVersion"
          IO.writeLines(buildProps, line :: buildPropsLines)
          println(s"Updated file $buildProps setting sbt.version to: $sbtVersion")
        } else
          println(errorMessage)
      } catch {
        case _: IOException => println(errorMessage)
      }
    }
  }

  private def isInteractive = System.console() != null
  private def hasShell(state: State) = state.remainingCommands contains Shell

  /**
   * The "boot" command adds "iflast shell" ("if last shell")
   * which basically means it falls back to shell if there are no further commands
   */
  private def endsWithBoot(state: State) = state.remainingCommands.lastOption exists (_ == BootCommand)

  private def notifyUsersAboutShell(state: State) =
    if (isInteractive && !hasShell(state) && !endsWithBoot(state)) {
      state.log warn "Executing in batch mode."
      state.log warn "  For better performance, hit [ENTER] to switch to interactive mode, or"
      state.log warn "  consider launching sbt without any commands, or explicitly passing 'shell'"
    }
}

final class ScriptMain extends xsbti.AppMain {
  def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
    {
      import BasicCommandStrings.runEarly
      runManaged(initialState(configuration,
        BuiltinCommands.ScriptCommands,
        runEarly(Level.Error.toString) :: Script.Name :: Nil)
      )
    }
}
final class ConsoleMain extends xsbti.AppMain {
  def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
    runManaged(initialState(configuration,
      BuiltinCommands.ConsoleCommands,
      IvyConsole.Name :: Nil)
    )
}

object StandardMain {
  def runManaged(s: State): xsbti.MainResult =
    {
      val previous = TrapExit.installManager()
      try MainLoop.runLogged(s)
      finally TrapExit.uninstallManager(previous)
    }

  /** The common interface to standard output, used for all built-in ConsoleLoggers. */
  val console = ConsoleOut.systemOutOverwrite(ConsoleOut.overwriteContaining("Resolving "))

  def initialGlobalLogging: GlobalLogging = GlobalLogging.initial(MainLogging.globalDefault(console), File.createTempFile("sbt", ".log"), console)

  def initialState(configuration: xsbti.AppConfiguration, initialDefinitions: Seq[Command], preCommands: Seq[String]): State =
    {
      import BasicCommandStrings.isEarlyCommand
      val userCommands = configuration.arguments.map(_.trim)
      val (earlyCommands, normalCommands) = (preCommands ++ userCommands).partition(isEarlyCommand)
      val commands = earlyCommands ++ normalCommands
      val initAttrs = BuiltinCommands.initialAttributes
      val s = State(configuration, initialDefinitions, Set.empty, None, commands, State.newHistory, initAttrs, initialGlobalLogging, State.Continue)
      s.initializeClassLoaderCache
    }
}

import DefaultParsers._
import CommandStrings._
import BasicCommandStrings._
import BasicCommands._
import CommandUtil._
import TemplateCommandUtil.templateCommand

object BuiltinCommands {
  def initialAttributes = AttributeMap.empty

  def ConsoleCommands: Seq[Command] = Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop)
  def ScriptCommands: Seq[Command] = Seq(ignore, exit, Script.command, setLogLevel, early, act, nop)
  def DefaultCommands: Seq[Command] = Seq(ignore, help, completionsCommand, about, tasks, settingsCommand, loadProject, templateCommand,
    projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
    setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
    ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++
    compatCommands
  def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil

  def boot = Command.make(BootCommand)(bootParser)

  def about = Command.command(AboutCommand, aboutBrief, aboutDetailed) { s => s.log.info(aboutString(s)); s }

  def setLogLevel = Command.arb(const(logLevelParser), logLevelHelp)(LogManager.setGlobalLogLevel)
  private[this] def logLevelParser: Parser[Level.Value] = oneOf(Level.values.toSeq.map(v => v.toString ^^^ v))

  // This parser schedules the default boot commands unless overridden by an alias
  def bootParser(s: State) =
    {
      val orElse = () => DefaultBootCommands ::: s
      delegateToAlias(BootCommand, success(orElse))(s)
    }

  def sbtName(s: State): String = s.configuration.provider.id.name
  def sbtVersion(s: State): String = s.configuration.provider.id.version
  def scalaVersion(s: State): String = s.configuration.provider.scalaProvider.version
  def aboutProject(s: State): String =
    if (Project.isProjectLoaded(s)) {
      val e = Project.extract(s)
      val version = e.getOpt(Keys.version) match { case None => ""; case Some(v) => " " + v }
      val current = "The current project is " + Reference.display(e.currentRef) + version + "\n"
      val sc = aboutScala(s, e)
      val built = if (sc.isEmpty) "" else "The current project is built against " + sc + "\n"
      current + built + aboutPlugins(e)
    } else "No project is currently loaded"

  def aboutPlugins(e: Extracted): String =
    {
      def list(b: BuildUnit) = b.plugins.detected.autoPlugins.map(_.value.label) ++ b.plugins.detected.plugins.names
      val allPluginNames = e.structure.units.values.flatMap(u => list(u.unit)).toSeq.distinct
      if (allPluginNames.isEmpty) "" else allPluginNames.mkString("Available Plugins: ", ", ", "")
    }
  def aboutScala(s: State, e: Extracted): String =
    {
      val scalaVersion = e.getOpt(Keys.scalaVersion)
      val scalaHome = e.getOpt(Keys.scalaHome).flatMap(idFun)
      val instance = e.getOpt(Keys.scalaInstance.task).flatMap(_ => quiet(e.runTask(Keys.scalaInstance, s)._2))
      (scalaVersion, scalaHome, instance) match {
        case (sv, Some(home), Some(si)) => "local Scala version " + selectScalaVersion(sv, si) + " at " + home.getAbsolutePath
        case (_, Some(home), None)      => "a local Scala build at " + home.getAbsolutePath
        case (sv, None, Some(si))       => "Scala " + selectScalaVersion(sv, si)
        case (Some(sv), None, None)     => "Scala " + sv
        case (None, None, None)         => ""
      }
    }
  def aboutString(s: State): String =
    {
      val (name, ver, scalaVer, about) = (sbtName(s), sbtVersion(s), scalaVersion(s), aboutProject(s))
      """This is %s %s
			|%s
			|%s, %s plugins, and build definitions are using Scala %s
			|""".stripMargin.format(name, ver, about, name, name, scalaVer)
    }
  private[this] def selectScalaVersion(sv: Option[String], si: ScalaInstance): String = sv match { case Some(si.version) => si.version; case _ => si.actualVersion }
  private[this] def quiet[T](t: => T): Option[T] = try { Some(t) } catch { case e: Exception => None }

  def settingsCommand = showSettingLike(SettingsCommand, settingsPreamble, KeyRanks.MainSettingCutoff, key => !isTask(key.manifest))

  def tasks = showSettingLike(TasksCommand, tasksPreamble, KeyRanks.MainTaskCutoff, key => isTask(key.manifest))

  def showSettingLike(command: String, preamble: String, cutoff: Int, keep: AttributeKey[_] => Boolean) =
    Command(command, settingsBrief(command), settingsDetailed(command))(showSettingParser(keep)) {
      case (s: State, (verbosity: Int, selected: Option[String])) =>
        if (selected.isEmpty) System.out.println(preamble)
        val prominentOnly = verbosity <= 1
        val verboseFilter = if (prominentOnly) highPass(cutoff) else topNRanked(25 * verbosity)
        System.out.println(tasksHelp(s, keys => verboseFilter(keys filter keep), selected))
        System.out.println()
        if (prominentOnly) System.out.println(moreAvailableMessage(command, selected.isDefined))
        s
    }
  def showSettingParser(keepKeys: AttributeKey[_] => Boolean)(s: State): Parser[(Int, Option[String])] =
    verbosityParser ~ selectedParser(s, keepKeys).?
  def selectedParser(s: State, keepKeys: AttributeKey[_] => Boolean): Parser[String] =
    singleArgument(allTaskAndSettingKeys(s).filter(keepKeys).map(_.label).toSet)
  def verbosityParser: Parser[Int] = success(1) | ((Space ~ "-") ~> (
    'v'.id.+.map(_.size + 1) |
    ("V" ^^^ Int.MaxValue)
  ))
  def taskDetail(keys: Seq[AttributeKey[_]]): Seq[(String, String)] =
    sortByLabel(withDescription(keys)) flatMap taskStrings

  def allTaskAndSettingKeys(s: State): Seq[AttributeKey[_]] =
    {
      val extracted = Project.extract(s)
      import extracted._
      val index = structure.index
      index.keyIndex.keys(Some(currentRef)).toSeq.map { key =>
        try
          Some(index.keyMap(key))
        catch {
          case NonFatal(ex) =>
            s.log error ex.getMessage
            None
        }
      }.collect { case Some(s) => s }.distinct
    }

  def sortByLabel(keys: Seq[AttributeKey[_]]): Seq[AttributeKey[_]] = keys.sortBy(_.label)
  def sortByRank(keys: Seq[AttributeKey[_]]): Seq[AttributeKey[_]] = keys.sortBy(_.rank)
  def withDescription(keys: Seq[AttributeKey[_]]): Seq[AttributeKey[_]] = keys.filter(_.description.isDefined)
  def isTask(mf: Manifest[_])(implicit taskMF: Manifest[Task[_]], inputMF: Manifest[InputTask[_]]): Boolean =
    mf.runtimeClass == taskMF.runtimeClass || mf.runtimeClass == inputMF.runtimeClass
  def topNRanked(n: Int) = (keys: Seq[AttributeKey[_]]) => sortByRank(keys).take(n)
  def highPass(rankCutoff: Int) = (keys: Seq[AttributeKey[_]]) => sortByRank(keys).takeWhile(_.rank <= rankCutoff)

  def tasksHelp(s: State, filter: Seq[AttributeKey[_]] => Seq[AttributeKey[_]], arg: Option[String]): String =
    {
      val commandAndDescription = taskDetail(filter(allTaskAndSettingKeys(s)))
      arg match {
        case Some(selected) => detail(selected, commandAndDescription.toMap)
        case None           => aligned("  ", "   ", commandAndDescription) mkString ("\n", "\n", "")
      }
    }

  def taskStrings(key: AttributeKey[_]): Option[(String, String)] = key.description map { d => (key.label, d) }

  def defaults = Command.command(DefaultsCommand) { s =>
    s.copy(definedCommands = DefaultCommands)
  }

  def initialize = Command.command(InitCommand) { s =>
    /*"load-commands -base ~/.sbt/commands" :: */ readLines(readable(sbtRCs(s))) ::: s
  }

  def eval = Command.single(EvalCommand, Help.more(EvalCommand, evalDetailed)) { (s, arg) =>
    if (Project.isProjectLoaded(s)) loadedEval(s, arg) else rawEval(s, arg)
    s
  }
  private[this] def loadedEval(s: State, arg: String): Unit = {
    val extracted = Project extract s
    import extracted._
    val result = session.currentEval().eval(arg, srcName = "<eval>", imports = autoImports(extracted))
    s.log.info(s"ans: ${result.tpe} = ${result.getValue(currentLoader)}")
  }
  private[this] def rawEval(s: State, arg: String): Unit = {
    val app = s.configuration.provider
    val classpath = app.mainClasspath ++ app.scalaProvider.jars
    val result = Load.mkEval(classpath, s.baseDir, Nil).eval(arg, srcName = "<eval>", imports = new EvalImports(Nil, ""))
    s.log.info(s"ans: ${result.tpe} = ${result.getValue(app.loader)}")
  }
  def sessionCommand = Command.make(SessionCommand, sessionBrief, SessionSettings.Help)(SessionSettings.command)
  def reapply(newSession: SessionSettings, structure: BuildStructure, s: State): State =
    {
      s.log.info("Reapplying settings...")
      // Here, for correct behavior, we also need to re-inject a settings logger, as we'll be re-evaluating settings.
      val loggerInject = LogManager.settingsLogger(s)
      val withLogger = newSession.appendRaw(loggerInject :: Nil)
      val newStructure = Load.reapply(withLogger.mergeSettings, structure)(Project.showContextKey(newSession, structure))
      Project.setProject(newSession, newStructure, s)
    }
  def set = Command(SetCommand, setBrief, setDetailed)(setParser) {
    case (s, (all, arg)) =>
      val extracted = Project extract s
      import extracted._
      val dslVals = extracted.currentUnit.unit.definitions.dslDefinitions
      // TODO - This is possibly inefficient (or stupid).  We should try to only attach the
      // classloader + imports NEEDED to compile the set command, rather than
      // just ALL of them.
      val ims = (imports(extracted) ++ dslVals.imports.map(i => (i, -1)))
      val cl = dslVals.classloader(currentLoader)
      val settings = EvaluateConfigurations.evaluateSetting(
        session.currentEval(),
        "<set>",
        ims,
        arg,
        LineRange(0, 0)
      )(cl)
      val setResult = if (all) SettingCompletions.setAll(extracted, settings) else SettingCompletions.setThis(s, extracted, settings, arg)
      s.log.info(setResult.quietSummary)
      s.log.debug(setResult.verboseSummary)
      reapply(setResult.session, structure, s)
  }
  // @deprecated("Use SettingCompletions.setThis", "0.13.0")
  def setThis(s: State, extracted: Extracted, settings: Seq[Def.Setting[_]], arg: String) =
    SettingCompletions.setThis(s, extracted, settings, arg)
  def inspect = Command(InspectCommand, inspectBrief, inspectDetailed)(Inspect.parser) {
    case (s, (option, sk)) =>
      s.log.info(Inspect.output(s, option, sk))
      s
  }

  @deprecated("Use Inspect.output", "0.13.0")
  def inspectOutput(s: State, option: Inspect.Mode, sk: Def.ScopedKey[_]): String = Inspect.output(s, option, sk)

  def lastGrep = Command(LastGrepCommand, lastGrepBrief, lastGrepDetailed)(lastGrepParser) {
    case (s, (pattern, Some(sks))) =>
      val (str, ref, display) = extractLast(s)
      Output.lastGrep(sks, str.streams(s), pattern, printLast(s))(display)
      keepLastLog(s)
    case (s, (pattern, None)) =>
      for (logFile <- lastLogFile(s)) yield Output.lastGrep(logFile, pattern, printLast(s))
      keepLastLog(s)
  }
  def extractLast(s: State) = {
    val ext = Project.extract(s)
    (ext.structure, Select(ext.currentRef), ext.showKey)
  }

  def setParser = (s: State) => {
    val extracted = Project.extract(s)
    import extracted._
    token(Space ~> flag("every" ~ Space)) ~
      SettingCompletions.settingParser(structure.data, structure.index.keyMap, currentProject)
  }

  @deprecated("Use Inspect.parser", "0.13.0")
  def inspectParser: State => Parser[(Inspect.Mode, Def.ScopedKey[_])] = Inspect.parser

  @deprecated("Use Inspect.spacedModeParser", "0.13.0")
  val spacedModeParser: State => Parser[Inspect.Mode] = Inspect.spacedModeParser

  @deprecated("Use Inspect.allKeyParser", "0.13.0")
  def allKeyParser(s: State): Parser[AttributeKey[_]] = Inspect.allKeyParser(s)

  @deprecated("Use Inspect.spacedKeyParser", "0.13.0")
  val spacedKeyParser: State => Parser[Def.ScopedKey[_]] = Inspect.spacedKeyParser

  val spacedAggregatedParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.aggregatedKeyParser(s))
  val aggregatedKeyValueParser: State => Parser[Option[AnyKeys]] = (s: State) => spacedAggregatedParser(s).map(x => Act.keyValues(s)(x)).?

  val exportParser: State => Parser[() => State] = (s: State) => Act.requireSession(s, token(Space) ~> exportParser0(s))
  private[sbt] def exportParser0(s: State): Parser[() => State] =
    {
      val extracted = Project extract s
      import extracted.{ showKey, structure }
      val keysParser = token(flag("--last" <~ Space)) ~ Act.aggregatedKeyParser(extracted)
      val show = Aggregation.ShowConfig(settingValues = true, taskValues = false, print = println _, success = false)
      for {
        lastOnly_keys <- keysParser
        kvs = Act.keyValues(structure)(lastOnly_keys._2)
        f <- if (lastOnly_keys._1) success(() => s) else Aggregation.evaluatingParser(s, structure, show)(kvs)
      } yield () => {
        def export0(s: State): State = lastImpl(s, kvs, Some(ExportStream))
        val newS = try f() catch {
          case e: Exception =>
            try export0(s)
            finally { throw e }
        }
        export0(newS)
      }
    }

  def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "<pattern>")) ~ aggregatedKeyValueParser(s))
  def last = Command(LastCommand, lastBrief, lastDetailed)(aggregatedKeyValueParser) {
    case (s, Some(sks)) => lastImpl(s, sks, None)
    case (s, None) =>
      for (logFile <- lastLogFile(s)) yield Output.last(logFile, printLast(s))
      keepLastLog(s)
  }
  def export = Command(ExportCommand, exportBrief, exportDetailed)(exportParser)((s, f) => f())

  private[this] def lastImpl(s: State, sks: AnyKeys, sid: Option[String]): State =
    {
      val (str, ref, display) = extractLast(s)
      Output.last(sks, str.streams(s), printLast(s), sid)(display)
      keepLastLog(s)
    }

  /** Determines the log file that last* commands should operate on.  See also isLastOnly. */
  def lastLogFile(s: State) =
    {
      val backing = s.globalLogging.backing
      if (isLastOnly(s)) backing.last else Some(backing.file)
    }

  /**
   * If false, shift the current log file to be the log file that 'last' will operate on.
   * If true, keep the previous log file as the one 'last' operates on because there is nothing useful in the current one.
   */
  def keepLastLog(s: State): State = if (isLastOnly(s)) s.keepLastLog else s

  /**
   * The last* commands need to determine whether to read from the current log file or the previous log file
   * and whether to keep the previous log file or not.  This is selected based on whether the previous command
   * was 'shell', which meant that the user directly entered the 'last' command.  If it wasn't directly entered,
   * the last* commands operate on any output since the last 'shell' command and do shift the log file.
   * Otherwise, the output since the previous 'shell' command is used and the log file is not shifted.
   */
  def isLastOnly(s: State): Boolean = s.history.previous.forall(_ == Shell)

  def printLast(s: State): Seq[String] => Unit = _ foreach println

  def autoImports(extracted: Extracted): EvalImports = new EvalImports(imports(extracted), "<auto-imports>")
  def imports(extracted: Extracted): Seq[(String, Int)] =
    {
      val curi = extracted.currentRef.build
      extracted.structure.units(curi).imports.map(s => (s, -1))
    }

  def listBuild(uri: URI, build: LoadedBuildUnit, current: Boolean, currentID: String, log: Logger) =
    {
      log.info("In " + uri)
      def prefix(id: String) = if (currentID != id) "   " else if (current) " * " else "(*)"
      for (id <- build.defined.keys.toSeq.sorted) log.info("\t" + prefix(id) + id)
    }

  def act = Command.customHelp(Act.actParser, actHelp)
  def actHelp = (s: State) => CommandStrings.showHelp ++ CommandStrings.multiTaskHelp ++ keysHelp(s)
  def keysHelp(s: State): Help =
    if (Project.isProjectLoaded(s))
      Help.detailOnly(taskDetail(allTaskAndSettingKeys(s)))
    else
      Help.empty
  def plugins = Command.command(PluginsCommand, pluginsBrief, pluginsDetailed) { s =>
    val helpString = PluginsDebug.helpAll(s)
    System.out.println(helpString)
    s
  }
  val pluginParser: State => Parser[AutoPlugin] = s => {
    val autoPlugins: Map[String, AutoPlugin] = PluginsDebug.autoPluginMap(s)
    token(Space) ~> Act.knownPluginParser(autoPlugins, "plugin")
  }
  def plugin = Command(PluginCommand)(pluginParser) { (s, plugin) =>
    val helpString = PluginsDebug.help(plugin, s)
    System.out.println(helpString)
    s
  }

  def projects = Command(ProjectsCommand, (ProjectsCommand, projectsBrief), projectsDetailed)(s => projectsParser(s).?) {
    case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds)
    case (s, None)               => showProjects(s); s
  }
  def showProjects(s: State): Unit = {
    val extracted = Project extract s
    import extracted._
    import currentRef.{ build => curi, project => cid }
    listBuild(curi, structure.units(curi), true, cid, s.log)
    for ((uri, build) <- structure.units if curi != uri) listBuild(uri, build, false, cid, s.log)
  }
  def transformExtraBuilds(s: State, f: List[URI] => List[URI]): State =
    {
      val original = Project.extraBuilds(s)
      val extraUpdated = Project.updateExtraBuilds(s, f)
      try doLoadProject(extraUpdated, LoadAction.Current)
      catch {
        case e: Exception =>
          s.log.error("Project loading failed: reverting to previous state.")
          Project.setExtraBuilds(s, original)
      }
    }

  def projectsParser(s: State): Parser[List[URI] => List[URI]] =
    {
      val addBase = token(Space ~> "add") ~> token(Space ~> basicUri, "<build URI>").+
      val removeBase = token(Space ~> "remove") ~> token(Space ~> Uri(Project.extraBuilds(s).toSet)).+
      addBase.map(toAdd => (xs: List[URI]) => (toAdd.toList ::: xs).distinct) |
        removeBase.map(toRemove => (xs: List[URI]) => xs.filterNot(toRemove.toSet))
    }

  def project = Command.make(ProjectCommand, projectBrief, projectDetailed)(ProjectNavigation.command)

  def loadFailed = Command(LoadFailed)(loadProjectParser)(doLoadFailed)

  @deprecated("No longer used.", "0.13.2")
  def handleLoadFailed(s: State): State = doLoadFailed(s, "")

  @tailrec
  private[this] def doLoadFailed(s: State, loadArg: String): State =
    {
      val result = (SimpleReader.readLine("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? ") getOrElse Quit).toLowerCase(Locale.ENGLISH)
      def matches(s: String) = !result.isEmpty && (s startsWith result)

      if (result.isEmpty || matches("retry"))
        loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
      else if (matches(Quit))
        s.exit(ok = false)
      else if (matches("ignore")) {
        val hadPrevious = Project.isProjectLoaded(s)
        s.log.warn("Ignoring load failure: " + (if (hadPrevious) "using previously loaded project." else "no project loaded."))
        s
      } else if (matches("last"))
        LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s
      else {
        println("Invalid response.")
        doLoadFailed(s, loadArg)
      }
    }

  def loadProjectCommands(arg: String) =
    StashOnFailure ::
      (OnFailure + " " + loadProjectCommand(LoadFailed, arg)) ::
      loadProjectCommand(LoadProjectImpl, arg) ::
      PopOnFailure ::
      State.FailureWall ::
      Nil
  def loadProject = Command(LoadProject, LoadProjectBrief, LoadProjectDetailed)(loadProjectParser) { (s, arg) => loadProjectCommands(arg) ::: s }
  private[this] def loadProjectParser = (s: State) => matched(Project.loadActionParser)
  private[this] def loadProjectCommand(command: String, arg: String): String = s"$command $arg".trim

  def loadProjectImpl = Command(LoadProjectImpl)(_ => Project.loadActionParser)(doLoadProject)
  def doLoadProject(s0: State, action: LoadAction.Value): State =
    {
      val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
      IO.createDirectory(base)
      val s = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1)

      val (eval, structure) =
        try Load.defaultLoad(s, base, s.log, Project.inPluginProject(s), Project.extraBuilds(s))
        catch {
          case ex: compiler.EvalException =>
            s0.log.debug(ex.getMessage)
            ex.getStackTrace map (ste => s"\tat $ste") foreach (s0.log.debug(_))
            ex.setStackTrace(Array.empty)
            throw ex
        }

      val session = Load.initialSession(structure, eval, s0)
      SessionSettings.checkSession(session, s)
      Project.setProject(session, structure, s)
    }
  def registerCompilerCache(s: State): State =
    {
      val maxCompilers = System.getProperty("sbt.resident.limit")
      val cache =
        if (maxCompilers == null)
          CompilerCache.fresh
        else {
          val num = try maxCompilers.toInt catch {
            case e: NumberFormatException => throw new RuntimeException("Resident compiler limit must be an integer.", e)
          }
          if (num <= 0) CompilerCache.fresh else CompilerCache(num)
        }
      s.put(Keys.stateCompilerCache, cache)
    }
}