///|
// Line breaking of formatted text, following Prawn's greedy wrap
// (Prawn::Text::Formatted::LineWrap): each fragment is cut into tokens —
// runs of word characters (ending in hyphens or a soft hyphen when they
// have them), runs of whitespace, and runs of hyphens with the word after
// them — and a line may break between any two tokens, and between
// fragments. A token longer than the line wraps by character, spaces
// starting a line are dropped and trailing ones do not count, and a
// justified line widens its spaces, except on the last line of a paragraph
// and before a hard break.
//
// Two widths are tracked per character. `fit` is Prawn's (glyph widths
// truncated to 1/1000 em, kerned within a token only, as Prawn measures
// token by token) and decides where lines break, so breaks match Ruby;
// `draw` is the width the PDF declares for the glyph, kerned with the next
// character of the fragment as Prawn draws it, and places glyphs, so the
// emitted text needs a displacement only where kerning or justification
// moves a glyph.

///|
priv enum Alignment {
  Left
  Center
  Right
  Justify
} derive(Eq)

///|
priv struct Item {
  unit : Int
  fragment : Int
  mut fit : Double
  draw : Double
  /// kerning with the next character, points; part of `draw`, and of
  /// `fit` unless a token boundary falls between the two
  kern : Double
  hard_break : Bool
  anchor : String?
  /// the position of its code unit in its fragment's text, or -1
  offset : Int
}

///|
/// One placed run of a line; `x` is relative to the line's left edge.
priv struct LineRun {
  /// the index of the fragment it is part of
  fragment : Int
  text : String
  style : Style
  face : Face
  x : Double
  advances : Array[Double]
  width : Double
}

///|
priv struct Line {
  runs : Array[LineRun]
  anchors : Array[(String, Double)]
  /// the width Prawn measures (truncated glyph widths)
  fit_width : Double
  ascender : Double
  descender : Double
  height : Double
  /// links whose fragment has nothing on the line but the spaces ending
  /// it: Prawn still annotates them, with no width, at the line's end
  /// (x, style, face)
  empty_links : Array[(Double, Style, Face)]
  /// inline images: x, their index, width, height, whether they make the
  /// line taller, and their fragment's style and face
  images : Array[(Double, Int, Double, Double, Bool, Style, Face)]
}

///|
/// The unit of an item that only takes up room: the padding a fragment
/// with a `border_offset` has on each side.
let pad_unit = -1

///|
fn is_break_space(unit : Int) -> Bool {
  unit == 0x20
}

///|
/// Prawn's whitespace break characters: space, tab, zero-width space.
fn is_whitespace(unit : Int) -> Bool {
  unit == 0x20 || unit == 0x09 || unit == 0x200B
}

///|
/// Where Prawn's tokens start: `starts[i]` when a line may break before
/// `items[i]`.
fn token_starts(items : Array[Item]) -> Array[Bool] {
  let n = items.length()
  let starts = Array::make(n, false)
  let kind = fn(i : Int) -> Int {
    let unit = items[i].unit
    // an anchor stands for asciidoctor-pdf's placeholder text, a NUL: part
    // of the word it touches, and no break opportunity
    if items[i].anchor is Some(_) {
      0
    } else if is_whitespace(unit) {
      1
    } else if unit == 0x2D {
      2
    } else if unit == 0xAD {
      3
    } else {
      0
    }
  }
  let mut i = 0
  while i < n {

    // tokens run across fragments: Prawn pulls the last word of a fragment
    // onto the next line with the word the next fragment starts with (its
    // `pull_preceding_fragment_to_join_this_one?`), so a style change
    // inside a word is no break opportunity; a hard break ends every token
    let mut end = i + 1
    while end < n && !items[end].hard_break {
      end += 1
    }
    let mut pos = i
    while pos < end {
      starts[pos] = true
      let mut j = pos
      match kind(pos) {
        0 => {
          while j < end && kind(j) == 0 {
            j += 1
          }
          // the hyphens ending a word are its own, not those a next
          // fragment starts with (Prawn tokenizes each fragment: `-common`
          // is a token of its own, and a place to break)
          let own = fn(k : Int) { items[k].fragment == items[k - 1].fragment }
          if j < end && kind(j) == 3 && own(j) {
            j += 1
          } else {
            while j < end && kind(j) == 2 && own(j) {
              j += 1
            }
          }
        }
        1 =>
          while j < end && kind(j) == 1 {
            j += 1
          }
        2 => {
          while j < end && kind(j) == 2 {
            j += 1
          }
          while j < end && kind(j) == 0 {
            j += 1
          }
        }
        _ => j += 1
      }
      pos = j
    }
    i = end
  }
  starts
}

