/*
 * sbt
 * Copyright 2023, Scala center
 * Copyright 2011 - 2022, Lightbend, Inc.
 * Copyright 2008 - 2010, Mark Harrah
 * Licensed under Apache License 2.0 (see LICENSE)
 */

package sbt.internal.util

import java.io.{ PrintStream, PrintWriter }
import java.lang.StringBuilder
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }

import sbt.internal.util.ConsoleAppender.*
import sbt.util.*

object ConsoleLogger {

  /**
   * A new `ConsoleLogger` that logs to `out`.
   *
   * @param out
   *   Where to log the messages.
   * @return
   *   A new `ConsoleLogger` that logs to `out`.
   */
  def apply(out: PrintStream): ConsoleLogger = apply(ConsoleOut.printStreamOut(out))

  /**
   * A new `ConsoleLogger` that logs to `out`.
   *
   * @param out
   *   Where to log the messages.
   * @return
   *   A new `ConsoleLogger` that logs to `out`.
   */
  def apply(out: PrintWriter): ConsoleLogger = apply(ConsoleOut.printWriterOut(out))

  /**
   * A new `ConsoleLogger` that logs to `out`.
   *
   * @param out
   *   Where to log the messages.
   * @param ansiCodesSupported
   *   `true` if `out` supported ansi codes, `false` otherwise.
   * @param useFormat
   *   `true` to show formatting, `false` to remove it from messages.
   * @param suppressedMessage
   *   How to show suppressed stack traces.
   * @return
   *   A new `ConsoleLogger` that logs to `out`.
   */
  def apply(
      out: ConsoleOut = ConsoleOut.systemOut,
      ansiCodesSupported: Boolean = Terminal.isAnsiSupported,
      useFormat: Boolean = Terminal.isColorEnabled,
      suppressedMessage: SuppressedTraceContext => Option[String] =
        ConsoleAppender.noSuppressedMessage
  ): ConsoleLogger =
    new ConsoleLogger(out, ansiCodesSupported, useFormat, suppressedMessage)
}

/**
 * A logger that logs to the console. On supported systems, the level labels are colored.
 */
class ConsoleLogger private[ConsoleLogger] (
    out: ConsoleOut,
    ansiCodesSupported: Boolean,
    useFormat: Boolean,
    suppressedMessage: SuppressedTraceContext => Option[String]
) extends BasicLogger {

  private[sbt] val appender: Appender =
    ConsoleAppender(generateName(), out, ansiCodesSupported, useFormat, suppressedMessage)

  override def control(event: ControlEvent.Value, message: => String): Unit =
    appender.control(event, message)

  override def log(level: Level.Value, message: => String): Unit =
    if (atLevel(level)) {
      appender.appendLog(level, message)
    }

  override def success(message: => String): Unit =
    if (successEnabled) {
      appender.success(message)
    }

  override def trace(t: => Throwable): Unit =
    appender.trace(t, getTrace)

  override def logAll(events: Seq[LogEvent]) = events.foreach(log)
}

object ConsoleAppender {
  private[sbt] def cursorLeft(n: Int): String = s"\u001B[${n}D"
  private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A"
  private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B"
  private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S"
  private[sbt] def clearScreen(n: Int): String = s"\u001B[${n}J"
  private[sbt] def clearLine(n: Int): String = s"\u001B[${n}K"
  private[sbt] final val DeleteLine = "\u001B[2K"
  private[sbt] final val ClearScreenAfterCursor = clearScreen(0)
  private[sbt] final val CursorLeft1000 = cursorLeft(1000)
  private[sbt] final val CursorDown1 = cursorDown(1)
  private[sbt] final val ClearPromptLine = CursorLeft1000 + ClearScreenAfterCursor
  private val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
  def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
  def showProgress: Boolean = showProgressHolder.get
  private[sbt] trait Properties {
    def isAnsiSupported: Boolean
    def isColorEnabled: Boolean
    def out: ConsoleOut
  }
  private[sbt] object Properties {
    def from(terminal: Terminal): Properties =
      from(ConsoleOut.terminalOut(terminal), terminal.isAnsiSupported, terminal.isColorEnabled)

    def safelyFrom(terminal: Terminal): Properties =
      from(ConsoleOut.safeTerminalOut(terminal), terminal.isAnsiSupported, terminal.isColorEnabled)

    def from(o: ConsoleOut, ansi: Boolean, color: Boolean): Properties = new Properties {
      override def isAnsiSupported: Boolean = ansi
      override def isColorEnabled: Boolean = color
      override def out = o
    }
  }

