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

import java.io.{BufferedWriter, PrintStream, PrintWriter}
import java.util.Locale

object ConsoleLogger
{
	@deprecated("Moved to ConsoleOut", "0.13.0")
	def systemOut: ConsoleOut = ConsoleOut.systemOut

	@deprecated("Moved to ConsoleOut", "0.13.0")
	def overwriteContaining(s: String): (String,String) => Boolean = ConsoleOut.overwriteContaining(s)

	@deprecated("Moved to ConsoleOut", "0.13.0")
	def systemOutOverwrite(f: (String,String) => Boolean): ConsoleOut = ConsoleOut.systemOutOverwrite(f)

	@deprecated("Moved to ConsoleOut", "0.13.0")
	def printStreamOut(out: PrintStream): ConsoleOut = ConsoleOut.printStreamOut(out)

	@deprecated("Moved to ConsoleOut", "0.13.0")
	def printWriterOut(out: PrintWriter): ConsoleOut = ConsoleOut.printWriterOut(out)

	@deprecated("Moved to ConsoleOut", "0.13.0")
	def bufferedWriterOut(out: BufferedWriter): ConsoleOut = bufferedWriterOut(out)

	/** Escape character, used to introduce an escape sequence. */
	final val ESC = '\u001B'

	/** An escape terminator is a character in the range `@` (decimal value 64) to `~` (decimal value 126). 
	* It is the final character in an escape sequence. */
	def isEscapeTerminator(c: Char): Boolean =
		c >= '@' && c <= '~'

	/** Returns true if the string contains the ESC character. */
	def hasEscapeSequence(s: String): Boolean =
		s.indexOf(ESC) >= 0

	/** Returns the string `s` with escape sequences removed.
	* An escape sequence starts with the ESC character (decimal value 27) and ends with an escape terminator.
	* @see isEscapeTerminator
	*/
	def removeEscapeSequences(s: String): String =
		if(s.isEmpty || !hasEscapeSequence(s))
			s
		else
		{
			val sb = new java.lang.StringBuilder
			nextESC(s, 0, sb)
			sb.toString
		}
	private[this] def nextESC(s: String, start: Int, sb: java.lang.StringBuilder)
	{
		val escIndex = s.indexOf(ESC, start)
		if(escIndex < 0)
			sb.append(s, start, s.length)
		else {
			sb.append(s, start, escIndex)
			val next = skipESC(s, escIndex+1)
			nextESC(s, next, sb)
		}
	}
	

	/** Skips the escape sequence starting at `i-1`.  `i` should be positioned at the character after the ESC that starts the sequence. */
	private[this] def skipESC(s: String, i: Int): Int =
		if(i >= s.length)
			i
		else if( isEscapeTerminator(s.charAt(i)) )
			i+1
		else
			skipESC(s, i+1)

	val formatEnabled =
	{
		import java.lang.Boolean.{getBoolean, parseBoolean}
		val value = System.getProperty("sbt.log.format")
		if(value eq null) (ansiSupported && !getBoolean("sbt.log.noformat")) else parseBoolean(value)
	}
	private[this] def jline1to2CompatMsg = "Found class jline.Terminal, but interface was expected"

	private[this] def ansiSupported =
		try {
			val terminal = jline.TerminalFactory.get
			terminal.restore // #460
			terminal.isAnsiSupported
		} catch {
			case e: Exception => !isWindows

			// sbt 0.13 drops JLine 1.0 from the launcher and uses 2.x as a normal dependency
			// when 0.13 is used with a 0.12 launcher or earlier, the JLine classes from the launcher get loaded
			// this results in a linkage error as detected below.  The detection is likely jvm specific, but the priority
			// is avoiding mistakenly identifying something as a launcher incompatibility when it is not
			case e: IncompatibleClassChangeError if e.getMessage == jline1to2CompatMsg =>
				throw new IncompatibleClassChangeError("JLine incompatibility detected.  Check that the sbt launcher is version 0.13.x or later.")
		}

	val noSuppressedMessage = (_: SuppressedTraceContext) => None

	private[this] def os = System.getProperty("os.name")
	private[this] def isWindows = os.toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
	
	def apply(out: PrintStream): ConsoleLogger = apply(ConsoleOut.printStreamOut(out))
	def apply(out: PrintWriter): ConsoleLogger = apply(ConsoleOut.printWriterOut(out))
	def apply(out: ConsoleOut = ConsoleOut.systemOut, ansiCodesSupported: Boolean = formatEnabled,
		useColor: Boolean = formatEnabled, suppressedMessage: SuppressedTraceContext => Option[String] = noSuppressedMessage): ConsoleLogger =
			new ConsoleLogger(out, ansiCodesSupported, useColor, suppressedMessage)

	private[this] val EscapeSequence = (27.toChar + "[^@-~]*[@-~]").r
	def stripEscapeSequences(s: String): String =
		EscapeSequence.pattern.matcher(s).replaceAll("")
}

/** A logger that logs to the console.  On supported systems, the level labels are
* colored.
*
* This logger is not thread-safe.*/
class ConsoleLogger private[ConsoleLogger](val out: ConsoleOut, override val ansiCodesSupported: Boolean, val useColor: Boolean, val suppressedMessage: SuppressedTraceContext => Option[String]) extends BasicLogger
{
		import scala.Console.{BLUE, GREEN, RED, RESET, YELLOW}
	def messageColor(level: Level.Value) = RESET
	def labelColor(level: Level.Value) =
		level match
		{
			case Level.Error => RED
			case Level.Warn => YELLOW
			case _ => RESET
		}
	def successLabelColor = GREEN
	def successMessageColor = RESET
	override def success(message: => String)
	{
		if(successEnabled)
			log(successLabelColor, Level.SuccessLabel, successMessageColor, message)
	}
	def trace(t: => Throwable): Unit =
		out.lockObject.synchronized
		{
			val traceLevel = getTrace
			if(traceLevel >= 0)
				out.print(StackTrace.trimmed(t, traceLevel))
			if(traceLevel <= 2)
				for(msg <- suppressedMessage(new SuppressedTraceContext(traceLevel, ansiCodesSupported && useColor)))
					printLabeledLine(labelColor(Level.Error), "trace", messageColor(Level.Error), msg)
		}
	def log(level: Level.Value, message: => String)
	{
		if(atLevel(level))
			log(labelColor(level), level.toString, messageColor(level), message)
	}
	private def reset(): Unit = setColor(RESET)
	
	private def setColor(color: String)
	{
		if(ansiCodesSupported && useColor)
			out.lockObject.synchronized { out.print(color) }
	}
	private def log(labelColor: String, label: String, messageColor: String, message: String): Unit =
		out.lockObject.synchronized
		{
			for(line <- message.split("""\n"""))
				printLabeledLine(labelColor, label, messageColor, line)
		}
	private def printLabeledLine(labelColor: String, label: String, messageColor: String, line: String): Unit =
	{
		reset()
		out.print("[")
		setColor(labelColor)
		out.print(label)
		reset()
		out.print("] ")
		setColor(messageColor)
		out.print(line)
		reset()
		out.println()
	}

	def logAll(events: Seq[LogEvent]) = out.lockObject.synchronized { events.foreach(log) }
	def control(event: ControlEvent.Value, message: => String)
		{ log(labelColor(Level.Info), Level.Info.toString, BLUE, message) }
}
final class SuppressedTraceContext(val traceLevel: Int, val useColor: Boolean)