///|
// The theme's settings for inline text (asciidoctor-pdf's
// `FormattedText::Transform` and its `@theme_settings`): the `button`,
// `codespan`, `kbd`, `link`, `mark` and `menu` categories for their tags,
// and the `role__*` keys for the roles of an element's `class`.

///|
/// Apply the font settings of a theme category to `style`: its font
/// family, size (points or relative), colour, style (`font_style`, and
/// `text_decoration` where the category has one), background (with its
/// `border_offset`) and text transform. `family` is the family key, when
/// not `_font_family`.
fn apply_text_category(
  style : Style,
  category : String,
  family_key? : String,
  decoration? : Bool = false,
  background? : Bool = true,
) -> Style {
  let mut s = style
  match t_color(category + "_font_color") {
    Some(color) => s = { ..s, color, }
    None => ()
  }
  let family = t_str(family_key.unwrap_or(category + "_font_family"))
  match family {
    Some(family) => s = { ..s, family, font_set: true, }
    None => ()
  }
  match resolve_font_size(tv(category + "_font_size"), s.size) {
    Some(size) => s = { ..s, size, }
    None => ()
  }
  let font_style = t_str(category + "_font_style")
  let text_decoration = if decoration {
    t_str(category + "_text_decoration")
  } else {
    None
  }
  s = apply_text_styles(s, category, font_style, text_decoration)
  if background {
    let bg = t_color(category + "_background_color")
    let border = t_num(category + "_border_width").unwrap_or(0.0) > 0.0
    match bg {
      Some(color) => s = { ..s, background: Some(color), }
      None => ()
    }
    if bg is Some(_) || border {
      match t_num(category + "_border_offset") {
        Some(offset) => s = { ..s, border_offset: offset, }
        None => ()
      }
    }
  }
  match t_str(category + "_text_transform") {
    Some("none") => s = { ..s, text_transform: None, }
    Some(transform) => s = { ..s, text_transform: Some(transform), }
    None => ()
  }
  s
}

///|
/// Merge a theme's `font_style` and `text_decoration` into `style`
/// (`update_fragment`): `normal` (of `normal_italic`) drops the bold and
/// italic the text has, but only the first time the setting is applied
/// (asciidoctor-pdf deletes it from the theme's own set as it applies it).
fn apply_text_styles(
  style : Style,
  setting : String,
  font_style : String?,
  text_decoration : String?,
) -> Style {
  let mut s = style
  match font_style {
    Some(fs) => {
      if fs == "normal_italic" && !session().normal_spent.contains(setting) {
        session().normal_spent[setting] = true
        s = { ..s, bold: false, italic: false, }
      }
      let (bold, italic) = font_style_of(fs)
      if bold {
        s = { ..s, bold: true, }
      }
      if italic {
        s = { ..s, italic: true, }
      }
    }
    None => ()
  }
  match text_decoration {
    Some("underline") => s = { ..s, underline: true, }
    Some("line-through") => s = { ..s, strike: true, }
    _ => ()
  }
  s
}

///|
/// The roles of an element's `class`, applied in order: each role's
/// `role__*` theme keys (a role name with an underscore cannot have
/// any, as asciidoctor-pdf splits the key at the first one), else the
/// built-in `underline`, `line-through`, `big` and `small`; and `wj`, a
/// word joiner.
fn apply_roles(style : Style, classes : String) -> Style {
  let mut s = style
  for role in classes.split(" ") {
    let role = role.to_owned()
    if role == "" {
      continue
    }
    if role == "wj" {
      s = { ..s, wj: true, }
    }
    let themed = !role.contains("_") && role_has_settings(role)
    if themed {
      s = apply_role_settings(s, role)
    } else {
      match role {
        "underline" => s = { ..s, underline: true, }
        "line-through" => s = { ..s, strike: true, }
        "big" => s = { ..s, size: s.size * relative_size("large", 1.1667), }
        "small" => s = { ..s, size: s.size * relative_size("small", 0.8333), }
        _ => ()
      }
    }
  }
  s
}

