///|
// Prawn's bounding boxes, modelled as Prawn keeps them: one current box
// (`@bounding_box`), each box with its parent, and a margin box per page
// (`generate_margin_box`). A block opening a box restores the box it saved
// when it ends, whatever happened in between. Coordinates here are Prawn's
// (points, y up from the bottom of the page).

///|
/// A bounding box: its top left corner on the page, its width, and its
/// height, or none for a stretchy box that grows with its content.
priv struct Box {
  /// with the left padding (`indent`) in it, as Prawn's `@x`
  mut left : Double
  /// a column box's moves to its parent's top on a new page when it
  /// reflows its margins
  mut top : Double
  /// with the padding taken off (a column box's is not: its columns are
  /// narrowed instead)
  mut width : Double
  height : Double?
  /// how far a stretchy box has grown
  mut stretched : Double
  /// the box it was opened in; none for a margin box
  parent : Box?
  /// a column box's columns
  columns : ColumnState?
  /// the indentation added (`total_left_padding`, `total_right_padding`)
  mut total_left : Double
  mut total_right : Double
}

///|
/// A column box's columns (Prawn's `ColumnBox`).
priv struct ColumnState {
  count : Int
  spacer : Double
  mut current : Int
  reflow_margins : Bool
}

///|
/// Where a box's content goes across: its left side on the page and its
/// width (a column box's current column, `(width - spacer * (n - 1)) / n`
/// wide less its padding).
fn Box::side(self : Box) -> (Double, Double) {
  match self.columns {
    None => (self.left, self.width)
    Some(c) => {
      let bare = (self.width - c.spacer * (c.count - 1).to_double()) /
        c.count.to_double()
      (
        self.left + (bare + c.spacer) * c.current.to_double(),
        bare - (self.total_left + self.total_right),
      )
    }
  }
}

///|
/// A box's height: a stretchy box's grows to the document's position.
fn Box::height_at(self : Box, y : Double) -> Double {
  match self.height {
    Some(h) => h
    None => {
      if self.top - y > self.stretched {
        self.stretched = self.top - y
      }
      self.stretched
    }
  }
}

///|
fn Box::bottom_at(self : Box, y : Double) -> Double {
  self.top - self.height_at(y)
}

///|
/// Prawn's `add_left_padding` and `add_right_padding` (negative amounts
/// subtract): a box moves its left side in and narrows; a column box
/// moves and narrows its columns, keeping its width.
fn Box::pad(self : Box, left : Double, right : Double) -> Unit {
  self.total_left += left
  self.total_right += right
  self.left += left
  if self.columns is None {
    self.width -= left + right
  }
}

///|
/// The bounds content is set in, as Prawn's `bounds` reports them.
pub(all) struct Bounds {
  /// relative to the box itself: 0, or a column box's column's offset
  left : Double
  bottom : Double
  width : Double
  height : Double
  /// on the page
  absolute_left : Double
  absolute_top : Double
}

///|
/// The document's position (Prawn's `y`): up from the page bottom.
fn Document::y(self : Document) -> Double {
  self.position
}

///|
fn Document::set_y(self : Document, y : Double) -> Unit {
  self.position = y
  self.track()
}

///|
/// Prawn's `y=` updates the current box's height (`update_height`): a
/// stretchy box keeps the depth its content reached, even when the
/// position moves back up (`float`). Called after every move.
fn Document::track(self : Document) -> Unit {
  if self.bbox.height is None {
    self.bbox.height_at(self.y()) |> ignore
  }
}

///|
/// The current box: a bounding box, or a margin box.
fn Document::current_box(self : Document) -> Box {
  self.bbox
}

