///|
// Prawn's Document (PLAN.md §4, milestone 1): pages, margins, the cursor
// and text, on pagelayout's page model; lines are broken as `typeset.mbt`
// breaks them. Coordinates in this API are Prawn's: points,
// y up, the cursor measured from the bottom of the bounds.

///|
/// Page sizes Prawn knows by name (`PDF::Core::PageGeometry::SIZES`), in
/// points: the common ones.
let page_sizes : Array[(String, Double, Double)] = [
  ("LETTER", 612.0, 792.0),
  ("LEGAL", 612.0, 1008.0),
  ("A3", 841.89, 1190.55),
  ("A4", 595.28, 841.89),
  ("A5", 419.53, 595.28),
  ("A6", 297.64, 419.53),
  ("EXECUTIVE", 521.86, 756.0),
  ("TABLOID", 792.0, 1224.0),
]

///|
/// The size of a page Prawn names (`"A4"`), in points.
pub fn page_size_named(name : String) -> (Double, Double)? {
  for entry in page_sizes {
    if entry.0 == name {
      return Some((entry.1, entry.2))
    }
  }
  None
}

///|
/// Margins as Prawn takes them (`margin: 36`, `[v, h]`, `[t, h, b]`,
/// `[t, r, b, l]`, CSS order): top, right, bottom, left.
pub fn margins_of(values : Array[Double]) -> (Double, Double, Double, Double) {
  match values {
    [all] => (all, all, all, all)
    [v, h] => (v, h, v, h)
    [t, h, b] => (t, h, b, h)
    [t, r, b, l, ..] => (t, r, b, l)
    [] => (36.0, 36.0, 36.0, 36.0)
  }
}

///|
/// A Prawn document.
pub struct Document {
  /// the pages (pagelayout's page model) and the current one
  priv model : @pagelayout.PageModel
  priv mut page_index : Int
  /// Prawn's `y`: the position, up from the bottom of the current page
  priv mut position : Double
  /// the margins pages are made with: top, right, bottom, left
  priv margins : (Double, Double, Double, Double)
  priv mut catalog : FontCatalog
  /// the font families registered (`font_families_update`): family → its
  /// styles' files; the TrueType faces loaded so far, which the catalog is
  /// made from; the fallback families (`fallback_fonts`); and where files
  /// are read from (`ttf.mbt`)
  priv families : Map[String, Array[(FontStyle, String)]]
  priv mut font_files : Array[(String, Bytes)]
  priv mut fallbacks : Array[String]
  priv assets : (String) -> Bytes?
  /// what text is set with: the fonts
  priv mut text_context : TextContext
  /// the current font (`font`) and size (`font_size`)
  mut family : String
  mut size : Double
  /// the current font's style, bold and italic (a fragment's while it is
  /// drawn)
  priv mut face_style : (Bool, Bool)
  /// whether the current font is a family's (Prawn's `font.family`), not a
  /// font set by its own name
  priv mut font_family_known : Bool
  /// the catalog's face of the current font, as it was found when the font
  /// was set (Prawn keeps the font object until another is set)
  priv mut font_key : (String, Bool, Bool)
  /// each page's record (`page.mbt`), as the model's pages are ordered
  priv pages : Array[PageState]
  /// the current bounding box (Prawn's `@bounding_box`) and the current
  /// page's margin box (`bounds.mbt`)
  priv mut bbox : Box
  priv mut margin : Box
  /// what Prawn knows of each fragment set that its style does not carry
  /// (`formatted.mbt`), by the style's `callback` index
  priv fragment_meta : Array[FragmentMeta]
  /// during a dry run (measuring, shrinking, aligning): callbacks are not
  /// called
  priv mut dry_run : Bool
  /// during a `draw_text_callback` text: what draws its pieces instead
  /// the fragments being drawn, innermost last: text set from inside a
  /// callback nests (`formatted.mbt`)
  priv scopes : Array[FragmentScope]
  priv mut draw_text_callback : ((String, (Double, Double), Bool) -> Unit raise PrawnError)?
  /// what Prawn's warnings (`Kernel#warn`) go to (`on_warning`), and
  /// whether the one about built-in fonts and text beyond ASCII was given:
  /// once per document here; Prawn's flag (`Prawn::Fonts::AFM
  /// .hide_m17n_warning`) lasts the process, which no library state should
  /// the leading text is set with when none is given (`default_leading`)
  priv mut default_leading : Double
  priv mut warn : (String) -> Unit
  priv mut m17n_warned : Bool
}

