package webui

// A minimal PDF writer — exactly enough of the spec to paginate a table, and no
// more. Stdlib only (the gowasm engine takes no third-party dependency), which is
// also why the TSX's pdf-lib could not simply be swapped for a Go port of it.
//
// What is implemented:
//
//   - Document structure: header, indirect objects, a classic cross-reference
//     TABLE (not an xref stream), trailer, startxref, %%EOF.
//   - Pages in portrait or landscape, US Letter (612x792 pt) — the same page size
//     the TSX used (pdf-lib's default), so exports look identical.
//   - Text in the two standard Type1 fonts that need no embedding: Helvetica and
//     Helvetica-Bold, in WinAnsiEncoding.
//   - Text measurement from the Adobe AFM glyph-width tables (below). Without
//     widths you cannot size columns, truncate to fit, or right-align a number.
//   - Filled rectangles and stroked lines (the grid, the zebra band, the rules).
//
// What is deliberately NOT implemented: images (so no logo — see
// autotable_export.go), compression (streams are plain, which makes the output
// greppable and the tests meaningful), transparency, annotations, outlines,
// metadata, encryption, and any font that needs embedding.
//
// Coordinates are PDF user space: origin bottom-left, y grows UP, units are
// points (1/72"). A caller lays a table out top-down by starting at
// height-margin and subtracting.

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
)

// PDFOrientation mirrors the TSX's PDFOrientation union (landscape is 0, and so
// is Go's zero value — landscape is the default for a table, as it was there).
type PDFOrientation int

const (
	PDF_ORIENTATION_LANDSCAPE PDFOrientation = 0
	PDF_ORIENTATION_PORTRAIT  PDFOrientation = 1
)

// US Letter, in points. Portrait is 612x792; landscape swaps them.
const (
	pdfLetterShort = 612.0
	pdfLetterLong  = 792.0
)

// PDFColor is an RGB fill/stroke color, each component in [0,1].
type PDFColor struct{ R, G, B float64 }

// PDFGray is the shade of gray at v (0 = black, 1 = white).
func PDFGray(v float64) PDFColor { return PDFColor{v, v, v} }

// PDFTextStyle is how a run of text is drawn.
type PDFTextStyle struct {
	Size  float64 // in points; 0 means 10
	Bold  bool    // Helvetica-Bold instead of Helvetica
	Color PDFColor
}

// PDFOptions configures a new document.
type PDFOptions struct {
	Orientation PDFOrientation
	// Width/Height override the page size in points. Zero means US Letter in the
	// chosen orientation.
	Width, Height float64
}

type pdfPage struct {
	content bytes.Buffer
}

// PDF is a document being written. Draw onto the current page (the one AddPage
// last created); SetPage rewinds to an earlier one, which is how a footer like
// "Page 2 of 7" gets stamped onto pages that were finished before the total was
// known.
type PDF struct {
	w, h  float64
	pages []*pdfPage
	cur   int
}

// NewPDF creates an empty document. It has no pages until AddPage is called;
// Bytes on a page-less document emits one blank page, because a PDF with zero
// pages is not a valid PDF.
func NewPDF(o PDFOptions) *PDF {
	w, h := pdfLetterLong, pdfLetterShort // landscape
	if o.Orientation == PDF_ORIENTATION_PORTRAIT {
		w, h = pdfLetterShort, pdfLetterLong
	}
	if o.Width > 0 {
		w = o.Width
	}
	if o.Height > 0 {
		h = o.Height
	}
	return &PDF{w: w, h: h, cur: -1}
}

// Width / Height are the page size in points.
func (p *PDF) Width() float64  { return p.w }
func (p *PDF) Height() float64 { return p.h }

// PageCount is how many pages exist so far.
func (p *PDF) PageCount() int { return len(p.pages) }

// AddPage appends a blank page and makes it current.
func (p *PDF) AddPage() {
	p.pages = append(p.pages, &pdfPage{})
	p.cur = len(p.pages) - 1
}

