///|
/// Metadata of Python's `RtfFormatter`.
pub let rtf_info : FormatterInfo = {
  class_name: "RtfFormatter",
  name: "RTF",
  aliases: ["rtf"],
  filenames: ["*.rtf"],
  description: "Format tokens as RTF markup. This formatter automatically outputs full RTF documents with color information and other useful stuff. Perfect for Copy and Paste into Microsoft(R) Word(R) documents.",
}

///|
/// Python's `RtfFormatter._escape`.
fn rtf_escape(text : String) -> String {
  text
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="{", new="\\{")
  .replace_all(old="}", new="\\}")
}

///|
/// Python's `RtfFormatter._escape_text`: escapes RTF specials, writes
/// non-ASCII characters as `{\uN}` (surrogate pairs above U+FFFF) and
/// newlines as `\par`.
fn rtf_escape_text(text : String) -> String {
  if text == "" {
    return ""
  }
  let buf = StringBuilder()
  for c in rtf_escape(text) {
    let cn = c.to_int()
    if cn < 0x80 {
      buf.write_char(c)
    } else if cn < 0x10000 {
      buf.write_string("{\\u\{cn}}")
    } else {
      let hi = 0xd7c0 + (cn >> 10)
      let lo = 0xdc00 + (cn & 0x3ff)
      buf.write_string("{\\u\{hi}}{\\u\{lo}}")
    }
  }
  buf.to_string().replace_all(old="\n", new="\\par")
}

///|
/// Python's `RtfFormatter.hex_to_rtf_color`.
fn hex_to_rtf_color(color : String) -> String raise FormatterError {
  let c = if color.has_prefix("#") { @pystr.slice(color, 1) } else { color }
  let channel = (i : Int) => {
    let part = @pystr.slice(c, i, end=i + 2)
    match parse_hex(part) {
      Some(v) => v
      None =>
        raise FormatterError(
          "ValueError: invalid literal for int() with base 16: \{@pystr.repr(part)}",
        )
    }
  }
  "\\red\{channel(0)}\\green\{channel(2)}\\blue\{channel(4)};"
}

///|
/// Python's `_split_tokens_on_newlines`.
fn split_tokens_on_newlines(tokens : Tokens) -> Tokens {
  let out = []
  for tok in tokens {
    let (ttype, value) = tok
    if value == "\n" || !value.contains("\n") {
      out.push(tok)
    } else {
      let lines = @pystr.split(value, "\n")
      for i in 0..<(lines.length() - 1) {
        out.push((ttype, lines[i] + "\n"))
      }
      if lines[lines.length() - 1] != "" {
        out.push((ttype, lines[lines.length() - 1]))
      }
    }
  }
  out
}

