///|
/// Public configuration record for the LaTeX printer.
///
/// - Does: Stores printer options shared by `latex` and `latex_with_settings`.
/// - Input: Usually built with `latex_settings(...)`.
/// - Returns: A record carrying mode, fraction, trig, multiplication, root,
///   imaginary-unit, differential-operator, and symbol-name options.
/// - Limits: This config only affects the LaTeX printer; it does not change
///   `pretty_string` or structural `Debug` output.
///
/// Current Limits:
/// - `symprint` currently exposes plain-text and LaTeX front doors only.
/// - some advanced expression families still fall back to generic function-style
///   notation.
pub struct LatexSettings {
  mode : String
  full_prec : Bool
  fold_frac_powers : Bool
  fold_func_brackets : Bool
  fold_short_frac : Bool
  inv_trig_style : String
  itex : Bool
  ln_notation : Bool
  long_frac_ratio : Int?
  mul_symbol : String?
  mul_symbol_latex : String
  mul_symbol_latex_numbers : String
  root_notation : Bool
  imaginary_unit : String
  imaginary_unit_latex : String
  diff_operator : String
  diff_operator_latex : String
  parenthesize_super : Bool
  symbol_names : Map[String, String]
}

///|
fn mul_symbol_latex(value : String?) -> String {
  match value {
    None => " "
    Some("dot") => " \\cdot "
    Some("times") => " \\times "
    Some("ldot") => " \\\\,.\\\\, "
    Some(other) => other
  }
}

///|
fn mul_symbol_latex_numbers(value : String?) -> String {
  match value {
    None => " \\cdot "
    Some("dot") => " \\cdot "
    Some("times") => " \\times "
    Some("ldot") => " \\\\,.\\\\, "
    Some(other) => other
  }
}

///|
fn imaginary_unit_latex(value : String) -> String {
  match value {
    "i" => "i"
    "ri" => "\\mathrm{i}"
    "ti" => "\\text{i}"
    "j" => "j"
    "rj" => "\\mathrm{j}"
    "tj" => "\\text{j}"
    other => other
  }
}

///|
fn diff_operator_latex(value : String) -> String {
  match value {
    "d" => "d"
    "rd" => "\\mathrm{d}"
    "td" => "\\text{d}"
    other => other
  }
}

///|
/// Build a reusable `LatexSettings` record.
///
/// - Does: Collects LaTeX printer keyword options into a single settings value.
/// - Input: Optional keyword arguments such as `mode`, `mul_symbol`,
///   `inv_trig_style`, `imaginary_unit`, and `symbol_names`.
/// - Returns: A `LatexSettings` record.
/// - Limits: String-valued knobs are permissive; unrecognized values are carried
///   through or fall back to generic rendering instead of being rejected.
pub fn latex_settings(
  mode? : String = "plain",
  full_prec? : Bool = false,
  fold_frac_powers? : Bool = false,
  fold_func_brackets? : Bool = false,
  fold_short_frac? : Bool? = None,
  inv_trig_style? : String = "abbreviated",
  itex? : Bool = false,
  ln_notation? : Bool = false,
  long_frac_ratio? : Int? = None,
  mul_symbol? : String? = None,
  root_notation? : Bool = true,
  imaginary_unit? : String = "i",
  diff_operator? : String = "d",
  parenthesize_super? : Bool = true,
  symbol_names? : Map[String, String] = {},
) -> LatexSettings {
  let resolved_fold_short_frac = match fold_short_frac {
    Some(value) => value
    None => mode == "inline"
  }
  LatexSettings::{
    mode,
    full_prec,
    fold_frac_powers,
    fold_func_brackets,
    fold_short_frac: resolved_fold_short_frac,
    inv_trig_style,
    itex,
    ln_notation,
    long_frac_ratio,
    mul_symbol,
    mul_symbol_latex: mul_symbol_latex(mul_symbol),
    mul_symbol_latex_numbers: mul_symbol_latex_numbers(mul_symbol),
    root_notation,
    imaginary_unit,
    imaginary_unit_latex: imaginary_unit_latex(imaginary_unit),
    diff_operator,
    diff_operator_latex: diff_operator_latex(diff_operator),
    parenthesize_super,
    symbol_names,
  }
}

///|
/// Render an expression as LaTeX using an explicit settings record.
///
/// - Does: Prints LaTeX from an expression and a prepared `LatexSettings`.
/// - Input: A `@symcore.Expr` and a `LatexSettings`.
/// - Returns: A `String`.
/// - Limits: It shares the same rendering coverage and fallback behavior as
///   `latex`.
pub fn latex_with_settings(expr : Expr, settings : LatexSettings) -> String {
  let body = latex_format_expr(expr, 0, settings)
  match settings.mode {
    "plain" => body
    "inline" => "$\{body}$"
    _ if settings.itex => "$$\{body}$$"
    "equation" => "\\begin{equation}\{body}\\end{equation}"
    "equation*" => "\\begin{equation*}\{body}\\end{equation*}"
    _ => body
  }
}

///|
/// Render an expression as LaTeX with convenience keyword arguments.
///
/// - Does: Prints LaTeX directly from an expression and printer options.
/// - Input: A `@symcore.Expr` plus optional printer settings.
/// - Returns: A `String`.
/// - Limits: Unknown mode strings fall back to the bare LaTeX body rather than
///   raising an error.
///
/// ```mbt check
/// test "symprint latex renders with inline mode and custom multiplication" {
///   let x = @symcore.Expr::Symbol("x")
///   let expr = @symcore.mul([@symcore.int(2), @symcore.pow(x, @symcore.int(2))])
///   inspect(
///     latex(expr, mode="inline", mul_symbol=Some("times")),
///     content="$2 \\times x^{2}$",
///   )
/// }
/// ```
pub fn latex(
  expr : Expr,
  mode? : String = "plain",
  full_prec? : Bool = false,
  fold_frac_powers? : Bool = false,
  fold_func_brackets? : Bool = false,
  fold_short_frac? : Bool? = None,
  inv_trig_style? : String = "abbreviated",
  itex? : Bool = false,
  ln_notation? : Bool = false,
  long_frac_ratio? : Int? = None,
  mul_symbol? : String? = None,
  root_notation? : Bool = true,
  imaginary_unit? : String = "i",
  diff_operator? : String = "d",
  parenthesize_super? : Bool = true,
  symbol_names? : Map[String, String] = {},
) -> String {
  latex_with_settings(
    expr,
    latex_settings(
      mode~,
      full_prec~,
      fold_frac_powers~,
      fold_func_brackets~,
      fold_short_frac~,
      inv_trig_style~,
      itex~,
      ln_notation~,
      long_frac_ratio~,
      mul_symbol~,
      root_notation~,
      imaginary_unit~,
      diff_operator~,
      parenthesize_super~,
      symbol_names~,
    ),
  )
}

///|
fn latex_parens(s : String) -> String {
  "\\left(\{s}\\right)"
}

///|
fn latex_parens_lspace(s : String) -> String {
  "\\left( \{s}\\right)"
}

///|
fn char_at(src : String, index : Int) -> Char? {
  src.get_char(index)
}

///|
fn first_index_of_char(src : String, target : Char, start? : Int = 0) -> Int? {
  let mut i = start
  while i < src.length() {
    match char_at(src, i) {
      Some(ch) if ch == target => return Some(i)
      _ => i += 1
    }
  }
  None
}

///|
fn contains_caret(src : String) -> Bool {
  first_index_of_char(src, '^') is Some(_)
}

