package main

import (
	"fmt"
	"log"
	"strconv"
	"strings"

	"github.com/go-pdf/fpdf"
)

// InlineSpan represents a run of text with consistent inline formatting.
// Used to pass parsed inline Markdown nodes from the parser to the renderer.
type InlineSpan struct {
	Text   string
	Bold   bool
	Italic bool
	Code   bool // inline code (`…`)
}

// RenderHeader renders a section heading per IHK/DIN 5008 rules:
//   - Font: Helvetica Bold, 14pt
//   - Before: 2 blank lines if not at the top of the page
//   - After: 1 blank line
//   - Level 1 headings always start on a new page
//
// The heading is also recorded in the TOC.
func (r *IHKRenderer) RenderHeader(level int, title string) {
	if level == 1 {
		// Major sections start on a new page per IHK convention.
		r.pdf.AddPage()
	} else if !r.isAtPageTop() {
		// IHK: two blank lines before a heading that is not at the page top.
		r.pdf.Ln(dinSpaceBeforeHeading)
	}

	r.RecordHeader(level, title)

	r.pdf.SetFont("Helvetica", "B", dinFontHeading)
	r.pdf.MultiCell(0, dinLineHtHeading, r.tr(title), "", "L", false)
	// IHK: one blank line (Leerzeile) after every heading.
	r.pdf.Ln(dinSpaceAfterHeading)
}

// RenderParagraphSpans renders a paragraph from pre-parsed inline spans.
//
// Plain paragraphs (no inline formatting) use MultiCell with Blocksatz (justified)
// alignment as required by IHK. Paragraphs with mixed bold/italic use Write()
// which produces left-aligned output — an inherent fpdf limitation for mixed fonts.
func (r *IHKRenderer) RenderParagraphSpans(spans []InlineSpan) {
	if len(spans) == 0 {
		return
	}

	hasFormatting := false
	for _, s := range spans {
		if s.Bold || s.Italic || s.Code {
			hasFormatting = true
			break
		}
	}

	if !hasFormatting {
		// All plain text: collect and render justified (Blocksatz).
		text := ""
		for _, s := range spans {
			text += s.Text
		}
		r.pdf.SetFont("Helvetica", "", dinFontBody)
		r.pdf.MultiCell(0, dinLineHtBody, r.tr(text), "", "J", false)
	} else {
		// Mixed formatting: render span-by-span using Write().
		for _, span := range spans {
			style := fontStyle(span.Bold, span.Italic)
			if span.Code {
				r.pdf.SetFont("Courier", style, dinFontBody)
			} else {
				r.pdf.SetFont("Helvetica", style, dinFontBody)
			}
			r.pdf.Write(dinLineHtBody, r.tr(span.Text))
		}
		r.pdf.Ln(dinLineHtBody)
	}
	r.pdf.Ln(dinSpaceAfterParagraph)
}

// RenderListItem renders a single list item with a bullet prefix.
// ordered: true → bullet "•"; false → numbered with index.
// indent: nesting depth (0 = top level).
func (r *IHKRenderer) RenderListItem(spans []InlineSpan, ordered bool, index, indent int) {
	r.pdf.SetFont("Helvetica", "", dinFontBody)

	lm, _, _, _ := r.pdf.GetMargins()

	// 3 mm base indent + 5 mm per nesting level (reduced from previous 8 mm per level)
	indentMM := 3.0 + float64(indent)*5.0

	prefix := "• "
	if ordered {
		prefix = strconv.Itoa(index) + ". "
	}
	prefixW := r.pdf.GetStringWidth(prefix) + 1.0

	// textX is where the item text starts; wrapped lines must align here too.
	textX := lm + indentMM + prefixW
	textW := r.usableWidth() - indentMM - prefixW

	// Temporarily move the left margin to textX so that MultiCell wraps
	// continuation lines flush with the text, not back to the page margin.
	r.pdf.SetLeftMargin(textX)
	defer r.pdf.SetLeftMargin(lm)

	// Render bullet/number; CellFormat advances X to textX automatically.
	r.pdf.SetX(lm + indentMM)
	r.pdf.CellFormat(prefixW, dinLineHtList, r.tr(prefix), "", 0, "L", false, 0, "")

	hasFormatting := false
	for _, s := range spans {
		if s.Bold || s.Italic || s.Code {
			hasFormatting = true
			break
		}
	}

	if !hasFormatting {
		text := ""
		for _, s := range spans {
			text += s.Text
		}
		r.pdf.MultiCell(textW, dinLineHtList, r.tr(text), "", "L", false)
	} else {
		for _, span := range spans {
			style := fontStyle(span.Bold, span.Italic)
			if span.Code {
				r.pdf.SetFont("Courier", style, dinFontBody)
			} else {
				r.pdf.SetFont("Helvetica", style, dinFontBody)
			}
			r.pdf.Write(dinLineHtList, r.tr(span.Text))
		}
		r.pdf.Ln(dinLineHtList)
	}
}

