///|
// SVG images, drawn as vector graphics by the `svg` package (a port of
// prawn-svg, which asciidoctor-pdf draws them with): block images after
// the SVG branch of asciidoctor-pdf's `convert_image`, inline images after
// its `InlineImageArranger` and `InlineImageRenderer`.

///|
/// The transformation stack of the Prawn document asciidoctor-pdf draws
/// SVG images into: the document's, or the scratch document's in dry runs
/// (see `@svg.TransformationStack`); each document has its own.
fn Flow::svg_stack(self : Flow) -> @svg.TransformationStack {
  session().svg_stacks[if self.scratch { 1 } else { 0 }]
}

///|
/// The families Prawn knows (`font_families`), with their styles: the
/// catalog's (not the icon sets'), and Prawn's built-in AFM families.
fn FontCatalog::svg_families(
  self : FontCatalog,
) -> Array[(String, Array[String])] {
  let families : Array[(String, Array[String])] = []
  for key, _ in self.faces {
    let parts = key.split("|").map(p => p.to_owned()).collect()
    guard parts.length() == 3 else { continue }
    let family = parts[0]
    if self.icons.contains(family) {
      continue
    }
    let style = match (parts[1] == "true", parts[2] == "true") {
      (false, false) => "normal"
      (true, false) => "bold"
      (false, true) => "italic"
      (true, true) => "bold_italic"
    }
    match families.iter().find_first(f => f.0 == family) {
      Some(f) => f.1.push(style)
      None => families.push((family, [style]))
    }
  }
  for afm in ["Courier", "Times-Roman", "Helvetica"] {
    if !families.iter().any(f => f.0 == afm) {
      families.push((afm, ["normal", "bold", "italic", "bold_italic"]))
    }
  }
  families
}

///|
/// The font SVG text in a font Prawn lacks is drawn in (asciidoctor-pdf's
/// `fallback_svg_font_name`).
fn fallback_svg_font_name() -> String {
  t_str("svg_fallback_font_family").unwrap_or(base_font_family())
}

///|
/// The width of a page's content area: the bounds an inline SVG image is
/// sized in.
fn content_width() -> Double {
  let setup = page_setup.val
  setup.width - setup.margin_left - setup.margin_right
}

///|
/// What SVG images in `node`'s document are drawn with: the catalog's
/// fonts (added to `model` as text uses them) and files relative to
/// `svg_path`'s directory.
fn svg_environment(
  catalog : FontCatalog,
  model : @pagelayout.PageModel,
  node : @core.Node,
  svg_path : String,
) -> @svg.Environment {
  let dir = match svg_path.rev_find("/") {
    Some(i) => svg_path[:i].to_owned()
    None => "."
  }
  let families = catalog.svg_families()
  let load_font = fn(family : String, subfamily : String) -> @svg.SvgFont? {
    guard families.iter().any(f => f.0 == family) else { return None }
    let (bold, italic) = (
      subfamily.contains("bold"),
      subfamily.contains("italic"),
    )
    // the catalog holds the PDF standard fonts Prawn knows (its AFM fonts)
    // unless a theme family takes their name; one it lacks altogether is
    // drawn in the base font
    let family = if catalog.faces.contains(face_key(family, false, false)) {
      family
    } else {
      unsupported("image", "SVG text in the \{family} font", node)
      base_font_family()
    }
    let face = catalog.face(family, bold, italic)
    let index = model.add_font({ family, bold, italic, })
    Some({
      index,
      advances: (text, size) => {
        let out = []
        let n = text.length()
        for i in 0..= 0xDC00 && unit <= 0xDFFF {
            out.push(0.0)
            continue
          }
          let (cp, next) = if unit >= 0xD800 && unit <= 0xDBFF && i + 1 < n {
            (
              0x10000 +
              ((unit - 0xD800) << 10) +
              (text[i + 1].to_int() - 0xDC00),
              i + 2,
            )
          } else {
            (unit, i + 1)
          }
          let mut w = face.prawn_width(cp)
          if next < n {
            w += face.kerning(cp, text[next].to_int())
          }
          out.push(w * size / 1000.0)
        }
        out
      },
      height: size => face.height_pt(size),
    })
  }
  {
    families,
    load_font,
    fallback_font: fallback_svg_font_name(),
    read_file: path => {
      let resolved = node.normalize_system_path(path, start=dir)
      node.vfs().read(resolved)
    },
    ambient_font_size: base_style().size,
  }
}

///|
/// An inline SVG image's document, sized in the bounds of the page's
/// content (asciidoctor-pdf's `intrinsic_image_dimensions` reads it in the
/// bounds current where the paragraph is converted); None when the SVG
/// cannot be read.
fn inline_svg(
  data : Bytes,
  path : String,
  node : @core.Node,
) -> @svg.SvgDocument? {
  try
    @svg.SvgDocument::parse(
      @utf8.decode_lossy(data),
      (content_width(), content_height()),
    )
  catch {
    @svg.SvgError(message) => {
      if session().scratch == 0 {
        warning("could not embed image: \{path}; \{message}", node)
      }
      None
    }
  } noraise {
    doc => Some(doc)
  }
}

