package main

import (
	"encoding/base64"
	"fmt"
	"html"
	"strings"
)

// ---------------------------------------------------------------------------
// The rendered document model, shared by all three writers
// ---------------------------------------------------------------------------

// renderStep is one finished step: text plus the ANNOTATED png bytes.
type renderStep struct {
	Number    int
	Title     string
	Body      string
	Note      string
	SourceRel string // image name as written in the manifest
	MediaName string // file name used inside the outputs
	PNG       []byte // annotated image
	W, H      int
	Callouts  []resolvedCallout
}

// renderDoc is the whole document handed to a writer.
type renderDoc struct {
	Title     string
	Author    string
	Intro     string
	Generator string
	Steps     []renderStep
}

// legendLines describes the callouts of a step in plain words, so the callouts
// are readable even in a printed copy or by a screen reader.
func legendLines(s renderStep) []string {
	var out []string
	for _, c := range s.Callouts {
		switch c.Kind {
		case "circle":
			label := c.Label
			if strings.TrimSpace(label) == "" {
				label = fmt.Sprintf("%d", c.Number)
			}
			out = append(out, fmt.Sprintf("Marker %s at (%d, %d)", label, c.X, c.Y))
		case "rect":
			t := fmt.Sprintf("Box %dx%d at (%d, %d)", c.W, c.H, c.X, c.Y)
			if c.Label != "" {
				t += " - " + c.Label
			}
			out = append(out, t)
		case "arrow":
			t := fmt.Sprintf("Arrow from (%d, %d) to (%d, %d)", c.X, c.Y, c.X2, c.Y2)
			if c.Label != "" {
				t += " - " + c.Label
			}
			out = append(out, t)
		}
	}
	return out
}

func esc(s string) string { return html.EscapeString(s) }

// paragraphs splits a body into paragraphs on blank lines and keeps single
// newlines as line breaks.
func paragraphs(body string) []string {
	body = strings.ReplaceAll(body, "\r\n", "\n")
	parts := strings.Split(body, "\n\n")
	var out []string
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			out = append(out, p)
		}
	}
	return out
}

// ---------------------------------------------------------------------------
// HTML: one self-contained file, images inlined as data: URIs
// ---------------------------------------------------------------------------

const styleSheet = `:root{color-scheme:light dark;--fg:#1b1d21;--bg:#ffffff;--muted:#5f6368;--line:#d8dbe0;--card:#f6f7f9;--accent:#1757c0}
@media (prefers-color-scheme:dark){:root{--fg:#e8eaed;--bg:#15171a;--muted:#9aa0a6;--line:#33373d;--card:#1e2126;--accent:#8ab4f8}}
*{box-sizing:border-box}
body{margin:0;padding:2rem 1rem 4rem;background:var(--bg);color:var(--fg);
font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
main{max-width:52rem;margin:0 auto}
h1{font-size:1.9rem;line-height:1.2;margin:0 0 .3rem}
.byline{color:var(--muted);margin:0 0 1.6rem;font-size:.95rem}
.intro{margin:0 0 2rem}
nav{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:1rem 1.2rem;margin:0 0 2.4rem}
nav h2{font-size:.8rem;letter-spacing:.09em;text-transform:uppercase;color:var(--muted);margin:0 0 .6rem}
nav ol{margin:0;padding-left:1.4rem}
nav li{margin:.2rem 0}
nav a{color:var(--accent);text-decoration:none}
nav a:hover{text-decoration:underline}
section.step{border-top:1px solid var(--line);padding-top:1.6rem;margin-top:2.4rem}
section.step:first-of-type{border-top:0;margin-top:0}
h2.step{font-size:1.25rem;margin:0 0 .6rem;display:flex;gap:.6rem;align-items:baseline}
h2.step .n{display:inline-flex;align-items:center;justify-content:center;min-width:1.9rem;height:1.9rem;
padding:0 .4rem;border-radius:999px;background:var(--accent);color:#fff;font-size:.9rem;font-weight:700;flex:none}
@media (prefers-color-scheme:dark){h2.step .n{color:#15171a}}
p{margin:0 0 .8rem}
figure{margin:1rem 0 .4rem}
figure img{display:block;max-width:100%;height:auto;border:1px solid var(--line);border-radius:6px}
figcaption{color:var(--muted);font-size:.82rem;margin-top:.45rem}
.note{border-left:3px solid var(--accent);background:var(--card);padding:.7rem .9rem;margin:.9rem 0;border-radius:0 6px 6px 0}
.note b{display:block;font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:.2rem}
ul.legend{margin:.4rem 0 0;padding-left:1.2rem;color:var(--muted);font-size:.86rem}
footer{margin-top:3rem;border-top:1px solid var(--line);padding-top:1rem;color:var(--muted);font-size:.82rem}
@media print{body{padding:0}nav{break-inside:avoid}section.step{break-inside:avoid}}`

