///|
// A cursor-driven page flow in the manner of Prawn: content is inked top
// to bottom at a cursor, inside horizontal bounds that blocks indent and
// pad, and a new page starts when the cursor runs out. It emits the
// pagelayout page-model IR directly.
//
// Coordinates are pagelayout's: points, y down from the page top.

///|
/// Prawn's line metrics for a line height factor (asciidoctor-pdf's
/// `calc_line_metrics`).
priv struct LineMetrics {
  leading : Double
  padding_top : Double
  padding_bottom : Double
}

///|
fn line_metrics(factor : Double, face : Face, size : Double) -> LineMetrics {
  let leading = factor * size - size
  let half = leading / 2.0
  { leading, padding_top: half + face.line_gap_pt(size), padding_bottom: half, }
}

///|
/// A point in the flow: page, cursor, and how many items the page held,
/// so backgrounds can be slid underneath content inked after it.
priv struct Mark {
  page : Int
  y : Double
  item : Int
}

///|
/// A column box (Prawn's ColumnBox with `reflow_margins`): the bounds are
/// split into `count` columns `gap` apart; running out of room moves to
/// the next column at the box's top, and past the last one to a new page,
/// where the box starts at the page's top.
priv struct Columns {
  count : Int
  /// the distance between the left edges of adjacent columns
  stride : Double
  mut current : Int
}

///|
priv struct Flow {
  catalog : FontCatalog
  model : @pagelayout.PageModel
  /// a dry run (asciidoctor-pdf's scratch document): destinations and
  /// index pages are not recorded
  scratch : Bool
  /// the top of the page's content area
  page_top : Double
  /// the top of the current frame: `page_top`, or where a column box
  /// starts on its first page
  mut top : Double
  mut bottom : Double
  mut page : Int
  mut y : Double
  mut left : Double
  mut right : Double
  mut columns : Columns?
  /// the last line `typeset` placed: the line, its page and its baseline
  mut last_line : (Line, Int, Double)?
  /// whether that line was the first of its text (no hanging indent)
  mut last_line_first : Bool
  /// while above zero, what is inked does not count as content (Prawn's
  /// `tare_first_page_content_stream`: captions and backgrounds)
  mut taring : Int
  /// content was inked since the flow started, before its first page break
  mut inked : Bool
  /// at the flow's first page break: whether nothing had been inked
  /// (asciidoctor-pdf's `DetectEmptyFirstPage`); None before it
  mut first_break_empty : Bool?
  /// the left and right margins of a page by index, when they vary by page
  /// (`media=prepress`)
  mut page_margins : ((Int) -> (Double, Double))?
  /// the current page's left and right margins
  mut margin_left : Double
  mut margin_right : Double
  /// the size of the page the bounds were set for
  mut page_width_now : Double
  mut page_height_now : Double
}

///|
fn Flow::new(catalog : FontCatalog, scratch? : Bool = false) -> Flow {
  let setup = page_setup.val
  {
    catalog,
    model: @pagelayout.PageModel::new(),
    scratch,
    page_top: setup.margin_top,
    top: setup.margin_top,
    bottom: setup.height - setup.margin_bottom,
    page: -1,
    y: setup.margin_top,
    left: setup.margin_left,
    right: setup.width - setup.margin_right,
    columns: None,
    last_line: None,
    last_line_first: false,
    taring: 0,
    inked: false,
    first_break_empty: None,
    page_margins: None,
    margin_left: setup.margin_left,
    margin_right: setup.margin_right,
    page_width_now: setup.width,
    page_height_now: setup.height,
  }
}

///|
/// Take the left and right margins of page `index` (`page_margins`; for
/// `media=prepress`, asciidoctor-pdf's `init_page` sets the recto or verso
/// margins), keeping any indentation from the margins.
fn Flow::apply_page_margins(self : Flow, index : Int) -> Unit {
  match self.page_margins {
    Some(margins_of) => {
      let (l, r) = margins_of(index)
      let width = self.model.pages[index].width_pt
      self.left = l + (self.left - self.margin_left)
      self.right = width - r - (width - self.margin_right - self.right)
      self.margin_left = l
      self.margin_right = r
    }
    None => ()
  }
}

