///|
// What the document keeps for each page, in one record (Prawn's
// `PDF::Core::Page` and what Prawn::Document keeps for it): the page's
// items, its graphics state stack, the transformation the blocks around
// the current op set up, the path being built, and its size and layout.
// Holding the record keeps hold of the page however pages are inserted
// before it.

///|
priv struct PageState {
  /// the page's items (pagelayout's page model): what is drawn on it
  items : Array[@pagelayout.PageItem]
  /// pdf-core's `GraphicStateStack` (`graphics.mbt`): a new page starts
  /// with a copy of the state of the page it was made from
  stack : Array[GraphicState]
  /// set up by the blocks around the current op (`transform.mbt`), in
  /// pagelayout's page space; a new page starts without one
  mut ctm : @pagelayout.Matrix
  /// the path being built (Prawn writes it into the page's content, to be
  /// painted there)
  path : Array[@pagelayout.PathSegment]
  /// the size as given and the layout (Prawn keeps them apart: a
  /// landscape page reverses the size, whatever its shape); a new page
  /// takes the current page's
  geometry : ((Double, Double), String)
  /// where the page's content saves and restores the graphics state
  /// (PDF's q, with what it sets up, and Q), by the position among its
  /// items: Prawn writes a block's q on the page it starts on and its Q on
  /// the page current when it ends, so a q may stay open (`render` closes
  /// what is open at the end of the page) and a Q may restore nothing
  marks : Array[(Int, ContentMark)]
  /// the transformations the page's open q's saved, innermost last
  saved_ctms : Array[@pagelayout.Matrix]
}

///|
/// A save or a restore in a page's content.
priv enum ContentMark {
  /// q (PDF's save), then what the block sets up (a transformation, an
  /// opacity)
  Q(Array[@pagelayout.GraphicOp])
  /// Q (PDF's restore)
  EndQ
}

///|
/// PDF's q on the page, setting up `setup`: the transformation the page
/// is drawn under changes with it.
fn PageState::save(
  self : PageState,
  setup : Array[@pagelayout.GraphicOp],
) -> Unit {
  self.marks.push((self.items.length(), Q(setup)))
  self.saved_ctms.push(self.ctm)
  match setup {
    [Transform(m)] => self.ctm = self.ctm.after(m)
    _ => ()
  }
}

///|
/// PDF's Q on the page: back to the transformation of its innermost open
/// q (none open: it restores nothing).
fn PageState::restore(self : PageState) -> Unit {
  self.marks.push((self.items.length(), EndQ))
  match self.saved_ctms.pop() {
    Some(m) => self.ctm = m
    None => ()
  }
}

///|
/// The page's items nested as its saves and restores nest them: what lies
/// between a q and its Q in a group, a q still open at the end of the page
/// closed there, a Q with no q open ignored (as PDF readers do). What a
/// group cannot hold (a link) stays on the page, outside.
fn PageState::nested(self : PageState) -> Array[@pagelayout.PageItem] {
  let root : Array[@pagelayout.PageItem] = []
  let open : Array[(Array[@pagelayout.GraphicOp], Array[@pagelayout.GraphicOp])] = []
  let close = fn(
    group : (Array[@pagelayout.GraphicOp], Array[@pagelayout.GraphicOp]),
  ) {
    let ops : Array[@pagelayout.GraphicOp] = [
      Save,
      ..group.0,
      ..group.1,
      Restore,
    ]
    match open.last() {
      Some(parent) => parent.1.append(ops)
      None => root.push(Graphic({ x_pt: 0.0, y_pt: 0.0, ops, }))
    }
  }
  let mut m = 0
  for i in 0..<=self.items.length() {
    while m < self.marks.length() && self.marks[m].0 <= i {
      match self.marks[m].1 {
        Q(setup) => open.push((setup, []))
        EndQ =>
          match open.pop() {
            Some(group) => close(group)
            None => ()
          }
      }
      m += 1
    }
    guard i < self.items.length() else { break }
    let item = self.items[i]
    match (open.last(), item_ops(item)) {
      (Some(group), Some(ops)) => group.1.append(ops)
      _ => root.push(item)
    }
  }
  while open.pop() is Some(group) {
    close(group)
  }
  root
}

///|
/// The page's items cut back to `length` (a dry run's), with the saves and
/// restores among what is cut.
fn PageState::truncate(self : PageState, length : Int) -> Unit {
  self.items.truncate(length)
  while self.marks.last() is Some((at, _)) && at > length {
    self.marks.pop() |> ignore
  }
}

///|
/// The current page's record.
fn Document::page(self : Document) -> PageState {
  self.pages[self.page_index]
}

///|
/// The current page's height (the PDF's y axis is measured up from its
/// bottom).
fn Document::page_h(self : Document) -> Double {
  self.model.pages[self.page_index].height_pt
}

///|
/// Make a page `size` (points, as laid out) right after the current one,
/// where Prawn's `start_new_page` inserts it, and make it current;
/// `geometry` is its size as given and its layout.
fn Document::make_page(
  self : Document,
  size : (Double, Double),
  geometry : ((Double, Double), String),
) -> Unit {
  let from = self.page_index
  let to = from + 1
  self.model.pages.insert(to, {
    width_pt: size.0,
    height_pt: size.1,
    items: [],
  })
  self.page_made(from, to, geometry)
  self.page_index = to
}

///|
/// A page was made, at `to`, from the page at `from` (none for the
/// first): its record starts with that page's graphics state.
fn Document::page_made(
  self : Document,
  from : Int,
  to : Int,
  geometry : ((Double, Double), String),
) -> Unit {
  let source = if from >= 0 && from < self.pages.length() {
    Some(self.pages[from])
  } else {
    None
  }
  let state = match source.bind(p => p.stack.last()) {
    Some(s) => s.copy()
    None => GraphicState::new()
  }
  self.pages.insert(to, {
    items: self.model.pages[to].items,
    stack: [state],
    ctm: @pagelayout.identity,
    path: [],
    geometry,
    marks: [],
    saved_ctms: [],
  })
}