///|
/// A foreground/background color of an `EscapeSequence`: an index into the
/// xterm palette, an ANSI color name, or an RGB triple (true color).
priv enum TermColor {
  Index(Int)
  Ansi(String)
  Rgb(Int, Int, Int)
}

///|
/// Python's `terminal256.EscapeSequence`.
priv struct EscapeSequence {
  mut fg : TermColor?
  mut bg : TermColor?
  mut bold : Bool
  mut underline : Bool
  mut italic : Bool
}

///|
fn escape_attrs(attrs : Array[String]) -> String {
  if attrs.length() > 0 {
    "\u{1b}[" + attrs.join(";") + "m"
  } else {
    ""
  }
}

///|
/// The two-digit SGR code of an ANSI color name (`esc[2:4]` in Python).
fn ansi_sgr(name : String) -> String {
  let key = name.replace_all(old="ansi", new="")
  let esc = console_codes.get(key).unwrap_or("")
  if esc.length() >= 4 {
    esc.unsafe_substring(start=2, end=4)
  } else {
    ""
  }
}

///|
/// Python's `EscapeSequence.color_string`.
fn EscapeSequence::color_string(self : EscapeSequence) -> String {
  let attrs = []
  match self.fg {
    Some(Ansi(name)) => {
      let key = name.replace_all(old="ansi", new="")
      if console_codes.get(key).unwrap_or("").contains(";01m") {
        self.bold = true
      }
      attrs.push(ansi_sgr(name))
    }
    Some(Index(i)) => attrs.append(["38", "5", i.to_string()])
    Some(Rgb(_, _, _)) | None => ()
  }
  match self.bg {
    Some(Ansi(name)) => attrs.push((sgr_int(ansi_sgr(name)) + 10).to_string())
    Some(Index(i)) => attrs.append(["48", "5", i.to_string()])
    Some(Rgb(_, _, _)) | None => ()
  }
  if self.bold {
    attrs.push("01")
  }
  if self.underline {
    attrs.push("04")
  }
  if self.italic {
    attrs.push("03")
  }
  escape_attrs(attrs)
}

///|
/// `int(s)` for the SGR codes extracted from `console_codes`.
fn sgr_int(s : String) -> Int {
  py_int(s).unwrap_or(0)
}

///|
/// Python's `EscapeSequence.true_color_string`.
fn EscapeSequence::true_color_string(self : EscapeSequence) -> String {
  let attrs = []
  if self.fg is Some(Rgb(r, g, b)) {
    attrs.append(["38", "2", r.to_string(), g.to_string(), b.to_string()])
  }
  if self.bg is Some(Rgb(r, g, b)) {
    attrs.append(["48", "2", r.to_string(), g.to_string(), b.to_string()])
  }
  if self.bold {
    attrs.push("01")
  }
  if self.underline {
    attrs.push("04")
  }
  if self.italic {
    attrs.push("03")
  }
  escape_attrs(attrs)
}

///|
/// Python's `EscapeSequence.reset_string`.
fn EscapeSequence::reset_string(self : EscapeSequence) -> String {
  let attrs = []
  if self.fg is Some(_) {
    attrs.push("39")
  }
  if self.bg is Some(_) {
    attrs.push("49")
  }
  if self.bold || self.underline || self.italic {
    attrs.push("00")
  }
  escape_attrs(attrs)
}

///|
/// Python's `Terminal256Formatter._build_color_table`: the RGB values of the
/// 256 xterm colors.
let xterm_colors : Array[(Int, Int, Int)] = {
  let colors = [
    (0x00, 0x00, 0x00),
    (0xcd, 0x00, 0x00),
    (0x00, 0xcd, 0x00),
    (0xcd, 0xcd, 0x00),
    (0x00, 0x00, 0xee),
    (0xcd, 0x00, 0xcd),
    (0x00, 0xcd, 0xcd),
    (0xe5, 0xe5, 0xe5),
    (0x7f, 0x7f, 0x7f),
    (0xff, 0x00, 0x00),
    (0x00, 0xff, 0x00),
    (0xff, 0xff, 0x00),
    (0x5c, 0x5c, 0xff),
    (0xff, 0x00, 0xff),
    (0x00, 0xff, 0xff),
    (0xff, 0xff, 0xff),
  ]
  let valuerange = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
  for i in 0..<216 {
    colors.push(
      (valuerange[i / 36 % 6], valuerange[i / 6 % 6], valuerange[i % 6]),
    )
  }
  for i in 0..<24 {
    let v = 8 + i * 10
    colors.push((v, v, v))
  }
  colors
}

///|
/// Python's `_closest_color`: the xterm color nearest to `(r, g, b)`.
fn closest_color(r : Int, g : Int, b : Int) -> Int {
  let mut distance = 257 * 257 * 3
  let mut best = 0
  for i in 0..<256 {
    let (vr, vg, vb) = xterm_colors[i]
    let rd = r - vr
    let gd = g - vg
    let bd = b - vb
    let d = rd * rd + gd * gd + bd * bd
    if d < distance {
      best = i
      distance = d
    }
  }
  best
}