///|
fn build_items(
  fragments : Array[Fragment],
  catalog : FontCatalog,
  available? : Double = 1.0e9,
) -> (Array[Item], Array[Face]) {
  let items : Array[Item] = []
  let faces : Array[Face] = []
  for index, fragment in fragments {
    let style = fragment.style
    let (bold, italic) = style.face_style()
    let face = catalog.face(style.family, bold, italic)
    faces.push(face)
    let blank : Item = {
      unit: 0,
      fragment: index,
      fit: 0.0,
      draw: 0.0,
      kern: 0.0,
      hard_break: false,
      anchor: None,
      offset: -1,
    }
    match fragment.anchor {
      Some(name) => {
        items.push({ ..blank, anchor: Some(name), })
        continue
      }
      None => ()
    }
    if style.image >= 0 {
      // an inline image: one placeholder as wide as the image
      let (width, _, _) = inline_image_size(
        session().inline_images[style.image],
        available,
        face.height_pt(style.size),
      )
      items.push({ ..blank, unit: 0x2063, fit: width, draw: width, offset: 0, })
      continue
    }
    let text = fragment.text
    let n = text.length()
    let scale = style.size / 1000.0
    let pad : Item = {
      ..blank,
      unit: pad_unit,
      fit: style.border_offset,
      draw: style.border_offset,
    }
    if style.border_offset > 0.0 && n > 0 {
      items.push(pad)
    }
    for i in 0..= 0xDC00 && unit <= 0xDFFF) {
        items.push({ ..blank, unit, offset: i, })
        continue
      }
      let cp = if unit >= 0xD800 && unit <= 0xDBFF && i + 1 < n {
        0x10000 + ((unit - 0xD800) << 10) + (text[i + 1].to_int() - 0xDC00)
      } else {
        unit
      }
      let kern = if i + 1 < n {
        face.kerning(cp, text[i + 1].to_int()) * scale
      } else {
        0.0
      }
      items.push({
        ..blank,
        unit,
        offset: i,
        fit: face.prawn_width(cp) * scale,
        draw: face.drawn_width(cp) * scale + kern,
        kern,
      })
    }
    if style.border_offset > 0.0 && n > 0 {
      items.push(pad)
    }
  }
  (items, faces)
}

///|
/// Break `fragments` into lines of at most `width` (the first line
/// `first_width`, and with `indent_paragraphs` also each line after a hard
/// break: Prawn's `indent_paragraphs` starts a paragraph there) and place
/// them.
fn typeset_lines(
  fragments : Array[Fragment],
  catalog : FontCatalog,
  width : Double,
  align : Alignment,
  first_width? : Double = width,
  indent_paragraphs? : Bool = false,
) -> Array[Line] {
  typeset_paragraph_lines(
    fragments,
    catalog,
    width,
    align,
    first_width~,
    indent_paragraphs~,
  ).0
}

///|
/// `typeset_lines`, and whether each line starts a paragraph (the first
/// line, and with `indent_paragraphs` each line after a hard break).
fn typeset_paragraph_lines(
  fragments : Array[Fragment],
  catalog : FontCatalog,
  width : Double,
  align : Alignment,
  first_width? : Double = width,
  indent_paragraphs? : Bool = false,
) -> (Array[Line], Array[Bool]) {
  let fragments = catalog.apply_fallbacks(fragments)
  let (items, faces, ranges) = break_lines(
    fragments,
    catalog,
    width,
    first_width,
    indent_paragraphs~,
  )
  let lines : Array[Line] = []
  let starts : Array[Bool] = []
  for index, range in ranges {
    let (from, to, hard) = range
    let is_last = index == ranges.length() - 1
    let start = index == 0 || (indent_paragraphs && ranges[index - 1].2)
    let limit = if start { first_width } else { width }
    lines.push(
      place_line(
        fragments,
        faces,
        items,
        from,
        to,
        limit,
        align,
        align == Justify && !is_last && !hard,
        hard,
      ),
    )
    starts.push(start)
  }
  (lines, starts)
}

