///|
// Prawn's text boxes (`text_box`, `Prawn::Text::Box`): text set in a box
// at a point of the bounds, as much as fits, without moving the cursor.

///|
/// What a text box does with text that does not fit (`overflow`).
pub(all) enum Overflow {
  /// set what fits; the rest is returned
  Truncate
  /// set smaller, half a point at a time, down to `min_font_size`
  ShrinkToFit
  /// the box reaches down to the bottom of the bounds
  Expand
}

///|
/// Where a text box puts its lines vertically (`valign`).
pub(all) enum VerticalAlignment {
  Top
  Center
  Bottom
}

///|
/// The text of fragments.
fn text_of_fragments(fragments : Array[Fragment]) -> String {
  let sb = StringBuilder()
  for f in fragments {
    sb.write_string(f.text)
  }
  sb.to_string()
}

///|
/// Sets `string` in a box whose top left corner is `at` on the page (Prawn's
/// coordinates), `width` by `height`, at `size`: the fragments that did not
/// fit, and how tall what was set is (Prawn's box `height`). With `dry_run`
/// nothing stays on the page.
fn Document::set_box(
  self : Document,
  fragments : Array[Fragment],
  base : Style,
  at : (Double, Double),
  width : Double,
  height : Double,
  align : Alignment,
  leading : Double,
  dry_run? : Bool = false,
) -> (Array[Fragment], Double, Double) raise PrawnError {
  // a dry run calls no callbacks
  let was_dry = self.dry_run
  self.dry_run = was_dry || dry_run
  defer {
    self.dry_run = was_dry
  }
  let page = self.page()
  let mark = page.items.length()
  // split across the fallback fonts first (Prawn's fallback processing)
  let fragments = self.split_fallbacks(fragments)
  let ctx = self.text_context
  let face = self.face_of(base)
  // the box's top left in the bounds (where Prawn keeps a box's `at`):
  // each line is drawn on the page current then, which a callback may
  // change
  let box = self.current_box()
  let left = at.0 - box.left
  let top = at.1 - box.bottom_at(self.y())
  // Prawn's `Wrap#wrap`: line by line, each stripped of its leading
  // whitespace and wrapped, CannotFit when not even its first character
  // fits, set when it fits the height; the first that does not is where
  // the box ends
  // the text, as Prawn's arranger takes it in (an empty fragment is none)
  let text = ctx
    .apply_fallbacks(fragments)
    .filter(f => !f.text.is_empty() || f.anchor is Some(_) || f.style.image >= 0)
  // where the text not yet set starts: a fragment, and an offset in it
  let mut at_fragment = 0
  let mut at_offset = 0
  let mut baseline = 0.0
  let mut placed = 0
  // the last line measured (whether or not it fit): Prawn takes the box's
  // height and final gap from it
  let mut depth = 0.0
  let mut gap = 0.0
  let mut rejected = false
  while at_fragment < text.length() {
    let window = line_window(text, at_fragment, at_offset, self, width)
    let (stripped, origins) = strip_leading_fragments(window)
    // measured in the font it is set in (a fallback font, maybe)
    match stripped.search_by(f => !f.text.is_empty()) {
      Some(i) => {
        let first = stripped[i]
        let first_face = self.face_of(first.style)
        match first.text.iter().next() {
          Some(c) if c != '\n' &&
            first_face.prawn_width(c.to_int()) * (first.style.size / 1000.0) >
            width => {
            if dry_run {
              page.truncate(mark)
            }
            raise CannotFit
          }
          _ => ()
        }
      }
      None => ()
    }
    // a fragment emptied counts towards the line's height (Prawn keeps
    // it): a zero-width space holds its place
    let held = stripped.map(f => {
      if f.text.is_empty() && f.anchor is None && f.style.image < 0 {
        { ..f, text: "\u{200B}", }
      } else {
        f
      }
    })
    let (line, after) = first_line_break(held, ctx, width, align)
    let (a, d, h) = line_extent(line, face, base.size, false)
    depth = d
    // the box's line gap is its last line's (its largest font's)
    gap = h - a - d
    let next = if placed == 0 { a } else { baseline + h + leading }
    if next + d > height + 0.0001 {
      rejected = true
      break
    }
    baseline = next
    self.ink_line(line, left, top - baseline)
    placed += 1
    // the text after the line, in the text's terms
    let (w, offset) = match after {
      Some((i, o)) => {
        let (w, removed) = origins[i]
        (w, removed + (if held[i].text == "\u{200B}" { 0 } else { o }))
      }
      None => (window.length() - 1, window[window.length() - 1].text.length())
    }
    let base_offset = if w == 0 { at_offset } else { 0 }
    let before = (at_fragment, at_offset)
    at_fragment += w
    at_offset = base_offset + offset
    let length = text[at_fragment].text.length()
    if (after is None || at_offset > 0) && at_offset >= length {
      at_fragment += 1
      at_offset = 0
    }
    // every line takes some of the text
    guard (at_fragment, at_offset) != before else {
      abort("a line took none of the text")
    }
  }
  if dry_run {
    page.truncate(mark)
  }
  let remaining : Array[Fragment] = []
  if at_fragment < text.length() {
    let f = text[at_fragment]
    remaining.push({ ..f, text: f.text[at_offset:].to_owned(), })
    for i in (at_fragment + 1).. Unit raise PrawnError,
) -> String raise {
  // the current font in `style` while this is set (Prawn's `:style`, which
  // its boxes apply and undo)
  let saved_font = self.apply_style(
    style,
    BadFontFamily("PDF::Core::Text::BadFontFamily"),
  )
  defer self.restore_style(saved_font)
  // the document's default leading unless given (Prawn's `default_leading`)
  let leading = leading.unwrap_or(self.default_leading)
  let saved_callback = self.draw_text_callback
  self.draw_text_callback = draw_text_callback
  // a text box does not move the position; callbacks may, and it stays
  // where they leave it
  defer {
    self.draw_text_callback = saved_callback
    self.track()
  }
  let bounds = self.bounds()
  let mut at = at.unwrap_or((0.0, bounds.height))
  let width = width.unwrap_or(bounds.width - at.0)
  // the default height reaches the bottom of the bounds, or of the fixed
  // box a stretchy one is in (Prawn's `default_height`)
  let default_height = () => {
    let y = self.y()
    let box = self.current_box()
    // out through stretchy boxes to one that has a bottom
    let mut frame = box
    while frame.height is None {
      match frame.parent {
        Some(p) => frame = p
        None => break
      }
    }
    at.1 + box.bottom_at(y) - frame.bottom_at(y)
  }
  let (box_height, overflow) = match overflow {
    Expand => (default_height(), Truncate)
    _ => (height.unwrap_or(default_height()), overflow)
  }
  let mut height = box_height
  // encoding first, for the whole string; whether a character fits is
  // checked on the lines Prawn tries to set (`set_box`)
  let style = self.style(size?)
  self.check(string, style, None)
  let mut size = style.size
  // the box on the page
  let origin = self.absolute(at)
  let mut top = origin.1
  let left = origin.0
  // the string set at `size` in the box `h` tall with its top at `y`: a
  // dry run, or for good
  let set_at = fn(
    size : Double,
    y : Double,
    h : Double,
    dry : Bool,
  ) raise PrawnError {
    let st = self.style(size~)
    self.set_box(
      [{ text: string, style: st, anchor: None, }],
      st,
      (left, y),
      width,
      h,
      align,
      leading,
      dry_run=dry,
    )
  }
  if overflow is ShrinkToFit {
    while true {
      let (rest, _, _) = set_at(size, top, height, true)
      if rest.is_empty() || size <= min_font_size {
        break
      }
      size = @cmp.maximum(size - 0.5, min_font_size)
    }
  }
  if !(valign is Top) {
    let (_, printed, _) = set_at(size, top, height, true)
    let descender = self.face_of(self.style(size~)).descender_pt(size)
    match valign {
      Center => top -= (height - printed + descender) * 0.5
      _ => top -= height - printed
    }
    at = (at.0, at.1 - (origin.1 - top))
    height = printed
  }
  let set = () => {
    let (rest, _, _) = set_at(size, top, height, false)
    text_of_fragments(rest)
  }
  if rotate == 0.0 {
    return set()
  }
  // about a corner or the center of the box, as a point of the bounds
  let around = match rotate_around {
    "center" => (at.0 + width * 0.5, at.1 - height * 0.5)
    "upper_right" => (at.0 + width, at.1)
    "lower_right" => (at.0 + width, at.1 - height)
    "lower_left" => (at.0, at.1 - height)
    _ => at
  }
  let mut rest = ""
  self.rotate(rotate, origin=around, () => rest = set())
  rest
}

///|
/// Prawn's whitespace (`LineWrap#whitespace`): space, tab, zero-width
/// space.
fn is_prawn_space(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\u{200B}'
}

///|
fn strip_leading_spaces(text : String) -> String {
  let mut i = 0
  while i < text.length() && is_prawn_space(text[i].unsafe_to_char()) {
    i = i + 1
  }
  text[i:].to_owned()
}

///|
/// Fragments without the whitespace they start with (Prawn's line wrap
/// strips a line's leading whitespace), and where each comes from: its
/// index in `fragments` and how much was stripped from its start. A
/// fragment it empties stays, as Prawn keeps it, unless a line break
/// follows it.
fn strip_leading_fragments(
  fragments : Array[Fragment],
) -> (Array[Fragment], Array[(Int, Int)]) {
  let out : Array[Fragment] = []
  let origins : Array[(Int, Int)] = []
  let mut stripping = true
  for i, f in fragments {
    if stripping {
      let text = strip_leading_spaces(f.text)
      let removed = f.text.length() - text.length()
      if text.is_empty() && f.anchor is None && f.style.image < 0 {
        if i + 1 < fragments.length() && fragments[i + 1].text.has_prefix("\n") {
          continue
        }
        out.push({ ..f, text, })
        origins.push((i, removed))
        continue
      }
      stripping = false
      out.push({ ..f, text, })
      origins.push((i, removed))
    } else {
      out.push(f)
      origins.push((i, 0))
    }
  }
  (out, origins)
}

///|
/// A window of `text` from fragment `start` at `offset` that surely holds
/// the first line set `width` wide: up to a line break, or the second
/// break opportunity past twice the width, or four times the width. A
/// long text is then not wrapped (nor copied) in full for each line.
fn line_window(
  text : Array[Fragment],
  start : Int,
  offset : Int,
  doc : Document,
  width : Double,
) -> Array[Fragment] {
  let window : Array[Fragment] = []
  let mut w = 0.0
  let mut breaks = 0
  // the whitespace the line starts with is stripped: it takes no room
  let mut leading = true
  // the window holds a character past it whatever the width (one wider
  // than the line is Prawn's CannotFit)
  let mut taken = false
  for i in start..= 0xDC00 && u <= 0xDFFF
      if taken && w > 4.0 * width && !low {
        cut = j
        break
      }
      if taken &&
        w > 2.0 * width &&
        (
          u == ' '.to_int() ||
          u == '\t'.to_int() ||
          u == 0x200B ||
          u == '-'.to_int() ||
          u == 0xAD
        ) {
        breaks += 1
        if breaks >= 2 {
          cut = j
          break
        }
      }
      // a soft hyphen takes no room in a line (but the one ending it)
      if u != 0xAD {
        w += face.prawn_width(u) * scale
      }
      taken = true
    }
    let to = if cut >= 0 { cut } else { s.length() }
    window.push(
      if from == 0 && to == s.length() {
        f
      } else {
        { ..f, text: s[from:to].to_owned(), }
      },
    )
    if cut >= 0 {
      break
    }
  }
  window
}