// tableRowData holds pre-computed line-wrapped content for one table row.
type tableRowData struct {
	cells  [][]string // wrapped lines per cell
	height float64    // total row height in mm
}

// prepareRow measures how many lines each cell needs and returns a tableRowData
// with the wrapped content. Font must be set for measurement before calling.
func (r *IHKRenderer) prepareRow(rawCells []string, numCols int, colW, lineHt float64, bold bool) tableRowData {
	style := ""
	if bold {
		style = "B"
	}
	r.pdf.SetFont("Helvetica", style, dinFontCaption)

	cells := make([][]string, numCols)
	maxLines := 0
	for j := 0; j < numCols; j++ {
		raw := ""
		if j < len(rawCells) {
			raw = rawCells[j]
		}
		split := r.pdf.SplitLines([]byte(r.tr(raw)), colW-2)
		lines := make([]string, len(split))
		for k, b := range split {
			lines[k] = string(b)
		}
		if len(lines) == 0 {
			lines = []string{""}
		}
		cells[j] = lines
		if len(lines) > maxLines {
			maxLines = len(lines)
		}
	}
	if maxLines == 0 {
		maxLines = 1
	}
	return tableRowData{cells: cells, height: float64(maxLines) * lineHt}
}

// drawRow renders a pre-computed row at the current Y position.
// Cell borders are drawn as rectangles so all cells share a uniform height
// regardless of how many lines they contain.
func (r *IHKRenderer) drawRow(row tableRowData, numCols int, colW, lineHt float64, isHeader bool) {
	startY := r.pdf.GetY()
	lm, _, _, _ := r.pdf.GetMargins()

	style := ""
	align := "L"
	if isHeader {
		style = "B"
		align = "C"
		r.pdf.SetFillColor(230, 230, 230)
	} else {
		r.pdf.SetFillColor(255, 255, 255)
	}

	// Draw all cell backgrounds and borders first (uniform height via Rect).
	for j := 0; j < numCols; j++ {
		r.pdf.Rect(lm+float64(j)*colW, startY, colW, row.height, "FD")
	}

	// Render text on top, line by line per cell.
	r.pdf.SetFont("Helvetica", style, dinFontCaption)
	for j, cellLines := range row.cells {
		x := lm + float64(j)*colW
		for k, line := range cellLines {
			r.pdf.SetXY(x+1, startY+float64(k)*lineHt)
			r.pdf.CellFormat(colW-2, lineHt, line, "", 0, align, false, 0, "")
		}
	}
	r.pdf.SetY(startY + row.height)
}

// RenderTable numbers a table, records it in the Tabellenverzeichnis, and
// renders it with multi-line cell support and an auto-repeating header on
// page breaks. data[0] is the header row; data[1:] are body rows.
func (r *IHKRenderer) RenderTable(data [][]string, caption string) {
	if len(data) == 0 {
		return
	}
	r.tableCount++
	label := fmt.Sprintf("Tab. %d", r.tableCount)
	if caption != "" {
		label += ": " + caption
	}
	r.RecordTable(label)
	r.renderTableBody(data, label)
}