///|
/// Break `fragments` into lines: their items (see `build_items`), faces,
/// and each line's item range (start, end, whether a hard break ends it).
fn break_lines(
  fragments : Array[Fragment],
  catalog : FontCatalog,
  width : Double,
  first_width : Double,
  indent_paragraphs? : Bool = false,
) -> (Array[Item], Array[Face], Array[(Int, Int, Bool)]) {
  let (items, faces) = build_items(fragments, catalog, available=width)
  let n = items.length()
  let starts = token_starts(items)
  // kerning counts towards fitting only inside a token
  for i in 0..= n || !starts[i + 1] {
      items[i].fit += items[i].kern
    }
  }
  let joined = joined_widths(fragments, faces)
  let ranges : Array[(Int, Int, Bool)] = [] // start, end, ended by hard break
  let mut start = 0
  while start <= n {
    if start >= n {
      if ranges.is_empty() || ranges[ranges.length() - 1].2 {
        ranges.push((start, start, false))
      }
      break
    }
    let limit = if ranges.is_empty() ||
      (indent_paragraphs && ranges[ranges.length() - 1].2) {
      first_width
    } else {
      width
    }
    let mut w = 0.0
    // spaces a line starts with are dropped, as Prawn strips the first
    // fragment of each line (anchors in front of them notwithstanding)
    let mut visible = false
    let mut last_break = -1
    let mut i = start
    let mut end = -1
    let mut hard = false
    while i < n {
      let item = items[i]
      if item.hard_break {
        end = i
        hard = true
        break
      }
      if visible && starts[i] {
        last_break = i
      }
      if is_break_space(item.unit) {
        if !visible {
          // dropped: it becomes a zero-width space, which draws nothing
          items[i] = { ..item, unit: 0x200B, fit: 0.0, draw: 0.0, }
        } else if w + item.fit > limit + 0.0001 {
          // a space that does not fit ends the line before it, as any
          // segment does: it goes to the next line (and is dropped there),
          // so its fragment takes no part in this line's height
          end = i
          break
        } else {
          w += item.fit
        }
        i += 1
        continue
      }
      // the last word of a fragment a word joiner follows must fit with
      // the joiner's first word (asciidoctor-pdf's LineWrap)
      let last_of_fragment = i + 1 >= n ||
        items[i + 1].fragment != item.fragment
      let joiner = if last_of_fragment { joined[item.fragment] } else { 0.0 }
      if w + item.fit + joiner > limit + 0.0001 && visible {
        end = if last_break > start {
          pull_back_limit(items, start, last_break, i)
        } else {
          char_wrap_limit(items, starts, start, i, w, limit)
        }
        break
      }
      w += item.fit
      if item.anchor is None && item.unit != 0x200B {
        visible = true
      }
      i += 1
    }
    if end < 0 {
      end = n
    }
    ranges.push((start, end, hard))
    start = if hard { end + 1 } else { end }
    if hard && start == n {
      // a trailing hard break ends the text
      break
    }
  }
  (items, faces, ranges)
}

///|
/// Prawn's first-line box (`text_with_formatted_first_line`): the first
/// line of `first` (the fragments in the first line's style), placed and
/// justified when text follows, and what follows of `rest` (the same
/// fragments in the regular style) from where the next line starts.
fn split_first_line(
  first : Array[Fragment],
  rest : Array[Fragment],
  catalog : FontCatalog,
  width : Double,
  align : Alignment,
) -> (Line, Array[Fragment]) {
  let (items, faces, ranges) = break_lines(first, catalog, width, width)
  let (from, to, hard) = ranges[0]
  let next = if hard { to + 1 } else { to }
  let remaining : Array[Fragment] = []
  let mut previous = -1
  for k in next..= 0 {
      let fragment = rest[item.fragment]
      remaining.push({
        ..fragment,
        text: fragment.text[item.offset:].to_owned(),
      })
      previous = item.fragment
    }
  }
  let justify = align == Justify && !hard && !remaining.is_empty()
  let line = place_line(
    first, faces, items, from, to, width, align, justify, hard,
  )
  (line, remaining)
}

///|
/// `fragments` set in `style`'s document font style (bold, italic) where
/// their markup does not name one (Prawn's box `:style` option).
fn restyle_fragments(
  fragments : Array[Fragment],
  style : Style,
) -> Array[Fragment] {
  fragments.map(f => {
    ..f,
    style: {
      ..f.style,
      doc_bold: style.doc_bold,
      doc_italic: style.doc_italic,
    },
  })
}