// SetPage makes page i (0-based) current, so a later pass can draw on it. Out of
// range is a no-op — a footer loop must never take the app down.
func (p *PDF) SetPage(i int) {
	if i >= 0 && i < len(p.pages) {
		p.cur = i
	}
}

// page returns the current page, creating one if the caller drew before adding.
func (p *PDF) page() *pdfPage {
	if p.cur < 0 || p.cur >= len(p.pages) {
		p.AddPage()
	}
	return p.pages[p.cur]
}

// Text draws s with its left edge at x and its BASELINE at y.
func (p *PDF) Text(x, y float64, s string, st PDFTextStyle) {
	if s == "" {
		return
	}
	size := st.Size
	if size <= 0 {
		size = 10
	}
	font := "/F1"
	if st.Bold {
		font = "/F2"
	}
	c := &p.page().content
	fmt.Fprintf(c, "BT\n%s %s Tf\n%s %s %s rg\n1 0 0 1 %s %s Tm\n%s Tj\nET\n",
		font, pdfNum(size),
		pdfNum(st.Color.R), pdfNum(st.Color.G), pdfNum(st.Color.B),
		pdfNum(x), pdfNum(y),
		pdfString(s),
	)
}

// TextRight draws s with its RIGHT edge at x — how a number lands under the right
// edge of its column, and the only reason the width table has to be correct.
func (p *PDF) TextRight(x, y float64, s string, st PDFTextStyle) {
	p.Text(x-PDFTextWidth(s, styleSize(st), st.Bold), y, s, st)
}

// TextCenter draws s centered on x.
func (p *PDF) TextCenter(x, y float64, s string, st PDFTextStyle) {
	p.Text(x-PDFTextWidth(s, styleSize(st), st.Bold)/2, y, s, st)
}

func styleSize(st PDFTextStyle) float64 {
	if st.Size <= 0 {
		return 10
	}
	return st.Size
}

// Line strokes a straight line.
func (p *PDF) Line(x1, y1, x2, y2, thickness float64, color PDFColor) {
	if thickness <= 0 {
		thickness = 1
	}
	fmt.Fprintf(&p.page().content, "%s %s %s RG\n%s w\n%s %s m\n%s %s l\nS\n",
		pdfNum(color.R), pdfNum(color.G), pdfNum(color.B),
		pdfNum(thickness),
		pdfNum(x1), pdfNum(y1), pdfNum(x2), pdfNum(y2),
	)
}

// Rect fills a rectangle whose lower-left corner is (x,y). There is no stroked
// variant: a table's borders are drawn as lines, so nothing needs one.
func (p *PDF) Rect(x, y, w, h float64, color PDFColor) {
	if w <= 0 || h <= 0 {
		return
	}
	fmt.Fprintf(&p.page().content, "%s %s %s rg\n%s %s %s %s re\nf\n",
		pdfNum(color.R), pdfNum(color.G), pdfNum(color.B),
		pdfNum(x), pdfNum(y), pdfNum(w), pdfNum(h),
	)
}

// ---- serialization ----

// Object layout is fixed, which is what keeps the xref arithmetic honest:
//
//	1        Catalog
//	2        Pages
//	3        Helvetica       (/F1)
//	4        Helvetica-Bold  (/F2)
//	5, 6     page 0: the page object, then its content stream
//	7, 8     page 1: …
const (
	pdfObjCatalog  = 1
	pdfObjPages    = 2
	pdfObjFont     = 3
	pdfObjFontBold = 4
)

// pageObjNum / contentObjNum are the object numbers for page i.
func pageObjNum(i int) int    { return 5 + 2*i }
func contentObjNum(i int) int { return 6 + 2*i }

