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

	import java.io.File
	import java.util.concurrent.Callable

/**
Data structure representing all command execution information.

@param configuration provides access to the launcher environment, including the application configuration, Scala versions, jvm/filesystem wide locking, and the launcher itself
@param definedCommands the list of command definitions that evaluate command strings.  These may be modified to change the available commands.
@param onFailure the command to execute when another command fails.  `onFailure` is cleared before the failure handling command is executed.
@param remainingCommands the sequence of commands to execute.  This sequence may be modified to change the commands to be executed.  Typically, the `::` and `:::` methods are used to prepend new commands to run.
@param exitHooks code to run before sbt exits, usually to ensure resources are cleaned up.
@param history tracks the recently executed commands
@param attributes custom command state.  It is important to clean up attributes when no longer needed to avoid memory leaks and class loader leaks.
@param next the next action for the command processor to take.  This may be to continue with the next command, adjust global logging, or exit.
*/
final case class State(
	configuration: xsbti.AppConfiguration,
	definedCommands: Seq[Command],
	exitHooks: Set[ExitHook],
	onFailure: Option[String],
	remainingCommands: Seq[String],
	history: State.History,
	attributes: AttributeMap,
	globalLogging: GlobalLogging,
	next: State.Next
) extends Identity {
	lazy val combinedParser = Command.combine(definedCommands)(this)
}

trait Identity {
	override final def hashCode = super.hashCode
	override final def equals(a: Any) = super.equals(a)
	override final def toString = super.toString
}

/** StateOps methods to be merged at the next binary incompatible release. */
private[sbt] trait NewStateOps
{
	def interactive: Boolean
	def setInteractive(flag: Boolean): State
}

/** Convenience methods for State transformations and operations. */
trait StateOps {
	def process(f: (String, State) => State): State

	/** Schedules `commands` to be run before any remaining commands.*/
	def ::: (commands: Seq[String]): State

	/** Schedules `command` to be run before any remaining commands.*/
	def :: (command: String): State

	/** Sets the next command processing action to be to continue processing the next command.*/
	def continue: State

	/** Reboots sbt.  A reboot restarts execution from the entry point of the launcher.
	* A reboot is designed to be as close as possible to actually restarting the JVM without actually doing so.
	* Because the JVM is not restarted, JVM exit hooks are not run.
	* State.exitHooks should be used instead and those will be run before rebooting.
	* If `full` is true, the boot directory is deleted before starting again.
	* This command is currently implemented to not return, but may be implemented in the future to only reboot at the next command processing step. */
	def reboot(full: Boolean): State

	/** Sets the next command processing action to do.*/
	def setNext(n: State.Next): State

	@deprecated("Use setNext", "0.11.0") def setResult(ro: Option[xsbti.MainResult]): State

	/** Restarts sbt without dropping loaded Scala classes.  It is a shallower restart than `reboot`.
	* This method takes a snapshot of the remaining commands and will resume executing those commands after reload.
	* This means that any commands added to this State will be dropped.*/
	def reload: State

	/** Sets the next command processing action to be to rotate the global log and continue executing commands.*/
	def clearGlobalLog: State
	/** Sets the next command processing action to be to keep the previous log and continue executing commands.  */
	def keepLastLog: State

	/** Sets the next command processing action to be to exit with a zero exit code if `ok` is true and a nonzero exit code if `ok` if false.*/
	def exit(ok: Boolean): State
	/** Marks the currently executing command as failing.  This triggers failure handling by the command processor.  See also `State.onFailure`*/
	def fail: State

	/** Schedules `newCommands` to be run after any remaining commands. */
	def ++ (newCommands: Seq[Command]): State
	/** Schedules `newCommand` to be run after any remaining commands. */
	def + (newCommand: Command): State

	/** Gets the value associated with `key` from the custom attributes map.*/
	def get[T](key: AttributeKey[T]): Option[T]
	/** Sets the value associated with `key` in the custom attributes map.*/
	def put[T](key: AttributeKey[T], value: T): State
	/** Removes the `key` and any associated value from the custom attributes map.*/
	def remove(key: AttributeKey[_]): State
	/** Sets the value associated with `key` in the custom attributes map by transforming the current value.*/
	def update[T](key: AttributeKey[T])(f: Option[T] => T): State
	/** Returns true if `key` exists in the custom attributes map, false if it does not exist.*/
	def has(key: AttributeKey[_]): Boolean

	/** The application base directory, which is not necessarily the current working directory.*/
	def baseDir: File

	/** The Logger used for general command logging.*/
	def log: Logger

	/** Evaluates the provided expression with a JVM-wide and machine-wide lock on `file`.*/
	def locked[T](file: File)(t: => T): T

	/** Runs any defined exitHooks and then clears them.*/
	def runExitHooks(): State
	/** Registers a new exit hook, which will run when sbt exits or restarts.*/
	def addExitHook(f: => Unit): State
}

object State
{
	final val FailureWall = "---"