  /** Hide stack trace altogether. */
  val noSuppressedMessage = (_: SuppressedTraceContext) => None

  /**
   * Indicates whether formatting has been disabled in environment variables.
   *   1. -Dsbt.log.noformat=true means no formatting. 2. -Dsbt.color=always/auto/never/true/false
   *      3. -Dsbt.colour=always/auto/never/true/false 4.
   * -Dsbt.log.format=always/auto/never/true/false
   */
  @deprecated("Use Terminal.isAnsiSupported or Terminal.isColorEnabled", "1.4.0")
  lazy val formatEnabledInEnv: Boolean = Terminal.isAnsiSupported

  private[sbt] def parseLogOption(s: String): LogOption = Terminal.parseLogOption(s) match {
    case Some(true)  => LogOption.Always
    case Some(false) => LogOption.Never
    case _           => LogOption.Auto
  }

  private val generateId: AtomicInteger = new AtomicInteger

  /**
   * A new `ConsoleAppender` that writes to standard output.
   *
   * @return
   *   A new `ConsoleAppender` that writes to standard output.
   */
  def apply(): Appender = apply(ConsoleOut.systemOut)

  /**
   * A new `ConsoleAppender` that appends log message to `out`.
   *
   * @param out
   *   Where to write messages.
   * @return
   *   A new `ConsoleAppender`.
   */
  def apply(out: PrintStream): Appender = apply(ConsoleOut.printStreamOut(out))

  /**
   * A new `ConsoleAppender` that appends log messages to `out`.
   *
   * @param out
   *   Where to write messages.
   * @return
   *   A new `ConsoleAppender`.
   */
  def apply(out: PrintWriter): Appender = apply(ConsoleOut.printWriterOut(out))

  /**
   * A new `ConsoleAppender` that writes to `out`.
   *
   * @param out
   *   Where to write messages.
   * @return
   *   A new `ConsoleAppender that writes to `out`.
   */
  def apply(out: ConsoleOut): Appender = apply(generateName(), out)

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to standard output.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @return
   *   A new `ConsoleAppender` that writes to standard output.
   */
  def apply(name: String): Appender = apply(name, ConsoleOut.systemOut)

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @param out
   *   Where to write messages.
   * @return
   *   A new `ConsoleAppender` that writes to `out`.
   */
  def apply(name: String, out: ConsoleOut): Appender = apply(name, out, Terminal.isAnsiSupported)

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @param out
   *   Where to write messages.
   * @param suppressedMessage
   *   How to handle stack traces.
   * @return
   *   A new `ConsoleAppender` that writes to `out`.
   */
  def apply(
      name: String,
      out: ConsoleOut,
      suppressedMessage: SuppressedTraceContext => Option[String]
  ): Appender = {
    val ansi = Terminal.isAnsiSupported
    apply(name, out, ansi, ansi, suppressedMessage)
  }

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @param out
   *   Where to write messages.
   * @param useFormat
   *   `true` to enable format (color, bold, etc.), `false` to remove formatting.
   * @return
   *   A new `ConsoleAppender` that writes to `out`.
   */
  def apply(name: String, out: ConsoleOut, useFormat: Boolean): Appender =
    apply(name, out, useFormat || Terminal.isAnsiSupported, useFormat, noSuppressedMessage)

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @param terminal
   *   The terminal to which this appender corresponds
   * @return
   *   A new `ConsoleAppender` that writes to `out`.
   */
  def apply(name: String, terminal: Terminal): Appender = {
    new ConsoleAppender(name, Properties.from(terminal), noSuppressedMessage)
  }

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `terminal`.
   * Printing to this Appender will not throw if the Terminal has been closed.
   *
   * @param name      An identifier for the `ConsoleAppender`.
   * @param terminal  The terminal to which this appender corresponds
   * @return A new `ConsoleAppender` that writes to `terminal`.
   */
  def safe(name: String, terminal: Terminal): Appender = {
    new ConsoleAppender(name, Properties.safelyFrom(terminal), noSuppressedMessage)
  }

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @param terminal
   *   The terminal to which this appender corresponds
   * @param suppressedMessage
   *   How to handle stack traces.
   * @return
   *   A new `ConsoleAppender` that writes to `out`.
   */
  def apply(
      name: String,
      terminal: Terminal,
      suppressedMessage: SuppressedTraceContext => Option[String]
  ): Appender = {
    new ConsoleAppender(name, Properties.from(terminal), suppressedMessage)
  }

