///|
type PdfObject = @pdf.PdfObject

///|
type Op = @pdfops.Op

///|
const TAB_W : Double = 92.0

///|
const TAB_H : Double = 30.0

///|
fn font_type1(base_font : String) -> PdfObject {
  Dictionary([
    ("/Type", Name(PdfName(b"/Font"))),
    ("/Subtype", Name(PdfName(b"/Type1"))),
    ("/BaseFont", Name(@pdf.PdfName::of_string("/" + base_font))),
  ])
}

///|
fn resources() -> PdfObject {
  Dictionary([
    (
      "/Font",
      Dictionary([
        ("/F0", font_type1("Helvetica-Bold")),
        ("/F1", font_type1("Helvetica")),
        ("/F2", font_type1("Courier")),
      ]),
    ),
  ])
}

///|
fn paper() -> @pdfpaper.Paper {
  @pdfpaper.usletter
}

///|
fn mediabox() -> PdfObject {
  paper().rectangle()
}

///|
fn w() -> Double {
  paper().unit().to_points(paper().width())
}

///|
fn h() -> Double {
  paper().unit().to_points(paper().height())
}

///|
fn ops_text(
  x : Double,
  y : Double,
  font : String,
  size : Double,
  text : String,
  rgb : (Double, Double, Double),
) -> Array[Op] {
  let (r, g, b) = rgb
  let tr = @pdftransform.Transform::new([Translate(x, y)]).to_matrix()
  [Opq, Oprg(r, g, b), OpBT, OpTf(font, size), OpTm(tr), OpTj(text), OpET, OpQ]
}

///|
fn ops_fill_rect(
  x : Double,
  y : Double,
  ww : Double,
  hh : Double,
  rgb : (Double, Double, Double),
) -> Array[Op] {
  let (r, g, b) = rgb
  [Opq, Oprg(r, g, b), Opre(x, y, ww, hh), Opf, OpQ]
}

///|
fn ops_stroke_rect(
  x : Double,
  y : Double,
  ww : Double,
  hh : Double,
  rgb : (Double, Double, Double),
  width : Double,
) -> Array[Op] {
  let (r, g, b) = rgb
  [Opq, Opw(width), OpRG(r, g, b), Opre(x, y, ww, hh), OpS, OpQ]
}

///|
fn ops_polyline(
  points : Array[(Double, Double)],
  rgb : (Double, Double, Double),
  width : Double,
) -> Array[Op] {
  match points {
    [] => []
    [(x0, y0), .. rest] => {
      let (r, g, b) = rgb
      let ops : Array[Op] = [Opq, Opw(width), OpRG(r, g, b), Opm(x0, y0)]
      for p in rest {
        let (x, y) = p
        ops.push(Opl(x, y))
      }
      ops.push(OpS)
      ops.push(OpQ)
      ops
    }
  }
}

