///|
// Loading the document's theme and its fonts (asciidoctor-pdf's
// `load_theme`, `prepare_theme` and `register_fonts`). Theme files, fonts
// and icon fonts are read through the document's VFS, so a conversion run
// with `@io.run_with_files` loads whatever the theme names.

///|
/// The theme loader's view of the document's VFS: text files by absolute
/// path.
fn theme_files(doc : @core.Node) -> @theme.Files {
  let vfs = doc.vfs()
  @theme.Files::new(
    read=path => vfs.read(path).map(@core.decode_text),
    cwd=vfs.cwd(),
    home=vfs.home(),
  )
}

///|
/// `str.sub '{docdir}', docdir`
fn sub_docdir(value : String, doc : @core.Node) -> String {
  match value.find("{docdir}") {
    Some(i) =>
      value[:i].to_owned() +
      doc.attr("docdir").unwrap_or("") +
      value[i + 8:].to_owned()
    None => value
  }
}

///|
/// asciidoctor-pdf's `load_theme doc`: the theme `pdf-theme` names (looked
/// up in `pdf-themesdir`), else the default theme (`default-for-print` for
/// `media=print` or `prepress`); a theme that cannot be loaded is reported
/// and the default theme is used instead. Prepared (`prepare_theme`), and
/// the theme of the conversion from then on.
fn load_document_theme(doc : @core.Node) -> @theme.Theme {
  let doc = doc.document()
  let files = theme_files(doc)
  let themesdir = doc.attr("pdf-themesdir").map(d => sub_docdir(d, doc))
  // the `pdf_theme` option wins over `pdf-theme` and `media` (a copy: the
  // conversion's defaults must not leak into the next one)
  let option_theme = match session().registration.pdf_theme {
    Some(build) =>
      Some(
        build(files).copy() catch {
          e => {
            report(
              "WARNING could not load the pdf_theme option because of \{e}; reverting to default theme at ",
            )
            @theme.load_theme(files) catch {
              _ => @theme.Theme::new()
            }
          }
        },
      )
    None => None
  }
  let theme = match (option_theme, doc.attr("pdf-theme")) {
    (Some(theme), _) => theme
    (None, Some(name)) =>
      @theme.load_theme(files, theme_name=name, theme_dir?=themesdir) catch {
        e => {
          let where_ = match themesdir {
            Some(dir) => "the pdf theme `\{name}' in \{dir}"
            None => "the built-in pdf theme `\{name}'"
          }
          report(
            "WARNING could not locate or load \{where_} because of \{e}; reverting to default theme at ",
          )
          @theme.load_theme(files) catch {
            _ => @theme.Theme::new()
          }
        }
      }
    (None, None) => {
      let name = if doc.attr("media").unwrap_or("screen") == "screen" {
        None
      } else {
        Some("default-for-print")
      }
      @theme.load_theme(files, theme_name?=name) catch {
        _ => @theme.Theme::new()
      }
    }
  }
  prepare_theme(theme)
  theme
}

///|
/// The theme of `doc`'s conversion, loaded once per document (the inline
/// conversions of the parse ask for it before the conversion starts, as
/// asciidoctor-pdf's `convert_inline_*` do with `load_theme node.document`).
fn document_theme(doc : @core.Node) -> @theme.Theme {
  match session().theme {
    Some(theme) => theme
    None => {
      let theme = load_document_theme(doc)
      session().theme = Some(theme)
      current_theme.val = theme
      theme
    }
  }
}

///|
/// The font catalog of `doc`'s conversion: the one `register` was given,
/// else its theme's fonts, loaded once per document.
fn document_catalog(doc : @core.Node) -> FontCatalog raise {
  match session().catalog {
    Some(catalog) => catalog
    None => {
      let catalog = load_document_fonts(doc)
      session().catalog = Some(catalog)
      catalog
    }
  }
}