///|
fn split_super_sub_name(
  text : String,
) -> (String, Array[String], Array[String]) {
  if text.length() == 0 || text.contains("{") {
    return (text, [], [])
  }
  let mut pos = 0
  let mut name : String? = None
  let supers : Array[String] = []
  let subs : Array[String] = []
  while pos < text.length() {
    let mut start = pos + 1
    let has_double_underscore = match
      (char_at(text, pos), char_at(text, pos + 1)) {
      (Some('_'), Some('_')) => true
      _ => false
    }
    if has_double_underscore {
      start += 1
    }
    let mut pos_hat = text.length()
    let mut pos_usc = text.length()
    let mut i = start
    while i < text.length() {
      match char_at(text, i) {
        Some('^') if pos_hat == text.length() => pos_hat = i
        Some('_') if pos_usc == text.length() => pos_usc = i
        _ => ()
      }
      if pos_hat != text.length() && pos_usc != text.length() {
        break
      }
      i += 1
    }
    let pos_next = if pos_hat < pos_usc { pos_hat } else { pos_usc }
    let part = text[pos:pos_next].to_owned()
    pos = pos_next
    match name {
      None => name = Some(part)
      Some(_) =>
        if part.has_prefix("^") {
          supers.push(part[1:part.length()].to_owned())
        } else if part.has_prefix("__") {
          supers.push(part[2:part.length()].to_owned())
        } else if part.has_prefix("_") {
          subs.push(part[1:part.length()].to_owned())
        } else {
          return (text, [], [])
        }
    }
  }
  let mut base = match name {
    Some(value) => value
    None => text
  }
  if base.length() == 0 {
    return (base, supers, subs)
  }
  let mut digit_start = base.length()
  let mut saw_digit = false
  let mut i = base.length() - 1
  while base.length() > 0 && i >= 0 {
    match char_at(base, i) {
      Some(ch) if ch.is_ascii_digit() => {
        saw_digit = true
        digit_start = i
      }
      _ => break
    }
    if i == 0 {
      break
    }
    i -= 1
  }
  if saw_digit && digit_start > 0 {
    let mut alpha_prefix = true
    for ch in base[:digit_start] {
      if !ch.is_ascii_alphabetic() {
        alpha_prefix = false
        break
      }
    }
    if alpha_prefix {
      let with_digit_sub : Array[String] = [
        base[digit_start:base.length()].to_owned(),
      ]
      for item in subs {
        with_digit_sub.push(item)
      }
      base = base[:digit_start].to_owned()
      return (base, supers, with_digit_sub)
    }
  }
  (base, supers, subs)
}

///|
fn translate_symbol_base(name : String) -> String? {
  match name {
    "Alpha" => Some("\\mathrm{A}")
    "Beta" => Some("\\mathrm{B}")
    "Gamma" => Some("\\Gamma")
    "Delta" => Some("\\Delta")
    "Epsilon" => Some("\\mathrm{E}")
    "Zeta" => Some("\\mathrm{Z}")
    "Eta" => Some("\\mathrm{H}")
    "Theta" => Some("\\Theta")
    "Iota" => Some("\\mathrm{I}")
    "Kappa" => Some("\\mathrm{K}")
    "Lambda" => Some("\\Lambda")
    "Mu" => Some("\\mathrm{M}")
    "Nu" => Some("\\mathrm{N}")
    "Xi" => Some("\\Xi")
    "omicron" => Some("o")
    "Omicron" => Some("\\mathrm{O}")
    "Pi" => Some("\\Pi")
    "Rho" => Some("\\mathrm{P}")
    "Sigma" => Some("\\Sigma")
    "Tau" => Some("\\mathrm{T}")
    "Upsilon" => Some("\\Upsilon")
    "Phi" => Some("\\Phi")
    "Chi" => Some("\\mathrm{X}")
    "Psi" => Some("\\Psi")
    "Omega" => Some("\\Omega")
    "alpha" => Some("\\alpha")
    "beta" => Some("\\beta")
    "gamma" => Some("\\gamma")
    "delta" => Some("\\delta")
    "epsilon" => Some("\\epsilon")
    "zeta" => Some("\\zeta")
    "eta" => Some("\\eta")
    "theta" => Some("\\theta")
    "iota" => Some("\\iota")
    "kappa" => Some("\\kappa")
    "lambda" | "lamda" => Some("\\lambda")
    "mu" => Some("\\mu")
    "nu" => Some("\\nu")
    "xi" => Some("\\xi")
    "pi" => Some("\\pi")
    "rho" => Some("\\rho")
    "sigma" => Some("\\sigma")
    "tau" => Some("\\tau")
    "upsilon" => Some("\\upsilon")
    "phi" => Some("\\phi")
    "chi" | "khi" => Some("\\chi")
    "psi" => Some("\\psi")
    "omega" => Some("\\omega")
    "varepsilon" => Some("\\varepsilon")
    "varkappa" => Some("\\varkappa")
    "varphi" => Some("\\varphi")
    "varpi" => Some("\\varpi")
    "varrho" => Some("\\varrho")
    "varsigma" => Some("\\varsigma")
    "vartheta" => Some("\\vartheta")
    "aleph" => Some("\\aleph")
    "beth" => Some("\\beth")
    "daleth" => Some("\\daleth")
    "gimel" => Some("\\gimel")
    "ell" => Some("\\ell")
    "eth" => Some("\\eth")
    "hbar" => Some("\\hbar")
    "hslash" => Some("\\hslash")
    "mho" => Some("\\mho")
    "wp" => Some("\\wp")
    _ => None
  }
}

///|
fn translate_symbol_name(name : String) -> String {
  match translate_symbol_base(name) {
    Some(tex) => tex
    None => {
      let lower = name.to_lower()
      if lower.has_suffix("ddddot") && name.length() > 6 {
        return "\\ddddot{\{translate_symbol_name(name[:name.length() - 6].to_owned())}}"
      }
      if lower.has_suffix("dddot") && name.length() > 5 {
        return "\\dddot{\{translate_symbol_name(name[:name.length() - 5].to_owned())}}"
      }
      if lower.has_suffix("ddot") && name.length() > 4 {
        return "\\ddot{\{translate_symbol_name(name[:name.length() - 4].to_owned())}}"
      }
      if lower.has_suffix("dot") && name.length() > 3 {
        return "\\dot{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}"
      }
      if lower.has_suffix("tilde") && name.length() > 5 {
        return "\\tilde{\{translate_symbol_name(name[:name.length() - 5].to_owned())}}"
      }
      if lower.has_suffix("hat") && name.length() > 3 {
        return "\\hat{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}"
      }
      if lower.has_suffix("bar") && name.length() > 3 {
        return "\\bar{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}"
      }
      if lower.has_suffix("vec") && name.length() > 3 {
        return "\\vec{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}"
      }
      if lower.has_suffix("prime") && name.length() > 5 {
        return "{\{translate_symbol_name(name[:name.length() - 5].to_owned())}}'"
      }
      if lower.has_suffix("prm") && name.length() > 3 {
        return "{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}'"
      }
      if lower.has_suffix("bold") && name.length() > 4 {
        return "\\boldsymbol{\{translate_symbol_name(name[:name.length() - 4].to_owned())}}"
      }
      if lower.has_suffix("bm") && name.length() > 2 {
        return "\\boldsymbol{\{translate_symbol_name(name[:name.length() - 2].to_owned())}}"
      }
      if lower.has_suffix("cal") && name.length() > 3 {
        return "\\mathcal{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}"
      }
      if lower.has_suffix("scr") && name.length() > 3 {
        return "\\mathscr{\{translate_symbol_name(name[:name.length() - 3].to_owned())}}"
      }
      if lower.has_suffix("frak") && name.length() > 4 {
        return "\\mathfrak{\{translate_symbol_name(name[:name.length() - 4].to_owned())}}"
      }
      name
    }
  }
}

///|
fn deal_with_super_sub(name : String, style? : String = "plain") -> String {
  let (base_name, supers, subs) = split_super_sub_name(name)
  let mut base_tex = translate_symbol_name(base_name)
  if style == "bold" {
    base_tex = "\\mathbf{\{base_tex}}"
  }
  let mut out = base_tex
  if !supers.is_empty() {
    let items : Array[String] = []
    for item in supers {
      items.push(translate_symbol_name(item))
    }
    let supers_tex = items.join(" ")
    out += "^{\{supers_tex}}"
  }
  if !subs.is_empty() {
    let items : Array[String] = []
    for item in subs {
      items.push(translate_symbol_name(item))
    }
    let subs_tex = items.join(" ")
    out += "_{\{subs_tex}}"
  }
  out
}

///|
fn is_accepted_latex_function(name : String) -> Bool {
  match name {
    "arcsin"
    | "arccos"
    | "arctan"
    | "sin"
    | "cos"
    | "tan"
    | "sinh"
    | "cosh"
    | "tanh"
    | "sqrt"
    | "ln"
    | "log"
    | "sec"
    | "csc"
    | "cot"
    | "coth"
    | "re"
    | "im"
    | "frac"
    | "root"
    | "arg" => true
    _ => false
  }
}