// renderTableBody renders table content without assigning a number or recording
// in the Tabellenverzeichnis. Pass an empty label to suppress the caption line.
func (r *IHKRenderer) renderTableBody(data [][]string, label string) {
	if len(data) == 0 {
		return
	}
	header := data[0]
	numCols := len(header)
	if numCols == 0 {
		return
	}

	uw := r.usableWidth()
	colW := uw / float64(numCols)
	lineHt := dinLineHtCaption + 2

	_, pageH := r.pdf.GetPageSize()
	_, _, _, bm := r.pdf.GetMargins()

	// Pre-compute all rows so we know heights before rendering.
	headerRow := r.prepareRow(header, numCols, colW, lineHt, true)
	bodyRows := make([]tableRowData, len(data)-1)
	for i := 1; i < len(data); i++ {
		bodyRows[i-1] = r.prepareRow(data[i], numCols, colW, lineHt, false)
	}

	renderHeader := func(continued bool) {
		title := label
		if continued && label != "" {
			title += " (Fortsetzung)"
		}
		if title != "" {
			r.pdf.SetFont("Helvetica", "B", dinFontCaption)
			r.pdf.CellFormat(0, lineHt, r.tr(title), "", 1, "L", false, 0, "")
		}
		r.drawRow(headerRow, numCols, colW, lineHt, true)
	}

	renderHeader(false)

	for _, row := range bodyRows {
		if r.pdf.GetY()+row.height > pageH-bm {
			r.pdf.AddPage()
			renderHeader(true)
		}
		r.drawRow(row, numCols, colW, lineHt, false)
	}
	r.pdf.Ln(dinSpaceAfterParagraph)
}

// RenderImage embeds an image with a caption below it.
// The image is scaled to fit the printable width and the remaining page height.
// If the image does not fit on the current page, a new page is started.
// The caption is recorded in the Abbildungsverzeichnis.
func (r *IHKRenderer) RenderImage(path string, caption string) {
	r.pdf.Ln(dinSpaceAfterParagraph)

	info := r.ensureImageRegistered(path)
	if info == nil {
		log.Printf("warning: image not found, rendering placeholder: %q", path)
		r.pdf.SetFont("Helvetica", "I", dinFontCaption)
		r.pdf.CellFormat(0, dinLineHtCaption, r.tr("[Bild nicht gefunden: "+path+"]"),
			"1", 1, "C", false, 0, "")
		return
	}

	imgW := info.Width() * ptToMM
	imgH := info.Height() * ptToMM

	uw := r.usableWidth()
	captionH := dinLineHtCaption + 4

	// Scale to available width
	displayW := uw
	displayH := imgH * (displayW / imgW)

	_, pageH := r.pdf.GetPageSize()
	_, _, _, bm := r.pdf.GetMargins()
	availH := pageH - r.pdf.GetY() - bm - captionH

	if displayH > availH {
		if availH < 20 {
			// Almost no space left — start a new page
			r.pdf.AddPage()
			availH = pageH - r.pdf.GetY() - bm - captionH
		}
		// Scale down to fit height
		displayH = availH
		displayW = imgW * (displayH / imgH)
		if displayW > uw {
			displayW = uw
			displayH = imgH * (displayW / imgW)
		}
	}

	lm, _, _, _ := r.pdf.GetMargins()
	posX := lm + (uw-displayW)/2
	currentY := r.pdf.GetY()

	r.pdf.ImageOptions(path, posX, currentY, displayW, displayH, false,
		fpdf.ImageOptions{ReadDpi: true}, 0, "")
	r.pdf.SetY(currentY + displayH + 2)

	// Figure caption and TOC registration
	r.figureCount++
	label := fmt.Sprintf("Abb. %d", r.figureCount)
	if caption != "" {
		label += ": " + caption
	}
	r.RecordFigure(label)

	r.pdf.SetFont("Helvetica", "I", dinFontCaption)
	r.pdf.CellFormat(0, captionH, r.tr(label), "", 1, "C", false, 0, "")
	r.pdf.Ln(dinSpaceAfterParagraph)
}

