print_and_play_paper_compact <- function(cfg, size, pieces, arrangement, quietly, size_bleed) {
	n_suits <- cfg$n_suits
	n_ranks <- cfg$n_ranks

	stopifnot(n_ranks <= 6)

	if (size == "letter") {
		xl <- inch(LETTER_HEIGHT / 2 - A5W / 2 + size_bleed$left)
		xr <- inch(LETTER_HEIGHT / 2 + A5W / 2 + size_bleed$left)
		y <- inch(LETTER_WIDTH / 2 + size_bleed$bottom)
	} else if (size == "A4") {
		xl <- inch(A4_HEIGHT / 2 - A5W / 2 + size_bleed$left)
		xr <- inch(A4_HEIGHT / 2 + A5W / 2 + size_bleed$left)
		y <- inch(A4_WIDTH / 2 + size_bleed$bottom)
	} else {
		# size is "A5"
		xl <- inch(A5W / 2 + size_bleed$left)
		xr <- inch(A5W / 2 + size_bleed$left)
		y <- inch(A5H / 2 + size_bleed$bottom)
	}

	vpl <- viewport(x = xl, y = y, width = inch(A5W), height = inch(A5H))
	vpr <- viewport(x = xr, y = y, width = inch(A5W), height = inch(A5H))
	gl <- list()
	pl <- list()

	if ("piecepack" %in% pieces || "subpack" %in% pieces) {
		psuits <- seq(cfg$n_suits + 2L)
		lpgf <- lapply(
			psuits,
			a5_piecepack_grob,
			cfg = cfg,
			front = TRUE,
			arrangement = arrangement
		)
		lpgb <- lapply(
			psuits,
			a5_piecepack_grob,
			cfg = cfg,
			front = FALSE,
			arrangement = arrangement
		)
	}

	## Front Matter
	gl <- gappend(gl, a5_title_grob(cfg, pieces, quietly))
	gl <- gappend(gl, a5_inst_grob(cfg, pieces))
	if (arrangement == "double-sided" && size != "A5") {
		gl <- gappend(gl, blank_grob)
		gl <- gappend(gl, blank_grob)
		pl[["Front Matter"]] <- 2
	} else {
		pl[["Front Matter"]] <- 1
	}

	if ("piecepack" %in% pieces) {
		n_pages <- n_suits
		if (is_odd(n_suits) && arrangement == "double-sided" && size != "A5") {
			n_pages <- n_pages + 1
		}
		for (suit in seq(n_pages)) {
			gl <- gappend(gl, lpgf[[suit]])
			gl <- gappend(gl, lpgb[[suit]])
		}
		pl$Piecepack <- n_pages
	}

	#### Fine-tune between pyramids, matchsticks, and misc.?
	#### Add misc, cards, dominoes accessories?
	if ("matchsticks" %in% pieces) {
		n_pages <- (n_suits - 1) %/% 4 + 1
		for (ii in seq(n_pages)) {
			ss <- seq(4 * ii - 3, 4 * ii)
			if (arrangement == "double-sided" && size == "A5") {
				gl <- gappend(gl, a5_matchsticks_grob(ss, cfg, TRUE))
				gl <- gappend(gl, a5_matchsticks_grob(ss, cfg, FALSE))
				gl <- gappend(gl, a5_matchsticks_grob2(ss, cfg, TRUE))
				gl <- gappend(gl, a5_matchsticks_grob2(ss, cfg, FALSE))
			} else {
				gl <- gappend(gl, a5_matchsticks_grob(ss, cfg, TRUE))
				gl <- gappend(gl, a5_matchsticks_grob2(ss, cfg, TRUE))
				if (arrangement == "double-sided") {
					gl <- gappend(gl, a5_matchsticks_grob2(ss, cfg, FALSE))
					gl <- gappend(gl, a5_matchsticks_grob(ss, cfg, FALSE))
				}
			}
		}
		pl$Matchsticks <- n_pages
	}
	if ("pyramids" %in% pieces) {
		n_pages <- (n_suits - 1) %/% 4 + 1
		for (ii in seq(n_pages)) {
			ss <- seq(4 * ii - 3, 4 * ii)
			s1 <- ss[1:2]
			s2 <- ss[3:4]
			if (arrangement == "double-sided" && size == "A5") {
				gl <- gappend(gl, a5_pyramids_grob(s1, cfg, TRUE))
				gl <- gappend(gl, blank_grob)
				gl <- gappend(gl, a5_pyramids_grob(s2, cfg, FALSE))
				gl <- gappend(gl, blank_grob)
			} else {
				gl <- gappend(gl, a5_pyramids_grob(s1, cfg, TRUE))
				gl <- gappend(gl, a5_pyramids_grob(s2, cfg, FALSE))
				if (arrangement == "double-sided") {
					gl <- gappend(gl, blank_grob)
					gl <- gappend(gl, blank_grob)
				}
			}
		}
		pl$Pyramids <- n_pages
	}
	if ("subpack" %in% pieces) {
		vpul <- viewport(x = 0.25, y = 0.75, width = inch(A5W / 2), height = inch(A5H / 2))
		vpur <- viewport(x = 0.75, y = 0.75, width = inch(A5W / 2), height = inch(A5H / 2))
		vpll <- viewport(x = 0.25, y = 0.25, width = inch(A5W / 2), height = inch(A5H / 2))
		vplr <- viewport(x = 0.75, y = 0.25, width = inch(A5W / 2), height = inch(A5H / 2))
		lsgf <- lapply(lpgf, as_picture, width = A5W, height = A5H)
		lsgb <- lapply(lpgb, as_picture, width = A5W, height = A5H)
		n_pages <- (n_suits - 1) %/% 4 + 1
		if (arrangement == "double-sided" && is_odd(n_pages) && size != "A5") {
			n_pages <- n_pages + 1
		}
		for (ii in seq(n_pages)) {
			ss <- seq(4 * ii - 3, 4 * ii)
			ss[which(ss > cfg$n_suits + 2L)] <- cfg$n_suits + 2L
			gf <- grobTree(
				grobTree(lsgf[[ss[1]]], vp = vpul),
				grobTree(lsgb[[ss[1]]], vp = vpur),
				grobTree(lsgf[[ss[2]]], vp = vpll),
				grobTree(lsgb[[ss[2]]], vp = vplr)
			)
			gb <- grobTree(
				grobTree(lsgf[[ss[3]]], vp = vpul),
				grobTree(lsgb[[ss[3]]], vp = vpur),
				grobTree(lsgf[[ss[4]]], vp = vpll),
				grobTree(lsgb[[ss[4]]], vp = vplr)
			)
			gl <- gappend(gl, gf)
			gl <- gappend(gl, gb)
		}
		pl$Subpack <- n_pages
	}
	if (size == "A5") {
		pl <- lapply(pl, function(x) 2 * x)
	}
	for (ii in seq(gl)) {
		if (is_odd(ii)) {
			grid.newpage()
			draw_a5_page(gl[[ii]], vpl)
		} else {
			if (size == "A5") {
				grid.newpage()
			}
			draw_a5_page(gl[[ii]], vpr)
		}
	}

	pl
}

