///|
/// Python's `latex.escape_tex`: escapes TeX special characters with
/// `\Zxx{}` macros.
pub fn escape_tex(text : String, commandprefix : String) -> String {
  let cp = commandprefix
  let buf = StringBuilder()
  for c in text {
    let name = match c {
      '\\' => "Zbs"
      '{' => "Zob"
      '}' => "Zcb"
      '^' => "Zca"
      '_' => "Zus"
      '&' => "Zam"
      '<' => "Zlt"
      '>' => "Zgt"
      '#' => "Zsh"
      '%' => "Zpc"
      '$' => "Zdl"
      '-' => "Zhy"
      '\'' => "Zsq"
      '"' => "Zdq"
      '~' => "Zti"
      _ => {
        buf.write_char(c)
        continue
      }
    }
    buf.write_string("\\\{cp}\{name}{}")
  }
  buf.to_string()
}

///|
/// Metadata of Python's `LatexFormatter`.
pub let latex_info : FormatterInfo = {
  class_name: "LatexFormatter",
  name: "LaTeX",
  aliases: ["latex", "tex"],
  filenames: ["*.tex"],
  description: "Format tokens as LaTeX code. This needs the `fancyvrb` and `color` standard packages.",
}

///|
/// Python's `'%.2f' % (int(hex, 16) / 255.0)` for one color channel.
fn channel_ratio(hex2 : String) -> String {
  let v = parse_hex(hex2).unwrap_or(0)
  // round(v * 100 / 255) exactly: v / 255 is never a tie at two decimals
  let hundredths = (v * 200 + 255) / 510
  let whole = hundredths / 100
  let frac = hundredths % 100
  "\{whole}." + (if frac < 10 { "0" } else { "" }) + frac.to_string()
}

///|
/// Python's `rgbcolor` in `LatexFormatter._create_stylesheet`.
fn latex_rgbcolor(col : String?) -> String {
  match col {
    Some(c) if c != "" => {
      let part = (i : Int) => channel_ratio(@pystr.slice(c, i, end=i + 2))
      "\{part(0)},\{part(2)},\{part(4)}"
    }
    _ => "1,1,1"
  }
}

///|
/// Python's `LatexFormatter`: LaTeX `fancyvrb` output. Options: `style`,
/// `full`, `title`, `docclass`, `preamble`, `nowrap`, `linenos`,
/// `linenostart`, `linenostep`, `verboptions`, `commandprefix`,
/// `texcomments`, `mathescape`, `escapeinside`, `envname` (and
/// `nobackground`, which Python accepts but ignores).
pub struct LatexFormatter {
  base : BaseOptions
  nowrap : Bool
  docclass : String
  preamble : String
  linenos : Bool
  linenostart : Int
  linenostep : Int
  verboptions : String
  nobackground : Bool
  commandprefix : String
  texcomments : Bool
  mathescape : Bool
  escapeinside : String
  envname : String
  priv ttype2name : Map[@token.TokenType, String]
  priv cmd2def : Map[String, String]
}

///|
/// Creates a LaTeX formatter from `options`.
pub fn LatexFormatter::new(
  options? : @lexer.Options = Map([]),
  style? : @styles.Style,
) -> LatexFormatter raise {
  let base = BaseOptions::new(options, style?)
  let escapeinside = options.get("escapeinside").unwrap_or("")
  let f : LatexFormatter = {
    base,
    nowrap: @lexer.get_bool_opt(options, "nowrap", false),
    docclass: options.get("docclass").unwrap_or("article"),
    preamble: options.get("preamble").unwrap_or(""),
    linenos: @lexer.get_bool_opt(options, "linenos", false),
    linenostart: abs_int_opt(options, "linenostart", 1),
    linenostep: abs_int_opt(options, "linenostep", 1),
    verboptions: options.get("verboptions").unwrap_or(""),
    nobackground: @lexer.get_bool_opt(options, "nobackground", false),
    commandprefix: options.get("commandprefix").unwrap_or("PY"),
    texcomments: @lexer.get_bool_opt(options, "texcomments", false),
    mathescape: @lexer.get_bool_opt(options, "mathescape", false),
    escapeinside: if escapeinside.char_length() == 2 {
      escapeinside
    } else {
      ""
    },
    envname: options.get("envname").unwrap_or("Verbatim"),
    ttype2name: Map([]),
    cmd2def: Map([]),
  }
  f.create_stylesheet()
  f
}

///|
/// Python's `_create_stylesheet`.
fn LatexFormatter::create_stylesheet(self : LatexFormatter) -> Unit {
  self.ttype2name[@token.token] = ""
  let cp = self.commandprefix
  for entry in self.base.style.iter() {
    let (ttype, ndef) = entry
    let name = @token.css_class(ttype)
    let d = StringBuilder()
    if ndef.bold {
      d.write_string("\\let\\$$@bf=\\textbf")
    }
    if ndef.italic {
      d.write_string("\\let\\$$@it=\\textit")
    }
    if ndef.underline {
      d.write_string("\\let\\$$@ul=\\underline")
    }
    if ndef.roman {
      d.write_string("\\let\\$$@ff=\\textrm")
    }
    if ndef.sans {
      d.write_string("\\let\\$$@ff=\\textsf")
    }
    if ndef.mono {
      d.write_string("\\let\\$$@ff=\\textsf")
    }
    if ndef.color is Some(_) {
      d.write_string(
        "\\def\\$$@tc##1{\\textcolor[rgb]{\{latex_rgbcolor(ndef.color)}}{##1}}",
      )
    }
    if ndef.border is Some(_) {
      d.write_string(
        "\\def\\$$@bc##1{{\\setlength{\\fboxsep}{\\string -\\fboxrule}\\fcolorbox[rgb]{\{latex_rgbcolor(ndef.border)}}{\{latex_rgbcolor(ndef.bgcolor)}}{\\strut ##1}}}",
      )
    } else if ndef.bgcolor is Some(_) {
      d.write_string(
        "\\def\\$$@bc##1{{\\setlength{\\fboxsep}{0pt}\\colorbox[rgb]{\{latex_rgbcolor(ndef.bgcolor)}}{\\strut ##1}}}",
      )
    }
    let cmndef = d.to_string()
    if cmndef == "" {
      continue
    }
    self.ttype2name[ttype] = name
    self.cmd2def[name] = cmndef.replace_all(old="$$", new=cp)
  }
}