///|
fn Flow::start_new_page(self : Flow, layout? : String) -> Unit {
  // a new page takes the current page's size and layout (Prawn's
  // `start_new_page`), or the layout asked for: landscape swaps the sides
  let (w, h) = match self.model.pages.last() {
    Some(p) => (p.width_pt, p.height_pt)
    None => (page_width(), page_height())
  }
  let (w, h) = match layout {
    Some("landscape") if w < h => (h, w)
    Some("portrait") if w > h => (h, w)
    _ => (w, h)
  }
  self.model.pages.push({ width_pt: w, height_pt: h, items: [], })
  self.page = self.model.pages.length() - 1
  self.fit_page_size()
  self.apply_page_margins(self.page)
  self.top = self.page_top
  self.y = self.top
}

///|
/// The current page's height (the PDF's y axis is measured up from its
/// bottom).
fn Flow::page_h(self : Flow) -> Double {
  match self.model.pages.get(self.page) {
    Some(p) => p.height_pt
    None => page_height()
  }
}

///|
/// Fit the bounds to the current page's size: its bottom margin and right
/// margin stay where the page setup puts them.
fn Flow::fit_page_size(self : Flow) -> Unit {
  guard self.model.pages.get(self.page) is Some(p) else { return }
  // only a change of size moves the bounds (a flow may have bounds of its
  // own, as a table cell's or a dry run's)
  if p.height_pt != self.page_height_now {
    self.bottom += p.height_pt - self.page_height_now
    self.page_height_now = p.height_pt
  }
  if p.width_pt != self.page_width_now {
    self.right += p.width_pt - self.page_width_now
    self.page_width_now = p.width_pt
  }
}

///|
/// Move to the next page, creating it unless an earlier pass already did
/// (a block's background revisits pages its content spilled onto); in a
/// column box, to the next column first (Prawn's `move_past_bottom`).
fn Flow::advance_page(self : Flow) -> Unit {
  match self.columns {
    Some(columns) if columns.current < columns.count - 1 => {
      columns.current += 1
      self.left += columns.stride
      self.right += columns.stride
      self.y = self.top
      return
    }
    Some(columns) => {
      let back = columns.stride * columns.current.to_double()
      self.left -= back
      self.right -= back
      columns.current = 0
    }
    None => ()
  }
  if self.first_break_empty is None {
    self.first_break_empty = Some(!self.inked)
  }
  if self.page + 1 < self.model.pages.length() {
    self.go_to_page(self.page + 1)
    self.top = self.page_top
    self.y = self.top
  } else {
    self.start_new_page()
  }
}

///|
/// Whether the current page is a verso page (an even page number).
fn Flow::on_verso(self : Flow) -> Bool {
  (self.page + 1) % 2 == 0
}

///|
/// Make page `index` the current page (Prawn's `go_to_page`), in its
/// margins; the cursor is left where it is.
fn Flow::go_to_page(self : Flow, index : Int) -> Unit {
  self.page = index
  self.fit_page_size()
  self.apply_page_margins(index)
}

///|
/// In a column box, move to its last column (Prawn's
/// `bounds.current_column = bounds.last_column`), so the next page break
/// starts a new page.
fn Flow::to_last_column(self : Flow) -> Unit {
  match self.columns {
    Some(columns) => {
      let shift = columns.stride *
        (columns.count - 1 - columns.current).to_double()
      self.left += shift
      self.right += shift
      columns.current = columns.count - 1
    }
    None => ()
  }
}