a5_inst_grob <- function(cfg = pp_cfg(), pieces) {
	grob_title <- textGrob(
		"Piece Anatomy",
		y = 0.97,
		just = "center",
		gp = gp_title,
		name = "title"
	)
	# Tile
	yt <- A5H - 1.3
	xt <- 1.5
	df <- tibble(
		piece_side = c("tile_face", "tile_back"),
		suit = 1,
		x = xt + c(-0.5, 0.5),
		y = yt,
		width = 1,
		height = 1
	)
	grob_tile <- grobTree(
		htg("Tile", xt, yt + 0.7),
		tg("Face", xt - 0.5, y = yt + 0.6),
		tg("Back", xt + 0.5, y = yt + 0.6),
		pmap_piece(
			df,
			default.units = "in",
			draw = FALSE,
			cfg = cfg,
			gp = gpar(cex = 0.5, lex = 0.5)
		)
	)
	# curveGrob(x1=xt[1]-0.8, y1=yt-1.1, x2=xt[2]+0.8, y2=yt-1.1,
	#           curvature=0.2, square=FALSE, arrow=arrow(),
	#           default.units="in"))
	# Coin
	cwr <- 0.75 / cfg$get_width("coin_face")
	cw <- 0.75
	yc <- yt - 0.5 - cw
	xc <- 0.5 + 2 * cw
	df <- tibble(
		piece_side = rep(c("coin_back", "coin_face"), 2),
		x = xc + c(1.7, 0.7, -0.7, -1.7) * cw,
		y = rep(yc, 4),
		width = 0.75,
		height = 0.75,
		angle = c(0, 180, 0, 0)
	)
	grob_coin <- grobTree(
		htg("Coin", xc, yc + 0.5 * cw + 0.2),
		tg("Back", xc + c(1.7, -0.7) * cw, yc + 0.5 * cw + 0.1),
		tg("Face", xc + c(0.7, -1.7) * cw, yc + 0.5 * cw + 0.1),
		pmap_piece(
			df,
			default.units = "in",
			cfg = cfg,
			draw = FALSE,
			gp = gpar(lex = cwr, cex = cwr)
		),
		textGrob("or", x = xc, y = yc, default.units = "in")
	)
	# Saucer
	swr <- 0.75 / cfg$get_width("saucer_face")
	sw <- 0.75
	ys <- yt - 1.6 - sw
	xs <- 0.5 + 2 * cw
	df <- tibble(
		piece_side = rep(c("saucer_back", "saucer_face"), 2),
		x = xs + c(1.7, 0.7, -0.7, -1.7) * sw,
		y = rep(ys, 4),
		width = 0.75,
		height = 0.75,
		angle = c(0, 180, 0, 0)
	)
	grob_saucer <- grobTree(
		htg("(Pawn) Saucer", xs, ys + 0.5 * sw + 0.25),
		tg("Back", xs + c(1.7, -0.7) * sw, ys + 0.5 * sw + 0.1),
		tg("Face", xs + c(0.7, -1.7) * sw, ys + 0.5 * sw + 0.1),
		pmap_piece(
			df,
			default.units = "in",
			cfg = cfg,
			draw = FALSE,
			gp = gpar(lex = swr, cex = swr)
		),
		textGrob("or", x = xs, y = ys, default.units = "in")
	)

	# Dice
	dwr <- 0.5 / cfg$get_width("die_face")
	dw <- 0.5
	yd <- 1.4
	d1 <- grobTree(
		piecepackDieGrob(1, cfg, arrangement = "counter_down"),
		vp = viewport(
			x = 0.2,
			y = inch(yd),
			width = inch(4 * dw),
			height = inch(3 * dw),
			gp = gpar(lex = dwr, cex = dwr)
		)
	)
	d2 <- grobTree(
		piecepackDieGrob(1, cfg, arrangement = "counter_up"),
		vp = viewport(
			x = 0.5,
			y = inch(yd),
			width = inch(4 * dw),
			height = inch(3 * dw),
			gp = gpar(lex = dwr, cex = dwr)
		)
	)
	d3 <- grobTree(
		piecepackDieGrob(1, cfg, arrangement = "opposites_sum_to_5"),
		vp = viewport(
			x = 0.8,
			y = inch(yd),
			width = inch(4 * dw),
			height = inch(3 * dw),
			gp = gpar(lex = dwr, cex = dwr)
		)
	)
	grob_die <- grobTree(
		htg("Die", A5W / 2, yd + 1.2),
		tg('"counter_down"\narrangement', 0.3 * A5W, yd + 0.9),
		tg('"counter_up"\narrangement', 0.6 * A5W, yd + 0.9),
		tg('"opposites_sum_to_5"\narrangement', 0.85 * A5W, yd + 0.9),
		d1,
		d2,
		d3
	)
	# Pawn
	y_pawn <- 4.0
	grob_pawn <- grobTree(
		htg("Pawn", A5W - 0.5, y_pawn + 1.4),
		tg("Back", A5W - 0.85, y_pawn + 0.5, rot = 90),
		tg("Face", A5W - 0.85, y_pawn - 0.5, rot = 90),
		tg("Stand", A5W - 0.85, y_pawn - 1.1, rot = 90),
		tg("Stand", A5W - 0.85, y_pawn + 1.0, rot = 90),
		pieceGrob("pawn_layout", 1, cfg = cfg, x = inch(A5W - 0.5), y = inch(y_pawn))
	)
	# Belt
	yb <- yt - 2.0 - sw - 0.5
	xb <- 2
	grob_belt <- grobTree(
		htg("(Pawn) Belt (Face)", xb, yb + 0.4),
		pieceGrob("belt_face", x = xb, y = yb, cfg = cfg, default.units = "in")
	)
	# Pyramids
	yy <- yt + 0.10
	xy <- A5W - 1.2
	df <- tibble(
		piece_side = c("pyramid_layout", "pyramid_top"),
		suit = 1,
		rank = 1,
		x = xy + c(-0.5, 0.6),
		y = yy
	)
	grob_pyramid <- grobTree(
		htg("Pyramid", xy, yy + 0.8),
		htg("Top (View)", xy + 0.7, yy + 0.50),
		tg("Face", xy + 0.6, yy + 0.33),
		tg("Back", xy + 0.6, yy - 0.33),
		tg("Left", xy + 0.25, yy, rot = 90),
		tg("Right", xy + 0.95, yy, rot = -90),
		tg("Back", xy - 0.0, yy, rot = -90),
		tg("Face", xy - 1.0, yy + 0.60, rot = 90),
		tg("Face", xy - 1.0, yy - 0.60, rot = 90),
		tg("Right", xy - 0.05, yy + 0.45),
		tg("Left", xy - 0.05, yy - 0.45),
		pmap_piece(df, default.units = "in", cfg = cfg, draw = FALSE)
	)
	# Matchsticks
	df <- tibble(
		piece_side = c("matchstick_face", "matchstick_back"),
		x = 2.5,
		y = c(0.4, 0.2),
		rank = 3,
		angle = 90
	)
	grob_matchsticks <- grobTree(
		htg("Matchstick", 1, 0.3),
		tg("Face", 1.7, 0.4),
		tg("Back", 1.7, 0.2),
		pmap_piece(df, default.units = "in", cfg = cfg, draw = FALSE)
	)

	grobTree(
		grob_title,
		grob_tile,
		grob_coin,
		grob_saucer,
		grob_die,
		grob_pawn,
		grob_belt,
		grob_pyramid,
		grob_matchsticks,
		name = "instructions",
		vp = a5_vp()
	)
}