// Code block layout constants.
const (
	codeFont    = 9.0  // pt — Courier, smaller than body text
	codeLineHt  = 4.5  // mm ≈ 9 pt × 1.3 line spacing
	codeGutterW = 11.0 // mm — column reserved for line numbers
)

// RenderCodeBlock renders a fenced code block with a line-number gutter.
//
// Layout:
//
//	┌──────┬──────────────────────────────────┐
//	│   1  │  package main                    │
//	│   2  │                                  │
//	│   3  │  func main() { … }               │
//	│   4  │  // long line wraps here …       │
//	│      │    … continuation                │
//	└──────┴──────────────────────────────────┘
//
// Lines that exceed the column width are wrapped. Continuation rows show
// an empty gutter so the source line number stays unambiguous.
// The language label is shown in small italic text above the block.
func (r *IHKRenderer) RenderCodeBlock(lang, code string) {
	lines := strings.Split(strings.TrimRight(code, "\n"), "\n")
	if len(lines) == 0 {
		return
	}

	r.pdf.Ln(dinSpaceAfterParagraph)

	// Language label — top-right, italic, grey
	if lang != "" {
		r.pdf.SetFont("Helvetica", "I", dinFontCaption)
		r.pdf.SetTextColor(100, 100, 100)
		r.pdf.CellFormat(0, dinLineHtCaption, r.tr(lang), "", 1, "R", false, 0, "")
		r.pdf.SetTextColor(0, 0, 0)
	}

	lm, _, _, bm := r.pdf.GetMargins()
	_, pageH := r.pdf.GetPageSize()
	uw := r.usableWidth()
	codeW := uw - codeGutterW

	r.pdf.SetFont("Courier", "", codeFont)

	// In Courier every glyph has the same advance width; one measurement suffices.
	// Reserve 1 mm right padding so characters never touch the column edge.
	charW := r.pdf.GetStringWidth("0")
	maxChars := int((codeW-1)/charW) - 1 // -1 for the leading space we prepend
	if maxChars < 1 {
		maxChars = 1
	}

	for i, line := range lines {
		chunks := wrapCodeLine(line, maxChars)
		for ci, chunk := range chunks {
			if r.pdf.GetY()+codeLineHt > pageH-bm {
				r.pdf.AddPage()
			}
			// Gutter: darker grey; line number on the first chunk, blank on continuations.
			r.pdf.SetFillColor(218, 218, 218)
			r.pdf.SetX(lm)
			if ci == 0 {
				r.pdf.CellFormat(codeGutterW, codeLineHt,
					fmt.Sprintf("%4d  ", i+1), "", 0, "R", true, 0, "")
			} else {
				r.pdf.CellFormat(codeGutterW, codeLineHt, "", "", 0, "R", true, 0, "")
			}
			// Code column: lighter grey, left-aligned, small leading space.
			r.pdf.SetFillColor(246, 246, 246)
			r.pdf.CellFormat(codeW, codeLineHt, r.tr(" "+chunk), "", 1, "L", true, 0, "")
		}
	}

	// Reset colours so subsequent content is unaffected.
	r.pdf.SetFillColor(255, 255, 255)
	r.pdf.Ln(dinSpaceAfterParagraph)
}

// wrapCodeLine splits a source code line into chunks of at most maxChars runes.
// A single-rune minimum prevents an infinite loop on extremely narrow columns.
func wrapCodeLine(line string, maxChars int) []string {
	runes := []rune(line)
	if len(runes) <= maxChars {
		return []string{line}
	}
	var chunks []string
	for len(runes) > 0 {
		n := maxChars
		if n > len(runes) {
			n = len(runes)
		}
		chunks = append(chunks, string(runes[:n]))
		runes = runes[n:]
	}
	return chunks
}

// fontStyle returns the fpdf font style string for a given bold/italic combination.
func fontStyle(bold, italic bool) string {
	switch {
	case bold && italic:
		return "BI"
	case bold:
		return "B"
	case italic:
		return "I"
	default:
		return ""
	}
}