///|
fn latex_hprint_function(name : String) -> String {
  let rendered = deal_with_super_sub(name)
  if is_accepted_latex_function(name) {
    return "\\\{name}"
  }
  let superscript_idx = first_index_of_char(rendered, '^')
  let subscript_idx = first_index_of_char(rendered, '_')
  let boundary = match (superscript_idx, subscript_idx) {
    (Some(a), Some(b)) => if a < b { Some(a) } else { Some(b) }
    (Some(a), None) => Some(a)
    (None, Some(b)) => Some(b)
    (None, None) => None
  }
  if rendered.length() == 1 || rendered.has_prefix("\\") || boundary == Some(1) {
    rendered
  } else {
    match boundary {
      Some(idx) =>
        "\\operatorname{\{rendered[:idx].to_owned()}}\{rendered[idx:rendered.length()].to_owned()}"
      None => "\\operatorname{\{rendered}}"
    }
  }
}

///|
fn latex_parenthesize_super(s : String, settings : LatexSettings) -> String {
  if contains_caret(s) {
    if settings.parenthesize_super {
      latex_parens(s)
    } else {
      "{\{s}}"
    }
  } else {
    s
  }
}

///|
fn latex_exact_number(
  num : BigInt,
  den : BigInt,
  settings : LatexSettings,
) -> String {
  if den.compare(1N) == 0 {
    num.to_string()
  } else if num.compare(0N) < 0 {
    if settings.fold_short_frac {
      "- \{(-num).to_string()} / \{den.to_string()}"
    } else {
      "- \\frac{\{(-num).to_string()}}{\{den.to_string()}}"
    }
  } else if settings.fold_short_frac {
    "\{num.to_string()} / \{den.to_string()}"
  } else {
    "\\frac{\{num.to_string()}}{\{den.to_string()}}"
  }
}

///|
fn latex_number(expr : Expr, settings : LatexSettings) -> String {
  match @symcore.exact_number_num_den(expr) {
    Some((num, den)) => latex_exact_number(num, den, settings)
    None => abort("latex_number expects exact number")
  }
}

///|
fn latex_float(value : @symcore.Float, settings : LatexSettings) -> String {
  if settings.full_prec {
    value.format(dps=@symnum.prec_to_dps(value.precision()))
  } else {
    value.to_string()
  }
}

///|
fn latex_complex_float(
  value : @symcore.ComplexFloat,
  settings : LatexSettings,
) -> String {
  let real = value.real_part()
  let imag = value.imag_part()
  let real_zero = @symnum.is_zero(real.to_mpf())
  let imag_zero = @symnum.is_zero(imag.to_mpf())
  let imag_negative = @symnum.mpf_sign(imag.to_mpf()) < 0
  if imag_zero {
    return latex_float(real, settings)
  }
  let imag_tex_raw = latex_float(imag, settings)
  let imag_tex = if imag_negative && imag_tex_raw.has_prefix("-") {
    imag_tex_raw[1:imag_tex_raw.length()].to_owned()
  } else {
    imag_tex_raw
  }
  let imag_unit = settings.imaginary_unit_latex
  if real_zero {
    if imag_negative {
      "- \{imag_tex} \{imag_unit}"
    } else {
      "\{imag_tex} \{imag_unit}"
    }
  } else if imag_negative {
    "\{latex_float(real, settings)} - \{imag_tex} \{imag_unit}"
  } else {
    "\{latex_float(real, settings)} + \{imag_tex} \{imag_unit}"
  }
}

///|
fn latex_number_symbol(
  kind : @symcore.NumberSymbolKind,
  settings : LatexSettings,
) -> String {
  match kind {
    @symcore.NumberSymbolKind::ImaginaryUnit => settings.imaginary_unit_latex
    @symcore.NumberSymbolKind::Pi => "\\pi"
    @symcore.NumberSymbolKind::Exp1 => "e"
    @symcore.NumberSymbolKind::EulerGamma => "\\gamma"
    @symcore.NumberSymbolKind::GoldenRatio => "\\phi"
    @symcore.NumberSymbolKind::Catalan => "G"
    @symcore.NumberSymbolKind::Infinity => "\\infty"
    @symcore.NumberSymbolKind::NegativeInfinity => "-\\infty"
    @symcore.NumberSymbolKind::ComplexInfinity => "\\tilde{\\infty}"
    @symcore.NumberSymbolKind::NaN => "\\text{NaN}"
  }
}

///|
fn latex_singleton_set_constant(name : String) -> String? {
  match name {
    "EmptySet" => Some("\\emptyset")
    "UniversalSet" => Some("\\mathbb{U}")
    "Naturals" => Some("\\mathbb{N}")
    "Naturals0" => Some("\\mathbb{N}_0")
    "Integers" => Some("\\mathbb{Z}")
    "Rationals" => Some("\\mathbb{Q}")
    "Reals" => Some("\\mathbb{R}")
    "Complexes" => Some("\\mathbb{C}")
    _ => None
  }
}

///|
fn latex_singleton_set_rank(name : String) -> Int? {
  match name {
    "EmptySet" => Some(0)
    "Naturals" => Some(1)
    "Naturals0" => Some(2)
    "Integers" => Some(3)
    "Rationals" => Some(4)
    "Reals" => Some(5)
    "Complexes" => Some(6)
    "UniversalSet" => Some(7)
    _ => None
  }
}

///|
fn latex_singleton_set_expr_rank(expr : Expr) -> Int? {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Symbol(name)
    | @symcore.ExprForm::FunctionHead(name)
    | @symcore.ExprForm::UndefinedFunction(name) =>
      latex_singleton_set_rank(name)
    _ => None
  }
}

///|
fn latex_is_atomic_index(expr : Expr) -> Bool {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Number(_)
    | @symcore.ExprForm::NumberSymbol(_)
    | @symcore.ExprForm::Symbol(_)
    | @symcore.ExprForm::Dummy(_, _)
    | @symcore.ExprForm::Wild(_, _, _)
    | @symcore.ExprForm::WildFunction(_, _)
    | @symcore.ExprForm::FunctionHead(_)
    | @symcore.ExprForm::UndefinedFunction(_) => true
    _ => false
  }
}

///|
fn latex_sorted_exprs(items : Array[Expr]) -> Array[Expr] {
  let sorted = items.copy()
  sorted.sort_by((lhs, rhs) => {
    let lhs_is_number = @symcore.is_number_atom(lhs)
    let rhs_is_number = @symcore.is_number_atom(rhs)
    if lhs_is_number && !rhs_is_number {
      -1
    } else if !lhs_is_number && rhs_is_number {
      1
    } else {
      match
        (latex_singleton_set_expr_rank(lhs), latex_singleton_set_expr_rank(rhs)) {
        (Some(lhs_rank), Some(rhs_rank)) =>
          if lhs_rank < rhs_rank {
            -1
          } else if lhs_rank > rhs_rank {
            1
          } else {
            0
          }
        (Some(_), None) => -1
        (None, Some(_)) => 1
        (None, None) => @symcore.compare_expr(lhs, rhs)
      }
    }
  })
  sorted
}

///|
fn latex_needs_mul_brackets(expr : Expr) -> Bool {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Add(_)
    | @symcore.ExprForm::Relational(_, _, _)
    | @symcore.ExprForm::Lambda(_, _)
    | @symcore.ExprForm::Subs(_, _, _) => true
    @symcore.ExprForm::ComplexFloat(value) =>
      !@symnum.is_zero(value.real_part().to_mpf()) &&
      !@symnum.is_zero(value.imag_part().to_mpf())
    _ => false
  }
}

///|
fn latex_needs_brackets(expr : Expr) -> Bool {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Number(_) =>
      match @symcore.exact_number_num_den(expr) {
        Some((num, den)) => !(den.compare(1N) == 0 && num.compare(0N) >= 0)
        None => true
      }
    @symcore.ExprForm::Float(value) => @symnum.mpf_sign(value.to_mpf()) < 0
    @symcore.ExprForm::ComplexFloat(_) => true
    @symcore.ExprForm::NumberSymbol(@symcore.NumberSymbolKind::NegativeInfinity) =>
      true
    @symcore.ExprForm::Add(_)
    | @symcore.ExprForm::Relational(_, _, _)
    | @symcore.ExprForm::Lambda(_, _)
    | @symcore.ExprForm::Subs(_, _, _)
    | @symcore.ExprForm::Mod(_, _) => true
    _ => false
  }
}