///|
/// `base_font_size_` relative to the base font size, to five
/// decimals, else `fallback` (the `big` and `small` roles' default sizes).
fn relative_size(which : String, fallback : Double) -> Double {
  match t_num("base_font_size_" + which) {
    Some(size) => {
      let ratio = size / base_font_size()
      (ratio * 100000.0).round() / 100000.0
    }
    None => fallback
  }
}

///|
/// The properties a role's theme keys can set (`ThemeKeyToFragmentProperty`
/// and the styles).
let role_properties : Array[String] = [
  "background_color", "border_color", "border_offset", "border_radius", "border_width",
  "font_color", "font_family", "font_size", "text_decoration_color", "text_decoration_width",
  "text_transform", "font_style", "text_decoration",
]

///|
fn role_has_settings(role : String) -> Bool {
  role_properties.iter().any(p => current_theme.val.has("role_\{role}_\{p}"))
}

///|
/// A role's `role__*` settings.
fn apply_role_settings(style : Style, role : String) -> Style {
  let key = "role_" + role
  let mut s = style
  match t_color(key + "_font_color") {
    Some(color) => s = { ..s, color, }
    None => ()
  }
  match t_str(key + "_font_family") {
    Some(family) => s = { ..s, family, font_set: true, }
    None => ()
  }
  match resolve_font_size(tv(key + "_font_size"), s.size) {
    Some(size) => s = { ..s, size, }
    None => ()
  }
  if current_theme.val.has(key + "_font_style") ||
    current_theme.val.has(key + "_text_decoration") {
    s = apply_text_styles(
      s,
      key,
      t_str(key + "_font_style"),
      t_str(key + "_text_decoration"),
    )
  }
  match t_color(key + "_background_color") {
    Some(color) => s = { ..s, background: Some(color), }
    None => ()
  }
  match t_num(key + "_border_offset") {
    Some(offset) => s = { ..s, border_offset: offset, }
    None => ()
  }
  match t_str(key + "_text_transform") {
    Some("none") => s = { ..s, text_transform: None, }
    Some(transform) => s = { ..s, text_transform: Some(transform), }
    None => ()
  }
  s
}

///|
/// asciidoctor-pdf's `TextTransformer#transform_text`: `uppercase`,
/// `lowercase`, `capitalize` (each run of non-blank characters
/// capitalized, as Ruby's `capitalize` does) or `smallcaps`, applied to
/// the text between tags and character references only; anything else
/// leaves the text as it is.
fn transform_text(text : String, transform : String) -> String {
  match transform {
    "uppercase" => pcdata_map(text, @core.ruby_upcase)
    "lowercase" =>
      if text.contains("<") {
        // TagFilterRx: character references are lowercased too
        pcdata_map(text, @core.ruby_downcase, entities=false)
      } else {
        @core.ruby_downcase(text)
      }
    "capitalize" => pcdata_map(text, capitalize_words)
    "smallcaps" => pcdata_map(text, smallcaps)
    _ => text
  }
}

///|
/// The length of the markup at `i` that `PCDATAFilterRx` leaves alone: a
/// tag (`<[^>]+>`), or with `entities` a character reference
/// (`&#?[a-z\d]+;`); 0 when there is none.
fn pcdata_markup_at(text : String, i : Int, entities : Bool) -> Int {
  let n = text.length()
  if text[i] == '<' {
    let mut j = i + 1
    while j < n && text[j] != '>' {
      j += 1
    }
    if j < n && j > i + 1 {
      j + 1 - i
    } else {
      0
    }
  } else if entities && text[i] == '&' {
    let mut j = i + 1
    if j < n && text[j] == '#' {
      j += 1
    }
    let start = j
    while j < n &&
          (
            (text[j] >= 'a' && text[j] <= 'z') ||
            (text[j] >= '0' && text[j] <= '9')
          ) {
      j += 1
    }
    if j > start && j < n && text[j] == ';' {
      j + 1 - i
    } else {
      0
    }
  } else {
    0
  }
}

