///|
/// Named ANSI color used by terminal themes.
pub(all) enum Color {
  Default
  Black
  Red
  Green
  Yellow
  Blue
  Magenta
  Cyan
  White
  BrightBlack
  BrightRed
  BrightYellow
  BrightBlue
  BrightCyan
  BrightWhite
} derive(Eq, Debug)

///|
fn Color::ansi(self : Color) -> Int {
  match self {
    Default => 39
    Black => 30
    Red => 31
    Green => 32
    Yellow => 33
    Blue => 34
    Magenta => 35
    Cyan => 36
    White => 37
    BrightBlack => 90
    BrightRed => 91
    BrightYellow => 93
    BrightBlue => 94
    BrightCyan => 96
    BrightWhite => 97
  }
}

///|
/// Semantic terminal colors for a diagnostic report.
pub struct Theme {
  error : Color
  warning : Color
  advice : Color
  line_number : Color
  primary : Color
  secondary : Color
  note : Color
  help : Color
  bold_header : Bool
} derive(Eq, Debug)

///|
/// A legible theme for dark terminal backgrounds.
pub fn Theme::dark() -> Theme {
  {
    error: BrightRed,
    warning: BrightYellow,
    advice: BrightCyan,
    line_number: BrightBlue,
    primary: BrightRed,
    secondary: BrightCyan,
    note: BrightWhite,
    help: Green,
    bold_header: true,
  }
}

///|
/// A lower-intensity theme for light terminal backgrounds.
pub fn Theme::light() -> Theme {
  {
    error: Red,
    warning: Yellow,
    advice: Blue,
    line_number: Blue,
    primary: Red,
    secondary: Cyan,
    note: Black,
    help: Green,
    bold_header: true,
  }
}

///|
pub fn Theme::severity(self : Theme, severity : Severity) -> Color {
  match severity {
    Error => self.error
    Warning => self.warning
    Advice => self.advice
  }
}

///|
fn ansi_start(color : Color, bold : Bool) -> String {
  if bold {
    "\u{001b}[1;\{color.ansi()}m"
  } else {
    "\u{001b}[\{color.ansi()}m"
  }
}

///|
fn ansi_wrap(text : String, color : Color, bold : Bool) -> String {
  "\{ansi_start(color, bold)}\{text}\u{001b}[0m"
}

///|
fn colorize_report_line(
  line : String,
  severity : Severity,
  theme : Theme,
) -> String {
  if line.has_prefix("error") ||
    line.has_prefix("warning") ||
    line.has_prefix("advice") {
    ansi_wrap(line, theme.severity(severity), theme.bold_header)
  } else if line.contains(" | ") && line.contains("^") {
    ansi_wrap(line, theme.primary, false)
  } else if line.contains(" | ") && line.contains("-") {
    ansi_wrap(line, theme.secondary, false)
  } else if line.contains("= help:") {
    ansi_wrap(line, theme.help, false)
  } else if line.contains("= note:") {
    ansi_wrap(line, theme.note, false)
  } else if line.contains(" |") || line.contains(" --> ") {
    ansi_wrap(line, theme.line_number, false)
  } else {
    line
  }
}

///|
/// Adds ANSI color to a plain report. When `enabled` is false, the output is
/// byte-for-byte identical to `render`.
pub fn render_ansi(
  sources : SourceMap,
  diagnostic : Diagnostic,
  enabled? : Bool = true,
  theme? : Theme = Theme::dark(),
  config? : RenderConfig = RenderConfig::default(),
) -> String {
  let plain = render(sources, diagnostic, config~)
  if !enabled {
    return plain
  }
  let output = StringBuilder::new()
  let lines = plain.split("\n")
  for index, line in lines {
    if index > 0 {
      output.write_char('\n')
    }
    output.write_string(
      colorize_report_line(line.to_owned(), diagnostic.severity(), theme),
    )
  }
  output.to_string()
}

///|
/// Removes common Select Graphic Rendition sequences emitted by MoonReport.
/// This is useful for snapshot tests and log sanitization.
pub fn strip_ansi(text : String) -> String {
  let bytes = @utf8.encode(text)
  let output = StringBuilder::new()
  let mut offset = 0
  while offset < bytes.length() {
    if bytes[offset] == 0x1b &&
      offset + 1 < bytes.length() &&
      bytes[offset + 1] == 0x5b {
      offset += 2
      while offset < bytes.length() && bytes[offset] != 0x6d {
        offset += 1
      }
      if offset < bytes.length() {
        offset += 1
      }
    } else {
      let count = utf8_sequence_length(bytes[offset])
      let end = if offset + count <= bytes.length() {
        offset + count
      } else {
        offset + 1
      }
      output.write_string(@utf8.decode_lossy(bytes[offset:end]))
      offset = end
    }
  }
  output.to_string()
}