///|
/// Python's `get_style_defs`: the macro definitions for the current style
/// (the argument is ignored).
pub fn LatexFormatter::get_style_defs(self : LatexFormatter) -> String {
  let cp = self.commandprefix
  let styles = []
  for name, definition in self.cmd2def {
    styles.push("\\@namedef{\{cp}@tok@\{name}}{\{definition}}")
  }
  percent_format(latex_style_template, { "cp": cp, "styles": styles.join("\n") })
}

///|
/// Python's `str.partition(sep)`.
fn partition(s : String, sep : String) -> (String, String, String) {
  match s.find(sep) {
    Some(i) =>
      (
        s.unsafe_substring(start=0, end=i),
        sep,
        s.unsafe_substring(start=i + sep.length(), end=s.length()),
      )
    None => (s, "", "")
  }
}

///|
/// Escapes the text of a comment token according to `texcomments`,
/// `mathescape` and `escapeinside`.
fn LatexFormatter::escape_comment(
  self : LatexFormatter,
  value : String,
) -> String {
  let cp = self.commandprefix
  if self.texcomments {
    // guess the comment starting lexeme and escape it, but nothing else
    let chars = value.to_array()
    let mut k = if chars.length() > 0 { 1 } else { 0 }
    while k < chars.length() && chars[k] == chars[0] {
      k += 1
    }
    let start = String::from_array(chars[0:k])
    let rest = String::from_array(chars[k:])
    escape_tex(start, cp) + rest
  } else if self.mathescape {
    let parts = @pystr.split(value, "$")
    let mut in_math = false
    for i in 0.. String {
  let cp = self.commandprefix
  let buf = StringBuilder()
  if !self.nowrap {
    buf.write_string("\\begin{\{self.envname}}[commandchars=\\\\\\{\\}")
    if self.linenos {
      let start = self.linenostart
      let step = self.linenostep
      buf.write_string(",numbers=left")
      if start != 0 {
        buf.write_string(",firstnumber=\{start}")
      }
      if step != 0 {
        buf.write_string(",stepnumber=\{step}")
      }
    }
    if self.mathescape || self.texcomments || self.escapeinside != "" {
      buf.write_string(
        ",codes={\\catcode`\\$=3\\catcode`\\^=7\\catcode`\\_=8\\relax}",
      )
    }
    if self.verboptions != "" {
      buf.write_string("," + self.verboptions)
    }
    buf.write_string("]\n")
  }
  for tok in tokens {
    let (ttype, value) = tok
    let value = if ttype.is_subtype_of(@token.comment) {
      self.escape_comment(value)
    } else if !ttype.is_subtype_of(@token.escape) {
      escape_tex(value, cp)
    } else {
      value
    }
    let styles = []
    let mut t = ttype
    while t != @token.token {
      styles.push(self.ttype2name.get(t).unwrap_or(@token.css_class(t)))
      t = t.parent().unwrap_or(@token.token)
    }
    styles.rev_in_place()
    let styleval = styles.join("+")
    if styleval != "" {
      let spl = @pystr.split(value, "\n")
      for i in 0..<(spl.length() - 1) {
        if spl[i] != "" {
          buf.write_string("\\\{cp}{\{styleval}}{\{spl[i]}}")
        }
        buf.write_string("\n")
      }
      let last = spl[spl.length() - 1]
      if last != "" {
        buf.write_string("\\\{cp}{\{styleval}}{\{last}}")
      }
    } else {
      buf.write_string(value)
    }
  }
  if !self.nowrap {
    buf.write_string("\\end{\{self.envname}}\n")
  }
  if !self.base.full {
    return buf.to_string()
  }
  let encoding = self.base.encoding.unwrap_or("utf8")
  let encoding = match encoding.replace_all(old="-", new="_") {
    "utf_8" => "utf8"
    "latin_1" | "iso_8859_1" => "latin1"
    _ => encoding
  }
  percent_format(latex_doc_template, {
    "docclass": self.docclass,
    "preamble": self.preamble,
    "title": self.base.title,
    "encoding": encoding,
    "styledefs": self.get_style_defs(),
    "code": buf.to_string(),
  })
}

///|
/// Wraps the LaTeX formatter as a generic `Formatter`.
pub fn LatexFormatter::to_formatter(self : LatexFormatter) -> Formatter {
  Formatter::new(latex_info, self.base, tokens => self.format(tokens), style_defs=_ => {
    self.get_style_defs()
  })
}