/*
Copyright 2018 The pdfcpu Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package pdfcpu

import (
	"bytes"
	"fmt"
	"io"
	"math"
	"os"
	"sort"
	"strconv"
	"strings"

	"github.com/pdfcpu/pdfcpu/pkg/filter"
	"github.com/pkg/errors"
)

var (
	errInvalidGridID    = errors.New("pdfcpu nup: n must be one of 2, 3, 4, 6, 8, 9, 12, 16")
	errInvalidGridDims  = errors.New("pdfcpu grid: dimensions: m >= 0, n >= 0")
	errInvalidNUpConfig = errors.New("pdfcpu: invalid configuration string")
)

var (
	nUpValues = []int{2, 3, 4, 6, 8, 9, 12, 16}
	nUpDims   = map[int]Dim{
		2:  {2, 1},
		3:  {3, 1},
		4:  {2, 2},
		6:  {3, 2},
		8:  {4, 2},
		9:  {3, 3},
		12: {4, 3},
		16: {4, 4},
	}
)

type nUpParamMap map[string]func(string, *NUp) error

var nupParamMap = nUpParamMap{
	"dimensions":      parseDimensionsNUp,
	"formsize":        parsePageFormatNUp,
	"papersize":       parsePageFormatNUp,
	"orientation":     parseOrientation,
	"border":          parseElementBorder,
	"margin":          parseElementMargin,
	"backgroundcolor": parseSheetBackgroundColor,
	"bgcolor":         parseSheetBackgroundColor,
	"guides":          parseBookletGuides,
}

// Handle applies parameter completion and if successful
// parses the parameter values into import.
func (m nUpParamMap) Handle(paramPrefix, paramValueStr string, nup *NUp) error {
	var param string

	// Completion support
	for k := range m {
		if !strings.HasPrefix(k, paramPrefix) {
			continue
		}
		if len(param) > 0 {
			return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix)
		}
		param = k
	}

	if param == "" {
		return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix)
	}

	return m[param](paramValueStr, nup)
}

// NUp represents the command details for the command "NUp".
type NUp struct {
	PageDim       *Dim         // Page dimensions in display unit.
	PageSize      string       // Paper size eg. A4L, A4P, A4(=default=A4P), see paperSize.go
	UserDim       bool         // true if one of dimensions or paperSize provided overriding the default.
	Orient        orientation  // One of rd(=default),dr,ld,dl
	Grid          *Dim         // Intra page grid dimensions eg (2,2)
	PageGrid      bool         // Create a mxn grid of pages for PDF inputfiles only (think "extra page n-Up").
	ImgInputFile  bool         // Process image or PDF input files.
	Margin        int          // Cropbox for n-Up content.
	Border        bool         // Draw bounding box.
	BookletGuides bool         // Draw folding and cutting lines
	InpUnit       DisplayUnit  // input display unit.
	BgColor       *SimpleColor // background color
}

// DefaultNUpConfig returns the default NUp configuration.
func DefaultNUpConfig() *NUp {
	return &NUp{
		PageSize: "A4",
		Orient:   RightDown,
		Margin:   3,
		Border:   true,
	}
}

func (nup NUp) String() string {
	return fmt.Sprintf("N-Up conf: %s %s, orient=%s, grid=%s, pageGrid=%t, isImage=%t\n",
		nup.PageSize, *nup.PageDim, nup.Orient, *nup.Grid, nup.PageGrid, nup.ImgInputFile)
}

// N returns the nUp value.
func (nup NUp) N() int {
	return int(nup.Grid.Height * nup.Grid.Width)
}

type orientation int

func (o orientation) String() string {
	switch o {

	case RightDown:
		return "right down"

	case DownRight:
		return "down right"

	case LeftDown:
		return "left down"

	case DownLeft:
		return "down left"

	}

	return ""
}

// These are the defined anchors for relative positioning.
const (
	RightDown orientation = iota
	DownRight
	LeftDown
	DownLeft
)

func parsePageFormatNUp(s string, nup *NUp) (err error) {
	if nup.UserDim {
		return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed")
	}
	nup.PageDim, nup.PageSize, err = parsePageFormat(s)
	nup.UserDim = true
	return err
}

func parseDimensionsNUp(s string, nup *NUp) (err error) {
	if nup.UserDim {
		return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed")
	}
	nup.PageDim, nup.PageSize, err = parsePageDim(s, nup.InpUnit)
	nup.UserDim = true
	return err
}

func parseOrientation(s string, nup *NUp) error {
	switch s {
	case "rd":
		nup.Orient = RightDown
	case "dr":
		nup.Orient = DownRight
	case "ld":
		nup.Orient = LeftDown
	case "dl":
		nup.Orient = DownLeft
	default:
		return errors.Errorf("pdfcpu: unknown nUp orientation: %s", s)
	}

	return nil
}

func parseElementBorder(s string, nup *NUp) error {
	switch strings.ToLower(s) {
	case "on", "true":
		nup.Border = true
	case "off", "false":
		nup.Border = false
	default:
		return errors.New("pdfcpu: nUp border, please provide one of: on/off true/false")
	}

	return nil
}

func parseBookletGuides(s string, nup *NUp) error {
	switch strings.ToLower(s) {
	case "on", "true":
		nup.BookletGuides = true
	case "off", "false":
		nup.BookletGuides = false
	default:
		return errors.New("pdfcpu: booklet guides, please provide one of: on/off true/false")
	}

	return nil
}

func parseElementMargin(s string, nup *NUp) error {
	f, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return err
	}

	if f < 0 {
		return errors.New("pdfcpu: nUp margin, Please provide a positive value")
	}

	nup.Margin = int(toUserSpace(f, nup.InpUnit))

	return nil
}

func parseSheetBackgroundColor(s string, nup *NUp) error {
	c, err := parseColor(s)
	if err != nil {
		return err
	}
	nup.BgColor = &c
	return nil
}

// ParseNUpDetails parses a NUp command string into an internal structure.
func ParseNUpDetails(s string, nup *NUp) error {
	if s == "" {
		return errInvalidNUpConfig
	}

	ss := strings.Split(s, ",")

	for _, s := range ss {

		ss1 := strings.Split(s, ":")
		if len(ss1) != 2 {
			return errInvalidNUpConfig
		}

		paramPrefix := strings.TrimSpace(ss1[0])
		paramValueStr := strings.TrimSpace(ss1[1])

		if err := nupParamMap.Handle(paramPrefix, paramValueStr, nup); err != nil {
			return err
		}
	}

	return nil
}

// PDFNUpConfig returns an NUp configuration for Nup-ing PDF files.
func PDFNUpConfig(val int, desc string) (*NUp, error) {
	nup := DefaultNUpConfig()
	if desc != "" {
		if err := ParseNUpDetails(desc, nup); err != nil {
			return nil, err
		}
	}
	return nup, ParseNUpValue(val, nup)
}

// ImageNUpConfig returns an NUp configuration for Nup-ing image files.
func ImageNUpConfig(val int, desc string) (*NUp, error) {
	nup, err := PDFNUpConfig(val, desc)
	if err != nil {
		return nil, err
	}
	nup.ImgInputFile = true
	return nup, nil
}

// PDFGridConfig returns a grid configuration for Nup-ing PDF files.
func PDFGridConfig(rows, cols int, desc string) (*NUp, error) {
	nup := DefaultNUpConfig()
	nup.PageGrid = true
	if desc != "" {
		if err := ParseNUpDetails(desc, nup); err != nil {
			return nil, err
		}
	}
	return nup, ParseNUpGridDefinition(rows, cols, nup)
}

// ImageGridConfig returns a grid configuration for Nup-ing image files.
func ImageGridConfig(rows, cols int, desc string) (*NUp, error) {
	nup, err := PDFGridConfig(rows, cols, desc)
	if err != nil {
		return nil, err
	}
	nup.ImgInputFile = true
	return nup, nil
}

// ParseNUpValue parses the NUp value into an internal structure.
func ParseNUpValue(n int, nUp *NUp) error {
	if !IntMemberOf(n, nUpValues) {
		return errInvalidGridID
	}

	// The n-Up layout depends on the orientation of the chosen output paper size.
	// This optional paper size may also be specified by dimensions in user unit.
	// The default paper size is A4 or A4P (A4 in portrait mode) respectively.
	var portrait bool
	if nUp.PageDim == nil {
		portrait = PaperSize[nUp.PageSize].Portrait()
	} else {
		portrait = RectForDim(nUp.PageDim.Width, nUp.PageDim.Height).Portrait()
	}

	d := nUpDims[n]
	if portrait {
		d.Width, d.Height = d.Height, d.Width
	}

	nUp.Grid = &d

	return nil
}

// ParseNUpGridDefinition parses NUp grid dimensions into an internal structure.
func ParseNUpGridDefinition(rows, cols int, nUp *NUp) error {
	m := cols
	if m <= 0 {
		return errInvalidGridDims
	}

	n := rows
	if m <= 0 {
		return errInvalidGridDims
	}

	nUp.Grid = &Dim{float64(m), float64(n)}

	return nil
}

func rectsForGrid(nup *NUp) []*Rectangle {
	cols := int(nup.Grid.Width)
	rows := int(nup.Grid.Height)

	maxX := float64(nup.PageDim.Width)
	maxY := float64(nup.PageDim.Height)

	gw := maxX / float64(cols)
	gh := maxY / float64(rows)

	var llx, lly float64
	rr := []*Rectangle{}

	switch nup.Orient {

	case RightDown:
		for i := rows - 1; i >= 0; i-- {
			for j := 0; j < cols; j++ {
				llx = float64(j) * gw
				lly = float64(i) * gh
				rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
			}
		}

	case DownRight:
		for i := 0; i < cols; i++ {
			for j := rows - 1; j >= 0; j-- {
				llx = float64(i) * gw
				lly = float64(j) * gh
				rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
			}
		}

	case LeftDown:
		for i := rows - 1; i >= 0; i-- {
			for j := cols - 1; j >= 0; j-- {
				llx = float64(j) * gw
				lly = float64(i) * gh
				rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
			}
		}

	case DownLeft:
		for i := cols - 1; i >= 0; i-- {
			for j := rows - 1; j >= 0; j-- {
				llx = float64(i) * gw
				lly = float64(j) * gh
				rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
			}
		}
	}

	return rr
}

func bestFitRectIntoRect(rSrc, rDest *Rectangle) (w, h, dx, dy, rot float64) {
	if rSrc.FitsWithin(rDest) {
		// Translate rSrc into center of rDest without scaling.
		w = rSrc.Width()
		h = rSrc.Height()
		dx = rDest.Width()/2 - rSrc.Width()/2
		dy = rDest.Height()/2 - rSrc.Height()/2
		return
	}

	if rSrc.Landscape() {
		if rDest.Landscape() {
			if rSrc.AspectRatio() > rDest.AspectRatio() {
				w = rDest.Width()
				h = rSrc.ScaledHeight(w)
				dy = (rDest.Height() - h) / 2
			} else {
				h = rDest.Height()
				w = rSrc.ScaledWidth(h)
				dx = (rDest.Width() - w) / 2
			}
		} else {
			rot = 90
			if 1/rSrc.AspectRatio() < rDest.AspectRatio() {
				w = rDest.Height()
				h = rSrc.ScaledHeight(w)
				dx = (rDest.Width() - h) / 2
			} else {
				h = rDest.Width()
				w = rSrc.ScaledWidth(h)
				dy = (rDest.Height() - w) / 2
			}
		}
		return
	}

	if rSrc.Portrait() {
		if rDest.Portrait() {
			if rSrc.AspectRatio() < rDest.AspectRatio() {
				h = rDest.Height()
				w = rSrc.ScaledWidth(h)
				dx = (rDest.Width() - w) / 2
			} else {
				w = rDest.Width()
				h = rSrc.ScaledHeight(w)
				dy = (rDest.Height() - h) / 2
			}
		} else {
			rot = 90
			if 1/rSrc.AspectRatio() > rDest.AspectRatio() {
				h = rDest.Width()
				w = rSrc.ScaledWidth(h)
				dy = (rDest.Height() - w) / 2
			} else {
				w = rDest.Height()
				h = rSrc.ScaledHeight(w)
				dx = (rDest.Width() - h) / 2
			}
		}
		return
	}

	w = rDest.Height()
	if rDest.Portrait() {
		w = rDest.Width()
	}
	h = w
	dx = rDest.Width()/2 - rSrc.Width()/2
	dy = rDest.Height()/2 - rSrc.Height()/2

	return
}

func nUpTilePDFBytes(wr io.Writer, rSrc, rDest *Rectangle, formResID string, nup *NUp, rotate bool) {

	// rScr is a rectangular region represented by form formResID in form space.

	// rDest is an arbitrary rectangular region in dest space.
	// It is the location where we want the form content to get rendered on a "best fit" basis.
	// Accounting for the aspect ratios of rSrc and rDest "best fit" tries to fit the largest version of rScr into rDest.
	// This may result in a 90 degree rotation.

	// rotate indicates if we need to apply a post rotation of 180 degrees eg for booklets.

	// Draw bounding box.
	if nup.Border {
		fmt.Fprintf(wr, "[]0 d 0.1 w %.2f %.2f m %.2f %.2f l %.2f %.2f l %.2f %.2f l s ",
			rDest.LL.X, rDest.LL.Y, rDest.UR.X, rDest.LL.Y, rDest.UR.X, rDest.UR.Y, rDest.LL.X, rDest.UR.Y,
		)
	}

	// Apply margin to rDest which potentially makes it smaller.
	rDestCr := rDest.CroppedCopy(float64(nup.Margin))

	// Calculate transform matrix.

	// Best fit translation of a source rectangle into a destination rectangle.
	w, h, dx, dy, r := bestFitRectIntoRect(rSrc, rDestCr)

	if nup.BgColor != nil {
		if nup.ImgInputFile {
			// Fill background.
			FillRectStacked(wr, rDest, *nup.BgColor)
		} else if nup.Margin > 0 {
			// Fill margins.
			m := float64(nup.Margin)
			drawMargins(wr, *nup.BgColor, rDest, 0, m, m, m, m)
		}
	}

	// Apply additional rotation.
	if rotate {
		r += 180
	}

	sx := w
	sy := h
	if !nup.ImgInputFile {
		sx /= rSrc.Width()
		sy /= rSrc.Height()
	}

	sin := math.Sin(r * float64(degToRad))
	cos := math.Cos(r * float64(degToRad))

	switch r {
	case 90:
		dx += h
	case 180:
		dx += w
		dy += h
	case 270:
		dy += w
	}

	dx += rDestCr.LL.X
	dy += rDestCr.LL.Y

	m := calcTransformMatrix(sx, sy, sin, cos, dx, dy)

	// Apply transform matrix and display form.
	fmt.Fprintf(wr, "q %.2f %.2f %.2f %.2f %.2f %.2f cm /%s Do Q ",
		m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1], formResID)
}

func nUpImagePDFBytes(w io.Writer, imgWidth, imgHeight int, nup *NUp, formResID string) {
	for _, r := range rectsForGrid(nup) {
		// Append to content stream.
		nUpTilePDFBytes(w, RectForDim(float64(imgWidth), float64(imgHeight)), r, formResID, nup, false)
	}
}

func createNUpFormForImage(xRefTable *XRefTable, imgIndRef *IndirectRef, w, h, i int) (*IndirectRef, error) {
	imgResID := fmt.Sprintf("Im%d", i)
	bb := RectForDim(float64(w), float64(h))

	var b bytes.Buffer
	fmt.Fprintf(&b, "/%s Do ", imgResID)

	d := Dict(
		map[string]Object{
			"ProcSet": NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"),
			"XObject": Dict(map[string]Object{imgResID: *imgIndRef}),
		},
	)

	ir, err := xRefTable.IndRefForNewObject(d)
	if err != nil {
		return nil, err
	}

	sd := StreamDict{
		Dict: Dict(
			map[string]Object{
				"Type":      Name("XObject"),
				"Subtype":   Name("Form"),
				"BBox":      bb.Array(),
				"Matrix":    NewIntegerArray(1, 0, 0, 1, 0, 0),
				"Resources": *ir,
			},
		),
		Content:        b.Bytes(),
		FilterPipeline: []PDFFilter{{Name: filter.Flate, DecodeParms: nil}},
	}

	sd.InsertName("Filter", filter.Flate)

	if err = sd.Encode(); err != nil {
		return nil, err
	}

	return xRefTable.IndRefForNewObject(sd)
}

func createNUpFormForPDF(xRefTable *XRefTable, resDict *IndirectRef, content []byte, cropBox *Rectangle) (*IndirectRef, error) {
	sd := StreamDict{
		Dict: Dict(
			map[string]Object{
				"Type":      Name("XObject"),
				"Subtype":   Name("Form"),
				"BBox":      cropBox.Array(),
				"Matrix":    NewNumberArray(1, 0, 0, 1, -cropBox.LL.X, -cropBox.LL.Y),
				"Resources": *resDict,
			},
		),
		Content:        content,
		FilterPipeline: []PDFFilter{{Name: filter.Flate, DecodeParms: nil}},
	}

	sd.InsertName("Filter", filter.Flate)

	if err := sd.Encode(); err != nil {
		return nil, err
	}

	return xRefTable.IndRefForNewObject(sd)
}

// NewNUpPageForImage creates a new page dict in xRefTable for given image filename and n-up conf.
func NewNUpPageForImage(xRefTable *XRefTable, fileName string, parentIndRef *IndirectRef, nup *NUp) (*IndirectRef, error) {
	f, err := os.Open(fileName)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	// create image dict.
	imgIndRef, w, h, err := createImageResource(xRefTable, f)
	if err != nil {
		return nil, err
	}

	resID := 0

	formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, resID)
	if err != nil {
		return nil, err
	}

	formResID := fmt.Sprintf("Fm%d", resID)

	resourceDict := Dict(
		map[string]Object{
			"XObject": Dict(map[string]Object{formResID: *formIndRef}),
		},
	)

	resIndRef, err := xRefTable.IndRefForNewObject(resourceDict)
	if err != nil {
		return nil, err
	}

	var buf bytes.Buffer
	nUpImagePDFBytes(&buf, w, h, nup, formResID)
	sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes())
	if err = sd.Encode(); err != nil {
		return nil, err
	}

	contentsIndRef, err := xRefTable.IndRefForNewObject(*sd)
	if err != nil {
		return nil, err
	}

	dim := nup.PageDim
	mediaBox := RectForDim(dim.Width, dim.Height)

	pageDict := Dict(
		map[string]Object{
			"Type":      Name("Page"),
			"Parent":    *parentIndRef,
			"MediaBox":  mediaBox.Array(),
			"Resources": *resIndRef,
			"Contents":  *contentsIndRef,
		},
	)

	return xRefTable.IndRefForNewObject(pageDict)
}

// NUpFromOneImage creates one page with instances of one image.
func NUpFromOneImage(ctx *Context, fileName string, nup *NUp, pagesDict Dict, pagesIndRef *IndirectRef) error {
	indRef, err := NewNUpPageForImage(ctx.XRefTable, fileName, pagesIndRef, nup)
	if err != nil {
		return err
	}

	if err = AppendPageTree(indRef, 1, pagesDict); err != nil {
		return err
	}

	ctx.PageCount++

	return nil
}

func wrapUpPage(ctx *Context, nup *NUp, d Dict, buf bytes.Buffer, pagesDict Dict, pagesIndRef *IndirectRef) error {
	xRefTable := ctx.XRefTable

	var fm FontMap
	if nup.BookletGuides {
		// For booklets only.
		fm = drawBookletGuides(nup, &buf)
	}

	resourceDict := Dict(
		map[string]Object{
			"XObject": d,
		},
	)

	fontRes, err := fontResources(xRefTable, fm)
	if err != nil {
		return err
	}

	if len(fontRes) > 0 {
		resourceDict["Font"] = fontRes
	}

	resIndRef, err := xRefTable.IndRefForNewObject(resourceDict)
	if err != nil {
		return err
	}

	sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes())
	if err = sd.Encode(); err != nil {
		return err
	}

	contentsIndRef, err := xRefTable.IndRefForNewObject(*sd)
	if err != nil {
		return err
	}

	dim := nup.PageDim
	mediaBox := RectForDim(dim.Width, dim.Height)

	pageDict := Dict(
		map[string]Object{
			"Type":      Name("Page"),
			"Parent":    *pagesIndRef,
			"MediaBox":  mediaBox.Array(),
			"Resources": *resIndRef,
			"Contents":  *contentsIndRef,
		},
	)

	indRef, err := xRefTable.IndRefForNewObject(pageDict)
	if err != nil {
		return err
	}

	if err = AppendPageTree(indRef, 1, pagesDict); err != nil {
		return err
	}

	ctx.PageCount++

	return nil
}

// NUpFromMultipleImages creates pages in NUp-style rendering each image once.
func NUpFromMultipleImages(ctx *Context, fileNames []string, nup *NUp, pagesDict Dict, pagesIndRef *IndirectRef) error {
	if nup.PageGrid {
		nup.PageDim.Width *= nup.Grid.Width
		nup.PageDim.Height *= nup.Grid.Height
	}

	xRefTable := ctx.XRefTable
	formsResDict := NewDict()
	var buf bytes.Buffer
	rr := rectsForGrid(nup)

	// fileCount must be a multiple of n.
	// If not, we will insert blank pages at the end.
	fileCount := len(fileNames)
	if fileCount%nup.N() != 0 {
		fileCount += nup.N() - fileCount%nup.N()
	}

	for i := 0; i < fileCount; i++ {

		if i > 0 && i%len(rr) == 0 {
			// Wrap complete nUp page.
			if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil {
				return err
			}
			buf.Reset()
			formsResDict = NewDict()
		}

		rDest := rr[i%len(rr)]

		var fileName string
		if i < len(fileNames) {
			fileName = fileNames[i]
		}

		if fileName == "" {
			// This is an empty page at the end.
			if nup.BgColor != nil {
				FillRectStacked(&buf, rDest, *nup.BgColor)
			}
			continue
		}

		f, err := os.Open(fileName)
		if err != nil {
			return err
		}

		imgIndRef, w, h, err := createImageResource(xRefTable, f)
		if err != nil {
			return err
		}

		if err := f.Close(); err != nil {
			return err
		}

		formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, i)
		if err != nil {
			return err
		}

		formResID := fmt.Sprintf("Fm%d", i)
		formsResDict.Insert(formResID, *formIndRef)

		// Append to content stream of page i.
		nUpTilePDFBytes(&buf, RectForDim(float64(w), float64(h)), rr[i%len(rr)], formResID, nup, false)
	}

	// Wrap incomplete nUp page.
	return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef)
}

func sortSelectedPages(pages IntSet) []int {
	var pageNumbers []int
	for k, v := range pages {
		if v {
			pageNumbers = append(pageNumbers, k)
		}
	}
	sort.Ints(pageNumbers)
	return pageNumbers
}

func nupPageNumber(i int, sortedPageNumbers []int) int {
	var pageNumber int
	if i < len(sortedPageNumbers) {
		pageNumber = sortedPageNumbers[i]
	}
	return pageNumber
}

func (ctx *Context) nupPages(selectedPages IntSet, nup *NUp, pagesDict Dict, pagesIndRef *IndirectRef) error {
	var buf bytes.Buffer
	xRefTable := ctx.XRefTable
	formsResDict := NewDict()
	rr := rectsForGrid(nup)

	sortedPageNumbers := sortSelectedPages(selectedPages)
	pageCount := len(sortedPageNumbers)
	// pageCount must be a multiple of n.
	// If not, we will insert blank pages at the end.
	if pageCount%nup.N() != 0 {
		pageCount += nup.N() - pageCount%nup.N()
	}

	for i := 0; i < pageCount; i++ {

		if i > 0 && i%len(rr) == 0 {
			// Wrap complete nUp page.
			if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil {
				return err
			}
			buf.Reset()
			formsResDict = NewDict()
		}

		rDest := rr[i%len(rr)]

		pageNumber := nupPageNumber(i, sortedPageNumbers)
		if pageNumber == 0 {
			// This is an empty page at the end of a booklet.
			if nup.BgColor != nil {
				FillRectStacked(&buf, rDest, *nup.BgColor)
			}
			continue
		}

		consolidateRes := true
		d, inhPAttrs, err := ctx.PageDict(pageNumber, consolidateRes)
		if err != nil {
			return err
		}
		if d == nil {
			return errors.Errorf("pdfcpu: unknown page number: %d\n", i)
		}

		// Retrieve content stream bytes.
		bb, err := xRefTable.PageContent(d)
		if err == errNoContent {
			continue
		}
		if err != nil {
			return err
		}

		// Create an object for this resDict in xRefTable.
		ir, err := ctx.IndRefForNewObject(inhPAttrs.resources)
		if err != nil {
			return err
		}

		cropBox := inhPAttrs.mediaBox
		if inhPAttrs.cropBox != nil {
			cropBox = inhPAttrs.cropBox
		}
		formIndRef, err := createNUpFormForPDF(xRefTable, ir, bb, cropBox)
		if err != nil {
			return err
		}

		formResID := fmt.Sprintf("Fm%d", i)
		formsResDict.Insert(formResID, *formIndRef)

		// Append to content stream of page i.
		nUpTilePDFBytes(&buf, cropBox, rDest, formResID, nup, false)
	}

	// Wrap incomplete nUp page.
	return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef)
}

// NUpFromPDF creates an n-up version of the PDF represented by xRefTable.
func (ctx *Context) NUpFromPDF(selectedPages IntSet, nup *NUp) error {
	var mb *Rectangle
	if nup.PageDim == nil {
		// No page dimensions specified, use mediaBox of page 1.
		consolidateRes := false
		d, inhPAttrs, err := ctx.PageDict(1, consolidateRes)
		if err != nil {
			return err
		}
		if d == nil {
			return errors.Errorf("unknown page number: %d\n", 1)
		}
		mb = inhPAttrs.mediaBox
	} else {
		mb = RectForDim(nup.PageDim.Width, nup.PageDim.Height)
	}

	if nup.PageGrid {
		mb.UR.X = mb.LL.X + float64(nup.Grid.Width)*mb.Width()
		mb.UR.Y = mb.LL.Y + float64(nup.Grid.Height)*mb.Height()
	}

	pagesDict := Dict(
		map[string]Object{
			"Type":     Name("Pages"),
			"Count":    Integer(0),
			"MediaBox": mb.Array(),
		},
	)

	pagesIndRef, err := ctx.IndRefForNewObject(pagesDict)
	if err != nil {
		return err
	}

	nup.PageDim = &Dim{mb.Width(), mb.Height()}

	if err = ctx.nupPages(selectedPages, nup, pagesDict, pagesIndRef); err != nil {
		return err
	}

	// Replace original pagesDict.
	rootDict, err := ctx.Catalog()
	if err != nil {
		return err
	}

	rootDict.Update("Pages", *pagesIndRef)

	return nil
}