///|
fn latex_needs_function_brackets(expr : Expr) -> Bool {
  if !latex_needs_brackets(expr) {
    return false
  }
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Mul(_) => false
    @symcore.ExprForm::Pow(base, _) => latex_needs_brackets(base)
    @symcore.ExprForm::Add(_)
    | @symcore.ExprForm::Apply(_, _)
    | @symcore.ExprForm::Relational(_, _, _)
    | @symcore.ExprForm::Lambda(_, _)
    | @symcore.ExprForm::Subs(_, _, _)
    | @symcore.ExprForm::Mod(_, _) => true
    _ => false
  }
}

///|
fn latex_wrap_power_base(
  expr : Expr,
  base_tex : String,
  settings : LatexSettings,
) -> String {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Add(_)
    | @symcore.ExprForm::Mul(_)
    | @symcore.ExprForm::Pow(_, _)
    | @symcore.ExprForm::Mod(_, _)
    | @symcore.ExprForm::Relational(_, _, _)
    | @symcore.ExprForm::Apply(_, _)
    | @symcore.ExprForm::Derivative(_, _)
    | @symcore.ExprForm::Subs(_, _, _)
    | @symcore.ExprForm::Lambda(_, _) => latex_parens(base_tex)
    @symcore.ExprForm::Number(_) =>
      match @symcore.exact_number_num_den(expr) {
        Some((num, den)) if num.compare(0N) < 0 || den.compare(1N) != 0 =>
          latex_parens(base_tex)
        _ => base_tex
      }
    @symcore.ExprForm::Float(value) =>
      if @symnum.mpf_sign(value.to_mpf()) < 0 {
        latex_parens(base_tex)
      } else {
        base_tex
      }
    @symcore.ExprForm::ComplexFloat(_) => latex_parens(base_tex)
    _ => latex_parenthesize_super(base_tex, settings)
  }
}

///|
fn latex_tuple_items(expr : Expr) -> Array[Expr]? {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Tuple(items) => Some(items)
    _ =>
      match ordinary_application_name_args(expr) {
        Some((name, args)) if name == "Tuple" => Some(args)
        _ => None
      }
  }
}

///|
fn latex_limit_items(expr : Expr) -> Array[Expr] {
  match latex_tuple_items(expr) {
    Some(items) => items
    None => [expr]
  }
}

///|
fn latex_mul_level_parenthesize(
  expr : Expr,
  settings : LatexSettings,
  is_neg? : Bool = false,
) -> String {
  let tex = latex_format_expr(expr, 0, settings)
  if is_neg || precedence(expr) < 3 {
    latex_parens(tex)
  } else {
    tex
  }
}

///|
fn latex_atomish_parenthesize(expr : Expr, settings : LatexSettings) -> String {
  let tex = latex_format_expr(expr, 0, settings)
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Number(_) =>
      match @symcore.exact_number_num_den(expr) {
        Some((num, den)) if den.compare(1N) == 0 && num.compare(0N) >= 0 => tex
        _ => latex_parens(tex)
      }
    @symcore.ExprForm::Float(value) =>
      if @symnum.mpf_sign(value.to_mpf()) < 0 {
        latex_parens(tex)
      } else {
        tex
      }
    @symcore.ExprForm::ComplexFloat(_) => latex_parens(tex)
    @symcore.ExprForm::NumberSymbol(@symcore.NumberSymbolKind::NegativeInfinity) =>
      latex_parens(tex)
    @symcore.ExprForm::Symbol(_)
    | @symcore.ExprForm::Dummy(_, _)
    | @symcore.ExprForm::Wild(_, _, _)
    | @symcore.ExprForm::WildFunction(_, _)
    | @symcore.ExprForm::IdentityFunction
    | @symcore.ExprForm::FunctionHead(_)
    | @symcore.ExprForm::UndefinedFunction(_) => tex
    _ => latex_parens(tex)
  }
}

///|
fn latex_integral_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.is_empty() {
    return None
  }
  let integrand = args[0]
  let limits = args[1:args.length()].map(latex_limit_items)
  let mut tex = ""
  let symbols : Array[String] = []
  let mut all_single_limits = true
  for limit in limits {
    if limit.length() != 1 {
      all_single_limits = false
      break
    }
  }
  if !limits.is_empty() && limits.length() <= 4 && all_single_limits {
    let mut repeated_i = ""
    for _ in 0..<(limits.length() - 1) {
      repeated_i += "i"
    }
    tex = "\\i\{repeated_i}nt"
    for limit in limits {
      symbols.push(
        "\\, \{settings.diff_operator_latex}\{latex_format_expr(limit[0], 0, settings)}",
      )
    }
  } else {
    let mut i = limits.length()
    while i > 0 {
      i -= 1
      let limit = limits[i]
      if limit.is_empty() {
        return None
      }
      tex += "\\int"
      if limit.length() > 1 {
        if settings.mode != "inline" && !settings.itex {
          tex += "\\limits"
        }
        if limit.length() == 3 {
          tex += "_{\{latex_format_expr(limit[1], 0, settings)}}^{\{latex_format_expr(limit[2], 0, settings)}}"
        } else if limit.length() == 2 {
          tex += "^{\{latex_format_expr(limit[1], 0, settings)}}"
        }
      }
      symbols.insert(
        0,
        "\\, \{settings.diff_operator_latex}\{latex_format_expr(limit[0], 0, settings)}",
      )
    }
  }
  if tex == "" {
    tex = "\\int"
  }
  let body = latex_mul_level_parenthesize(
    integrand,
    settings,
    is_neg=split_negative_addend(integrand) is Some(_),
  )
  let symbols_tex = symbols.join("")
  Some("\{tex} \{body}\{symbols_tex}")
}

///|
fn latex_bigop_application(
  op_tex : String,
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.length() < 2 {
    return None
  }
  let func = args[0]
  let limits = args[1:args.length()].map(latex_limit_items)
  let head = if limits.length() == 1 {
    let limit = limits[0]
    if limit.length() == 3 {
      "\{op_tex}_{\{latex_format_expr(limit[0], 0, settings)}=\{latex_format_expr(limit[1], 0, settings)}}^{\{latex_format_expr(limit[2], 0, settings)}} "
    } else if limit.length() == 2 {
      "\{op_tex}_{\{latex_format_expr(limit[0], 0, settings)}=\{latex_format_expr(limit[1], 0, settings)}} "
    } else if limit.length() == 1 {
      "\{op_tex}_{\{latex_format_expr(limit[0], 0, settings)}} "
    } else {
      return None
    }
  } else {
    let rows : Array[String] = []
    for limit in limits {
      if limit.length() == 3 {
        rows.push(
          "\{latex_format_expr(limit[1], 0, settings)} \\leq \{latex_format_expr(limit[0], 0, settings)} \\leq \{latex_format_expr(limit[2], 0, settings)}",
        )
      } else {
        return None
      }
    }
    let rows_tex = rows.join("\\\\")
    "\{op_tex}_{\\substack{\{rows_tex}}} "
  }
  let body = match @symcore.expr_form(func) {
    @symcore.ExprForm::Add(_) =>
      latex_parens(latex_format_expr(func, 0, settings))
    _ => latex_format_expr(func, 0, settings)
  }
  Some(head + body)
}

///|
fn latex_limit_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.length() < 3 {
    return None
  }
  let expr = args[0]
  let variable = args[1]
  let point = args[2]
  let dir = if args.length() >= 4 {
    match args[3] {
      @symcore.Expr::Symbol(name) => name
      _ => "+"
    }
  } else {
    "+"
  }
  let point_tex = latex_format_expr(point, 0, settings)
  let mut tex = "\\lim_{\{latex_format_expr(variable, 0, settings)} \\to "
  let point_is_infinite = match @symcore.expr_form(point) {
    @symcore.ExprForm::NumberSymbol(@symcore.NumberSymbolKind::Infinity)
    | @symcore.ExprForm::NumberSymbol(
      @symcore.NumberSymbolKind::NegativeInfinity
    ) => true
    _ => false
  }
  if dir == "+-" || point_is_infinite {
    tex += "\{point_tex}}"
  } else {
    tex += "\{point_tex}^\{dir}}"
  }
  let body = match @symcore.expr_form(expr) {
    @symcore.ExprForm::Add(_) | @symcore.ExprForm::Mul(_) =>
      latex_parens(latex_format_expr(expr, 0, settings))
    _ => latex_format_expr(expr, 0, settings)
  }
  Some("\{tex} \{body}")
}