func renderHTML(d renderDoc) string {
	var b strings.Builder
	b.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n")
	b.WriteString("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n")
	fmt.Fprintf(&b, "<title>%s</title>\n", esc(d.Title))
	fmt.Fprintf(&b, "<meta name=\"generator\" content=\"%s\">\n", esc(d.Generator))
	fmt.Fprintf(&b, "<style>\n%s\n</style>\n</head>\n<body>\n<main>\n", styleSheet)

	fmt.Fprintf(&b, "<h1>%s</h1>\n", esc(d.Title))
	byline := fmt.Sprintf("%d step", len(d.Steps))
	if len(d.Steps) != 1 {
		byline += "s"
	}
	if d.Author != "" {
		byline = esc(d.Author) + " &middot; " + byline
	}
	fmt.Fprintf(&b, "<p class=\"byline\">%s</p>\n", byline)
	if d.Intro != "" {
		for _, p := range paragraphs(d.Intro) {
			fmt.Fprintf(&b, "<p class=\"intro\">%s</p>\n", escMultiline(p))
		}
	}

	b.WriteString("<nav>\n<h2>Contents</h2>\n<ol>\n")
	for _, s := range d.Steps {
		fmt.Fprintf(&b, "<li value=\"%d\"><a href=\"#step-%d\">%s</a></li>\n", s.Number, s.Number, esc(s.Title))
	}
	b.WriteString("</ol>\n</nav>\n")

	for _, s := range d.Steps {
		fmt.Fprintf(&b, "<section class=\"step\" id=\"step-%d\">\n", s.Number)
		fmt.Fprintf(&b, "<h2 class=\"step\"><span class=\"n\">%d</span><span>%s</span></h2>\n", s.Number, esc(s.Title))
		for _, p := range paragraphs(s.Body) {
			fmt.Fprintf(&b, "<p>%s</p>\n", escMultiline(p))
		}
		enc := base64.StdEncoding.EncodeToString(s.PNG)
		fmt.Fprintf(&b, "<figure>\n<img src=\"data:image/png;base64,%s\" width=\"%d\" height=\"%d\" alt=\"%s\">\n",
			enc, s.W, s.H, esc(fmt.Sprintf("Screenshot for step %d: %s", s.Number, s.Title)))
		fmt.Fprintf(&b, "<figcaption>%s &middot; %d&times;%d px &middot; annotated copy of %s</figcaption>\n</figure>\n",
			esc(s.MediaName), s.W, s.H, esc(s.SourceRel))
		if lines := legendLines(s); len(lines) > 0 {
			b.WriteString("<ul class=\"legend\">\n")
			for _, l := range lines {
				fmt.Fprintf(&b, "<li>%s</li>\n", esc(l))
			}
			b.WriteString("</ul>\n")
		}
		if strings.TrimSpace(s.Note) != "" {
			fmt.Fprintf(&b, "<div class=\"note\"><b>Note</b>%s</div>\n", escMultiline(s.Note))
		}
		b.WriteString("</section>\n")
	}

	fmt.Fprintf(&b, "<footer>Generated by %s. Every image in this file is embedded; there are no external references.</footer>\n",
		esc(d.Generator))
	b.WriteString("</main>\n</body>\n</html>\n")
	return b.String()
}

// escMultiline escapes text and turns newlines into <br> elements.
func escMultiline(s string) string {
	lines := strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n")
	for i := range lines {
		lines[i] = esc(lines[i])
	}
	return strings.Join(lines, "<br>")
}

// ---------------------------------------------------------------------------
// Markdown: relative image references, for a repo or a wiki
// ---------------------------------------------------------------------------

