///|
// Images placed in a box rather than in the flow (asciidoctor-pdf's
// `resolve_background_image` and `resolve_image_options`, drawn with
// Prawn's `image` in a canvas or bounding box): page background and
// foreground images, the title page background, cover pages, and images
// in the running content.

///|
/// An image resolved for a box, with Prawn's sizing options.
priv struct BoxImage {
  data : Bytes
  svg : Bool
  /// the file (the target for a data URI), for SVG references and messages
  path : String
  /// Prawn's `:fit` (the box to fit in), `:width` and `:height`
  fit : (Double, Double)?
  width : Double?
  height : Double?
  /// `:position` (left, center, right) and `:vposition` (top, center,
  /// bottom)
  position : String
  vposition : String
  /// the macro's `link`
  link : String?
  /// the macro's alt text, in brackets (`image_opts[:alt]`)
  alt : String?
}

///|
/// What `resolve_background_image` finds: nothing, a symbolic value (`""`
/// or `~` where the caller accepts them), or an image.
priv enum Background {
  /// no value, or an image that cannot be read
  NoImage
  /// `none`
  Cleared
  /// the image named cannot be read (its resolved path)
  Unreadable(String)
  Symbolic(String)
  Found(BoxImage)
}

///|
/// `ImageAttributeValueRx`: `image:target[attrs]` (or `image::`), as the
/// target and the attribute list.
fn image_macro(value : String) -> (String, String)? {
  let rest = if value.has_prefix("image::") {
    value[7:].to_owned()
  } else if value.has_prefix("image:") {
    value[6:].to_owned()
  } else {
    return None
  }
  guard rest.has_suffix("]") && rest.find("[") is Some(i) else { return None }
  Some((rest[:i].to_owned(), rest[i + 1:rest.length() - 1].to_owned()))
}

///|
/// A string attribute of a parsed attribute list.
fn attrs_str(attrs : @core.Attributes, name : String) -> String? {
  match attrs.get(name) {
    Some(Str(s)) => Some(s)
    Some(Int(i)) => Some(i.to_string())
    _ => None
  }
}

///|
/// `resolve_explicit_width` of an image macro's attributes: `pdfwidth`
/// (`%` of the bounds, else a length), `scaledwidth`, then `width` (`%` of
/// the bounds, or digits taken as pixels). `iw`, `vw` and `scale` widths
/// are left to the caller (None).
fn macro_explicit_width(attrs : @core.Attributes, bounds : Double) -> Double? {
  match attrs_str(attrs, "pdfwidth") {
    Some(w) if w.has_suffix("%") =>
      Some(@theme.ruby_string_to_f(w) / 100.0 * bounds)
    Some(w) if w.has_suffix("iw") || w.has_suffix("vw") => None
    Some(w) => Some(@theme.str_to_pt(w))
    None =>
      if attrs.contains("scale") {
        None
      } else {
        match attrs_str(attrs, "scaledwidth") {
          Some(w) if w.has_suffix("%") =>
            Some(@theme.ruby_string_to_f(w) / 100.0 * bounds)
          Some(w) => Some(@theme.str_to_pt(w))
          None =>
            match attrs_str(attrs, "width") {
              Some(w) if w.has_suffix("%") =>
                Some(@theme.ruby_string_to_f(w) / 100.0 * bounds)
              Some(w) if w != "" && w.iter().all(c => c >= '0' && c <= '9') =>
                Some(@theme.ruby_string_to_f(w) * 0.75)
              _ => None
            }
        }
      }
  }
}

///|
/// `resolve_background_position`: `left`, `right` or `center` (centered
/// vertically), `top` or `bottom` (centered horizontally), or two keywords;
/// None for anything else.
fn background_position(value : String) -> (String, String)? {
  if value.contains(" ") {
    let mut position : String? = None
    let mut vposition : String? = None
    let mut center = false
    let parts = match value.find(" ") {
      Some(i) => [value[:i].to_owned(), value[i + 1:].to_owned()]
      None => [value]
    }
    for keyword in parts {
      match keyword {
        "left" | "right" => position = Some(keyword)
        "top" | "bottom" => vposition = Some(keyword)
        "center" => center = true
        _ => ()
      }
    }
    if center {
      Some((position.unwrap_or("center"), vposition.unwrap_or("center")))
    } else {
      match (position, vposition) {
        (Some(p), Some(v)) => Some((p, v))
        _ => None
      }
    }
  } else {
    match value {
      "left" | "right" | "center" => Some((value, "center"))
      "top" | "bottom" => Some(("center", value))
      _ => None
    }
  }
}