///|
fn latex_variadic_named_application(
  latex_name : String,
  args : Array[Expr],
  settings : LatexSettings,
  exp? : String? = None,
) -> String {
  let rendered = args
    .map(child => latex_format_expr(child, 0, settings))
    .join(", ")
  let tex = "\\\{latex_name}\\left(\{rendered}\\right)"
  match exp {
    Some(power) => "\{tex}^{\{power}}"
    None => tex
  }
}

///|
fn latex_logic_is_compound(expr : Expr) -> Bool {
  match ordinary_application_name_args(expr) {
    Some((name, _)) =>
      match name {
        "And" | "Or" | "Xor" | "Implies" | "Equivalent" => true
        _ => false
      }
    None => false
  }
}

///|
fn latex_logic_arg(expr : Expr, settings : LatexSettings) -> String {
  let tex = latex_format_expr(expr, 0, settings)
  if latex_logic_is_compound(expr) {
    latex_parens(tex)
  } else {
    tex
  }
}

///|
fn latex_logical_operation(
  args : Array[Expr],
  settings : LatexSettings,
  char : String,
  sort? : Bool = false,
) -> String {
  let items = if sort { latex_sorted_exprs(args) } else { args.copy() }
  if items.is_empty() {
    return ""
  }
  let rendered : Array[String] = []
  for item in items {
    rendered.push(latex_logic_arg(item, settings))
  }
  rendered.join(" \{char} ")
}

///|
fn latex_piecewise_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.is_empty() || args.length() % 2 != 0 {
    return None
  }
  let rows : Array[String] = []
  let mut i = 0
  while i < args.length() {
    let expr_tex = latex_format_expr(args[i], 0, settings)
    let cond = args[i + 1]
    let row = match @symcore.expr_form(cond) {
      @symcore.ExprForm::Boolean(true) if i + 2 == args.length() =>
        "\{expr_tex} & \\text{otherwise}"
      _ =>
        "\{expr_tex} & \\text{for}\\: \{latex_format_expr(cond, 0, settings)}"
    }
    rows.push(row)
    i += 2
  }
  let rows_tex = rows.join(" \\\\")
  Some("\\begin{cases} \{rows_tex} \\end{cases}")
}

///|
fn latex_unified_transform(
  symbol : String,
  args : Array[Expr],
  settings : LatexSettings,
  inverse? : Bool = false,
) -> String? {
  if args.length() < 3 {
    return None
  }
  let inverse_tex = if inverse { "^{-1}" } else { "" }
  Some(
    "\\mathcal{\{symbol}}\{inverse_tex}_{\{latex_format_expr(args[1], 0, settings)}}\\left[\{latex_format_expr(args[0], 0, settings)}\\right]\\left(\{latex_format_expr(args[2], 0, settings)}\\right)",
  )
}

///|
fn latex_is_universal_set_expr(expr : Expr) -> Bool {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Symbol(name)
    | @symcore.ExprForm::FunctionHead(name)
    | @symcore.ExprForm::UndefinedFunction(name) => name == "UniversalSet"
    _ => false
  }
}

///|
fn latex_set_parenthesize(expr : Expr, settings : LatexSettings) -> String {
  let tex = latex_format_expr(expr, 0, settings)
  match ordinary_application_name_args(expr) {
    Some((name, _)) =>
      match name {
        "Union" | "Intersection" | "Complement" => latex_parens(tex)
        _ => tex
      }
    None => tex
  }
}

///|
fn latex_finite_set_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String {
  let items = latex_sorted_exprs(args)
  if items.is_empty() {
    return "\\emptyset"
  }
  let rendered = items
    .map(item => latex_format_expr(item, 0, settings))
    .join(", ")
  "\\left\\{\{rendered}\\right\\}"
}

///|
fn latex_interval_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.length() < 2 {
    return None
  }
  let start = args[0]
  let end = args[1]
  if @symcore.compare_expr(start, end) == 0 {
    return Some("\\left\\{\{latex_format_expr(start, 0, settings)}\\right\\}")
  }
  let left_open = if args.length() >= 3 {
    @symcore.bool_value(args[2]).unwrap_or(false)
  } else {
    false
  }
  let right_open = if args.length() >= 4 {
    @symcore.bool_value(args[3]).unwrap_or(false)
  } else {
    false
  }
  let left = if left_open { "(" } else { "[" }
  let right = if right_open { ")" } else { "]" }
  Some(
    "\\left\{left}\{latex_format_expr(start, 0, settings)}, \{latex_format_expr(end, 0, settings)}\\right\{right}",
  )
}

///|
fn latex_exact_int_value(expr : Expr) -> Int? {
  match @symcore.exact_number_num_den(expr) {
    Some((num, den)) if den.compare(1N) == 0 => Some(num.to_int())
    _ => None
  }
}

///|
fn latex_range_symbolic(
  start : Expr,
  stop : Expr,
  step : Expr,
  settings : LatexSettings,
) -> String {
  let cont = if @symcore.compare_expr(start, @symcore.int(0)) == 0 {
    if @symcore.compare_expr(step, @symcore.int(1)) == 0 {
      latex_format_expr(stop, 0, settings)
    } else {
      [
        latex_format_expr(start, 0, settings),
        latex_format_expr(stop, 0, settings),
        latex_format_expr(step, 0, settings),
      ].join(", ")
    }
  } else if @symcore.compare_expr(step, @symcore.int(1)) == 0 {
    [
      latex_format_expr(start, 0, settings),
      latex_format_expr(stop, 0, settings),
    ].join(", ")
  } else {
    [
      latex_format_expr(start, 0, settings),
      latex_format_expr(stop, 0, settings),
      latex_format_expr(step, 0, settings),
    ].join(", ")
  }
  "\\text{Range}\\left(\{cont}\\right)"
}

///|
fn latex_range_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  let (start, stop, step) = match args {
    [stop] => (@symcore.int(0), stop, @symcore.int(1))
    [start, stop] => (start, stop, @symcore.int(1))
    [start, stop, step] => (start, stop, step)
    _ => return None
  }
  match
    (
      latex_exact_int_value(start),
      latex_exact_int_value(stop),
      latex_exact_int_value(step),
    ) {
    (Some(start_i), Some(stop_i), Some(step_i)) if step_i != 0 => {
      let values : Array[Int] = []
      let mut current = start_i
      let mut steps_taken = 0
      while (
              (step_i > 0 && current < stop_i) ||
              (step_i < 0 && current > stop_i)
            ) &&
            steps_taken < 10000 {
        values.push(current)
        current += step_i
        steps_taken += 1
      }
      if steps_taken >= 10000 {
        return Some(latex_range_symbolic(start, stop, step, settings))
      }
      if values.is_empty() {
        return Some("\\left\\{\\right\\}")
      }
      let rendered = if values.length() < 4 {
        values.map(v => v.to_string()).join(", ")
      } else {
        [
          values[0].to_string(),
          values[1].to_string(),
          "\\ldots",
          values[values.length() - 1].to_string(),
        ].join(", ")
      }
      Some("\\left\\{\{rendered}\\right\\}")
    }
    _ => Some(latex_range_symbolic(start, stop, step, settings))
  }
}

///|
fn latex_set_operation_application(
  name : String,
  op_tex : String,
  args : Array[Expr],
  settings : LatexSettings,
  sort? : Bool = true,
) -> String? {
  if args.is_empty() {
    return None
  }
  let items = if sort { latex_sorted_exprs(args) } else { args.copy() }
  let rendered = items
    .map(item => latex_set_parenthesize(item, settings))
    .join(" \{op_tex} ")
  ignore(name)
  Some(rendered)
}

///|
fn latex_product_set_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.is_empty() {
    return None
  }
  let mut same = true
  for i in 1.. latex_set_parenthesize(item, settings)).join(" \\times "),
  )
}

///|
fn latex_condition_set_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.length() < 3 {
    return None
  }
  let vars_tex = latex_format_expr(args[0], 0, settings)
  let cond_tex = latex_format_expr(args[1], 0, settings)
  if latex_is_universal_set_expr(args[2]) {
    return Some("\\left\\{\{vars_tex}\\; \\middle|\\; \{cond_tex} \\right\\}")
  }
  let base_tex = latex_format_expr(args[2], 0, settings)
  Some(
    "\\left\\{\{vars_tex}\\; \\middle|\\; \{vars_tex} \\in \{base_tex} \\wedge \{cond_tex} \\right\\}",
  )
}