///|
/// The margin box of the current page (Prawn's `generate_margin_box`): the
/// page's size less its margins, with the indentation the last margin box
/// had; it becomes the current box unless a box with a parent is.
fn Document::generate_margin_box(self : Document) -> Unit {
  let page_h = self.page_h()
  let page_w = self.model.pages[self.page_index].width_pt
  let (top, right, bottom, left) = self.margins
  let margin : Box = {
    left,
    top: page_h - top,
    width: page_w - left - right,
    height: Some(page_h - top - bottom),
    stretched: 0.0,
    parent: None,
    columns: None,
    total_left: 0.0,
    total_right: 0.0,
  }
  margin.pad(self.margin.total_left, self.margin.total_right)
  self.margin = margin
  if self.bbox.parent is None {
    self.bbox = margin
  }
}

///|
/// After a change of page: the page's margin box, and the position at its top (`size_changed`: a new page of
/// another size or layout makes the margin box current, as Prawn does).
fn Document::page_changed(
  self : Document,
  size_changed? : Bool = false,
) -> Unit {
  self.generate_margin_box()
  if size_changed {
    self.bbox = self.margin
  }
  self.set_y(self.bbox.top)
}

///|
/// Prawn's `bounds`.
pub fn Document::bounds(self : Document) -> Bounds {
  let box = self.bbox
  // a column box: the current column, `left` its offset in the box
  let (side, width) = box.side()
  {
    left: side - box.left,
    bottom: 0.0,
    width,
    height: box.height_at(self.y()),
    absolute_left: box.left,
    absolute_top: box.top,
  }
}

///|
/// Opens `box` in the current box: `body`, then the box saved before is
/// current again (Prawn's `init_bounding_box`, `init_column_box`).
fn Document::open_box(
  self : Document,
  box : Box,
  body : () -> Unit raise,
  after : () -> Unit,
) -> Unit raise {
  let parent = self.bbox
  self.bbox = box
  self.set_y(box.top)
  // the parent is current again only when `body` returns: Prawn has no
  // `ensure` here, so a block that raises leaves its box current
  body()
  after()
  self.bbox = parent
}

///|
/// Prawn's `bounding_box`: `body` set in a box whose top left corner is
/// `at` (relative to the bottom left of the current bounds), `width` wide
/// and `height` tall, or stretchy. Afterwards the position is below the
/// current box (when it has a height), or where it was when nothing was
/// set.
pub fn Document::bounding_box(
  self : Document,
  at : (Double, Double),
  width : Double,
  height? : Double,
  body : () -> Unit raise,
) -> Unit raise {
  let parent = self.bbox
  let original_y = self.y()
  let box : Box = {
    left: parent.left + at.0,
    top: parent.bottom_at(original_y) + at.1,
    width,
    height,
    stretched: 0.0,
    parent: Some(parent),
    columns: None,
    total_left: 0.0,
    total_right: 0.0,
  }
  self.open_box(box, body, () => {
    // the box current now: a new page of another size may have made it
    // the margin box
    let current = self.bbox
    if self.y() == current.top {
      self.set_y(original_y)
    }
    if current.height is Some(_) {
      self.set_y(current.bottom_at(self.y()))
    }
  })
}

///|
/// Prawn's `column_box`: `body` set in `columns` columns `spacer` apart (by
/// default the font size) in a box at `at`, `width` wide and `height` tall
/// or stretchy; content runs down a column, then to the next, then to a
/// new page. Afterwards the position is at the current box's bottom.
pub fn Document::column_box(
  self : Document,
  at : (Double, Double),
  width : Double,
  height? : Double,
  columns? : Int = 3,
  spacer? : Double,
  reflow_margins? : Bool = false,
  body : () -> Unit raise,
) -> Unit raise {
  let parent = self.bbox
  let box : Box = {
    left: parent.left + at.0,
    top: parent.bottom_at(self.y()) + at.1,
    width,
    height,
    stretched: 0.0,
    parent: Some(parent),
    columns: Some({
      count: columns,
      spacer: spacer.unwrap_or(self.size),
      current: 0,
      reflow_margins,
    }),
    total_left: 0.0,
    total_right: 0.0,
  }
  self.open_box(box, body, () => self.set_y(self.bbox.bottom_at(self.y())))
}