a5_matchsticks_grob <- function(suits = 1:4, cfg = pp_cfg(), front = TRUE) {
	n_suits <- length(suits)
	y1t <- A5H - 0.5 * MATCHSTICK_HEIGHTS[1]
	y1b <- A5H - 1.5 * MATCHSTICK_HEIGHTS[1]
	x1s <- (0.5 + 0:(3 * n_suits - 1)) * MATCHSTICK_WIDTHS[1]
	xs <- (0.5 + 0:(6 * n_suits - 1)) * MATCHSTICK_WIDTHS[2]
	y2 <- y1b - 0.5 * MATCHSTICK_HEIGHTS[1] - 0.5 * MATCHSTICK_HEIGHTS[2]
	y3 <- y2 - 0.5 * MATCHSTICK_HEIGHTS[2] - 0.5 * MATCHSTICK_HEIGHTS[3]
	y5 <- y3 - 0.5 * MATCHSTICK_HEIGHTS[3] - 0.5 * MATCHSTICK_HEIGHTS[5]
	y6 <- y5 - 0.5 * MATCHSTICK_HEIGHTS[5] - 0.5 * MATCHSTICK_HEIGHTS[6]

	x <- c(rep(x1s, each = 2), rep(xs, 4))
	y <- c(rep(c(y1t, y1b), 3 * n_suits), rep(c(y2, y3, y5, y6), each = 6 * n_suits))
	suit <- rep(rep(suits, each = 6), 5)
	rank <- rep(c(1:3, 5:6), each = 6 * n_suits)
	if (front) {
		piece_side <- "matchstick_face"
	} else {
		x <- A5W - x
		piece_side <- "matchstick_back"
	}
	df <- tibble(piece_side, x, y, suit, rank)

	pmap_piece(df, cfg = cfg, default.units = "inches", draw = FALSE)
}