///|
/// For each fragment followed by word joiners (`class="wj"`) and not one
/// itself: the width of the joiners' first word, measured in the
/// fragment's own font (Arranger#preview_joined_string, as LineWrap
/// measures it: the joined text of the following joiners, their NUL
/// placeholders left out, up to its first break character); else 0.
fn joined_widths(
  fragments : Array[Fragment],
  faces : Array[Face],
) -> Array[Double] {
  let widths = Array::make(fragments.length(), 0.0)
  for f in 0..= fragments.length() ||
      !fragments[f + 1].style.wj {
      continue
    }
    let sb = StringBuilder()
    let mut g = f + 1
    while g < fragments.length() && fragments[g].style.wj {
      if fragments[g].anchor is None {
        sb.write_string(fragments[g].text)
      }
      g += 1
    }
    let text = sb.to_string()
    let face = faces[f]
    let size = fragments[f].style.size
    let mut width = 0.0
    let mut k = 0
    while k < text.length() && !is_break_char(text[k].to_int()) {
      width += face.prawn_width(text[k].to_int())
      if k + 1 < text.length() && !is_break_char(text[k + 1].to_int()) {
        width += face.kerning(text[k].to_int(), text[k + 1].to_int())
      }
      k += 1
    }
    widths[f] = width * size / 1000.0
  }
  widths
}