///|
/// Python's `_color_index`.
fn color_index(color : String) -> TermColor {
  if @styles.is_ansicolor(color) {
    return Ansi(color)
  }
  let rgb = parse_hex(color).unwrap_or(0)
  Index(closest_color((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff))
}

///|
/// Python's `TerminalTrueColorFormatter._color_tuple`.
fn color_tuple(color : String) -> TermColor? {
  match parse_hex(color) {
    Some(rgb) => Some(Rgb((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff))
    None => None
  }
}

///|
/// Metadata of Python's `Terminal256Formatter`.
pub let terminal256_info : FormatterInfo = {
  class_name: "Terminal256Formatter",
  name: "Terminal256",
  aliases: ["terminal256", "console256", "256"],
  filenames: [],
  description: "Format tokens with ANSI color sequences, for output in a 256-color terminal or console.  Like in `TerminalFormatter` color sequences are terminated at newlines, so that paging the output works correctly.",
}

///|
/// Metadata of Python's `TerminalTrueColorFormatter`.
pub let terminal_true_color_info : FormatterInfo = {
  class_name: "TerminalTrueColorFormatter",
  name: "TerminalTrueColor",
  aliases: ["terminal16m", "console16m", "16m"],
  filenames: [],
  description: "Format tokens with ANSI color sequences, for output in a true-color terminal or console.  Like in `TerminalFormatter` color sequences are terminated at newlines, so that paging the output works correctly.",
}

///|
/// Shared implementation of the 256-color and true-color formatters.
fn terminal256_impl(
  info : FormatterInfo,
  options : @lexer.Options,
  style : @styles.Style?,
  true_color : Bool,
) -> Formatter raise {
  let base = BaseOptions::new(options, style?)
  let usebold = !options.contains("nobold")
  let useunderline = !options.contains("nounderline")
  let useitalic = !options.contains("noitalic")
  let style_string : Map[@token.TokenType, (String, String)] = Map([])
  for entry in base.style.iter() {
    let (ttype, ndef) = entry
    let esc : EscapeSequence = {
      fg: None,
      bg: None,
      bold: false,
      underline: false,
      italic: false,
    }
    if true_color {
      if ndef.color is Some(c) {
        esc.fg = color_tuple(c)
      }
      if ndef.bgcolor is Some(c) {
        esc.bg = color_tuple(c)
      }
    } else {
      match (ndef.ansicolor, ndef.color) {
        (Some(c), _) | (None, Some(c)) => esc.fg = Some(color_index(c))
        _ => ()
      }
      match (ndef.bgansicolor, ndef.bgcolor) {
        (Some(c), _) | (None, Some(c)) => esc.bg = Some(color_index(c))
        _ => ()
      }
    }
    if usebold && ndef.bold {
      esc.bold = true
    }
    if useunderline && ndef.underline {
      esc.underline = true
    }
    if useitalic && ndef.italic {
      esc.italic = true
    }
    let on = if true_color {
      esc.true_color_string()
    } else {
      esc.color_string()
    }
    style_string[ttype] = (on, esc.reset_string())
  }
  let linenos = truthy_opt(options, "linenos")
  let lineno = Ref(0)
  let write_lineno = (buf : StringBuilder) => {
    lineno.val += 1
    buf.write_string(if lineno.val != 1 { "\n" } else { "" })
    buf.write_string(@pystr.rjust(lineno.val.to_string(), 4, fill='0'))
    buf.write_string(": ")
  }
  Formatter::new(info, base, tokens => {
    let buf = StringBuilder()
    if linenos {
      write_lineno(buf)
    }
    for tok in tokens {
      let (ttype, value) = tok
      // Python stops at the root: `Token` is an empty (false) tuple
      let mut t = ttype
      let mut found = None
      while t != @token.token {
        match style_string.get(t) {
          Some(s) => {
            found = Some(s)
            break
          }
          None => t = t.parent().unwrap_or(@token.token)
        }
      }
      match found {
        Some((on, off)) => {
          let spl = @pystr.split(value, "\n")
          for i in 0..<(spl.length() - 1) {
            let line = spl[i]
            if line != "" {
              buf.write_string(on + line + off)
            }
            if linenos {
              write_lineno(buf)
            } else {
              buf.write_string("\n")
            }
          }
          let last = spl[spl.length() - 1]
          if last != "" {
            buf.write_string(on + last + off)
          }
        }
        None => buf.write_string(value)
      }
    }
    if linenos {
      buf.write_string("\n")
    }
    buf.to_string()
  })
}

///|
/// Python's `Terminal256Formatter` (options `style`, `linenos`, and the
/// presence of `nobold`/`nounderline`/`noitalic`).
pub fn terminal256_formatter(
  options? : @lexer.Options = Map([]),
  style? : @styles.Style,
) -> Formatter raise {
  terminal256_impl(terminal256_info, options, style, false)
}

///|
/// Python's `TerminalTrueColorFormatter`.
pub fn terminal_true_color_formatter(
  options? : @lexer.Options = Map([]),
  style? : @styles.Style,
) -> Formatter raise {
  terminal256_impl(terminal_true_color_info, options, style, true)
}