///|
/// The graphic of an inline SVG image drawn `w` wide with its top-left
/// corner at `(x, y)`.
fn Flow::inline_svg_item(
  self : Flow,
  image : (@svg.SvgDocument, String, @core.Node),
  x : Double,
  y : Double,
  w : Double,
) -> @pagelayout.PageItem? {
  let (doc, path, node) = image
  doc.sizing.resize(width=w)
  let env = svg_environment(self.catalog, self.model, node, path)
  let ops = doc.render(
    env,
    origin=(x, y),
    page_height=self.page_h(),
    stack=self.svg_stack(),
  )
  report_svg_warnings(doc, path, node, self.scratch)
  if ops.is_empty() {
    None
  } else {
    Some(Graphic({ x_pt: x, y_pt: y, ops, }))
  }
}

///|
/// Report prawn-svg's warnings as asciidoctor-pdf logs them.
fn report_svg_warnings(
  doc : @svg.SvgDocument,
  path : String,
  node : @core.Node,
  scratch : Bool,
) -> Unit {
  if scratch {
    return
  }
  for w in doc.warnings {
    warning("problem encountered in image: \{path}; \{w}", node)
  }
}

///|
/// asciidoctor-pdf's `resolve_explicit_width` for an SVG: a width in
/// points, or a scale of the intrinsic width (`iw`, `scale`).
fn svg_explicit_width(node : @core.Node, bounds : Double) -> (Double?, Double?) {
  let percent = fn(s : String) { @string.parse_double(s) catch { _ => 100.0 } }
  match node.attr("pdfwidth") {
    Some(w) if w.has_suffix("iw") =>
      (None, Some(percent(w[:w.length() - 2].to_owned()) / 100.0))
    Some(w) if w.has_suffix("vw") =>
      (
        Some(page_width() * percent(w[:w.length() - 2].to_owned()) / 100.0),
        None,
      )
    Some(w) => (resolve_length(w, bounds, bare_px=false), None)
    None =>
      match node.attr("scale") {
        Some(s) => (None, Some(percent(s) / 100.0))
        None =>
          match node.attr("scaledwidth") {
            Some(w) => (resolve_length(w, bounds, bare_px=false), None)
            None =>
              match node.attr("width") {
                Some(w) if w.has_suffix("%") =>
                  (
                    resolve_length(w, bounds).map(v => @cmp.minimum(v, bounds)),
                    None,
                  )
                Some(w) if w != "" && w.iter().all(c => c >= '0' && c <= '9') =>
                  (
                    resolve_length(w, bounds).map(v => @cmp.minimum(v, bounds)),
                    None,
                  )
                _ => (None, None)
              }
          }
      }
  }
}

///|
/// A block SVG image (`convert_image`'s SVG branch). False when the SVG
/// cannot be read, for the caller to show the alt text.
fn Converter::convert_svg_image(
  self : Converter,
  node : @core.Node,
  path : String,
  data : Bytes,
  alignment : String,
) -> Bool {
  let flow = self.flow
  let available_w = flow.width()
  let bounds_h = flow.bottom - flow.page_top
  let (width, scale) = svg_explicit_width(node, available_w)
  let doc = @svg.SvgDocument::parse(
    @utf8.decode_lossy(data),
    (available_w, bounds_h),
    width?,
  ) catch {
    @svg.SvgError(message) => {
      if !flow.scratch {
        warning("could not embed image: \{path}; \{message}", node)
      }
      return false
    }
  }
  let sizing = doc.sizing
  let mut rendered_w = sizing.output_width
  match scale {
    Some(s) => {
      sizing.resize(width=@cmp.minimum(available_w, rendered_w * s))
      rendered_w = sizing.output_width
    }
    None =>
      if width is None &&
        doc.root.attr("width") is Some(_) &&
        rendered_w > available_w {
        sizing.resize(width=available_w)
        rendered_w = available_w
      }
  }
  let caption_height = self.image_caption_height(node, alignment, width)
  let mut rendered_h = sizing.output_height
  if rendered_h > flow.cursor() - caption_height {
    if !self.pinned && !flow.at_page_top() && !first_child_at_top(node) {
      flow.advance_page()
    }
    let available_h = flow.cursor() - caption_height
    if rendered_h > available_h {
      sizing.resize(height=available_h)
      rendered_h = available_h
      rendered_w = sizing.output_width
    }
  }
  self.add_dest(node)
  self.ink_image_caption(node, alignment, rendered_w, top=true)
  let left = match alignment {
    "center" => flow.left + (available_w - rendered_w) * 0.5
    "right" => flow.left + available_w - rendered_w
    _ => flow.left
  }
  let env = svg_environment(flow.catalog, flow.model, node, path)
  let top = flow.y
  let ops = doc.render(
    env,
    origin=(left, top),
    page_height=flow.page_h(),
    stack=flow.svg_stack(),
  )
  report_svg_warnings(doc, path, node, flow.scratch)
  if !ops.is_empty() {
    flow.push(Graphic({ x_pt: left, y_pt: top, ops, }))
  }
  match node.attr("link") {
    Some(link) if link != "" =>
      flow.push(
        Link({
          x_pt: left,
          y_pt: top,
          w_pt: rendered_w,
          h_pt: rendered_h,
          target: if link.has_prefix("#") {
            Named(pdf_anchor_name(link[1:].to_owned()))
          } else {
            Uri(link)
          },
        }),
      )
    _ => ()
  }
  if !sizing.invalid {
    flow.move_down(rendered_h)
  }
  self.finish_image(node, alignment, rendered_w, rendered_h, caption_height)
  true
}