///|
/// The intrinsic size of an image in points (`intrinsic_image_dimensions`:
/// prawn-svg's for SVG, 0.75 pt per pixel otherwise).
fn intrinsic_size(
  data : Bytes,
  svg : Bool,
  container : (Double, Double),
) -> (Double, Double) {
  if svg {
    let doc = @svg.SvgDocument::parse(@utf8.decode_lossy(data), container) catch {
      _ => return (0.0, 0.0)
    }
    (doc.sizing.output_width, doc.sizing.output_height)
  } else {
    match image_info(data) {
      Some((w, h, _)) => (w.to_double() * 0.75, h.to_double() * 0.75)
      None => (0.0, 0.0)
    }
  }
}

///|
/// asciidoctor-pdf's `resolve_background_image` (with `background: true`):
/// the image the `attr` document attribute names, else the theme's
/// `theme_key`; `image:target[attrs]` takes attributes (`fit`, `position`,
/// widths), a value from the theme is resolved against the theme's
/// directory after attribute substitution (`{page-layout}` included), and
/// a bare value from the document against the base directory. `none` is no
/// image; with `symbolic`, `""` and `~` come back as they are.
fn Converter::resolve_background_image(
  self : Converter,
  attr : String?,
  theme_key : String,
  container : (Double, Double),
  symbolic? : Bool = false,
  layout? : String = "portrait",
) -> Background {
  let doc = self.doc
  let (value, from_theme) = match attr.bind(a => doc.attr(a)) {
    Some(v) => (v, false)
    None =>
      match tv(theme_key) {
        Null | Bool(false) => return NoImage
        v => (v.to_ruby_s(), true)
      }
  }
  let key = attr.unwrap_or(theme_key)
  if symbolic && (value == "" || value == "~") {
    return Symbolic(value)
  }
  if value == "none" {
    return Cleared
  }
  match self.resolve_box_image(value, from_theme, container, layout~) {
    Unreadable(resolved) => {
      warning(
        "\{key.replace_all(old="-", new=" ").replace_all(old="_", new=" ")} not found or readable: \{resolved}",
        doc,
      )
      NoImage
    }
    found => found
  }
}

///|
/// The image `value` names (`resolve_background_image` from its value on,
/// `resolve_image_options`): an image macro's attributes size it in the
/// `container`; with `background`, fit defaults to `contain`, else an
/// explicit width applies before fitting.
fn Converter::resolve_box_image(
  self : Converter,
  value : String,
  from_theme : Bool,
  container : (Double, Double),
  layout? : String = "portrait",
  background? : Bool = true,
) -> Background {
  let doc = self.doc
  let mut path = value
  let mut attrs : @core.Attributes? = None
  // relative to: the images directory (an image macro), the theme's
  // directory (a theme value), else the base directory
  let mut relative_to : String? = None
  let mut relative_to_imagesdir = false
  match image_macro(value) {
    Some((target, attrlist)) => {
      attrs = Some(
        @core.parse_attribute_list(attrlist, positional_attrs=[
          Some("alt"),
          Some("width"),
        ]),
      )
      path = target
      relative_to_imagesdir = true
    }
    None => ()
  }
  if from_theme {
    let themesdir = current_theme.val.dir()
    path = with_attrs(
      doc,
      [("imagesdir", themesdir), ("page-layout", layout)],
      () => apply_subs_discretely(doc, path),
    )
    relative_to = Some(themesdir)
    relative_to_imagesdir = false
  } else if path.contains("{page-layout}") {
    path = path.replace(old="{page-layout}", new=layout)
  }
  let (target, format) = image_target_and_format(
    path,
    attrs.bind(a => attrs_str(a, "format")),
  )
  let (data, resolved) = match target {
    Data(bytes) => (Some(bytes), path)
    Path(p) => {
      if p.has_prefix("http://") || p.has_prefix("https://") {
        if !doc.has_attr("allow-uri-read") {
          warning(
            "cannot embed remote image: \{p} (allow-uri-read attribute not enabled)",
            doc,
          )
        }
        return NoImage
      }
      let start = if relative_to_imagesdir {
        match doc.attr("imagesdir") {
          Some("" | "." | "./") | None => None
          dir => dir
        }
      } else {
        relative_to
      }
      let resolved = if p.has_prefix("/") {
        p
      } else {
        match start {
          Some(dir) if dir.has_prefix("/") => @core.expand_path_from(dir, p)
          _ => doc.normalize_system_path(p, start?)
        }
      }
      (doc.vfs().read(resolved), resolved)
    }
  }
  guard data is Some(data) else { return Unreadable(resolved) }
  if format == "pdf" {
    unsupported("image", "format of \{path}", doc)
    return NoImage
  }
  let svg = format == "svg"
  let (cw, ch) = container
  let mut position = "center"
  let mut vposition = "center"
  let mut fit : (Double, Double)? = None
  let mut width : Double? = None
  let mut height : Double? = None
  match attrs {
    Some(attrs) if !background &&
      !attrs.contains("fit") &&
      macro_explicit_width(attrs, cw) is Some(w) => width = Some(w)
    Some(attrs) if !background && !attrs.contains("fit") =>
      fit = Some(container)
    Some(attrs) => {
      if background {
        match attrs_str(attrs, "position").bind(background_position) {
          Some((p, v)) => {
            position = p
            vposition = v
          }
          None => ()
        }
      }
      let image_fit = match attrs_str(attrs, "fit").unwrap_or("contain") {
        "fill" if svg => "contain"
        f => f
      }
      match image_fit {
        "none" => width = macro_explicit_width(attrs, cw)
        "scale-down" =>
          match macro_explicit_width(attrs, cw) {
            Some(w) =>
              if w > cw {
                fit = Some(container)
              } else {
                let (iw, ih) = intrinsic_size(data, svg, container)
                if iw > 0.0 && w * (ih / iw) > ch {
                  fit = Some(container)
                } else {
                  width = Some(w)
                }
              }
            None => {
              let (iw, ih) = intrinsic_size(data, svg, container)
              if iw > cw || ih > ch {
                fit = Some(container)
              }
            }
          }
        "cover" => {
          let (iw, ih) = intrinsic_size(data, svg, container)
          if iw > 0.0 && cw * (ih / iw) < ch {
            height = Some(ch)
          } else {
            width = Some(cw)
          }
        }
        "fill" => {
          width = Some(cw)
          height = Some(ch)
        }
        _ => fit = Some(container)
      }
    }
    None => fit = Some(container)
  }
  Found({
    data,
    svg,
    path: resolved,
    fit,
    width,
    height,
    position,
    vposition,
    link: attrs.bind(a => attrs_str(a, "link")),
    alt: attrs.bind(a => attrs_str(a, "alt")).map(a => "[\{a}]"),
  })
}