///|
fn oriented(size : (Double, Double), layout : String) -> (Double, Double) {
  if layout == "landscape" {
    (size.1, size.0)
  } else {
    size
  }
}

///|
/// A document with pages `size` (points; Prawn's default is LETTER) in
/// `layout` ("portrait" or "landscape") with `margin` (Prawn's default 36
/// all round). Font files are read through `assets`, given a path as it
/// was named (none by default). Like Prawn, it starts with a page.
pub fn Document::new(
  size? : (Double, Double) = (612.0, 792.0),
  layout? : String = "portrait",
  margin? : Array[Double] = [36.0],
  assets? : (String) -> Bytes? = _ => None,
) -> Document raise {
  let (w, h) = oriented(size, layout)
  let (top, right, bottom, left) = margins_of(margin)
  let catalog = FontCatalog::load([])
  // the margin box until the first page has its own
  let margin : Box = {
    left,
    top: h - top,
    width: w - left - right,
    height: Some(h - top - bottom),
    stretched: 0.0,
    parent: None,
    columns: None,
    total_left: 0.0,
    total_right: 0.0,
  }
  let doc = {
    model: @pagelayout.PageModel::new(),
    page_index: -1,
    position: 0.0,
    margins: (top, right, bottom, left),
    catalog,
    text_context: { catalog, hooks: Hooks::new(), },
    families: Map([]),
    font_files: [],
    fallbacks: [],
    assets,
    family: "Helvetica",
    face_style: (false, false),
    font_family_known: true,
    font_key: ("Helvetica", false, false),
    size: 12.0,
    pages: [],
    bbox: margin,
    margin,
    fragment_meta: [],
    dry_run: false,
    draw_text_callback: None,
    scopes: [],
    default_leading: 0.0,
    warn: _ => (),
    m17n_warned: false,
  }
  doc.make_page((w, h), (size, layout))
  doc.page_changed()
  doc
}

///|
/// A new page like the current one (what a page break makes).
fn Document::new_page(self : Document) -> Unit {
  self.start_new_page()
}

///|
/// Where Prawn's warnings go (Ruby's `Kernel#warn`): `handler` gets each
/// message. By default they go nowhere.
pub fn Document::on_warning(
  self : Document,
  handler : (String) -> Unit,
) -> Unit {
  self.warn = handler
}

///|
/// The face `style` is set in.
fn Document::face_of(self : Document, style : Style) -> Face {
  let (bold, italic) = style.face_style()
  self.text_context.face(style.family, bold, italic)
}

///|
/// The style text is set in: the current font and size, black.
fn Document::style(self : Document, size? : Double) -> Style {
  // the catalog's face of the current font (a TrueType one is its file)
  let (family, bold, italic) = self.font_key
  {
    family,
    size: size.unwrap_or(self.size),
    bold,
    italic,
    color: self.gs().fill,
    link: None,
    background: None,
    script: 0,
    border_offset: 0.0,
    underline: false,
    strike: false,
    wj: false,
    image: -1,
    callback: -1,
    doc_bold: false,
    doc_italic: false,
    font_set: false,
    text_transform: None,
    code_background: None,
    linenum: false,
    line_mark: false,
    wrap_break: false,
  }
}