///|
/// Lay out `body` in `count` columns `gap` apart, starting at the cursor
/// (Prawn's `column_box` with `reflow_margins: true`); the cursor ends
/// where the first column did, or, when the box went on past it, at the
/// bottom of the last one.
fn Flow::column_box(
  self : Flow,
  count : Int,
  gap : Double,
  body : () -> Unit,
) -> Unit {
  let width = self.width()
  let bare = (width - gap * (count - 1).to_double()) / count.to_double()
  let saved = (self.left, self.right, self.top, self.columns)
  let columns : Columns = { count, stride: bare + gap, current: 0, }
  self.columns = Some(columns)
  self.top = self.y
  self.right = self.left + bare
  let start_page = self.page
  body()
  let current = columns.current
  self.columns = saved.3
  self.left = saved.0
  self.right = saved.1
  self.top = if self.page == start_page { saved.2 } else { self.page_top }
  if current > 0 {
    // the box stretched to the bottom of the columns before the last
    self.y = self.bottom
  }
}

///|
fn Flow::items(self : Flow) -> Array[@pagelayout.PageItem] {
  self.model.pages[self.page].items
}

///|
fn Flow::push(self : Flow, item : @pagelayout.PageItem) -> Unit {
  if !(item is Anchor(_)) {
    self.mark_inked()
  }
  self.items().push(item)
}

///|
/// Content goes on the page (see `inked`).
fn Flow::mark_inked(self : Flow) -> Unit {
  if self.taring == 0 {
    self.inked = true
  }
}

///|
/// Ink `body` as tared content: captions and backgrounds, which do not
/// keep a block on a page by themselves.
fn Flow::tared(self : Flow, body : () -> Unit) -> Unit {
  self.taring += 1
  body()
  self.taring -= 1
}

///|
/// Space left above the bottom margin (Prawn's `cursor`).
fn Flow::cursor(self : Flow) -> Double {
  self.bottom - self.y
}

///|
fn Flow::at_page_top(self : Flow) -> Bool {
  (self.y - self.top).abs() < 0.0001
}

///|
fn Flow::width(self : Flow) -> Double {
  self.right - self.left
}

///|
fn Flow::move_down(self : Flow, amount : Double) -> Unit {
  self.y += amount
}

///|
/// asciidoctor-pdf's `margin`: nothing at the top of a page, a move down
/// when it fits, else a new page.
fn Flow::margin(self : Flow, amount : Double) -> Unit {
  if amount == 0.0 || self.at_page_top() {
    return
  }
  if self.cursor() > amount {
    self.move_down(amount)
  } else {
    self.advance_page()
  }
}

///|
/// The bottom padding of a padded box: a move down when it fits, else the
/// next page (Prawn's `move_past_bottom`).
fn Flow::pad_bottom(self : Flow, amount : Double) -> Unit {
  if self.at_page_top() {
    return
  }
  if self.cursor() > amount {
    self.move_down(amount)
  } else {
    self.advance_page()
  }
}

///|
fn[T] Flow::indent(
  self : Flow,
  left : Double,
  right : Double,
  body : () -> T,
) -> T {
  self.left += left
  self.right -= right
  let result = body()
  self.left -= left
  self.right += right
  result
}

///|
fn Flow::mark(self : Flow) -> Mark {
  { page: self.page, y: self.y, item: self.items().length(), }
}

///|
fn Flow::font(self : Flow, style : Style) -> Face {
  let (bold, italic) = style.face_style()
  self.catalog.face(style.family, bold, italic)
}