///|
/// `body` with document attributes temporarily set (asciidoctor-pdf's
/// `apply_subs_discretely` options).
fn[T] with_attrs(
  doc : @core.Node,
  values : Array[(String, String)],
  body : () -> T,
) -> T {
  let saved = values.map(v => (v.0, doc.attr(v.0)))
  for v in values {
    ignore(doc.set_attr(v.0, value=Str(v.1)))
  }
  let result = body()
  for s in saved {
    match s.1 {
      Some(old) => ignore(doc.set_attr(s.0, value=Str(old)))
      None => ignore(doc.remove_attr(s.0))
    }
  }
  result
}

///|
/// Prawn's `image` of `img` in the box at (`left`, `top`) `w` × `h` (y
/// down), positioned by its `position` and `vposition`: the page items,
/// and the size it was drawn at (None when the image cannot be drawn).
fn Converter::box_image_items(
  self : Converter,
  img : BoxImage,
  left : Double,
  top : Double,
  w : Double,
  h : Double,
  page_h? : Double,
) -> (Array[@pagelayout.PageItem], Double, Double)? {
  let flow = self.flow
  let place = (iw : Double, ih : Double) => {
    let x = match img.position {
      "left" => left
      "right" => left + w - iw
      _ => left + (w - iw) / 2.0
    }
    let y = match img.vposition {
      "top" => top
      "bottom" => top + h - ih
      _ => top + (h - ih) / 2.0
    }
    (x, y)
  }
  if img.svg {
    let doc = @svg.SvgDocument::parse(
      @utf8.decode_lossy(img.data),
      (w, h),
      width?=img.width,
      height?=img.height,
    ) catch {
      @svg.SvgError(message) => {
        if !flow.scratch {
          warning("could not embed image: \{img.path}; \{message}", self.doc)
        }
        return None
      }
    }
    let sizing = doc.sizing
    match (img.fit, img.width, img.height) {
      (Some((fw, fh)), None, None) => {
        // fit to the width, then shrink to the height
        if sizing.output_width != fw {
          sizing.resize(width=fw)
        }
        if sizing.output_height > fh {
          sizing.resize(height=fh)
        }
      }
      _ => ()
    }
    let (iw, ih) = (sizing.output_width, sizing.output_height)
    let (x, y) = place(iw, ih)
    let env = svg_environment(flow.catalog, flow.model, self.doc, img.path)
    let ops = doc.render(
      env,
      origin=(x, y),
      page_height=page_h.unwrap_or(flow.page_h()),
      stack=flow.svg_stack(),
    )
    report_svg_warnings(doc, img.path, self.doc, flow.scratch)
    Some(([Graphic({ x_pt: x, y_pt: y, ops, })], iw, ih))
  } else {
    guard image_info(img.data) is Some((px_w, px_h, mime)) else {
      if !flow.scratch {
        warning("could not embed image: \{img.path}", self.doc)
      }
      return None
    }
    // Prawn's `calc_image_dimensions`: the natural size is a point a pixel
    let (nw, nh) = (px_w.to_double(), px_h.to_double())
    let (iw, ih) = match (img.width, img.height, img.fit) {
      (Some(iw), Some(ih), _) => (iw, ih)
      (Some(iw), None, _) => (iw, nh * iw / nw)
      (None, Some(ih), _) => (nw * ih / nh, ih)
      (None, None, Some((fw, fh))) => {
        let bp = fw / fh
        let ip = nw / nh
        if ip > bp {
          (fw, fw / ip)
        } else {
          (fh * ip, fh)
        }
      }
      (None, None, None) => (nw, nh)
    }
    let (x, y) = place(iw, ih)
    Some(
      (
        [Image({ x_pt: x, y_pt: y, w_pt: iw, h_pt: ih, data: img.data, mime, })],
        iw,
        ih,
      ),
    )
  }
}