  /**
   * A new `ConsoleAppender` identified by `name`, and that writes to `out`.
   *
   * @param name
   *   An identifier for the `ConsoleAppender`.
   * @param out
   *   Where to write messages.
   * @param ansiCodesSupported
   *   `true` if the output stream supports ansi codes, `false` otherwise.
   * @param useFormat
   *   `true` to enable format (color, bold, etc.), `false` to remove formatting.
   * @return
   *   A new `ConsoleAppender` that writes to `out`.
   */
  def apply(
      name: String,
      out: ConsoleOut,
      ansiCodesSupported: Boolean,
      useFormat: Boolean,
      suppressedMessage: SuppressedTraceContext => Option[String]
  ): Appender = {
    new ConsoleAppender(
      name,
      Properties.from(out, ansiCodesSupported, useFormat),
      suppressedMessage
    )
  }

  private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
}

// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2
// for custom appender using Java.
// http://logging.apache.org/log4j/2.x/manual/customconfig.html
// https://logging.apache.org/log4j/2.x/log4j-core/apidocs/index.html

/**
 * A logger that logs to the console. On supported systems, the level labels are colored.
 *
 * This logger is not thread-safe.
 */
class ConsoleAppender(
    override private[sbt] val name: String,
    override private[sbt] val properties: Properties,
    override private[sbt] val suppressedMessage: SuppressedTraceContext => Option[String]
) extends Appender {
  override def close(): Unit = ()
}
trait Appender extends AutoCloseable {
  private[sbt] def name: String
  private[sbt] def properties: Properties
  private[sbt] def suppressedMessage: SuppressedTraceContext => Option[String]
  import scala.Console.{ BLUE, GREEN, RED, YELLOW }

  private[util] def out: ConsoleOut = properties.out
  private[util] def ansiCodesSupported: Boolean = properties.isAnsiSupported
  private[util] def useFormat: Boolean = properties.isColorEnabled

  private def reset: String = scala.Console.RESET

  private val SUCCESS_LABEL_COLOR = GREEN
  private val SUCCESS_MESSAGE_COLOR = reset
  private val NO_COLOR = reset

  private var traceEnabledVar: Int = Int.MaxValue

  def setTrace(level: Int): Unit = synchronized { traceEnabledVar = level }

  /**
   * Returns the number of lines for stacktrace.
   */
  def getTrace: Int = synchronized { traceEnabledVar }