///|
/// Prawn's `text`: `string` flowed from the cursor within the bounds,
/// across pages as needed; the cursor ends below it.
pub fn Document::text(
  self : Document,
  string : String,
  size? : Double,
  style? : FontStyle,
  align? : Alignment = Left,
  leading? : Double,
  inline_format? : Bool = false,
  draw_text_callback? : (String, (Double, Double), Bool) -> Unit raise PrawnError,
) -> Unit raise PrawnError {
  // 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)
  // set as one plain fragment, or the fragments of its markup
  // (`formatted.mbt`)
  let fragments = if inline_format {
    parse_inline_format(string)
  } else {
    [TextFragment::new(string)]
  }
  self.formatted_text(fragments, size?, align~, leading?, draw_text_callback?)
}

///|
/// Prawn's `move_down`.
pub fn Document::move_down(self : Document, amount : Double) -> Unit {
  self.set_y(self.y() - amount)
}

///|
/// Prawn's `cursor`: how far the current position is above the bottom of
/// the bounds (always 0 in a stretchy box, whose bottom is the position).
pub fn Document::cursor(self : Document) -> Double {
  let box = self.current_box()
  let y = self.y()
  y - (box.top - box.height_at(y))
}

///|
/// Prawn's `page_number` (from 1).
pub fn Document::page_number(self : Document) -> Int {
  self.page_index + 1
}

///|
/// Prawn's `page_count`.
pub fn Document::page_count(self : Document) -> Int {
  self.model.pages.length()
}

///|
/// Prawn's `start_new_page`.
pub fn Document::start_new_page(
  self : Document,
  layout? : String,
  size? : (Double, Double),
) -> Unit {
  let before = self.page().geometry
  let after = (size.unwrap_or(before.0), layout.unwrap_or(before.1))
  self.make_page(oriented(after.0, after.1), after)
  // a page of another size or layout: content goes in its margin box
  self.page_changed(size_changed=after != before)
}

///|
/// Prawn's `font_size=`.
pub fn Document::set_font_size(self : Document, size : Double) -> Unit {
  self.size = size
}

///|
/// The PDF.
pub fn Document::render(self : Document) -> Bytes raise {
  // pdf-core's finalize: on each page in turn (Prawn's render goes to
  // each), every graphics state left on its stack is restored, each a Q
  for i in 0.. {
    ..page,
    items: self.pages[i].nested(),
  })
  @pdf.render_pdf(
    { fonts: self.model.fonts, pages, },
    options=@pdf.RenderOptions::new(
      fonts=self.catalog.registry,
      truncate_widths=true,
      // a character the font lacks is drawn as its .notdef glyph and kept
      // in the text, as Prawn does
      notdef_text=true,
    ),
  )
}

///|
/// Prawn's `width_of`: how wide `string` is set in the current font at
/// `size` (the current font size), with the font's kerning only when
/// `kerning` asks for it (Prawn does not apply the document's default
/// here).
pub fn Document::width_of(
  self : Document,
  string : String,
  size? : Double,
  style? : FontStyle,
  kerning? : Bool = false,
) -> Double raise PrawnError {
  // the current font in `style` while this is measured (Prawn's `:style`)
  let saved_font = self.apply_style(style, UnknownFont(" is not a known font."))
  defer self.restore_style(saved_font)
  let style = self.style(size?)
  self.check(string, style, None, fallbacks=false)
  let face = self.face_of(style)
  let chars = string.iter().map(c => c.to_int()).to_array()
  let mut width = 0.0
  for i, cp in chars {
    // a TrueType font gives a newline no width (Prawn's TTF does so)
    if cp == 0x0A && !face.standard {
      continue
    }
    width += face.prawn_width(cp)
    if kerning && i + 1 < chars.length() {
      width += face.kerning(cp, chars[i + 1])
    }
  }
  // as Prawn scales: by size / 1000
  width * (style.size / 1000.0)
}