// Bytes serializes the document. The result is a complete, standalone PDF file.
func (p *PDF) Bytes() []byte {
	pages := p.pages
	if len(pages) == 0 {
		pages = []*pdfPage{{}} // a zero-page PDF is invalid; ship one blank page
	}
	nObjs := 4 + 2*len(pages)

	var buf bytes.Buffer
	buf.WriteString("%PDF-1.4\n")
	// The conventional binary marker: four bytes >127 on a comment line, so a tool
	// that sniffs the first lines classifies the file as binary and does not
	// helpfully mangle its line endings.
	buf.Write([]byte{'%', 0xE2, 0xE3, 0xCF, 0xD3, '\n'})

	// offsets[n] is the byte offset of object n (index 0 unused).
	offsets := make([]int, nObjs+1)

	obj := func(n int, body string) {
		offsets[n] = buf.Len()
		fmt.Fprintf(&buf, "%d 0 obj\n%s\nendobj\n", n, body)
	}
	stream := func(n int, data []byte) {
		offsets[n] = buf.Len()
		fmt.Fprintf(&buf, "%d 0 obj\n<< /Length %d >>\nstream\n", n, len(data))
		buf.Write(data)
		buf.WriteString("\nendstream\nendobj\n")
	}

	obj(pdfObjCatalog, fmt.Sprintf("<< /Type /Catalog /Pages %d 0 R >>", pdfObjPages))

	var kids strings.Builder
	for i := range pages {
		if i > 0 {
			kids.WriteByte(' ')
		}
		fmt.Fprintf(&kids, "%d 0 R", pageObjNum(i))
	}
	obj(pdfObjPages, fmt.Sprintf("<< /Type /Pages /Kids [%s] /Count %d >>", kids.String(), len(pages)))

	obj(pdfObjFont, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")
	obj(pdfObjFontBold, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>")

	for i, pg := range pages {
		obj(pageObjNum(i), fmt.Sprintf(
			"<< /Type /Page /Parent %d 0 R /MediaBox [0 0 %s %s] "+
				"/Resources << /Font << /F1 %d 0 R /F2 %d 0 R >> >> /Contents %d 0 R >>",
			pdfObjPages, pdfNum(p.w), pdfNum(p.h),
			pdfObjFont, pdfObjFontBold, contentObjNum(i),
		))
		stream(contentObjNum(i), pg.content.Bytes())
	}

	// The cross-reference table. Every entry is exactly 20 bytes — 10-digit
	// offset, space, 5-digit generation, space, type, and a two-byte EOL — because
	// readers index into it arithmetically rather than parsing it.
	xref := buf.Len()
	fmt.Fprintf(&buf, "xref\n0 %d\n", nObjs+1)
	buf.WriteString("0000000000 65535 f \n")
	for n := 1; n <= nObjs; n++ {
		fmt.Fprintf(&buf, "%010d 00000 n \n", offsets[n])
	}
	fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root %d 0 R >>\nstartxref\n%d\n%%%%EOF\n",
		nObjs+1, pdfObjCatalog, xref)

	return buf.Bytes()
}

// pdfNum formats a coordinate. PDF has no exponent notation, so strconv's 'g'/-1
// shortest form is unusable ("1e-07" is a syntax error in a content stream); and
// three decimals is well past what a 72dpi page can resolve.
func pdfNum(v float64) string {
	if v == 0 { // also collapses -0
		return "0"
	}
	s := strconv.FormatFloat(v, 'f', 3, 64)
	s = strings.TrimRight(s, "0")
	s = strings.TrimSuffix(s, ".")
	if s == "" || s == "-" {
		return "0"
	}
	return s
}

// pdfString renders s as a PDF literal string, transcoded to WinAnsi. Anything a
// parser could choke on — the delimiters, the escape character, every byte
// outside printable ASCII — is escaped, the last as three-digit octal.
func pdfString(s string) string {
	var b strings.Builder
	b.WriteByte('(')
	for _, r := range s {
		c := winAnsiByte(r)
		switch {
		case c == '(' || c == ')' || c == '\\':
			b.WriteByte('\\')
			b.WriteByte(c)
		case c < 32 || c > 126:
			fmt.Fprintf(&b, "\\%03o", c)
		default:
			b.WriteByte(c)
		}
	}
	b.WriteByte(')')
	return b.String()
}

// ---- encoding ----

// winAnsiByte maps a rune to its WinAnsiEncoding code. Latin-1 passes straight
// through; the printer's punctuation Windows squats in 0x80-0x9F (curly quotes,
// dashes, the ellipsis the truncator appends) is mapped explicitly. Anything else
// — CJK, emoji, a tab in the middle of a cell — becomes '?', because the standard
// 14 fonts have no glyph for it and silently dropping it would misalign the
// column instead.
func winAnsiByte(r rune) byte {
	switch {
	case r == '\t' || r == '\n' || r == '\r':
		return ' '
	case r >= 32 && r <= 126:
		return byte(r)
	case r >= 0xA0 && r <= 0xFF:
		return byte(r)
	}
	if c, ok := winAnsiSpecial[r]; ok {
		return c
	}
	return '?'
}

var winAnsiSpecial = map[rune]byte{
	'€': 0x80, // euro
	'‚': 0x82, // single low quote
	'ƒ': 0x83, // florin
	'„': 0x84, // double low quote
	'…': 0x85, // ellipsis  <- the truncation marker
	'†': 0x86, // dagger
	'‡': 0x87, // double dagger
	'ˆ': 0x88, // circumflex
	'‰': 0x89, // per mille
	'Š': 0x8A, // S caron
	'‹': 0x8B, // single left guillemet
	'Œ': 0x8C, // OE
	'Ž': 0x8E, // Z caron
	'‘': 0x91, // left single quote
	'’': 0x92, // right single quote (apostrophe)
	'“': 0x93, // left double quote
	'”': 0x94, // right double quote
	'•': 0x95, // bullet
	'–': 0x96, // en dash
	'—': 0x97, // em dash
	'˜': 0x98, // small tilde
	'™': 0x99, // trademark
	'š': 0x9A, // s caron
	'›': 0x9B, // single right guillemet
	'œ': 0x9C, // oe
	'ž': 0x9E, // z caron
	'Ÿ': 0x9F, // Y dieresis
}

// ---- measurement ----

// PDFTextWidth is the width of s in points, at the given size, in Helvetica (or
// Helvetica-Bold). Widths come from the Adobe AFM tables, in 1/1000 em.
func PDFTextWidth(s string, size float64, bold bool) float64 {
	table := &helveticaWidths
	if bold {
		table = &helveticaBoldWidths
	}
	total := 0.0
	for _, r := range s {
		total += float64(table[winAnsiByte(r)])
	}
	return total * size / 1000
}

// PDFTruncate shortens s until it fits maxWidth, appending an ellipsis — the
// ellipsis being measured too, so the result really does fit. Returns "" when not
// even the ellipsis fits, which is the honest answer for a column that narrow.
func PDFTruncate(s string, maxWidth, size float64, bold bool) string {
	if s == "" || PDFTextWidth(s, size, bold) <= maxWidth {
		return s
	}
	runes := []rune(s)
	for n := len(runes) - 1; n > 0; n-- {
		if PDFTextWidth(string(runes[:n])+"…", size, bold) <= maxWidth {
			return string(runes[:n]) + "…"
		}
	}
	if PDFTextWidth("…", size, bold) <= maxWidth {
		return "…"
	}
	return ""
}

// The Helvetica / Helvetica-Bold advance widths, indexed by WinAnsi code, in
// 1/1000 em — the Adobe Core-14 AFM data, which is what "no font embedding
// required" actually costs you: the widths have to live somewhere, and this is
// where.
//
// Codes 32-126 are the exact AFM values (note 39 is quotesingle and 96 is grave
// under WinAnsi, NOT quoteright/quoteleft as under StandardEncoding — a classic
// off-by-one-glyph). 0x80-0xFF are the AFM values for the Latin-1 and printer's
// punctuation glyphs; the accented letters carry the advance width of their base
// letter, which is exactly how the composite glyphs are built. Unmapped codes
// (0x81, 0x8D, 0x8F, 0x90, 0x9D) fall back to the width of 'n', so a stray byte
// costs a plausible amount of space rather than zero.
var (
	helveticaWidths     [256]uint16
	helveticaBoldWidths [256]uint16
)

// helveticaAscii/helveticaBoldAscii are codes 32..126, in order.
var helveticaAscii = [95]uint16{
	278, 278, 355, 556, 556, 889, 667, 191, 333, 333, // 32-41  space ! " # $ % & ' ( )
	389, 584, 278, 333, 278, 278, 556, 556, 556, 556, // 42-51  * + , - . / 0 1 2 3
	556, 556, 556, 556, 556, 556, 278, 278, 584, 584, // 52-61  4 5 6 7 8 9 : ; < =
	584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, // 62-71  > ? @ A B C D E F G
	722, 278, 500, 667, 556, 833, 722, 778, 667, 778, // 72-81  H I J K L M N O P Q
	722, 667, 611, 722, 667, 944, 667, 667, 611, 278, // 82-91  R S T U V W X Y Z [
	278, 278, 469, 556, 333, 556, 556, 500, 556, 556, // 92-101 \ ] ^ _ ` a b c d e
	278, 556, 556, 222, 222, 500, 222, 833, 556, 556, // 102-111 f g h i j k l m n o
	556, 556, 333, 500, 278, 556, 500, 722, 500, 500, // 112-121 p q r s t u v w x y
	500, 334, 260, 334, 584, // 122-126 z { | } ~
}

var helveticaBoldAscii = [95]uint16{
	278, 333, 474, 556, 556, 889, 722, 238, 333, 333, // 32-41
	389, 584, 278, 333, 278, 278, 556, 556, 556, 556, // 42-51
	556, 556, 556, 556, 556, 556, 333, 333, 584, 584, // 52-61
	584, 611, 975, 722, 722, 722, 722, 667, 611, 778, // 62-71
	722, 278, 556, 722, 611, 833, 722, 778, 667, 778, // 72-81
	722, 667, 611, 722, 667, 944, 667, 667, 611, 333, // 82-91
	278, 333, 584, 556, 333, 556, 611, 556, 611, 556, // 92-101
	333, 611, 611, 278, 278, 556, 278, 889, 611, 611, // 102-111
	611, 611, 389, 556, 333, 611, 556, 778, 556, 556, // 112-121
	500, 389, 280, 389, 584, // 122-126
}

// The high half: {code: {regular, bold}}.
var helveticaHigh = map[byte][2]uint16{
	0x80: {556, 556},   // euro
	0x82: {222, 278},   // quotesinglbase
	0x83: {556, 556},   // florin
	0x84: {333, 500},   // quotedblbase
	0x85: {1000, 1000}, // ellipsis
	0x86: {556, 556},   // dagger
	0x87: {556, 556},   // daggerdbl
	0x88: {333, 333},   // circumflex
	0x89: {1000, 1000}, // perthousand
	0x8A: {667, 667},   // Scaron
	0x8B: {333, 333},   // guilsinglleft
	0x8C: {1000, 1000}, // OE
	0x8E: {611, 611},   // Zcaron
	0x91: {222, 278},   // quoteleft
	0x92: {222, 278},   // quoteright
	0x93: {333, 500},   // quotedblleft
	0x94: {333, 500},   // quotedblright
	0x95: {350, 350},   // bullet
	0x96: {556, 556},   // endash
	0x97: {1000, 1000}, // emdash
	0x98: {333, 333},   // tilde
	0x99: {1000, 1000}, // trademark
	0x9A: {500, 556},   // scaron
	0x9B: {333, 333},   // guilsinglright
	0x9C: {944, 944},   // oe
	0x9E: {500, 500},   // zcaron
	0x9F: {667, 667},   // Ydieresis
	0xA0: {278, 278},   // nbsp
	0xA1: {333, 333},   // exclamdown
	0xA2: {556, 556},   // cent
	0xA3: {556, 556},   // sterling
	0xA4: {556, 556},   // currency
	0xA5: {556, 556},   // yen
	0xA6: {260, 280},   // brokenbar
	0xA7: {556, 556},   // section
	0xA8: {333, 333},   // dieresis
	0xA9: {737, 737},   // copyright
	0xAA: {370, 370},   // ordfeminine
	0xAB: {556, 556},   // guillemotleft
	0xAC: {584, 584},   // logicalnot
	0xAD: {333, 333},   // soft hyphen
	0xAE: {737, 737},   // registered
	0xAF: {333, 333},   // macron
	0xB0: {400, 400},   // degree
	0xB1: {584, 584},   // plusminus
	0xB2: {333, 333},   // twosuperior
	0xB3: {333, 333},   // threesuperior
	0xB4: {333, 333},   // acute
	0xB5: {556, 611},   // mu
	0xB6: {537, 556},   // paragraph
	0xB7: {278, 278},   // periodcentered
	0xB8: {333, 333},   // cedilla
	0xB9: {333, 333},   // onesuperior
	0xBA: {365, 365},   // ordmasculine
	0xBB: {556, 556},   // guillemotright
	0xBC: {834, 834},   // onequarter
	0xBD: {834, 834},   // onehalf
	0xBE: {834, 834},   // threequarters
	0xBF: {611, 611},   // questiondown
	0xC6: {1000, 1000}, // AE
	0xD0: {722, 722},   // Eth
	0xD7: {584, 584},   // multiply
	0xD8: {778, 778},   // Oslash
	0xDD: {667, 667},   // Yacute
	0xDE: {667, 667},   // Thorn
	0xDF: {611, 611},   // germandbls
	0xE6: {889, 889},   // ae
	0xF0: {556, 611},   // eth
	0xF7: {584, 584},   // divide
	0xF8: {611, 611},   // oslash
	0xFD: {500, 556},   // yacute
	0xFE: {556, 611},   // thorn
	0xFF: {500, 556},   // ydieresis
}

// accentBase maps the composite Latin-1 letters to the ASCII letter whose advance
// width they share (Helvetica builds them by stacking an accent over the base
// glyph, which does not widen it). The accented i's are the exception: they are
// built over dotlessi, which is wider than i.
var accentBase = map[byte]byte{
	0xC0: 'A', 0xC1: 'A', 0xC2: 'A', 0xC3: 'A', 0xC4: 'A', 0xC5: 'A',
	0xC7: 'C',
	0xC8: 'E', 0xC9: 'E', 0xCA: 'E', 0xCB: 'E',
	0xD1: 'N',
	0xD2: 'O', 0xD3: 'O', 0xD4: 'O', 0xD5: 'O', 0xD6: 'O',
	0xD9: 'U', 0xDA: 'U', 0xDB: 'U', 0xDC: 'U',
	0xE0: 'a', 0xE1: 'a', 0xE2: 'a', 0xE3: 'a', 0xE4: 'a', 0xE5: 'a',
	0xE7: 'c',
	0xE8: 'e', 0xE9: 'e', 0xEA: 'e', 0xEB: 'e',
	0xF1: 'n',
	0xF2: 'o', 0xF3: 'o', 0xF4: 'o', 0xF5: 'o', 0xF6: 'o',
	0xF9: 'u', 0xFA: 'u', 0xFB: 'u', 0xFC: 'u',
}

func init() {
	// dotlessi's advance — what the accented i's (0xCC-0xCF, 0xEC-0xEF) are built on.
	const dotlessI, dotlessIBold = 278, 278

	fill := func(dst *[256]uint16, ascii *[95]uint16, pick int, dotless uint16) {
		fallback := ascii['n'-32]
		for i := range dst {
			dst[i] = fallback
		}
		for i, w := range ascii {
			dst[32+i] = w
		}
		for c, w := range helveticaHigh {
			dst[c] = w[pick]
		}
		for c, base := range accentBase {
			dst[c] = ascii[base-32]
		}
		for _, c := range []byte{0xCC, 0xCD, 0xCE, 0xCF, 0xEC, 0xED, 0xEE, 0xEF} {
			dst[c] = dotless
		}
		// Codes 0-31 are unprintable and never emitted (winAnsiByte folds
		// whitespace to a space), but give them the space width anyway so a
		// measurement can never be wildly off.
		for i := 0; i < 32; i++ {
			dst[i] = ascii[0]
		}
	}
	fill(&helveticaWidths, &helveticaAscii, 0, dotlessI)
	fill(&helveticaBoldWidths, &helveticaBoldAscii, 1, dotlessIBold)
}