a5_matchsticks_grob2 <- function(suits = 1:4, cfg = pp_cfg(), front = TRUE) {
	n_suits <- length(suits)
	y4s <- A5H - (0.5 + 0:3) * MATCHSTICK_HEIGHTS[4]
	x4s <- (0.5 + 0:5) * MATCHSTICK_WIDTHS[6]

	x <- rep(x4s, 4)
	y <- rep(c(y4s), each = 6)
	suit <- rep(suits, each = 6)
	rank <- 4
	if (front) {
		piece_side <- "matchstick_face"
	} else {
		x <- A5W - x
		piece_side <- "matchstick_back"
	}
	df <- tibble(piece_side, x, y, suit, rank)

	# add some extra dice if there is some space
	if (cfg$get_width("die_face") <= 0.6) {
		xd1 <- max(x4s) + 0.0 + c(0.6) * cfg$get_width("die_layoutLF")
		dfd <- tibble::tibble(
			piece_side = "die_layoutLF",
			x = xd1,
			y = unique(y),
			suit = suits,
			rank = NA
		)
		df <- rbind(df, dfd)
	}
	if (cfg$get_width("die_face") <= 0.55) {
		xd2 <- xd1 + c(0.7) * cfg$get_width("die_layoutLF")
		dfd <- tibble::tibble(
			piece_side = "die_layoutLF",
			x = xd2,
			y = unique(y),
			suit = suits,
			rank = NA
		)
		df <- rbind(df, dfd)
	}

	pmap_piece(df, cfg = cfg, default.units = "inches", draw = FALSE)
}