///|
/// Prawn's `height_of`: how tall `string` would be set by `text` in boxes
/// `width` wide (the bounds' width), with the line gap and leading after
/// it (Prawn's final gap).
pub fn Document::height_of(
  self : Document,
  string : String,
  size? : Double,
  style? : FontStyle,
  width? : Double,
  leading? : Double,
) -> Double raise PrawnError {
  // 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)
  // an empty box: its final gap is the leading alone
  if string.is_empty() {
    return leading
  }
  let style = self.style(size?)
  self.check(string, style, Some(width.unwrap_or(self.bbox.side().1)))
  // Prawn's `height_of_formatted`: a box too tall to fill, set as a dry
  // run, and its final gap
  let (_, printed, gap) = self.set_box(
    [{ text: string, style, anchor: None, }],
    style,
    (0.0, 0.0),
    width.unwrap_or(self.bbox.side().1),
    1.0e8,
    Left,
    leading,
    dry_run=true,
  )
  printed + gap + leading
}

///|
/// What Prawn refuses before setting `string` in `style`: characters a
/// standard font cannot encode (Windows-1252), and, given the `width`
/// lines are set in, a character too wide for it (the line wrap cannot
/// fit even one).
fn Document::check(
  self : Document,
  string : String,
  style : Style,
  width : Double?,
  fallbacks? : Bool = true,
) -> Unit raise PrawnError {
  let face = self.face_of(style)
  // each character in the font it is set in: its own, or the first
  // fallback that has it (Prawn splits the text across them before it
  // encodes it; `width_of` and `draw_text` do not)
  let face_for = fn(cp : Int) raise PrawnError {
    if fallbacks && !self.fallbacks.is_empty() {
      self.glyph_face(style, cp)
    } else {
      face
    }
  }
  // the whole string is encoded before anything is wrapped
  for c in string {
    let cp = c.to_int()
    if face_for(cp).standard && cp >= 0x80 && winansi_code_of(cp) is None {
      raise IncompatibleStringEncoding(string)
    }
  }
  guard width is Some(w) else { return }
  let scale = style.size / 1000.0
  for c in string {
    if c != ' ' &&
      c != '\n' &&
      face_for(c.to_int()).prawn_width(c.to_int()) * scale > w {
      raise CannotFit
    }
  }
}

///|
/// Prawn's `go_to_page` (from 1): back to an existing page, at the top of
/// its margin box.
pub fn Document::go_to_page(self : Document, number : Int) -> Unit {
  self.page_index = number - 1
  self.page_changed()
}

///|
/// Prawn's `default_leading=`: the leading text is set with when none is
/// given.
pub fn Document::set_default_leading(self : Document, leading : Double) -> Unit {
  self.default_leading = leading
}

///|
/// Prawn's `default_leading` (without a value).
pub fn Document::default_leading(self : Document) -> Double {
  self.default_leading
}

///|
/// Prawn's `move_up`.
pub fn Document::move_up(self : Document, amount : Double) -> Unit {
  self.move_down(-amount)
}

///|
/// Prawn's `move_cursor_to`: the cursor at `cursor` points above the bottom
/// of the bounds (as they are before the move).
pub fn Document::move_cursor_to(self : Document, cursor : Double) -> Unit {
  let box = self.current_box()
  let y = self.y()
  self.set_y(cursor + (box.top - box.height_at(y)))
}

///|
/// Prawn's `pad_top`: down by `amount`, then `body`.
pub fn Document::pad_top(
  self : Document,
  amount : Double,
  body : () -> Unit raise,
) -> Unit raise {
  self.move_down(amount)
  body()
}

///|
/// Prawn's `pad_bottom`: `body`, then down by `amount`.
pub fn Document::pad_bottom(
  self : Document,
  amount : Double,
  body : () -> Unit raise,
) -> Unit raise {
  body()
  self.move_down(amount)
}

///|
/// Prawn's `pad`: down by `amount` before and after `body`.
pub fn Document::pad(
  self : Document,
  amount : Double,
  body : () -> Unit raise,
) -> Unit raise {
  self.move_down(amount)
  body()
  self.move_down(amount)
}