///|
/// asciidoctor-pdf's `prepare_theme`: the defaults of keys a theme may
/// leave out.
fn prepare_theme(theme : @theme.Theme) -> Unit {
  let default = (key : String, value : @theme.Value) => {
    if !theme[key].truthy() {
      theme[key] = value
    }
  }
  if theme["base_border_color"] is Transparent {
    theme["base_border_color"] = Null
  }
  default("base_font_color", Str("000000"))
  default("base_font_family", Str("Helvetica"))
  if !theme["base_font_style"].truthy() {
    theme["base_font_style"] = Str("normal")
  }
  default("page_numbering_start_at", Str("body"))
  default("running_content_start_at", Str("body"))
  default("heading_chapter_break_before", Str("always"))
  default("heading_part_break_before", Str("always"))
  default("heading_margin_page_top", Int(0))
  default("heading_margin_top", Int(0))
  default("heading_margin_bottom", Int(0))
  default("prose_text_indent", Int(0))
  default("prose_text_indent_inner", Int(0))
  default("prose_margin_bottom", Int(0))
  default("block_margin_bottom", Int(0))
  default("list_indent", Int(0))
  default("list_item_spacing", Int(0))
  default("description_list_term_spacing", Int(0))
  default("description_list_description_indent", Int(0))
  let border = if theme["base_border_color"].truthy() {
    theme["base_border_color"]
  } else {
    Str("000000")
  }
  default("table_border_color", border)
  default("table_border_width", Float(0.5))
  default("thematic_break_border_color", border)
  default("image_border_width", Int(0))
  default("code_linenum_font_color", Str("999999"))
  default("callout_list_margin_top_after_code", Int(0))
  default("role_unresolved_font_color", Str("FF0000"))
  default("footnotes_margin_top", Str("auto"))
  default("footnotes_item_spacing", Int(0))
  default("index_columns", Int(2))
  default("index_column_gap", theme["base_font_size"])
  default("kbd_separator", Str("+"))
  default("title_page_authors_delimiter", Str(", "))
  default("title_page_revision_delimiter", Str(", "))
  default("toc_indent", Int(0))
  default("toc_hanging_indent", Int(0))
  // `TypographicQuotes`: character references, which the substitutions
  // that follow the quotes' do not take for word characters
  let typographic = ["“", "”", "‘", "’"]
  match theme["quotes"] {
    Array(items) => {
      let items = items.copy()
      for i, q in typographic {
        if i >= items.length() {
          items.push(Str(q))
        } else if !items[i].truthy() {
          items[i] = Str(q)
        }
      }
      theme["quotes"] = Array(items)
    }
    _ => theme["quotes"] = Array(typographic.map(q => (Str(q) : @theme.Value)))
  }
}

///|
/// A file of asciidoctor-pdf's font directory (`GEM_FONTS_DIR`), by its
/// name there: from `fonts_dir` when `register` was given one, else from
/// the bundled fonts.
fn read_gem_font_file(vfs : &@core.Vfs, name : String) -> Bytes? {
  let sources = session().registration
  match sources.fonts_dir {
    Some(dir) => vfs.read(dir + "/" + name)
    None => {
      for bundle in sources.bundles {
        if bundle.get(name) is Some(data) {
          return Some(data)
        }
      }
      None
    }
  }
}

///|
/// Whether `doc` may draw font icons: `icons=font` in its header (or from
/// the API), or an attribute entry in its body (or in a nested document's)
/// that sets it, which takes effect only as the conversion reaches it.
fn may_use_font_icons(doc : @core.Node) -> Bool {
  if doc.attr("icons") == Some("font") {
    return true
  }
  let found = Ref(false)
  ignore(
    doc.find_by(traverse_documents=true, filter=node => {
      match node.attributes.entries {
        Some(entries) if entries
          .iter()
          .any(e => e.name == "icons" && e.value == Some("font")) => {
          found.val = true
          Stop
        }
        _ => Skip
      }
    }),
  )
  found.val
}

///|
/// Whether the theme sets text in an icon font without `icons=font`: a
/// font family (`ulist_marker_checked_font_family: fas`), a fallback font,
/// or `` in a content template.
fn theme_uses_icon_fonts(theme : @theme.Theme) -> Bool {
  let names = icon_set_names
  let mentions = (value : @theme.Value) => {
    match value {
      Str(s) =>
        names.contains(s) || names.iter().any(n => s.contains("\"\{n}\""))
      Array(items) => items.iter().any(v => v is Str(s) && names.contains(s))
      _ => false
    }
  }
  theme.keys().iter().any(key => mentions(theme.at(key)))
}

///|
/// A font file: one under the theme loader's virtual `GEM_FONTS_DIR` (see
/// `read_gem_font_file`), else a file of the document's VFS.
fn read_font_file(vfs : &@core.Vfs, path : String) -> Bytes? {
  if path.has_prefix(@theme.fonts_dir + "/") {
    read_gem_font_file(vfs, path[@theme.fonts_dir.length() + 1:].to_owned())
  } else {
    vfs.read(path)
  }
}