  /**
   * Logs the stack trace of `t`, possibly shortening it.
   *
   * The `traceLevel` parameter configures how the stack trace will be shortened. See
   * `StackTrace.trimmed`.
   *
   * @param t
   *   The `Throwable` whose stack trace to log.
   * @param traceLevel
   *   How to shorten the stack trace.
   */
  def trace(t: => Throwable, traceLevel: Int): Unit = {
    if (traceLevel >= 0)
      write(StackTrace.trimmed(t, traceLevel))
    if (traceLevel <= 2) {
      val ctx = new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)
      for (msg <- suppressedMessage(ctx))
        appendLog(NO_COLOR, "trace", NO_COLOR, msg)
    }
  }

  /**
   * Logs a `ControlEvent` to the log.
   *
   * @param event
   *   The kind of `ControlEvent`.
   * @param message
   *   The message to log.
   */
  def control(event: ControlEvent.Value, message: => String): Unit =
    appendLog(labelColor(Level.Info), Level.Info.toString, BLUE, message)

  /**
   * Appends the message `message` to the to the log at level `level`.
   *
   * @param level
   *   The importance level of the message.
   * @param message
   *   The message to log.
   */
  def appendLog(level: Level.Value, message: => String): Unit = {
    appendLog(labelColor(level), level.toString, NO_COLOR, message)
  }

  /**
   * Select the right color for the label given `level`.
   *
   * @param level
   *   The label to consider to select the color.
   * @return
   *   The color to use to color the label.
   */
  private def labelColor(level: Level.Value): String =
    level match {
      case Level.Error => RED
      case Level.Warn  => YELLOW
      case _           => NO_COLOR
    }

  /**
   * Appends a full message to the log. Each line is prefixed with `[$label]`, written in
   * `labelColor` if formatting is enabled. The lines of the messages are colored with
   * `messageColor` if formatting is enabled.
   *
   * @param labelColor
   *   The color to use to format the label.
   * @param label
   *   The label to prefix each line with. The label is shown between square brackets.
   * @param messageColor
   *   The color to use to format the message.
   * @param message
   *   The message to write.
   */
  private def appendLog(
      labelColor: String,
      label: String,
      messageColor: String,
      message: String
  ): Unit =
    try {
      // according to https://github.com/sbt/sbt/issues/5608, sometimes we get a null message
      if (message == null) ()
      else {
        val len = labelColor.length + label.length + messageColor.length + reset.length * 3
        val builder: StringBuilder = new StringBuilder(len)
        message.linesIterator.foreach { line =>
          builder.ensureCapacity(len + line.length + 4)
          builder.setLength(0)

          def fmted(a: String, b: String) = {
            if (useFormat) builder.append(reset).append(a).append(b).append(reset)
            else builder.append(b)
          }

          if (useFormat) builder.append(reset)
          builder.append('[')
          fmted(labelColor, label)
          builder.append("] ")
          fmted(messageColor, line)
          write(builder.toString)
        }
      }
    } catch { case _: InterruptedException => }

  // success is called by ConsoleLogger.
  private[sbt] def success(message: => String): Unit = {
    appendLog(SUCCESS_LABEL_COLOR, Level.SuccessLabel, SUCCESS_MESSAGE_COLOR, message)
  }

  private def write(msg: String): Unit = {
    // There is no api for removing only colors but not other ansi escape sequences
    // so we do nothing if useFormat is false but ansiCodesSupported is true which is
    // a rare use case but if ansiCodesSupported is true, color codes should work so
    // the output may have unwanted colors but it would still be legible. This should
    // only be relevant if the log message string itself contains ansi escape sequences
    // other than color codes which is very unlikely.
    val toWrite = if ((!ansiCodesSupported || !useFormat) && msg.getBytes.contains(27.toByte)) {
      val (bytes, len) =
        EscHelpers.strip(msg.getBytes, stripAnsi = !ansiCodesSupported, stripColor = !useFormat)
      new String(bytes, 0, len)
    } else msg
    out.println(toWrite)
  }

  private def appendTraceEvent(te: TraceEvent): Unit = {
    val traceLevel = getTrace
    if (traceLevel >= 0) {
      val throwableShowLines: ShowLines[Throwable] =
        ShowLines[Throwable]((t: Throwable) => {
          List(StackTrace.trimmed(t, traceLevel))
        })
      val codec: ShowLines[TraceEvent] =
        ShowLines[TraceEvent]((t: TraceEvent) => {
          throwableShowLines.showLines(t.message)
        })
      codec.showLines(te).toVector foreach { appendLog(Level.Error, _) }
    }
    if (traceLevel <= 2) {
      suppressedMessage(
        new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)
      ) foreach {
        appendLog(Level.Error, _)
      }
    }
  }

  private def appendMessageContent(level: Level.Value, o: AnyRef): Unit = {
    def appendEvent(oe: ObjectEvent[?]): Unit = {
      val contentType = oe.contentType
      contentType match {
        case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent])
        case "sbt.internal.util.ProgressEvent" =>
        case _                                 =>
          LogExchange.stringCodec[AnyRef](contentType) match {
            case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" =>
              codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach { success(_) }
            case Some(codec) =>
              codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach (appendLog(
                level,
                _
              ))
            case _ => appendLog(level, oe.message.toString)
          }
      }
    }

    o match {
      case x: StringEvent    => Vector(x.message) foreach { appendLog(level, _) }
      case x: ObjectEvent[?] => appendEvent(x)
      case _                 => Vector(o.toString) foreach { appendLog(level, _) }
    }
  }
  private[sbt] def appendObjectEvent[T](level: Level.Value, message: => ObjectEvent[T]): Unit =
    appendMessageContent(level, message)

}

final class SuppressedTraceContext(val traceLevel: Int, val useFormat: Boolean)