///|
/// Where a line without a break opportunity ends when its word overflows
/// at `items[i]` (`w` wide before it): Prawn wraps that word by character
/// (`wrap_by_char`), measuring each character on its own, without the
/// kerning its word was measured with, and keeps it while it fits.
fn char_wrap_limit(
  items : Array[Item],
  starts : Array[Bool],
  start : Int,
  i : Int,
  w : Double,
  limit : Double,
) -> Int {
  let n = items.length()
  // the width of an item without the kerning with the next character of
  // its token (see `typeset_lines`)
  let plain = fn(k : Int) {
    if k + 1 < n && !starts[k + 1] {
      items[k].fit - items[k].kern
    } else {
      items[k].fit
    }
  }
  // the segment that overflowed: its token, within its fragment (what
  // came before it in other fragments was measured as it fit)
  let mut token = i
  while token > start &&
        !starts[token] &&
        items[token - 1].fragment == items[i].fragment {
    token -= 1
  }
  let mut acc = w
  for k in token.. Bool {
  is_whitespace(unit) || unit == 0xAD || unit == 0x2D
}

///|
/// Where a line that overflows at `items[i]` ends, when the word that does
/// not fit starts at `token` (a token start after the line's `start`).
///
/// Prawn wraps fragment by fragment: when not even the first segment of a
/// fragment fits, it moves the last word of the fragment before it to the
/// next line with it (`pull_preceding_fragment_to_join_this_one?`), but no
/// further back. So a word made of several fragments (`(` `iss91v2` `),`)
/// breaks at the start of the last word of the fragment before the one
/// that overflowed, even when the word began in an earlier fragment.
fn pull_back_limit(
  items : Array[Item],
  start : Int,
  token : Int,
  i : Int,
) -> Int {
  let fragment = items[i].fragment
  if items[token].fragment == fragment {
    return token
  }
  // the first item of the overflowing fragment on this line
  let mut first = i
  while first > start && items[first - 1].fragment == fragment {
    first -= 1
  }
  if first <= token {
    return token
  }
  // the last word of the fragment before it
  let previous = items[first - 1].fragment
  let mut word = first
  while word > start &&
        items[word - 1].fragment == previous &&
        !is_break_char(items[word - 1].unit) {
    word -= 1
  }
  @cmp.maximum(word, token)
}

///|
fn place_line(
  fragments : Array[Fragment],
  faces : Array[Face],
  items : Array[Item],
  from : Int,
  to : Int,
  limit : Double,
  align : Alignment,
  justify : Bool,
  hard : Bool,
) -> Line {
  // trailing spaces are not part of the line, nor are the spaces before
  // anchors that end it: Prawn's `omit_trailing_whitespace_from_line_width`
  // takes a fragment whose text Ruby's `strip` empties as white space, and
  // asciidoctor-pdf's anchor text is a NUL, which `strip` removes
  let mut visible_end = to
  while visible_end > from &&
        (
          is_break_space(items[visible_end - 1].unit) ||
          items[visible_end - 1].anchor is Some(_)
        ) {
    visible_end -= 1
  }
  let mut draw_width = 0.0
  let mut fit_width = 0.0
  let mut spaces = 0
  for i in from.. 0 && limit > draw_width {
    (limit - draw_width) / spaces.to_double()
  } else {
    0.0
  }
  let offset = match align {
    Center => (limit - draw_width) / 2.0
    Right => limit - draw_width
    _ => 0.0
  }
  let runs : Array[LineRun] = []
  let anchors : Array[(String, Double)] = []
  let mut x = offset
  let mut i = from
  let mut ascender = 0.0
  let mut descender = 0.0
  let mut height = 0.0
  let images : Array[(Double, Int, Double, Double, Bool, Style, Face)] = []
  fn measure(fragment : Int) {
    let style = fragments[fragment].style
    let face = faces[fragment]
    let mut a = face.ascender_pt(style.size)
    let d = face.descender_pt(style.size)
    let mut h = face.height_pt(style.size)
    if style.image >= 0 {
      // an image more than one and a half lines tall makes the line as
      // tall as the image, over the text's descender
      let (_, image_h, increased) = inline_image_size(
        session().inline_images[style.image],
        limit,
        h,
      )
      if increased {
        a = image_h - d
        h = image_h
      }
    }
    if a > ascender {
      ascender = a
    }
    if d > descender {
      descender = d
    }
    if h > height {
      height = h
    }
  }

  while i < visible_end {
    let fragment = items[i].fragment
    if items[i].unit == pad_unit {
      x += items[i].draw
      i += 1
      continue
    }
    match items[i].anchor {
      Some(name) => {
        anchors.push((name, x))
        i += 1
        continue
      }
      None => ()
    }
    let style = fragments[fragment].style
    if style.image >= 0 {
      let face = faces[fragment]
      let (w, h, increased) = inline_image_size(
        session().inline_images[style.image],
        limit,
        face.height_pt(style.size),
      )
      images.push((x, style.image, w, h, increased, style, face))
      x += items[i].draw
      measure(fragment)
      i += 1
      continue
    }
    let sb = StringBuilder()
    let advances : Array[Double] = []
    let run_x = x
    while i < visible_end &&
          items[i].fragment == fragment &&
          items[i].anchor is None &&
          items[i].unit != pad_unit {
      let item = items[i]
      if item.unit != 0x200B {
        sb.write_char(item.unit.unsafe_to_char())
        let advance = item.draw +
          (if is_break_space(item.unit) { extra } else { 0.0 })
        advances.push(advance)
        x += advance
      }
      i += 1
    }
    if advances.length() > 0 {
      runs.push({
        fragment,
        text: sb.to_string(),
        style: fragments[fragment].style,
        face: faces[fragment],
        x: run_x,
        advances,
        width: x - run_x,
      })
      measure(fragment)
    }
  }
  // the anchors among the trailing white space, where the line's text ends
  for k in visible_end.. anchors.push((name, x))
      None => ()
    }
  }
  // every fragment the line holds counts towards its height, as Prawn's
  // arranger measures each fragment it consumed for the line: spaces that
  // end it or were dropped from its start, anchors (asciidoctor-pdf's
  // placeholder text), and the line break that ends it
  let last = if hard { to + 1 } else { to }
  let mut previous = -1
  for k in from..<@cmp.minimum(last, items.length()) {
    if items[k].fragment != previous {
      previous = items[k].fragment
      measure(previous)
    }
  }
  if runs.is_empty() && height == 0.0 {
    // an empty line takes the metrics of the fragment it sits in
    let fragment = if from < items.length() {
      items[from].fragment
    } else if from > 0 {
      items[from - 1].fragment
    } else {
      -1
    }
    if fragment >= 0 {
      measure(fragment)
    }
  }
  // a link fragment the line holds that draws nothing on it (spaces
  // dropped from its start or left out at its end) still gets its link,
  // zero wide where the fragment sits, as Prawn annotates every fragment
  // of the line
  let empty_links : Array[(Double, Style, Face)] = []
  let mut seen = -1
  let mut position = offset
  for k in from.. r.fragment == f) {
      empty_links.push(
        (if k < visible_end { position } else { x }, style, faces[f]),
      )
    }
    seen = f
    if k < visible_end && item.anchor is None && item.unit != 0x200B {
      position += item.draw +
        (if is_break_space(item.unit) { extra } else { 0.0 })
    }
  }
  {
    runs,
    anchors,
    fit_width,
    ascender,
    descender,
    height,
    empty_links,
    images,
  }
}