///|
fn circle_points(
  cx : Double,
  cy : Double,
  r : Double,
  n : Int,
) -> Array[(Double, Double)] {
  let out = Array(capacity=n)
  let two_pi = 8.0 * @math.atan(1.0)
  for i in 0.. Array[Op] {
  match points {
    [] => []
    [(x0, y0), .. rest] => {
      let (r, g, b) = rgb
      let ops : Array[Op] = [Opq, Oprg(r, g, b), Opm(x0, y0)]
      for p in rest {
        let (x, y) = p
        ops.push(Opl(x, y))
      }
      ops.push(Oph)
      ops.push(Opf)
      ops.push(OpQ)
      ops
    }
  }
}

///|
fn page_of_ops(ops : Array[Op]) -> @pdfpage.Page raise {
  let stream = @pdfops.Op::stream_of_ops(ops)
  Page(
    content=[stream],
    mediabox=mediabox(),
    resources=resources(),
    rotate=Rotate0,
    rest=Dictionary([]),
  )
}

///|
fn set_catalog_entry(
  pdf : @pdf.Pdf,
  key : String,
  value : PdfObject,
) -> Unit raise {
  let rootnum = pdf.root
  let root = pdf.lookup_obj(rootnum)
  let updated_root = root.add_entry(key, value)
  pdf.addobj_given_num((rootnum, updated_root))
  pdf.trailerdict = pdf.trailerdict.add_entry("/Root", Indirect(rootnum))
}

///|
fn add_layers(pdf : @pdf.Pdf) -> (Int, Int) raise {
  let night : PdfObject = Dictionary([
    ("/Type", Name(PdfName(b"/OCG"))),
    ("/Name", PdfString("NIGHT MODE")),
  ])
  let surprise : PdfObject = Dictionary([
    ("/Type", Name(PdfName(b"/OCG"))),
    ("/Name", PdfString("SURPRISE")),
  ])
  let night_num = pdf.addobj(night)
  let surprise_num = pdf.addobj(surprise)
  let default_dict : PdfObject = Dictionary([
    ("/BaseState", Name(PdfName(b"/ON"))),
    ("/Intent", Name(PdfName(b"/View"))),
    ("/Name", PdfString("Default")),
    ("/OFF", PdfArray([Indirect(night_num), Indirect(surprise_num)])),
  ])
  let ocprops : PdfObject = Dictionary([
    ("/OCGs", PdfArray([Indirect(night_num), Indirect(surprise_num)])),
    ("/D", default_dict),
  ])
  let ocprops_num = pdf.addobj(ocprops)
  set_catalog_entry(pdf, "/OCProperties", Indirect(ocprops_num))
  (night_num, surprise_num)
}

///|
fn add_layer_properties_to_pages(
  pdf : @pdf.Pdf,
  night_num : Int,
  surprise_num : Int,
) -> @pdf.Pdf raise {
  let pages = @pdfpage.PdfPageDoc::new(pdf).pages_of_pagetree()
  for i in 0.. PdfObject {
  let dest = @pdfdest.Destination::Fit(PageObject(refnum)).to_pdf_object()
  Dictionary([("/S", Name(PdfName(b"/GoTo"))), ("/D", dest)])
}

///|
fn uri_action(uri : String) -> PdfObject {
  Dictionary([("/S", Name(PdfName(b"/URI"))), ("/URI", PdfString(uri))])
}

///|
fn add_link(
  pdf : @pdf.Pdf,
  page : @pdfpage.Page,
  rect : (Double, Double, Double, Double),
  action : PdfObject,
) -> @pdfpage.Page raise {
  let annot : @pdfannot.Annotation = {
    subtype: Link,
    annot_contents: None,
    subject: None,
    rectangle: rect,
    border: @pdfannot.Border::new(0.0),
    colour: None,
    annotrest: Dictionary([("/A", action)]),
  }
  @pdfannot.PdfAnnot::new(pdf).add_annotation(page, annot)
}

///|
fn add_bookmarks(pdf : @pdf.Pdf, refs : Array[Int]) -> @pdf.Pdf raise {
  let titles : Array[String] = [
    "Cover", "Map", "Itinerary", "Coupons", "Postcard", "Packing List",
  ]
  let marks : Array[@pdfmarks.Bookmark] = []
  for i in 0.. Array[String] {
  ["MAP", "PLAN", "DEALS", "POSTCARD", "PACK"]
}

///|
fn tab_colors() -> Array[(Double, Double, Double)] {
  [
    (0.18, 0.48, 0.86),
    (0.18, 0.66, 0.52),
    (0.95, 0.75, 0.2),
    (0.92, 0.33, 0.36),
    (0.52, 0.4, 0.86),
  ]
}

///|
fn tab_rect(i : Int) -> (Double, Double, Double, Double) {
  let x1 = w() - 10.0
  let x0 = x1 - TAB_W
  let y1 = h() - 120.0 - i.to_double() * (TAB_H + 10.0)
  let y0 = y1 - TAB_H
  (x0, y0, x1, y1)
}

///|
fn ops_tabs(active? : Int = -1) -> Array[Op] {
  let titles = tab_titles()
  let colors = tab_colors()
  let ops : Array[Op] = []
  for i in 0.. (Double, Double, Double, Double) {
  (18.0, h() - 54.0, 114.0, h() - 22.0)
}

///|
fn ops_home_badge() -> Array[Op] {
  let (x0, y0, x1, y1) = home_rect()
  let ww = x1 - x0
  let hh = y1 - y0
  let accent = (0.12, 0.14, 0.18)
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(x0, y0, ww, hh, (1.0, 1.0, 1.0))[:])
  ops.append(ops_stroke_rect(x0, y0, ww, hh, accent, 2.0)[:])
  ops.append(
    ops_text(x0 + 20.0, y0 + 10.0, "/F0", 12.0, "HOME", (0.12, 0.14, 0.18))[:],
  )
  ops
}

///|
fn ops_night_sky(
  x0 : Double,
  y0 : Double,
  x1 : Double,
  y1 : Double,
) -> Array[Op] {
  let ops : Array[Op] = []
  ops.push(OpBDC("/OC", Name(PdfName(b"/NIGHT"))))
  let ww = x1 - x0
  let hh = y1 - y0
  let stars : Array[(Double, Double)] = [
    (x0 + 24.0, y1 - 26.0),
    (x0 + ww * 0.22, y0 + hh * 0.62),
    (x0 + ww * 0.44, y0 + hh * 0.74),
    (x0 + ww * 0.66, y0 + hh * 0.58),
    (x0 + ww * 0.86, y0 + hh * 0.70),
    (x0 + ww * 0.78, y0 + hh * 0.86),
    (x0 + ww * 0.50, y0 + hh * 0.90),
  ]
  for s in stars {
    let (sx, sy) = s
    let p = circle_points(sx, sy, 2.2, 10)
    ops.append(ops_fill_polygon(p, (0.95, 0.96, 0.98))[:])
  }
  ops.append(
    ops_fill_rect(x0 + 12.0, y1 - 34.0, 196.0, 22.0, (0.06, 0.07, 0.1))[:],
  )
  ops.append(
    ops_stroke_rect(x0 + 12.0, y1 - 34.0, 196.0, 22.0, (0.95, 0.96, 0.98), 1.2)[:],
  )
  ops.append(
    ops_text(
      x0 + 20.0,
      y1 - 26.0,
      "/F2",
      10.0,
      "NIGHT MODE (Layers)",
      (0.95, 0.96, 0.98),
    )[:],
  )
  ops.push(OpEMC)
  ops
}

///|
fn ops_button(
  x : Double,
  y : Double,
  ww : Double,
  hh : Double,
  label : String,
  accent : (Double, Double, Double),
) -> (Array[Op], (Double, Double, Double, Double)) {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(x, y, ww, hh, (1.0, 1.0, 1.0))[:])
  ops.append(ops_stroke_rect(x, y, ww, hh, accent, 2.0)[:])
  ops.append(ops_fill_rect(x, y + hh - 7.0, ww, 7.0, accent)[:])
  ops.append(
    ops_text(x + 14.0, y + 16.0, "/F0", 14.0, label, (0.12, 0.14, 0.18))[:],
  )
  (ops, (x, y, x + ww, y + hh))
}

///|
fn cover_ops() -> (Array[Op], Array[(Double, Double, Double, Double)]) {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(0.0, 0.0, w(), h(), (0.98, 0.98, 0.99))[:])
  ops.append(ops_fill_rect(0.0, h() - 130.0, w(), 130.0, (0.18, 0.48, 0.86))[:])
  ops.append(
    ops_text(54.0, h() - 78.0, "/F0", 46.0, "Weekend Getaway", (1.0, 1.0, 1.0))[:],
  )
  ops.append(
    ops_text(
      56.0,
      h() - 110.0,
      "/F1",
      16.0,
      "An interactive mini travel brochure",
      (0.92, 0.94, 0.98),
    )[:],
  )
  ops.append(
    ops_text(
      54.0,
      114.0,
      "/F1",
      14.0,
      "Tip: toggle NIGHT MODE + SURPRISE in Layers.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  ops.append(ops_tabs()[:])
  ops.append(ops_night_sky(0.0, h() - 130.0, w(), h())[:])
  ops.push(OpBDC("/OC", Name(PdfName(b"/NIGHT"))))
  ops.append(ops_fill_rect(0.0, h() - 130.0, w(), 130.0, (0.06, 0.07, 0.1))[:])
  ops.append(
    ops_text(
      54.0,
      h() - 78.0,
      "/F0",
      46.0,
      "Weekend Getaway",
      (0.95, 0.96, 0.98),
    )[:],
  )
  ops.append(
    ops_text(
      56.0,
      h() - 110.0,
      "/F1",
      16.0,
      "An interactive mini travel brochure",
      (0.8, 0.84, 0.9),
    )[:],
  )
  ops.push(OpEMC)
  let x = 54.0
  let ww = 360.0
  let hh = 54.0
  let rects : Array[(Double, Double, Double, Double)] = []
  let (b1, r1) = ops_button(x, 440.0, ww, hh, "Open Map", (0.18, 0.48, 0.86))
  let (b2, r2) = ops_button(x, 372.0, ww, hh, "Itinerary", (0.18, 0.66, 0.52))
  let (b3, r3) = ops_button(x, 304.0, ww, hh, "Coupons", (0.95, 0.75, 0.2))
  let (b4, r4) = ops_button(x, 236.0, ww, hh, "Postcard", (0.92, 0.33, 0.36))
  let (b5, r5) = ops_button(x, 168.0, ww, hh, "Packing List", (0.52, 0.4, 0.86))
  ops.append(b1[:])
  ops.append(b2[:])
  ops.append(b3[:])
  ops.append(b4[:])
  ops.append(b5[:])
  rects.push(r1)
  rects.push(r2)
  rects.push(r3)
  rects.push(r4)
  rects.push(r5)
  (ops, rects)
}

///|
fn map_ops() -> (Array[Op], (Double, Double, Double, Double)) {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(0.0, 0.0, w(), h(), (1.0, 1.0, 1.0))[:])
  ops.append(ops_tabs(active=0)[:])
  ops.append(ops_home_badge()[:])
  ops.append(
    ops_text(54.0, h() - 78.0, "/F0", 40.0, "Map", (0.12, 0.14, 0.18))[:],
  )
  ops.append(
    ops_text(
      54.0,
      h() - 108.0,
      "/F1",
      14.0,
      "Tap markers. Or tap OPEN MAP for online view.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  let frame_x = 54.0
  let frame_y = 120.0
  let frame_w = w() - 140.0
  let frame_h = h() - 280.0
  ops.append(
    ops_stroke_rect(frame_x, frame_y, frame_w, frame_h, (0.12, 0.14, 0.18), 3.0)[:],
  )

  // River
  let river : Array[(Double, Double)] = [
    (frame_x + 40.0, frame_y + 520.0),
    (frame_x + 120.0, frame_y + 460.0),
    (frame_x + 220.0, frame_y + 420.0),
    (frame_x + 320.0, frame_y + 360.0),
    (frame_x + 420.0, frame_y + 300.0),
    (frame_x + 520.0, frame_y + 260.0),
  ]
  ops.append(ops_polyline(river, (0.18, 0.48, 0.86), 10.0)[:])

  // Landmarks
  fn marker(
    cx : Double,
    cy : Double,
    label : String,
    color : (Double, Double, Double),
  ) -> Unit {
    let p = circle_points(cx, cy, 16.0, 40)
    ops.append(ops_fill_polygon(p, color)[:])
    ops.append(
      ops_text(cx - 5.0, cy - 5.0, "/F0", 14.0, label, (1.0, 1.0, 1.0))[:],
    )
  }

  marker(frame_x + 140.0, frame_y + 420.0, "A", (0.18, 0.66, 0.52))
  marker(frame_x + 360.0, frame_y + 360.0, "B", (0.95, 0.75, 0.2))
  marker(frame_x + 480.0, frame_y + 240.0, "C", (0.92, 0.33, 0.36))
  ops.append(
    ops_text(
      frame_x + 60.0,
      frame_y + 90.0,
      "/F1",
      12.0,
      "A = Scenic lookout   B = Snack stop   C = Sunset pier",
      (0.12, 0.14, 0.18),
    )[:],
  )

  // Night overlay just on the frame area
  ops.append(
    ops_night_sky(frame_x, frame_y, frame_x + frame_w, frame_y + frame_h)[:],
  )
  let (btn, rect) = ops_button(
    54.0,
    60.0,
    220.0,
    44.0,
    "OPEN MAP",
    (0.18, 0.48, 0.86),
  )
  ops.append(btn[:])
  (ops, rect)
}

///|
fn itinerary_ops() -> Array[Op] {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(0.0, 0.0, w(), h(), (1.0, 1.0, 1.0))[:])
  ops.append(ops_tabs(active=1)[:])
  ops.append(ops_home_badge()[:])
  ops.append(
    ops_text(54.0, h() - 78.0, "/F0", 40.0, "Itinerary", (0.12, 0.14, 0.18))[:],
  )
  ops.append(
    ops_text(
      54.0,
      h() - 108.0,
      "/F1",
      14.0,
      "A weekend plan you can actually follow.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  fn checkbox(x : Double, y : Double) -> Unit {
    ops.append(ops_stroke_rect(x, y, 12.0, 12.0, (0.12, 0.14, 0.18), 1.6)[:])
  }

  fn item(y : Double, time : String, label : String) -> Unit {
    checkbox(56.0, y + 2.0)
    ops.append(
      ops_text(76.0, y + 2.0, "/F2", 11.0, time, (0.18, 0.48, 0.86))[:],
    )
    ops.append(
      ops_text(132.0, y + 2.0, "/F1", 12.0, label, (0.12, 0.14, 0.18))[:],
    )
  }

  ops.append(
    ops_fill_rect(54.0, 520.0, w() - 160.0, 30.0, (0.95, 0.96, 0.98))[:],
  )
  ops.append(ops_text(62.0, 530.0, "/F0", 14.0, "DAY 1", (0.12, 0.14, 0.18))[:])
  item(486.0, "09:30", "Coffee + a walk by the river")
  item(462.0, "11:00", "Lookout A (photos!)")
  item(438.0, "13:00", "Snack stop B")
  item(414.0, "16:30", "Sunset at pier C")
  item(390.0, "19:00", "Dinner + dessert")
  ops.append(
    ops_fill_rect(54.0, 334.0, w() - 160.0, 30.0, (0.95, 0.96, 0.98))[:],
  )
  ops.append(ops_text(62.0, 344.0, "/F0", 14.0, "DAY 2", (0.12, 0.14, 0.18))[:])
  item(300.0, "09:00", "Lazy breakfast")
  item(276.0, "10:30", "Pick a souvenir")
  item(252.0, "12:00", "One last photo at A")
  item(228.0, "13:30", "Head home")

  // Night sticker
  ops.push(OpBDC("/OC", Name(PdfName(b"/NIGHT"))))
  ops.append(
    ops_text(
      54.0,
      80.0,
      "/F2",
      11.0,
      "NIGHT MODE: add stars + mood (Layers)",
      (0.95, 0.96, 0.98),
    )[:],
  )
  ops.push(OpEMC)
  ops
}

///|
fn coupons_ops() -> Array[Op] {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(0.0, 0.0, w(), h(), (1.0, 1.0, 1.0))[:])
  ops.append(ops_tabs(active=2)[:])
  ops.append(ops_home_badge()[:])
  ops.append(
    ops_text(54.0, h() - 78.0, "/F0", 40.0, "Coupons", (0.12, 0.14, 0.18))[:],
  )
  ops.append(
    ops_text(
      54.0,
      h() - 108.0,
      "/F1",
      14.0,
      "Print these and cut along the dashed lines.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  ops.append(
    ops_text(
      54.0,
      h() - 132.0,
      "/F1",
      14.0,
      "Toggle SURPRISE layer to reveal codes.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  fn dashed_box(
    x : Double,
    y : Double,
    ww : Double,
    hh : Double,
    accent : (Double, Double, Double),
  ) -> Unit {
    // fake dashes by small segments
    let (r, g, b) = accent
    ops.push(Opq)
    ops.push(Opw(2.0))
    ops.push(OpRG(r, g, b))
    let step = 12.0
    let dash = 7.0
    let mut xx = x
    while xx < x + ww {
      ops.push(Opm(xx, y))
      ops.push(Opl(if xx + dash < x + ww { xx + dash } else { x + ww }, y))
      ops.push(Opm(xx, y + hh))
      ops.push(Opl(if xx + dash < x + ww { xx + dash } else { x + ww }, y + hh))
      xx = xx + step
    }
    let mut yy = y
    while yy < y + hh {
      ops.push(Opm(x, yy))
      ops.push(Opl(x, if yy + dash < y + hh { yy + dash } else { y + hh }))
      ops.push(Opm(x + ww, yy))
      ops.push(Opl(x + ww, if yy + dash < y + hh { yy + dash } else { y + hh }))
      yy = yy + step
    }
    ops.push(OpS)
    ops.push(OpQ)
  }

  fn coupon(
    y : Double,
    title : String,
    perk : String,
    accent : (Double, Double, Double),
    code : String,
  ) -> Unit {
    let x = 54.0
    let ww = w() - 170.0
    let hh = 120.0
    ops.append(ops_fill_rect(x, y, ww, hh, (0.98, 0.98, 0.99))[:])
    dashed_box(x, y, ww, hh, accent)
    ops.append(
      ops_text(x + 16.0, y + hh - 34.0, "/F0", 20.0, title, (0.12, 0.14, 0.18))[:],
    )
    ops.append(
      ops_text(x + 16.0, y + hh - 62.0, "/F1", 13.0, perk, (0.45, 0.48, 0.52))[:],
    )
    ops.append(
      ops_text(
        x + 16.0,
        y + 18.0,
        "/F2",
        10.0,
        "Show at checkout. One per person.",
        (0.45, 0.48, 0.52),
      )[:],
    )
    ops.push(OpBDC("/OC", Name(PdfName(b"/SURPRISE"))))
    ops.append(
      ops_fill_rect(x + ww - 170.0, y + 18.0, 150.0, 34.0, (1.0, 1.0, 1.0))[:],
    )
    ops.append(
      ops_stroke_rect(x + ww - 170.0, y + 18.0, 150.0, 34.0, accent, 2.0)[:],
    )
    ops.append(
      ops_text(
        x + ww - 158.0,
        y + 29.0,
        "/F2",
        12.0,
        "CODE: " + code,
        (0.12, 0.14, 0.18),
      )[:],
    )
    ops.push(OpEMC)
  }

  coupon(
    468.0,
    "Free Sticker",
    "Pick any tiny sticker at the shop.",
    (0.18, 0.48, 0.86),
    "STARS-7",
  )
  coupon(
    324.0,
    "Snack Upgrade",
    "Upgrade to a bigger treat for free.",
    (0.18, 0.66, 0.52),
    "CRUNCH-5",
  )
  coupon(
    180.0,
    "Postcard Bonus",
    "Free stamp with any postcard.",
    (0.92, 0.33, 0.36),
    "HELLO-9",
  )
  ops
}

///|
fn postcard_ops() -> Array[Op] {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(0.0, 0.0, w(), h(), (0.98, 0.98, 0.99))[:])
  ops.append(ops_tabs(active=3)[:])
  ops.append(ops_home_badge()[:])
  ops.append(
    ops_text(54.0, h() - 78.0, "/F0", 40.0, "Postcard", (0.12, 0.14, 0.18))[:],
  )
  ops.append(
    ops_text(
      54.0,
      h() - 108.0,
      "/F1",
      14.0,
      "Print this page. Cut on the border. Write on the back.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  let x = 54.0
  let y = 140.0
  let ww = w() - 170.0
  let hh = h() - 300.0
  ops.append(ops_fill_rect(x, y, ww, hh, (1.0, 1.0, 1.0))[:])
  ops.append(ops_stroke_rect(x, y, ww, hh, (0.12, 0.14, 0.18), 3.0)[:])

  // Simple "photo" (vector landscape)
  ops.append(ops_fill_rect(x, y + hh - 220.0, ww, 220.0, (0.18, 0.48, 0.86))[:])
  let hills : Array[(Double, Double)] = [
    (x, y + hh - 140.0),
    (x + 120.0, y + hh - 170.0),
    (x + 220.0, y + hh - 130.0),
    (x + 340.0, y + hh - 180.0),
    (x + ww, y + hh - 140.0),
    (x + ww, y + hh - 220.0),
    (x, y + hh - 220.0),
  ]
  ops.append(ops_fill_polygon(hills, (0.18, 0.66, 0.52))[:])
  ops.append(ops_fill_rect(x, y + hh - 64.0, ww, 64.0, (0.95, 0.75, 0.2))[:])
  ops.append(
    ops_text(
      x + 18.0,
      y + hh - 42.0,
      "/F0",
      22.0,
      "Wish you were here!",
      (0.12, 0.14, 0.18),
    )[:],
  )

  // Back layout lines
  ops.append(
    ops_polyline(
      [(x + ww / 2.0, y + 18.0), (x + ww / 2.0, y + hh - 236.0)],
      (0.78, 0.8, 0.84),
      1.5,
    )[:],
  )
  for i in 0..<6 {
    let yy = y + 32.0 + i.to_double() * 26.0
    ops.append(
      ops_polyline(
        [(x + ww / 2.0 + 16.0, yy), (x + ww - 16.0, yy)],
        (0.78, 0.8, 0.84),
        1.0,
      )[:],
    )
  }
  ops.append(
    ops_text(
      x + ww / 2.0 + 16.0,
      y + hh - 260.0,
      "/F2",
      10.0,
      "ADDRESS",
      (0.45, 0.48, 0.52),
    )[:],
  )
  ops.append(ops_night_sky(x, y + hh - 220.0, x + ww, y + hh)[:])
  ops
}

///|
fn packing_ops() -> Array[Op] {
  let ops : Array[Op] = []
  ops.append(ops_fill_rect(0.0, 0.0, w(), h(), (1.0, 1.0, 1.0))[:])
  ops.append(ops_tabs(active=4)[:])
  ops.append(ops_home_badge()[:])
  ops.append(
    ops_text(54.0, h() - 78.0, "/F0", 40.0, "Packing List", (0.12, 0.14, 0.18))[:],
  )
  ops.append(
    ops_text(
      54.0,
      h() - 108.0,
      "/F1",
      14.0,
      "Print and check things off.",
      (0.45, 0.48, 0.52),
    )[:],
  )
  fn checkbox(x : Double, y : Double) -> Unit {
    ops.append(ops_stroke_rect(x, y, 12.0, 12.0, (0.12, 0.14, 0.18), 1.6)[:])
  }

  fn line(y : Double, item : String) -> Unit {
    checkbox(56.0, y)
    ops.append(
      ops_text(76.0, y + 2.0, "/F1", 12.0, item, (0.12, 0.14, 0.18))[:],
    )
  }

  let items : Array[String] = [
    "Phone + charger", "Water bottle", "Sunglasses", "Light jacket", "Snacks", "Small umbrella",
    "Camera (optional)", "Postcards (from this PDF)", "One tiny gift for future-you",
  ]
  let mut y = 560.0
  for it in items {
    line(y, it)
    y = y - 26.0
  }
  ops.push(OpBDC("/OC", Name(PdfName(b"/SURPRISE"))))
  ops.append(
    ops_fill_rect(54.0, 120.0, w() - 170.0, 62.0, (0.95, 0.96, 0.98))[:],
  )
  ops.append(
    ops_stroke_rect(54.0, 120.0, w() - 170.0, 62.0, (0.52, 0.4, 0.86), 2.0)[:],
  )
  ops.append(
    ops_text(66.0, 158.0, "/F0", 16.0, "SURPRISE NOTE", (0.12, 0.14, 0.18))[:],
  )
  ops.append(
    ops_text(
      66.0,
      136.0,
      "/F1",
      12.0,
      "Send this PDF to someone and invite them.",
      (0.12, 0.14, 0.18),
    )[:],
  )
  ops.push(OpEMC)
  ops
}

///|
fn add_tabs_links(
  pdf : @pdf.Pdf,
  pages : Array[@pdfpage.Page],
  refs : Array[Int],
) -> Array[@pdfpage.Page] raise {
  let out = pages
  // tabs correspond to pages 1..5
  for p in 0.. Unit {
  try {
    let (cover, cover_rects) = cover_ops()
    let (map, map_open_rect) = map_ops()
    let pages : Array[@pdfpage.Page] = [
      page_of_ops(cover),
      page_of_ops(map),
      page_of_ops(itinerary_ops()),
      page_of_ops(coupons_ops()),
      page_of_ops(postcard_ops()),
      page_of_ops(packing_ops()),
    ]
    let (pdf, pageroot) = @pdfpage.PdfPageDoc::new(@pdf.Pdf::empty()).add_pagetree(
      pages,
    )
    let pdf = @pdfpage.PdfPageDoc::new(pdf).add_root(pageroot, [])
    let (night_num, surprise_num) = add_layers(pdf)
    let pdf = add_layer_properties_to_pages(pdf, night_num, surprise_num)
    let refs = pdf.page_reference_numbers()
    let pdf = add_bookmarks(pdf, refs)

    // Add navigation links
    let pages = @pdfpage.PdfPageDoc::new(pdf).pages_of_pagetree()
    let out_pages = pages

    // Cover buttons -> pages 1..5
    out_pages[0] = add_link(
      pdf,
      out_pages[0],
      cover_rects[0],
      goto_action(refs[1]),
    )
    out_pages[0] = add_link(
      pdf,
      out_pages[0],
      cover_rects[1],
      goto_action(refs[2]),
    )
    out_pages[0] = add_link(
      pdf,
      out_pages[0],
      cover_rects[2],
      goto_action(refs[3]),
    )
    out_pages[0] = add_link(
      pdf,
      out_pages[0],
      cover_rects[3],
      goto_action(refs[4]),
    )
    out_pages[0] = add_link(
      pdf,
      out_pages[0],
      cover_rects[4],
      goto_action(refs[5]),
    )

    // Home badge on pages 1..5
    let home = home_rect()
    for i in 1.. external link
    out_pages[1] = add_link(
      pdf,
      out_pages[1],
      map_open_rect,
      uri_action("https://www.openstreetmap.org"),
    )

    // Right-side tabs on every page
    let out_pages = add_tabs_links(pdf, out_pages, refs)
    let pdf = @pdfpage.PdfPageDoc::new(pdf).change_pages(true, out_pages)
    @pdfwritefs.PdfWriteFs::new().pdf_to_file(pdf, out_file)
  } catch {
    err =>
      if !quiet {
        println("Failed to produce output.")
        println(err)
      }
  }
}

///|
async fn main {
  let args = @env.args()
  match args {
    [_, out] => run(out_file=out)
    _ => run()
  }
}