///|
/// One side's page background image, as `get_page_bg_image` resolves it for
/// a page layout: `page-background-image`, overridden per side by
/// `page-background-image-recto` and `-verso` (`none` clears it).
fn Converter::page_bg_images(
  self : Converter,
  container : (Double, Double),
  layout : String,
) -> Map[String, BoxImage?] {
  let resolve = (attr : String) => {
    self.resolve_background_image(
      Some(attr),
      attr.replace_all(old="-", new="_"),
      container,
      layout~,
    )
  }
  let base = match resolve("page-background-image") {
    Found(img) => Some(img)
    _ => None
  }
  let sides : Map[String, BoxImage?] = Map([])
  for side in ["verso", "recto"] {
    sides[side] = match resolve("page-background-image-\{side}") {
      Found(img) => Some(img)
      Cleared => None
      _ => base
    }
  }
  sides
}

///|
/// The background of every page (`init_page`): `page_background_color`,
/// then the page background image of the page's side and layout; the title
/// page (`title_page`, an index) takes `title_page_background_color` and
/// `title-page-background-image` instead where they are set.
fn Converter::ink_page_backgrounds(
  self : Converter,
  title_page : Int?,
  front_cover? : Bool = false,
) -> Unit {
  let pages = self.flow.model.pages
  let color = page_background_color()
  let cache : Map[String, Map[String, BoxImage?]] = Map([])
  for index, page in pages {
    // a cover page has no background
    if front_cover && index == 0 {
      continue
    }
    let (w, h) = (page.width_pt, page.height_pt)
    let layout = if w > h { "landscape" } else { "portrait" }
    let side = if (index + 1) % 2 == 1 { "recto" } else { "verso" }
    let is_title = title_page == Some(index)
    let page_color = match (is_title, t_color("title_page_background_color")) {
      (true, Some(c)) => c
      _ => color
    }
    let by_side = match cache.get(layout) {
      Some(sides) => sides
      None => {
        let sides = self.page_bg_images((w, h), layout)
        cache[layout] = sides
        sides
      }
    }
    let mut image = by_side.get(side).unwrap_or(None)
    if is_title {
      match
        self.resolve_background_image(
          Some("title-page-background-image"),
          "title_page_background_image",
          (w, h),
          layout~,
        ) {
        Found(img) => image = Some(img)
        Cleared => image = None
        _ => ()
      }
    }
    let items : Array[@pagelayout.PageItem] = []
    if page_color != rgb_hex(0xFFFFFF) {
      items.push(rect_item(0.0, 0.0, w, h, fill=page_color))
    }
    match image {
      Some(img) =>
        match self.box_image_items(img, 0.0, 0.0, w, h, page_h=h) {
          Some((drawn, _, _)) => items.append(drawn)
          None => ()
        }
      None => ()
    }
    for i, item in items {
      page.items.insert(i, item)
    }
  }
}

///|
/// `ink_cover_page`: the `front-cover-image` or `back-cover-image` (theme
/// `cover_front_image`, `cover_back_image`) on a page of its own, without
/// page background or running content: the front cover on the current
/// (first) page, which the content then leaves, the back cover on a page
/// after the last; `""` is a blank page, `~` no cover. Returns whether a
/// cover page was added.
fn Converter::ink_cover_page(self : Converter, face : String) -> Bool {
  let flow = self.flow
  let (w, h) = (page_width(), page_height())
  let items = match
    self.resolve_background_image(
      Some("\{face}-cover-image"),
      "cover_\{face}_image",
      (w, h),
      symbolic=true,
    ) {
    Symbolic("") => []
    Found(img) =>
      match self.box_image_items(img, 0.0, 0.0, w, h) {
        Some((drawn, _, _)) => drawn
        None => if face == "front" { return false } else { [] }
      }
    _ => return false
  }
  if face == "back" {
    flow.start_new_page()
  }
  flow.model.pages[flow.page].items.append(items)
  if face == "front" {
    flow.start_new_page()
  }
  true
}