a5_piecepack_grob <- function(suit, cfg = pp_cfg(), front = TRUE, arrangement = "single-sided") {
	die_width <- cfg$get_width("die_width")
	pawn_width <- cfg$get_width("pawn_width")
	tile_width <- cfg$get_width("tile_back")
	xtl <- 1.5 * tile_width
	xtr <- 0.5 * tile_width
	ytb <- 0.5 * tile_width
	ytm <- 1.5 * tile_width
	ytt <- 2.5 * tile_width
	xc <- A5W - 0.25 * tile_width
	ycs <- rev((0.50 + seq(0, 5)) * 0.8)
	xdr <- c(0.5, 1.5, 2.5) * die_width
	ydt <- A5H - 0.5 * die_width
	ydb <- A5H - 1.5 * die_width
	xp <- A5W - 0.5 * cfg$get_height("pawn_layout")
	yp <- A5H - 0.5 * pawn_width
	xb <- A5W - 0.5 * cfg$get_width("belt_face")
	yb <- A5H - pawn_width - 0.5 * cfg$get_height("belt_face") - 0.25
	xsr <- A5W - 0.25 * tile_width
	ysb <- 2.75 * tile_width
	dft <- tibble(
		piece_side = "tile_face",
		x = rep(c(xtr, xtl), 3),
		y = rep(c(ytt, ytm, ytb), each = 2),
		suit,
		rank = 1:6,
		angle = 0
	)
	dfc <- tibble(
		piece_side = "coin_back",
		x = rep(xc, 6),
		y = ycs,
		suit,
		rank = 1:6,
		angle = ifelse(front, cfg$coin_arrangement, 0)
	)
	dfd <- tibble(
		piece_side = "die_face",
		x = rep(xdr, 2),
		y = rep(c(ydt, ydb), each = 3),
		suit,
		rank = 1:6,
		angle = 0
	)
	dfp <- tibble(piece_side = "pawn_layout", x = xp, y = yp, suit, rank = NA, angle = 90)
	dfb <- tibble(piece_side = "belt_face", x = xb, y = yb, suit, rank = NA, angle = 0)
	dfs <- tibble(
		piece_side = "saucer_face",
		x = xsr,
		y = ysb,
		suit,
		rank = NA,
		angle = ifelse(front, 0, cfg$coin_arrangement)
	)
	df <- rbind(dfc, dfd, dfp, dfb, dfs, dft)
	if (!front) {
		df$x <- A5W - df$x
		df$piece_side <- c(
			rep("coin_face", 6),
			rep("die_face", 6),
			"pawn_layout",
			"belt_face",
			"saucer_back",
			rep("tile_back", 6)
		)
		# Rotate tile backs to partially hide direction of face if tile back are not fully symmetric
		df$angle <- c(rep(0, 12), 270, 0, 0, 90 * ((df$suit[16:21] + df$rank[16:21]) %% 4))

		if (arrangement == "double-sided") {
			df <- df[df$piece_side %in% c("tile_back", "coin_face", "saucer_back"), ]
		}
	}
	pmap_piece(df, cfg = cfg, default.units = "inches", draw = FALSE)
}

a5_pyramids_grob <- function(suit = 1:2, cfg = pp_cfg(), front = TRUE) {
	grobTree(pyramid_grob_helper(suit[1], cfg, 0), pyramid_grob_helper(suit[2], cfg, A5W / 2))
}

pyramid_grob_helper <- function(suit, cfg = pp_cfg(), xleft = 0) {
	rank <- c(2, 4, 5, 1, 3, 6)
	plh <- PYRAMID_LAYOUT_HEIGHTS[rank]
	y_up <- cumsum(plh) - 0.5 * plh
	x_up <- 0.5 * PYRAMID_LAYOUT_WIDTHS[rank]
	x <- xleft + c(x_up[1:3], A5W / 2 - x_up[4:6])
	y <- c(y_up[1:3], c(plh[5] + plh[6] + 0.5 * plh[4], plh[6] + 0.5 * plh[5], 0.5 * plh[6]))
	piece_side <- "pyramid_layout"
	angle <- c(rep(0, 3), rep(180, 3))
	df <- tibble::tibble(piece_side, x, y, suit, rank, angle)
	# space for a pawn layout on top
	dfp <- tibble::tibble(
		piece_side = "pawn_layout",
		x = xleft + 0.25 * A5W,
		y = A5H - 0.5 * cfg$get_width("pawn_layout"),
		suit,
		rank = 1,
		angle = 90
	)
	df <- rbind(dfp, df)
	pmap_piece(df, cfg = cfg, default.units = "inches", draw = FALSE)
}