	/** Represents the next action for the command processor.*/
	sealed trait Next
	/** Indicates that the command processor should process the next command.*/
	object Continue extends Next
	/** Indicates that the application should exit with the given result.*/
	final class Return(val result: xsbti.MainResult) extends Next
	/** Indicates that global logging should be rotated.*/
	final object ClearGlobalLog extends Next
	/** Indicates that the previous log file should be preserved instead of discarded.*/
	final object KeepLastLog extends Next

	/** Provides a list of recently executed commands.  The commands are stored as processed instead of as entered by the user.
	* @param executed the list of the most recently executed commands, with the most recent command first.
	* @param maxSize the maximum number of commands to keep, or 0 to keep an unlimited number. */
	final class History private[State](val executed: Seq[String], val maxSize: Int)
	{
		/** Adds `command` as the most recently executed command.*/
		def :: (command: String): History =
		{
			val prependTo = if(maxSize > 0 && executed.size >= maxSize) executed.take(maxSize - 1) else executed
			new History(command +: prependTo, maxSize)
		}
		/** Changes the maximum number of commands kept, adjusting the current history if necessary.*/
		def setMaxSize(size: Int): History =
			new History(if(size <= 0) executed else executed.take(size), size)
		def current: String = executed.head
		def previous: Option[String] = executed.drop(1).headOption
	}
	/** Constructs an empty command History with a default, finite command limit.*/
	def newHistory = new History(Vector.empty, complete.HistoryCommands.MaxLines)

	def defaultReload(state: State): Reboot =
	{
		val app = state.configuration.provider
		new Reboot(app.scalaProvider.version, state.remainingCommands, app.id, state.configuration.baseDirectory)
	}

	private[sbt] implicit def newStateOps(s: State): NewStateOps = new NewStateOps {
		def interactive = s.get(BasicKeys.interactive).getOrElse(false)
		def setInteractive(i: Boolean) = s.put(BasicKeys.interactive, i)
	}

	/** Provides operations and transformations on State. */
	implicit def stateOps(s: State): StateOps = new StateOps {
		def process(f: (String, State) => State): State =
			s.remainingCommands match {
				case Seq(x, xs @ _*) => f(x, s.copy(remainingCommands = xs, history = x :: s.history))
				case Seq() => exit(true)
			}
			s.copy(remainingCommands = s.remainingCommands.drop(1))
		def ::: (newCommands: Seq[String]): State = s.copy(remainingCommands = newCommands ++ s.remainingCommands)
		def :: (command: String): State = (command :: Nil) ::: this
		def ++ (newCommands: Seq[Command]): State = s.copy(definedCommands = (s.definedCommands ++ newCommands).distinct)
		def + (newCommand: Command): State = this ++ (newCommand :: Nil)
		def baseDir: File = s.configuration.baseDirectory
		def setNext(n: Next) = s.copy(next = n)
		def setResult(ro: Option[xsbti.MainResult]) = ro match { case None => continue; case Some(r) => setNext(new Return(r)) }
		def continue = setNext(Continue)
		def reboot(full: Boolean) ={ runExitHooks(); throw new xsbti.FullReload(s.remainingCommands.toArray, full) }
		def reload = runExitHooks().setNext(new Return(defaultReload(s)))
		def clearGlobalLog = setNext(ClearGlobalLog)
		def keepLastLog = setNext(KeepLastLog)
		def exit(ok: Boolean) = runExitHooks().setNext(new Return(Exit(if(ok) 0 else 1)))
		def get[T](key: AttributeKey[T]) = s.attributes get key
		def put[T](key: AttributeKey[T], value: T) = s.copy(attributes = s.attributes.put(key, value))
		def update[T](key: AttributeKey[T])(f: Option[T] => T): State = put(key, f(get(key)))
		def has(key: AttributeKey[_]) = s.attributes contains key
		def remove(key: AttributeKey[_]) = s.copy(attributes = s.attributes remove key)
		def log = s.globalLogging.full
		def fail =
		{
			val remaining = s.remainingCommands.dropWhile(_ != FailureWall)
			if(remaining.isEmpty)
				applyOnFailure(s, Nil, exit(ok = false))
			else
				applyOnFailure(s, remaining, s.copy(remainingCommands = remaining))
		}
		private[this] def applyOnFailure(s: State, remaining: Seq[String], noHandler: => State): State =
			s.onFailure match
			{
				case Some(c) => s.copy(remainingCommands = c +: remaining, onFailure = None)
				case None => noHandler
			}

		def addExitHook(act: => Unit): State =
			s.copy(exitHooks = s.exitHooks + ExitHook(act))
		def runExitHooks(): State = {
			ExitHooks.runExitHooks(s.exitHooks.toSeq)
			s.copy(exitHooks = Set.empty)
		}
		def locked[T](file: File)(t: => T): T =
			s.configuration.provider.scalaProvider.launcher.globalLock.apply(file, new Callable[T] { def call = t })
	}
}