///|
/// Python's `RtfFormatter`. Options: `style`, `fontface`, `fontsize`,
/// `linenos`, `lineno_fontsize`, `lineno_padding`, `linenostart`,
/// `linenostep`, `lineno_color`, `hl_lines`, `hl_color`, `hl_linenostart`.
pub fn rtf_formatter(
  options? : @lexer.Options = Map([]),
  style? : @styles.Style,
) -> Formatter raise {
  let base = BaseOptions::new(options, style?)
  let st = base.style
  let fontface = options.get("fontface").unwrap_or("")
  let fontsize = @lexer.get_int_opt(options, "fontsize", 0)
  let linenos = @lexer.get_bool_opt(options, "linenos", false)
  let lineno_fontsize = @lexer.get_int_opt(options, "lineno_fontsize", fontsize)
  let lineno_padding = @lexer.get_int_opt(options, "lineno_padding", 2)
  let linenostart = abs_int_opt(options, "linenostart", 1)
  let linenostep = abs_int_opt(options, "linenostep", 1)
  let hl_linenostart = @lexer.get_bool_opt(options, "hl_linenostart", false)
  let hl_color = match options.get("hl_color") {
    Some(c) if c != "" => c
    _ => st.highlight_color.unwrap_or("None")
  }
  let hl_lines = []
  for s in @lexer.get_list_opt(options, "hl_lines", []) {
    match py_int(s) {
      Some(n) =>
        hl_lines.push(if hl_linenostart { n - linenostart + 1 } else { n })
      None => ()
    }
  }
  let lineno_color = match options.get("lineno_color") {
    Some(c) if c != "" => c
    _ =>
      if st.line_number_color == "inherit" {
        @styles.ansimap["ansibrightblack"]
      } else {
        st.line_number_color
      }
  }
  // Python's `_create_color_mapping`
  let color_mapping : Map[String, Int] = Map([])
  let mut offset = 1
  if linenos {
    color_mapping[lineno_color] = offset
    offset += 1
  }
  if hl_lines.length() > 0 {
    color_mapping[hl_color] = offset
    offset += 1
  }
  for entry in st.iter() {
    let ndef = entry.1
    for color in [ndef.color, ndef.bgcolor, ndef.border] {
      match color {
        Some(c) if !color_mapping.contains(c) => {
          color_mapping[c] = offset
          offset += 1
        }
        _ => ()
      }
    }
  }
  let padding = " ".repeat(if lineno_padding > 0 { lineno_padding } else { 0 })
  let lineno_template = (s : String) => {
    if lineno_fontsize != fontsize {
      "{\\fs\{lineno_fontsize} \\cf\{color_mapping[lineno_color]} \{s}\{padding}}"
    } else {
      "{\\cf\{color_mapping[lineno_color]} \{s}\{padding}}"
    }
  }
  Formatter::new(rtf_info, base, tokens => {
    let buf = StringBuilder()
    // Python's `_rtf_header`
    let face = if fontface != "" { " " + rtf_escape(fontface) } else { "" }
    buf.write_string(
      "{\\rtf1\\ansi\\uc0\\deff0{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0\{face};}}\n",
    )
    buf.write_string("{\\colortbl;\n")
    for color, _ in color_mapping {
      buf.write_string(hex_to_rtf_color(color) + "\n")
    }
    buf.write_string("}\n")
    buf.write_string("\\f0\\sa0\n")
    if fontsize != 0 {
      buf.write_string("\\fs\{fontsize}\n")
    }
    buf.write_string("\\dntblnsbdb\n")
    let tokens = split_tokens_on_newlines(tokens)
    let mut linenos_width = 0
    if linenos {
      let mut line_count = 0
      for tok in tokens {
        if tok.1.has_suffix("\n") {
          line_count += 1
        }
      }
      linenos_width = (line_count + linenostart - 1).to_string().char_length()
    }
    let mut lineno = 1
    let mut start_new_line = true
    for tok in tokens {
      let (ttype, value) = tok
      if start_new_line && hl_lines.contains(lineno) {
        buf.write_string("{\\highlight\{color_mapping[hl_color]} ")
      }
      if start_new_line && linenos {
        let lineno_str = if py_mod(lineno - linenostart + 1, linenostep) == 0 {
          @pystr.rjust((lineno + linenostart - 1).to_string(), linenos_width)
        } else {
          " ".repeat(linenos_width)
        }
        buf.write_string(lineno_template(lineno_str))
      }
      let mut t = ttype
      while !st.styles_token(t) {
        match t.parent() {
          Some(p) if p != @token.token => t = p
          _ => break
        }
      }
      let style = st.style_for_token(t) catch {
        _ => raise FormatterError("KeyError: \{t}")
      }
      let start = StringBuilder()
      if style.bgcolor is Some(c) {
        start.write_string("\\cb\{color_mapping[c]}")
      }
      if style.color is Some(c) {
        start.write_string("\\cf\{color_mapping[c]}")
      }
      if style.bold {
        start.write_string("\\b")
      }
      if style.italic {
        start.write_string("\\i")
      }
      if style.underline {
        start.write_string("\\ul")
      }
      if style.border is Some(c) {
        start.write_string("\\chbrdr\\chcfpat\{color_mapping[c]}")
      }
      let start = start.to_string()
      if start != "" {
        buf.write_string("{\{start} ")
      }
      buf.write_string(rtf_escape_text(value))
      if start != "" {
        buf.write_string("}")
      }
      start_new_line = false
      if value.has_suffix("\n") {
        if hl_lines.contains(lineno) {
          buf.write_string("}")
        }
        buf.write_string("\n")
        start_new_line = true
        lineno += 1
      }
    }
    buf.write_string("}\n")
    buf.to_string()
  })
}