///|
/// Prawn's `width_of` a plain string in `style`'s font, trailing spaces
/// included (asciidoctor-pdf's `rendered_width_of_string`): the truncated
/// glyph widths, kerned.
fn Flow::width_of(self : Flow, text : String, style : Style) -> Double {
  let face = self.font(style)
  let mut width = 0.0
  let n = text.length()
  for i in 0.. Unit {
  self.push(Anchor({ name: pdf_anchor_name(name), x_pt: x, y_pt: y, }))
}

///|
fn rect_item(
  x : Double,
  y : Double,
  w : Double,
  h : Double,
  fill? : Color,
  stroke? : Color,
  stroke_width? : Double = 0.0,
) -> @pagelayout.PageItem {
  Rect({
    x_pt: x,
    y_pt: y,
    w_pt: w,
    h_pt: h,
    fill,
    stroke,
    stroke_w_pt: stroke_width,
  })
}

///|
/// A horizontal rule `width` thick centred on `y`.
fn hrule(
  x1 : Double,
  x2 : Double,
  y : Double,
  width : Double,
  color : Color,
) -> @pagelayout.PageItem {
  rect_item(x1, y - width / 2.0, x2 - x1, width, fill=color)
}

///|
/// A dashed horizontal rule (dashes and gaps four times its width, as
/// Prawn's `dash` in asciidoctor-pdf's dashed style), as filled segments.
fn dashed_hrule(
  x1 : Double,
  x2 : Double,
  y : Double,
  width : Double,
  color : Color,
) -> Array[@pagelayout.PageItem] {
  let dash = width * 4.0
  let items = []
  let mut x = x1
  while x < x2 {
    items.push(hrule(x, @cmp.minimum(x + dash, x2), y, width, color))
    x += dash * 2.0
  }
  items
}

///|
/// A vertical rule `width` thick centred on `x`.
fn vrule(
  x : Double,
  y1 : Double,
  y2 : Double,
  width : Double,
  color : Color,
) -> @pagelayout.PageItem {
  rect_item(x - width / 2.0, y1, width, y2 - y1, fill=color)
}

///|
/// The items that draw one placed line with its left edge at `x0` and its
/// baseline at `baseline`: backgrounds, glyph runs, decorations, link
/// regions and anchors.
fn Flow::line_items(
  self : Flow,
  line : Line,
  x0 : Double,
  baseline : Double,
) -> Array[@pagelayout.PageItem] {
  let items : Array[@pagelayout.PageItem] = []
  // inline images (InlineImageRenderer): one taller than one and a half
  // lines sits on the text's descender, a smaller one is centred on its
  // placeholder's box
  for placed in line.images {
    let (x, index, w, h, increased, style, face) = placed
    let image = session().inline_images[index]
    let a = face.ascender_pt(style.size)
    let d = face.descender_pt(style.size)
    let (top, box_top, box_height) = if increased {
      (baseline + d - h, baseline + d - h, h)
    } else {
      (baseline - a + (a + d - h) / 2.0, baseline - a, a + d)
    }
    match image.svg {
      Some(svg) =>
        match self.inline_svg_item(svg, x0 + x, top, w) {
          Some(item) => items.push(item)
          None => ()
        }
      None =>
        items.push(
          Image({
            x_pt: x0 + x,
            y_pt: top,
            w_pt: w,
            h_pt: h,
            data: image.data,
            mime: image.mime,
          }),
        )
    }
    match style.link {
      Some(target) =>
        items.push(
          Link({
            x_pt: x0 + x,
            y_pt: box_top,
            w_pt: w,
            h_pt: box_height,
            target,
          }),
        )
      None => ()
    }
  }
  for link in line.empty_links {
    let (x, style, face) = link
    guard style.link is Some(target) else { continue }
    let a = face.ascender_pt(style.size)
    items.push(
      Link({
        x_pt: x0 + x,
        y_pt: baseline - a,
        w_pt: 0.0,
        h_pt: a + face.descender_pt(style.size),
        target,
      }),
    )
  }
  for anchor in line.anchors {
    items.push(
      Anchor({
        name: pdf_anchor_name(anchor.0),
        x_pt: x0 + anchor.1,
        y_pt: baseline - line.ascender,
      }),
    )
  }
  for run in line.runs {
    let style = run.style
    let x = x0 + run.x
    let a = run.face.ascender_pt(style.size)
    let d = run.face.descender_pt(style.size)
    let rise = match style.script {
      1 => 0.85 * a
      -1 => -d
      _ => 0.0
    }
    match style.background {
      Some(color) =>
        items.push(
          {
            let o = style.border_offset
            rect_item(
              x - o,
              baseline - rise - a - o,
              run.width + 2.0 * o,
              a + d + 2.0 * o,
              fill=color,
            )
          },
        )
      None => ()
    }
    let (bold, italic) = style.face_style()
    let font = self.model.add_font({ family: style.family, bold, italic, })
    items.push(
      Text({
        font,
        size_pt: style.size,
        x_pt: x,
        baseline_pt: baseline - rise,
        text: run.text,
        advances_pt: run.advances,
        color: style.color,
      }),
    )
    let thickness = style.size / 18.0
    if style.underline {
      items.push(
        hrule(
          x,
          x + run.width,
          baseline + style.size * 0.1,
          thickness,
          style.color,
        ),
      )
    }
    if style.strike {
      items.push(
        hrule(
          x,
          x + run.width,
          baseline - style.size * 0.3,
          thickness,
          style.color,
        ),
      )
    }
    match style.link {
      Some(target) =>
        items.push(
          Link({
            x_pt: x,
            y_pt: baseline - rise - a,
            w_pt: run.width,
            h_pt: a + d,
            target,
          }),
        )
      None => ()
    }
  }
  items
}

///|
/// Draw one placed line on the current page.
fn Flow::ink_line(
  self : Flow,
  line : Line,
  x0 : Double,
  baseline : Double,
) -> Unit {
  if !line.runs.is_empty() || !line.images.is_empty() {
    self.mark_inked()
  }
  self.items().append(self.line_items(line, x0, baseline))
  if !self.scratch {
    // an index term's anchor tells the index its page
    // (InlineDestinationMarker)
    for anchor in line.anchors {
      if anchor.0.has_prefix("__indexterm-") {
        session().index.link_dest_to_page(anchor.0, self.page + 1)
      }
    }
  }
}

///|
/// The ascender, descender and height a placed line takes: its fragments'
/// largest, or the document font's for an empty line. With
/// `normalize_line_height`, the document font's count on every line, as
/// asciidoctor-pdf's arranger puts a zero-width space in that font at the
/// start of each line (so a line of code in a list takes the prose line's
/// height, not the smaller one of the monospace font).
fn line_extent(
  line : Line,
  base_face : Face,
  size : Double,
  normalize_line_height : Bool,
) -> (Double, Double, Double) {
  let a = base_face.ascender_pt(size)
  let d = base_face.descender_pt(size)
  let h = base_face.height_pt(size)
  if line.height == 0.0 {
    (a, d, h)
  } else if normalize_line_height {
    (
      @cmp.maximum(a, line.ascender),
      @cmp.maximum(d, line.descender),
      @cmp.maximum(h, line.height),
    )
  } else {
    (line.ascender, line.descender, line.height)
  }
}

///|
/// Ink formatted text at the cursor, flowing onto new pages as needed —
/// asciidoctor-pdf's `typeset_text` over Prawn's `text`.
fn Flow::typeset(
  self : Flow,
  fragments : Array[Fragment],
  metrics : LineMetrics,
  base : Style,
  align? : Alignment = Left,
  bottom_gutter? : Double = 0.0,
  hanging_indent? : Double = 0.0,
  normalize_line_height? : Bool = false,
  first_line_style? : Style,
  text_indent? : Double = 0.0,
) -> Unit {
  self.last_line = None
  // a hanging indent: the first line takes the whole width, the next ones
  // are indented (Prawn's `indent_paragraphs` negated inside an indent);
  // a text indent (`indent_paragraphs`): the first line of each paragraph
  // (the text, and each line after a hard break) is indented;
  // a first line in a style of its own is broken off first
  let mut paragraph_starts : Array[Bool] = []
  let lines = match first_line_style {
    Some(first) => {
      // the first line box is indented (Prawn's `indent_paragraphs` in
      // `text_with_formatted_first_line`); the rest is not
      let (line, rest) = split_first_line(
        restyle_fragments(fragments, first),
        fragments,
        self.catalog,
        self.width() - text_indent,
        align,
      )
      if text_indent > 0.0 {
        paragraph_starts = [true]
      }
      if rest.is_empty() {
        [line]
      } else {
        [line, ..typeset_lines(rest, self.catalog, self.width(), align)]
      }
    }
    None if text_indent > 0.0 => {
      let (lines, starts) = typeset_paragraph_lines(
        fragments,
        self.catalog,
        self.width(),
        align,
        first_width=self.width() - text_indent,
        indent_paragraphs=true,
      )
      paragraph_starts = starts
      lines
    }
    None =>
      typeset_lines(
        fragments,
        self.catalog,
        self.width() - hanging_indent,
        align,
        first_width=self.width(),
      )
  }
  let base_face = self.font(base)
  let mut index = 0
  while index < lines.length() {
    // Prawn's text box: the first line hangs from the top by its ascender,
    // each next one drops by its line height plus the leading
    // a line taller than a fresh page is inked anyway rather than looping
    let fresh = self.at_page_top()
    self.y += metrics.padding_top
    let box_top = self.y
    let available = self.cursor()
    let mut baseline = 0.0
    let mut depth = 0.0
    let mut placed = 0
    while index < lines.length() {
      let line = lines[index]
      let (a, d, h) = line_extent(
        line,
        base_face,
        base.size,
        normalize_line_height,
      )
      let next = if placed == 0 { a } else { baseline + h + metrics.leading }
      // the last line keeps clear of the padding below it (asciidoctor-pdf's
      // bottom gutter)
      let gutter = if index == lines.length() - 1 { bottom_gutter } else { 0.0 }
      if next + d + gutter > available + 0.0001 && !(fresh && placed == 0) {
        break
      }
      baseline = next
      depth = d
      let x = if paragraph_starts.get(index) is Some(true) {
        self.left + text_indent
      } else if index == 0 {
        self.left
      } else {
        self.left + hanging_indent
      }
      self.ink_line(line, x, box_top + baseline)
      self.last_line = Some((line, self.page, box_top + baseline))
      self.last_line_first = index == 0
      placed += 1
      index += 1
    }
    if placed > 0 {
      self.y = box_top + baseline + depth
    }
    if index < lines.length() {
      self.advance_page()
    }
  }
  self.y += metrics.padding_bottom
}

///|
/// The height `typeset` would take for `fragments` in the current bounds
/// (asciidoctor-pdf's `height_of_typeset_text`), ignoring page breaks.
fn Flow::height_of(
  self : Flow,
  fragments : Array[Fragment],
  metrics : LineMetrics,
  base : Style,
  normalize_line_height? : Bool = false,
) -> Double {
  let lines = typeset_lines(fragments, self.catalog, self.width(), Left)
  let base_face = self.font(base)
  let mut baseline = 0.0
  let mut depth = 0.0
  for index, line in lines {
    let (a, d, h) = line_extent(
      line,
      base_face,
      base.size,
      normalize_line_height,
    )
    baseline = if index == 0 { a } else { baseline + h + metrics.leading }
    depth = d
  }
  metrics.padding_top + baseline + depth + metrics.padding_bottom
}

///|
/// Slide `make(page, top, bottom, first, last)` underneath everything inked
/// since `start`, on every page from `start` to the cursor: the painter's
/// order of a background drawn before its content, without a dry run.
fn Flow::underlay(
  self : Flow,
  start : Mark,
  make : (Double, Double, Bool, Bool) -> Array[@pagelayout.PageItem],
) -> Unit {
  let end_page = self.page
  let end_y = self.y
  for page in start.page..<=end_page {
    let first = page == start.page
    let last = page == end_page
    let top = if first { start.y } else { self.page_top }
    let bottom = if last { end_y } else { self.bottom }
    if bottom - top <= 0.0 {
      continue
    }
    let items = self.model.pages[page].items
    let at = if first { start.item } else { 0 }
    for offset, item in make(top, bottom, first, last) {
      items.insert(at + offset, item)
    }
  }
}