///|
fn latex_image_set_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.length() < 2 {
    return None
  }
  let (vars, body) = match @symcore.expr_form(args[0]) {
    @symcore.ExprForm::Lambda(vars, body) => (vars, body)
    _ => return None
  }
  let vars_items = match @symcore.expr_form(vars) {
    @symcore.ExprForm::Tuple(items) => items
    _ => [vars]
  }
  if vars_items.length() != args.length() - 1 {
    return None
  }
  let clauses : Array[String] = []
  for i in 0.. String? {
  if args.length() != 1 {
    return None
  }
  Some("\\mathcal{P}\\left(\{latex_format_expr(args[0], 0, settings)}\\right)")
}

///|
fn latex_contains_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.length() != 2 {
    return None
  }
  Some(
    "\{latex_format_expr(args[0], 0, settings)} \\in \{latex_format_expr(args[1], 0, settings)}",
  )
}

///|
fn latex_order_application(
  args : Array[Expr],
  settings : LatexSettings,
) -> String? {
  if args.is_empty() {
    return None
  }
  let mut s = latex_format_expr(args[0], 0, settings)
  if args.length() > 1 {
    match latex_tuple_items(args[1]) {
      Some(items) if items.length() == 2 =>
        s += "; \{latex_format_expr(items[0], 0, settings)}\\rightarrow \{latex_format_expr(items[1], 0, settings)}"
      Some(items) if items.length() > 0 =>
        s += "; " +
          latex_parens_lspace(
            items
            .map(child => latex_format_expr(child, 0, settings))
            .join(", \\  "),
          )
      _ => s += "; " + latex_format_expr(args[1], 0, settings)
    }
  }
  Some("O\\left(\{s}\\right)")
}