///|
/// Prawn's `indent`: `body` set with the current bounds narrowed by `left`
/// and `right`; afterwards the padding comes off the bounds current then.
pub fn Document::indent(
  self : Document,
  left : Double,
  right? : Double = 0.0,
  body : () -> Unit raise,
) -> Unit raise {
  self.bbox.pad(left, right)
  defer self.bbox.pad(-left, -right)
  body()
}

///|
/// Prawn's `float`: `body`, then back to the page and position it started
/// at.
pub fn Document::float(self : Document, body : () -> Unit raise) -> Unit raise {
  let page = self.page_index
  let y = self.y()
  body()
  // back only when `body` returns (Prawn has no `ensure` here)
  if self.page_index != page {
    self.page_index = page
    self.page_changed()
  }
  self.set_y(y)
}

///|
/// Prawn's `move_past_bottom`, when content runs out of room: a column
/// box moves to its next column, past the last one to a new page (with
/// `reflow_margins`, at the top of its parent); any other box to the next
/// page, made if there is none.
fn Document::move_past_bottom(self : Document) -> Unit {
  // a stretchy box keeps the depth reached before the position moves on
  self.track()
  let box = self.bbox
  match box.columns {
    Some(c) => {
      c.current = (c.current + 1) % c.count
      if c.current != 0 {
        self.set_y(box.top)
        return
      }
      if c.reflow_margins {
        match box.parent {
          Some(parent) => box.top = parent.top
          None => ()
        }
      }
      self.new_page()
    }
    None =>
      if self.page_index + 1 < self.model.pages.length() {
        self.page_index += 1
        self.page_changed()
      } else {
        self.new_page()
      }
  }
}

///|
/// Prawn's `canvas`: `body` in a box the size of the page, with no parent
/// (a new page makes the margin box current again), and the position left
/// where `body` leaves it.
pub fn Document::canvas(self : Document, body : () -> Unit raise) -> Unit raise {
  let page = self.model.pages[self.page_index]
  let box : Box = {
    left: 0.0,
    top: page.height_pt,
    width: page.width_pt,
    height: Some(page.height_pt),
    stretched: 0.0,
    parent: None,
    columns: None,
    total_left: 0.0,
    total_right: 0.0,
  }
  // Prawn's `init_bounding_box` with `hold_position`: the position is put
  // back only when `body` left it at the top of the box current then
  let original_y = self.y()
  self.open_box(box, body, () => {
    if self.y() == self.bbox.top {
      self.set_y(original_y)
    }
  })
}

///|
/// Where `span` puts its column across the margin box.
pub(all) enum SpanPosition {
  SpanLeft
  SpanCenter
  SpanRight
  /// this far from the margin box's left
  SpanAt(Double)
}

///|
/// Prawn's `span`: `body` in a column `width` wide across the margin box
/// (whatever box is current), at `position`, from the current position;
/// a stretchy box, so the position ends below what `body` set.
pub fn Document::span(
  self : Document,
  width : Double,
  position? : SpanPosition = SpanLeft,
  body : () -> Unit raise,
) -> Unit raise {
  let original_y = self.y()
  let margin = self.margin
  let (side, margin_width) = margin.side()
  let left = match position {
    SpanLeft => side
    SpanCenter => side + margin_width / 2.0 - width / 2.0
    SpanRight => side + margin_width - width
    SpanAt(x) => side + x
  }
  self.canvas(() => {
    self.bounding_box((left, margin.top), width, () => {
      self.set_y(original_y)
      body()
    })
  })
}

///|
/// Prawn's `bounds.right_side`: a box's right edge on the page. A column
/// box's is `absolute_right` (its left plus one column's width) less the
/// columns to the right of the current one, as Prawn computes it.
fn Box::right_side(self : Box) -> Double {
  match self.columns {
    None => self.left + self.width
    Some(c) => {
      let bare = (self.width - c.spacer * (c.count - 1).to_double()) /
        c.count.to_double()
      let column_width = bare - (self.total_left + self.total_right)
      self.left +
      column_width -
      (bare + c.spacer) * (c.count - 1 - c.current).to_double()
    }
  }
}