///|
/// `File.absolute_path(path, dir)`
fn absolute_path_in(path : String, dir : String) -> String {
  @core.expand_path_from(dir, path)
}

///|
/// The font catalog of `doc`'s theme (`register_fonts`): each family's
/// styles, looked for in the `pdf-fontsdir` directories (`;` or `,`
/// separated, `GEM_FONTS_DIR` or an empty entry for the bundled fonts),
/// else in the theme's directory and then the bundled fonts; plus
/// prawn-icon's icon fonts when `register` was told where they are.
fn load_document_fonts(doc : @core.Node) -> FontCatalog raise {
  let doc = doc.document()
  let theme = document_theme(doc)
  let vfs = doc.vfs()
  let spec = match doc.attr("pdf-fontsdir") {
    Some(dirs) => sub_docdir(dirs, doc)
    None =>
      if theme.dir() == @theme.themes_dir {
        "GEM_FONTS_DIR"
      } else {
        theme.dir() + ";GEM_FONTS_DIR"
      }
  }
  let dirs = []
  let mut start = 0
  for i in 0..<=spec.length() {
    if i == spec.length() || spec[i] == ';' || spec[i] == ',' {
      let dir = spec[start:i].to_owned()
      dirs.push(
        if dir == "GEM_FONTS_DIR" || dir == "" {
          @theme.fonts_dir
        } else {
          // relative to the working directory
          @core.expand_path_from(vfs.cwd(), dir)
        },
      )
      start = i + 1
    }
  }
  let files : Array[(String, Bool, Bool, Bytes)] = []
  match theme["font_catalog"] {
    Hash(families, ..) =>
      for family, styles in families {
        guard styles is Hash(styles, ..) else { continue }
        for style, path in styles {
          let (bold, italic) = match style {
            "normal" => (false, false)
            "bold" => (true, false)
            "italic" => (false, true)
            "bold_italic" => (true, true)
            _ => continue
          }
          let path = path.to_ruby_s()
          let mut data : Bytes? = None
          for dir in dirs {
            data = read_font_file(vfs, absolute_path_in(path, dir))
            if data is Some(_) {
              break
            }
          }
          match data {
            Some(bytes) => files.push((family, bold, italic, bytes))
            None =>
              fail(
                "font \{path} not found in \{spec.replace_all(old=";", new=" or ")}",
              )
          }
        }
      }
    _ => ()
  }
  // prawn-icon's fonts, which only `icons=font` draws with (prawn-icon
  // loads them on demand; the catalog is complete once loaded, so they are
  // loaded with it, when the document may ask for font icons)
  let icon_sets : Array[IconSet] = []
  let sources = session().registration
  // each set's font and its icon names: prawn-icon's legend in a copy of
  // its `data/fonts`, the bundle's own table otherwise (see `@icons.fonts`)
  let read_icon_set : ((String, String, String) -> (Bytes?, Bytes?))? = match
    (sources.icon_fonts_dir, sources.icon_bundle) {
    _ if !may_use_font_icons(doc) && !theme_uses_icon_fonts(theme) => None
    (Some(dir), _) =>
      Some((_, font, legend) => {
        (vfs.read(dir + "/" + font), vfs.read(dir + "/" + legend))
      })
    (None, Some(bundle)) =>
      Some((name, font, _) => {
        (bundle.get(font), bundle.get("\{name}/glyphs.txt"))
      })
    (None, None) => None
  }
  if read_icon_set is Some(read) {
    for entry in default_icon_font_files() {
      let (name, font, legend) = entry
      match read(name, font, legend) {
        (Some(font), Some(legend)) =>
          icon_sets.push({ name, font, legend: @core.decode_text(legend), })
        _ => ()
      }
    }
  }
  let legacy_icon_mapping = if icon_sets.is_empty() {
    ""
  } else {
    read_gem_font_file(vfs, "fa-legacy-mapping.yml")
    .map(@core.decode_text)
    .unwrap_or("")
  }
  let fallbacks = match theme["font_fallbacks"] {
    Array(items) => items.map(v => v.to_ruby_s())
    _ => []
  }
  FontCatalog::load(files, icon_sets~, legacy_icon_mapping~, fallbacks~)
}