func renderMarkdown(d renderDoc, mediaDir string) string {
	var b strings.Builder
	fmt.Fprintf(&b, "# %s\n\n", mdEscape(d.Title))
	if d.Author != "" {
		fmt.Fprintf(&b, "*%s*\n\n", mdEscape(d.Author))
	}
	if d.Intro != "" {
		for _, p := range paragraphs(d.Intro) {
			fmt.Fprintf(&b, "%s\n\n", mdEscape(p))
		}
	}
	b.WriteString("## Contents\n\n")
	for _, s := range d.Steps {
		fmt.Fprintf(&b, "%d. %s\n", s.Number, mdEscape(s.Title))
	}
	b.WriteString("\n")
	for _, s := range d.Steps {
		fmt.Fprintf(&b, "---\n\n## Step %d - %s\n\n", s.Number, mdEscape(s.Title))
		for _, p := range paragraphs(s.Body) {
			fmt.Fprintf(&b, "%s\n\n", mdEscape(p))
		}
		alt := fmt.Sprintf("Step %d: %s", s.Number, s.Title)
		fmt.Fprintf(&b, "![%s](%s/%s)\n\n", mdEscapeBracket(alt), mediaDir, s.MediaName)
		if lines := legendLines(s); len(lines) > 0 {
			for _, l := range lines {
				fmt.Fprintf(&b, "- %s\n", mdEscape(l))
			}
			b.WriteString("\n")
		}
		if strings.TrimSpace(s.Note) != "" {
			for _, line := range strings.Split(strings.TrimSpace(s.Note), "\n") {
				fmt.Fprintf(&b, "> %s\n", mdEscape(line))
			}
			b.WriteString("\n")
		}
	}
	fmt.Fprintf(&b, "---\n\nGenerated by %s. Images live in `%s/` beside this file.\n",
		mdEscape(d.Generator), mediaDir)
	return b.String()
}

// mdEscape neutralises the Markdown metacharacters that would otherwise let
// step text change the structure of the document. Inline markers are escaped
// everywhere; block markers are escaped only where they would actually start a
// block, so ordinary prose stays readable in the raw file.
func mdEscape(s string) string {
	var out []string
	for _, line := range strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n") {
		var b strings.Builder
		for _, r := range line {
			switch r {
			case '\\', '`', '*', '_', '[', ']', '<', '>', '|', '~':
				b.WriteByte('\\')
				b.WriteRune(r)
			default:
				b.WriteRune(r)
			}
		}
		out = append(out, escapeLineStart(b.String()))
	}
	return strings.Join(out, "\n")
}

// escapeLineStart backslashes a leading character that would open a heading,
// list item, block quote or setext rule.
func escapeLineStart(line string) string {
	t := strings.TrimLeft(line, " \t")
	indent := line[:len(line)-len(t)]
	if t == "" {
		return line
	}
	switch t[0] {
	case '#', '+', '=':
		return indent + "\\" + t
	case '-':
		// "- item" opens a list; "well-known" does not.
		if len(t) == 1 || t[1] == ' ' || t[1] == '-' {
			return indent + "\\" + t
		}
	}
	// "1. " and "1) " open an ordered list.
	i := 0
	for i < len(t) && t[i] >= '0' && t[i] <= '9' {
		i++
	}
	if i > 0 && i < len(t) && (t[i] == '.' || t[i] == ')') {
		return indent + t[:i] + "\\" + t[i:]
	}
	return line
}

func mdEscapeBracket(s string) string {
	r := strings.NewReplacer(`\`, `\\`, `[`, `\[`, `]`, `\]`, "\n", " ")
	return r.Replace(s)
}

// ---------------------------------------------------------------------------
// DOCX assembly
// ---------------------------------------------------------------------------

func renderDOCX(d renderDoc) ([]byte, error) {
	doc := newDocxDoc()
	doc.para(d.Title, true, 40, "1B1D21")
	if d.Author != "" {
		doc.para(d.Author, false, 20, "5F6368")
	}
	if d.Intro != "" {
		for _, p := range paragraphs(d.Intro) {
			doc.para(p, false, 22, "")
		}
	}
	doc.rule()

	doc.para("Contents", true, 26, "")
	for _, s := range d.Steps {
		doc.para(fmt.Sprintf("%d.  %s", s.Number, s.Title), false, 22, "")
	}
	doc.rule()

	for _, s := range d.Steps {
		doc.para(fmt.Sprintf("Step %d - %s", s.Number, s.Title), true, 28, "1757C0")
		for _, p := range paragraphs(s.Body) {
			doc.para(p, false, 22, "")
		}
		doc.image(s.PNG, s.W, s.H, fmt.Sprintf("Step %d: %s", s.Number, s.Title))
		doc.para(fmt.Sprintf("%s - %dx%d px - annotated copy of %s", s.MediaName, s.W, s.H, s.SourceRel),
			false, 16, "5F6368")
		for _, l := range legendLines(s) {
			doc.para("- "+l, false, 18, "5F6368")
		}
		if strings.TrimSpace(s.Note) != "" {
			doc.para("Note: "+strings.TrimSpace(s.Note), false, 20, "5F6368")
		}
		doc.rule()
	}
	doc.para("Generated by "+d.Generator, false, 16, "5F6368")
	return doc.zipBytes()
}