///|
fn latex_function_application(
  settings : LatexSettings,
  name : String,
  args : Array[Expr],
  exp? : String? = None,
) -> String {
  let do_exponent = fn(tex : String) -> String {
    match exp {
      Some(power) => "\\left(\{tex}\\right)^{\{power}}"
      None => tex
    }
  }
  if name == "sqrt" && args.length() == 1 && exp is None {
    return "\\sqrt{\{latex_format_expr(args[0], 0, settings)}}"
  }
  if name == "exp" && args.length() == 1 {
    let tex = "e^{\{latex_format_expr(args[0], 0, settings)}}"
    return match exp {
      Some(power) => "\\left(\{tex}\\right)^{\{power}}"
      None => tex
    }
  }
  if name == "Abs" && args.length() == 1 {
    let tex = "\\left|{\{latex_format_expr(args[0], 0, settings)}}\\right|"
    return match exp {
      Some(power) => "\{tex}^{\{power}}"
      None => tex
    }
  }
  if name == "conjugate" && args.length() == 1 {
    let tex = "\\overline{\{latex_format_expr(args[0], 0, settings)}}"
    return match exp {
      Some(power) => "\{tex}^{\{power}}"
      None => tex
    }
  }
  if name == "log" && args.length() == 2 {
    let argument = latex_format_expr(args[0], 0, settings)
    let base = latex_format_expr(args[1], 0, settings)
    let tex = if base.length() == 1 {
      "\\log_\{base}{\\left(\{argument} \\right)}"
    } else {
      "\\log_{\{base}}{\\left(\{argument} \\right)}"
    }
    return match exp {
      Some(power) => "\{tex}^{\{power}}"
      None => tex
    }
  }
  if name == "log" && args.length() == 1 {
    let func_tex = if settings.ln_notation { "\\ln" } else { "\\log" }
    let tex = "\{func_tex}{\\left(\{latex_format_expr(args[0], 0, settings)} \\right)}"
    return match exp {
      Some(power) => "\{tex}^{\{power}}"
      None => tex
    }
  }
  if name == "Integral" {
    match latex_integral_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Sum" {
    match latex_bigop_application("\\sum", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Product" {
    match latex_bigop_application("\\prod", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Limit" {
    match latex_limit_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Piecewise" {
    match latex_piecewise_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Not" && args.length() == 1 {
    match ordinary_application_name_args(args[0]) {
      Some(("Equivalent", inner_args)) =>
        return latex_logical_operation(
          inner_args,
          settings,
          "\\not\\Leftrightarrow",
          sort=true,
        )
      Some(("Implies", inner_args)) =>
        return latex_logical_operation(
          inner_args,
          settings,
          "\\not\\Rightarrow",
          sort=false,
        )
      _ => ()
    }
    let inner = latex_logic_arg(args[0], settings)
    return "\\neg \{inner}"
  }
  if name == "And" {
    return latex_logical_operation(args, settings, "\\wedge", sort=true)
  }
  if name == "Or" {
    return latex_logical_operation(args, settings, "\\vee", sort=true)
  }
  if name == "Xor" {
    return latex_logical_operation(args, settings, "\\veebar", sort=true)
  }
  if name == "Implies" {
    return latex_logical_operation(args, settings, "\\Rightarrow")
  }
  if name == "Equivalent" {
    return latex_logical_operation(
      args,
      settings,
      "\\Leftrightarrow",
      sort=true,
    )
  }
  if name == "Min" {
    return latex_variadic_named_application("min", args, settings, exp~)
  }
  if name == "Max" {
    return latex_variadic_named_application("max", args, settings, exp~)
  }
  if name == "floor" && args.length() == 1 {
    let tex = "\\left\\lfloor{\{latex_format_expr(args[0], 0, settings)}}\\right\\rfloor"
    return match exp {
      Some(power) => "\{tex}^{\{power}}"
      None => tex
    }
  }
  if name == "ceiling" && args.length() == 1 {
    let tex = "\\left\\lceil{\{latex_format_expr(args[0], 0, settings)}}\\right\\rceil"
    return match exp {
      Some(power) => "\{tex}^{\{power}}"
      None => tex
    }
  }
  if name == "re" && args.length() == 1 {
    return do_exponent(
      "\\operatorname{re}{\{latex_atomish_parenthesize(args[0], settings)}}",
    )
  }
  if name == "im" && args.length() == 1 {
    return do_exponent(
      "\\operatorname{im}{\{latex_atomish_parenthesize(args[0], settings)}}",
    )
  }
  if name == "O" {
    match latex_order_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "DiracDelta" && !args.is_empty() {
    let tex = if args.length() == 1 ||
      @symcore.compare_expr(args[1], @symcore.int(0)) == 0 {
      "\\delta\\left(\{latex_format_expr(args[0], 0, settings)}\\right)"
    } else {
      "\\delta^{\\left( \{latex_format_expr(args[1], 0, settings)} \\right)}\\left( \{latex_format_expr(args[0], 0, settings)} \\right)"
    }
    return match exp {
      Some(power) => "\\left(\{tex}\\right)^{\{power}}"
      None => tex
    }
  }
  if name == "Heaviside" && !args.is_empty() {
    let rendered_args = if args.length() == 2 {
      match @symcore.exact_number_num_den(args[1]) {
        Some((num, den)) if num.compare(1N) == 0 && den.compare(2N) == 0 =>
          [latex_format_expr(args[0], 0, settings)]
        _ => args.map(arg => latex_format_expr(arg, 0, settings))
      }
    } else {
      args.map(arg => latex_format_expr(arg, 0, settings))
    }
    let rendered_args_tex = rendered_args.join(", ")
    let tex = "\\theta\\left(\{rendered_args_tex}\\right)"
    return match exp {
      Some(power) => "\\left(\{tex}\\right)^{\{power}}"
      None => tex
    }
  }
  if name == "KroneckerDelta" && args.length() == 2 {
    let lhs = latex_format_expr(args[0], 0, settings)
    let rhs = latex_format_expr(args[1], 0, settings)
    let tex = if latex_is_atomic_index(args[0]) &&
      latex_is_atomic_index(args[1]) {
      "\\delta_{\{lhs} \{rhs}}"
    } else {
      "\\delta_{\{lhs}, \{rhs}}"
    }
    return match exp {
      Some(power) => "\\left(\{tex}\\right)^{\{power}}"
      None => tex
    }
  }
  if name == "LeviCivita" && !args.is_empty() {
    let rendered = args.map(arg => latex_format_expr(arg, 0, settings))
    let rendered_tex = if args.all(latex_is_atomic_index) {
      rendered.join(" ")
    } else {
      rendered.join(", ")
    }
    let tex = "\\varepsilon_{\{rendered_tex}}"
    return match exp {
      Some(power) => "\\left(\{tex}\\right)^{\{power}}"
      None => tex
    }
  }
  if name == "FiniteSet" {
    return latex_finite_set_application(args, settings)
  }
  if name == "Interval" {
    match latex_interval_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Range" {
    match latex_range_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Union" {
    match
      latex_set_operation_application(name, "\\cup", args, settings, sort=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Intersection" {
    match
      latex_set_operation_application(name, "\\cap", args, settings, sort=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Complement" {
    match
      latex_set_operation_application(
        name,
        "\\setminus",
        args,
        settings,
        sort=false,
      ) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "ProductSet" {
    match latex_product_set_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "ConditionSet" {
    match latex_condition_set_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "ImageSet" {
    match latex_image_set_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "PowerSet" {
    match latex_power_set_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "Contains" {
    match latex_contains_application(args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "MellinTransform" {
    match latex_unified_transform("M", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "InverseMellinTransform" {
    match latex_unified_transform("M", args, settings, inverse=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "LaplaceTransform" {
    match latex_unified_transform("L", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "InverseLaplaceTransform" {
    match latex_unified_transform("L", args, settings, inverse=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "FourierTransform" {
    match latex_unified_transform("F", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "InverseFourierTransform" {
    match latex_unified_transform("F", args, settings, inverse=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "SineTransform" {
    match latex_unified_transform("SIN", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "InverseSineTransform" {
    match latex_unified_transform("SIN", args, settings, inverse=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "CosineTransform" {
    match latex_unified_transform("COS", args, settings) {
      Some(tex) => return tex
      None => ()
    }
  }
  if name == "InverseCosineTransform" {
    match latex_unified_transform("COS", args, settings, inverse=true) {
      Some(tex) => return tex
      None => ()
    }
  }
  let mut func = if settings.ln_notation && name == "log" { "ln" } else { name }
  let inv_trig_table = [
    "asin", "acos", "atan", "acsc", "asec", "acot", "asinh", "acosh", "atanh", "acsch",
    "asech", "acoth",
  ]
  let mut inv_trig_power_case = false
  let mut can_fold_brackets = settings.fold_func_brackets &&
    args.length() == 1 &&
    !latex_needs_function_brackets(args[0])
  if inv_trig_table.contains(func) {
    match settings.inv_trig_style {
      "full" =>
        func = (if func.has_suffix("h") { "ar" } else { "arc" }) +
          func[1:func.length()].to_owned()
      "power" => {
        func = func[1:func.length()].to_owned()
        inv_trig_power_case = true
        if exp is Some(_) {
          can_fold_brackets = false
        }
      }
      _ => ()
    }
  }
  let mut head = if inv_trig_power_case {
    if is_accepted_latex_function(func) {
      "\\\{func}^{-1}"
    } else {
      "\\operatorname{\{func}}^{-1}"
    }
  } else {
    latex_hprint_function(func)
  }
  match exp {
    Some(power) =>
      head = "\{latex_parenthesize_super(head, settings)}^{\{power}}"
    None => ()
  }
  if args.is_empty() {
    return head
  }
  let rendered = args
    .map(child => latex_format_expr(child, 0, settings))
    .join(",")
  let arg_body = if can_fold_brackets {
    if is_accepted_latex_function(func) {
      " {\{rendered}}"
    } else {
      rendered
    }
  } else {
    "{\\left(\{rendered} \\right)}"
  }
  let suffix = if inv_trig_power_case {
    match exp {
      Some(power) => "^{\{power}}"
      None => ""
    }
  } else {
    ""
  }
  "\{head}\{arg_body}\{suffix}"
}

///|
fn expr_is_numericish(expr : Expr) -> Bool {
  match @symcore.expr_form(expr) {
    @symcore.ExprForm::Number(_)
    | @symcore.ExprForm::Float(_)
    | @symcore.ExprForm::ComplexFloat(_)
    | @symcore.ExprForm::NumberSymbol(_) => true
    _ => false
  }
}

///|
fn latex_mul(
  expr : Expr,
  args : Array[Expr],
  parent_prec : Int,
  settings : LatexSettings,
) -> String {
  let numerators : Array[Expr] = []
  let denominators : Array[Expr] = []
  let mut sign = ""
  for arg in args {
    match @symcore.expr_form(arg) {
      @symcore.ExprForm::Pow(base, exp) =>
        match exact_positive_denominator_pow(base, exp) {
          Some(den) => denominators.push(den)
          None => numerators.push(arg)
        }
      _ =>
        match @symcore.exact_number_num_den(arg) {
          Some((num, den)) => {
            let abs_num = if num.compare(0N) < 0 { -num } else { num }
            if num.compare(0N) < 0 {
              sign = if sign == "" { "-" } else { "" }
            }
            if den.compare(1N) != 0 {
              numerators.push(
                @symcore.Expr::Number(@symnum.BigRational::from_bigint(abs_num)),
              )
              denominators.push(
                @symcore.Expr::Number(@symnum.BigRational::from_bigint(den)),
              )
            } else {
              numerators.push(exact_abs_expr(arg))
            }
          }
          None => numerators.push(arg)
        }
    }
  }
  if numerators.is_empty() {
    numerators.push(@symcore.int(1))
  }
  if numerators.length() > 1 {
    let filtered : Array[Expr] = []
    for child in numerators {
      if !exact_one_expr(child) {
        filtered.push(child)
      }
    }
    if !filtered.is_empty() {
      numerators.clear()
      for child in filtered {
        numerators.push(child)
      }
    }
  }

  let join_product = fn(items : Array[Expr], wrap_single_add : Bool) -> String {
    let rendered : Array[String] = []
    let numeric_flags : Array[Bool] = []
    for child in items {
      numeric_flags.push(expr_is_numericish(child))
      let child_tex = latex_format_expr(child, 0, settings)
      let single_add = if items.length() == 1 {
        match @symcore.expr_form(child) {
          @symcore.ExprForm::Add(_) => true
          _ => false
        }
      } else {
        false
      }
      let wrap_child = latex_needs_mul_brackets(child) &&
        !(single_add && !wrap_single_add)
      if wrap_child {
        rendered.push(latex_parens(child_tex))
      } else {
        rendered.push(child_tex)
      }
    }
    if rendered.is_empty() {
      return ""
    }
    let mut out = rendered[0]
    for i in 1.. String {
  match ordinary_application_name_args(base) {
    Some((name, args)) if name != "exp" =>
      return latex_function_application(
        settings,
        name,
        args,
        exp=Some(latex_format_expr(exp, 0, settings)),
      )
    _ => ()
  }
  match @symcore.exact_number_num_den(exp) {
    Some((num, den)) if den.compare(1N) != 0 => {
      let abs_num = if num.compare(0N) < 0 { -num } else { num }
      if abs_num.compare(1N) == 0 && settings.root_notation {
        let base_tex = latex_format_expr(base, 0, settings)
        let root = if den.compare(2N) == 0 {
          "\\sqrt{\{base_tex}}"
        } else if settings.itex {
          "\\root{\{den.to_string()}}{\{base_tex}}"
        } else {
          "\\sqrt[\{den.to_string()}]{\{base_tex}}"
        }
        if num.compare(0N) < 0 {
          "\\frac{1}{\{root}}"
        } else {
          root
        }
      } else if settings.fold_frac_powers {
        match ordinary_application_name_args(base) {
          Some((name, args)) =>
            latex_function_application(
              settings,
              name,
              args,
              exp=Some("\{num.to_string()}/\{den.to_string()}"),
            )
          None => {
            let base_tex = latex_wrap_power_base(
              base,
              latex_format_expr(base, 0, settings),
              settings,
            )
            "\{base_tex}^{\{num.to_string()}/\{den.to_string()}}"
          }
        }
      } else if num.compare(0N) < 0 {
        let pos_exp = latex_exact_number(abs_num, den, latex_settings())
        let base_tex = latex_wrap_power_base(
          base,
          latex_format_expr(base, 0, settings),
          settings,
        )
        "\\frac{1}{\{base_tex}^{\{pos_exp}}}"
      } else {
        let exp_tex = latex_exact_number(num, den, latex_settings())
        let base_tex = latex_wrap_power_base(
          base,
          latex_format_expr(base, 0, settings),
          settings,
        )
        "\{base_tex}^{\{exp_tex}}"
      }
    }
    Some((num, den)) if den.compare(1N) == 0 && num.compare(-1N) == 0 =>
      if settings.fold_short_frac {
        "1 / " +
        latex_wrap_power_base(
          base,
          latex_format_expr(base, 0, settings),
          settings,
        )
      } else {
        "\\frac{1}{\{latex_format_expr(base, 0, settings)}}"
      }
    _ => {
      let base_tex = latex_wrap_power_base(
        base,
        latex_format_expr(base, 0, settings),
        settings,
      )
      "\{base_tex}^{\{latex_format_expr(exp, 0, settings)}}"
    }
  }
}

///|
fn latex_format_expr(
  expr : Expr,
  parent_prec : Int,
  settings : LatexSettings,
) -> String {
  match ordinary_application_name_args(expr) {
    Some((name, args)) =>
      return latex_function_application(settings, name, args)
    None => ()
  }
  let prec = precedence(expr)
  let body = match @symcore.expr_form(expr) {
    @symcore.ExprForm::Number(_) => latex_number(expr, settings)
    @symcore.ExprForm::Float(value) => latex_float(value, settings)
    @symcore.ExprForm::ComplexFloat(value) =>
      latex_complex_float(value, settings)
    @symcore.ExprForm::NumberSymbol(kind) => latex_number_symbol(kind, settings)
    @symcore.ExprForm::Symbol(name) =>
      match settings.symbol_names.get(name) {
        Some(rendered) => rendered
        None =>
          match latex_singleton_set_constant(name) {
            Some(tex) => tex
            None => deal_with_super_sub(name)
          }
      }
    @symcore.ExprForm::Dummy(name, _) => deal_with_super_sub(name)
    @symcore.ExprForm::Wild(name, _, _) => deal_with_super_sub(name)
    @symcore.ExprForm::WildFunction(name, _) => latex_hprint_function(name)
    @symcore.ExprForm::IdentityFunction => "\\left( x \\mapsto x \\right)"
    @symcore.ExprForm::FunctionHead(name) =>
      match latex_singleton_set_constant(name) {
        Some(tex) => tex
        None => latex_hprint_function(name)
      }
    @symcore.ExprForm::UndefinedFunction(name) =>
      match latex_singleton_set_constant(name) {
        Some(tex) => tex
        None => latex_hprint_function(name)
      }
    @symcore.ExprForm::Boolean(true) => "\\text{True}"
    @symcore.ExprForm::Boolean(false) => "\\text{False}"
    @symcore.ExprForm::Add(args) =>
      if args.is_empty() {
        "0"
      } else if args.length() == 2 {
        match split_negative_addend(args[0]) {
          Some(positive0) if exact_positive_expr(args[1]) =>
            latex_format_expr(args[1], prec, settings) +
            " - " +
            latex_format_expr(positive0, prec, settings)
          _ => {
            let rendered : Array[String] = []
            rendered.push(latex_format_expr(args[0], prec, settings))
            for i in 1..
                  rendered.push(
                    "- " + latex_format_expr(positive, prec, settings),
                  )
                None =>
                  rendered.push(
                    "+ " + latex_format_expr(args[i], prec, settings),
                  )
              }
            }
            rendered.join(" ")
          }
        }
      } else {
        let rendered : Array[String] = []
        rendered.push(latex_format_expr(args[0], prec, settings))
        for i in 1..
              rendered.push("- " + latex_format_expr(positive, prec, settings))
            None =>
              rendered.push("+ " + latex_format_expr(args[i], prec, settings))
          }
        }
        rendered.join(" ")
      }
    @symcore.ExprForm::Mul(args) =>
      return latex_mul(expr, args, parent_prec, settings)
    @symcore.ExprForm::Pow(base, exp) => latex_pow(base, exp, settings)
    @symcore.ExprForm::Mod(lhs, rhs) => {
      let lhs_tex_raw = latex_format_expr(lhs, 0, settings)
      let rhs_tex_raw = latex_format_expr(rhs, 0, settings)
      let lhs_tex = if latex_needs_mul_brackets(lhs) {
        latex_parens(lhs_tex_raw)
      } else {
        lhs_tex_raw
      }
      let rhs_tex = if latex_needs_mul_brackets(rhs) {
        latex_parens(rhs_tex_raw)
      } else {
        rhs_tex_raw
      }
      "\{lhs_tex} \\bmod \{rhs_tex}"
    }
    @symcore.ExprForm::Tuple(args) =>
      if args.length() == 1 {
        latex_parens_lspace(latex_format_expr(args[0], 0, settings) + ",")
      } else {
        latex_parens_lspace(
          args
          .map(child => latex_format_expr(child, 0, settings))
          .join(", \\  "),
        )
      }
    @symcore.ExprForm::Dict(items) => {
      let rendered : Array[String] = []
      for item in @symcore.sorted_dict_entries(items) {
        let (key, value) = item
        rendered.push(
          "\{latex_format_expr(key, 0, settings)} : \{latex_format_expr(value, 0, settings)}",
        )
      }
      let dict_tex = rendered.join(", \\  ")
      "\\left\\{ \{dict_tex}\\right\\}"
    }
    @symcore.ExprForm::Relational(op, lhs, rhs) => {
      let rel = match op {
        @symcore.RelOp::Eq => "="
        @symcore.RelOp::Ne => "\\neq"
        @symcore.RelOp::Lt => "<"
        @symcore.RelOp::Le => "\\leq"
        @symcore.RelOp::Gt => ">"
        @symcore.RelOp::Ge => "\\geq"
      }
      "\{latex_format_expr(lhs, 0, settings)} \{rel} \{latex_format_expr(rhs, 0, settings)}"
    }
    @symcore.ExprForm::Derivative(inner, deriv_args) => {
      let pair_count = deriv_args.length() / 2
      let wrts : Array[(Expr, Expr)] = []
      for i in 0.. {
              total_order += num.to_int()
              if num.compare(1N) == 0 {
                parts.push(
                  "\{settings.diff_operator_latex} " +
                  latex_parenthesize_super(
                    latex_format_expr(wrt, 0, settings),
                    settings,
                  ),
                )
              } else {
                parts.push(
                  "\{settings.diff_operator_latex} " +
                  latex_parenthesize_super(
                    latex_format_expr(wrt, 0, settings),
                    settings,
                  ) +
                  "^{\{num.to_string()}}",
                )
              }
            }
            _ => {
              total_order += 1
              parts.push(
                "\{settings.diff_operator_latex} " +
                latex_parenthesize_super(
                  latex_format_expr(wrt, 0, settings),
                  settings,
                ),
              )
            }
          }
          if i == 0 {
            break
          }
          i -= 1
        }
      }
      let use_partial = @symcore.free_symbols(inner).length() > 1 ||
        wrts.length() > 1
      let diff_symbol = if use_partial {
        "\\partial"
      } else {
        settings.diff_operator_latex
      }
      let numerator = if total_order <= 1 {
        diff_symbol
      } else {
        "\{diff_symbol}^{\{total_order.to_string()}}"
      }
      let denominator = if use_partial {
        parts
        .join("")
        .replace_all(
          old=settings.diff_operator_latex + " ",
          new=diff_symbol + " ",
        )
      } else {
        parts.join("")
      }
      let inner_tex_raw = latex_format_expr(inner, 0, settings)
      let inner_tex = if latex_needs_mul_brackets(inner) {
        latex_parens(inner_tex_raw)
      } else {
        inner_tex_raw
      }
      "\\frac{\{numerator}}{\{denominator}} \{inner_tex}"
    }
    @symcore.ExprForm::Subs(inner, variable, value) =>
      "\\left. \{latex_format_expr(inner, 0, settings)} \\right|_{\\substack{ \{latex_format_expr(variable, 0, settings)}=\{latex_format_expr(value, 0, settings)} }}"
    @symcore.ExprForm::Lambda(vars, body) => {
      let vars_tex = match @symcore.expr_form(vars) {
        @symcore.ExprForm::Tuple(items) if items.length() == 1 =>
          latex_format_expr(items[0], 0, settings)
        _ => latex_format_expr(vars, 0, settings)
      }
      "\\left( \{vars_tex} \\mapsto \{latex_format_expr(body, 0, settings)} \\right)"
    }
    @symcore.ExprForm::Apply(head, args) => {
      let head_tex = latex_wrap_power_base(
        head,
        latex_format_expr(head, 0, settings),
        settings,
      )
      let rendered = args
        .map(child => latex_format_expr(child, 0, settings))
        .join(",")
      "\{head_tex}{\\left(\{rendered} \\right)}"
    }
  }
  if prec < parent_prec {
    latex_parens(body)
  } else {
    body
  }
}