///|
/// `string.gsub(PCDATAFilterRx) { $2 ? f($2) : $1 }` (`TagFilterRx`
/// without `entities`), when the text has markup (`XMLMarkupRx`); `f` of
/// the whole text otherwise. A `&` or `<` that starts no markup matches
/// neither group and is kept as is.
fn pcdata_map(
  text : String,
  f : (String) -> String,
  entities? : Bool = true,
) -> String {
  let n = text.length()
  let mut has_markup = false
  for i in 0.. 0) {
      has_markup = true
      break
    }
  }
  if !has_markup {
    return f(text)
  }
  let sb = StringBuilder()
  let mut i = 0
  while i < n {
    let markup = pcdata_markup_at(text, i, entities)
    if markup > 0 {
      sb.write_string(text.unsafe_substring(start=i, end=i + markup))
      i += markup
      continue
    }
    if text[i] == '<' || (entities && text[i] == '&') {
      sb.write_char(text[i].unsafe_to_char())
      i += 1
      continue
    }
    let start = i
    while i < n && text[i] != '<' && !(entities && text[i] == '&') {
      i += 1
    }
    sb.write_string(f(text.unsafe_substring(start~, end=i)))
  }
  sb.to_string()
}

///|
/// `capitalize_words`: `string.gsub(/\p{Graph}+/) { $&.capitalize }`.
fn capitalize_words(text : String) -> String {
  let sb = StringBuilder()
  let word = StringBuilder()
  let flush = () => {
    let w = word.to_string()
    if w != "" {
      sb.write_string(@core.ruby_capitalize(w))
      word.reset()
    }
  }
  for c in text {
    if c.is_whitespace() || c.to_int() < 0x20 || c.to_int() == 0x7f {
      flush()
      sb.write_char(c)
    } else {
      word.write_char(c)
    }
  }
  flush()
  sb.to_string()
}

///|
/// `smallcaps`: `string.tr 'a-z', 'ᴀʙᴄᴅᴇғɢʜɪᴊᴋʟᴍɴoᴘǫʀsᴛᴜᴠᴡxʏᴢ'`.
fn smallcaps(text : String) -> String {
  let chars = "ᴀʙᴄᴅᴇғɢʜɪᴊᴋʟᴍɴoᴘǫʀsᴛᴜᴠᴡxʏᴢ".to_array()
  let sb = StringBuilder()
  for c in text {
    if c >= 'a' && c <= 'z' {
      sb.write_char(chars[c.to_int() - 'a'.to_int()])
    } else {
      sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
/// The style a tag gives its content.
fn tag_style(tag : String, attrs : Map[String, String], style : Style) -> Style {
  match tag {
    "strong" | "b" => { ..style, bold: true, }
    "em" | "i" => { ..style, italic: true, }
    "code" => apply_text_category(style, "codespan")
    "kbd" =>
      apply_text_category(
        style,
        "kbd",
        family_key=if current_theme.val.has("kbd_font_family") {
          "kbd_font_family"
        } else {
          "codespan_font_family"
        },
      )
    "button" => apply_text_category(style, "button")
    "menu" => apply_text_category(style, "menu", background=false)
    "mark" => apply_text_category(style, "mark")
    "sup" => { ..style, size: style.size * 0.583, script: 1, }
    "sub" => { ..style, size: style.size * 0.583, script: -1, }
    "u" => { ..style, underline: true, }
    "del" | "s" | "strikethrough" => { ..style, strike: true, }
    "a" =>
      match attrs.get("href") {
        Some(href) =>
          apply_text_category(
            { ..style, link: Some(Uri(href)), },
            "link",
            decoration=true,
          )
        None =>
          match attrs.get("anchor") {
            Some(anchor) =>
              apply_text_category(
                { ..style, link: Some(Named(pdf_anchor_name(anchor))), },
                "link",
                decoration=true,
              )
            None => style
          }
      }
    "font" | "color" | "span" => {
      let mut s = style
      match attrs.get("size") {
        Some(size) => s = { ..s, size: parse_size(size, s.size), }
        None => ()
      }
      match attrs.get("name") {
        Some(family) => s = { ..s, family, font_set: true, }
        None => ()
      }
      match
        (match attrs.get("color") {
          Some(c) => Some(c)
          None => attrs.get("rgb")
        }) {
        Some(color) =>
          match parse_color(color) {
            Some(c) => s = { ..s, color: c, }
            None => ()
          }
        None => ()
      }
      s
    }
    _ => style
  }
}