package app

import (
	"strings"

	"kjol/lexer" // syntax highlighting for the code blocks — a string in, HTML out
	. "kjol/vdom"
	// The host API is dual-build — real under js/wasm, no-ops natively — so neutral page
	// code can measure the browser and still server-render. This page uses it for exactly
	// one thing: reading the clock when hydration commits.
	"kjol/wasmruntime"
	ui "kjol/webui"
)

// Documentation chrome.
//
// The app routes are the framework's documentation, so they are built from one small
// vocabulary rather than each page inventing its own headings and spacing: a page has a
// title and a lede, then sections; a section explains something in prose, shows the Go
// that does it, and then RUNS that Go on the page you are reading. The last part is the
// point — a docs page for a UI framework that only shows screenshots of its components
// is a docs page that cannot tell you when it has gone stale.

// docsNav is the sidebar: the sections of the documentation, in reading order.
//
// It is data, not markup, because it is consumed twice — once by the sidebar and once
// by the /docs index, which lists the same pages as cards. Two hand-written copies of a
// nav is two copies to forget to update.
type docsGroup struct {
	Title string
	Items []docsItem
}

type docsItem struct {
	Path  string
	Label string
	Blurb string // shown on the /docs index; too long for the sidebar
	Icon  string
}

// The Components group is not a list of PAGES — it is a list of anchors into the one
// components page. There used to be three pages there ("UI kit", "Overlays",
// "AutoTable"), which split the kit along the lines of its source files rather than
// along anything a reader wants: a person hunting for a date picker does not know, and
// should not have to guess, whether it was filed under forms or under overlays.
//
// So the whole kit is one page, and the sidebar jumps you down it. The groups come from
// componentGroups(), which is also what BUILDS the sections — so the sidebar cannot
// offer a jump to a section that does not exist, and a section cannot go missing from
// the sidebar.
func docsNav() []docsGroup {
	items := make([]docsItem, 0, len(componentGroups()))
	for _, g := range componentGroups() {
		items = append(items, docsItem{
			Path:  "/wasm/components#" + g.ID,
			Label: g.Label,
			Icon:  g.Icon,
			Blurb: g.Blurb,
		})
	}

	return []docsGroup{{
		Title: "Introduction",
		Items: []docsItem{
			{Path: "/wasm", Label: "Overview", Icon: "book-open",
				Blurb: "What Kjøl Wasm Web is, how a page becomes a WebAssembly binary, and what runs where."},
		},
	}, {
		Title: "Rendering",
		Items: []docsItem{
			{Path: "/wasm/chart", Label: "SSR & hydration", Icon: "chart-column",
				Blurb: "The same Go renders HTML on the server and takes over in the browser. Charts, drawn by the WebAssembly."},
			{Path: "/wasm/server", Label: "Server components", Icon: "server",
				Blurb: "Components whose state and code stay on the server. Calling one looks like calling any other."},
			{Path: "/wasm/data", Label: "Data fetching", Icon: "cloud-arrow-down",
				Blurb: "gob to your own server (Go types end to end, no JSON) and JSON to a third-party API."},
		},
	}, {
		Title: "Components",
		Items: items,
	}}
}

// ---- page scaffolding ---------------------------------------------------

// docPage is the frame every documentation page shares: an eyebrow, a title, a lede,
// and then its sections.
func docPage(eyebrow, title, lede string, sections ...*VNode) *VNode {
	mods := []Mod{Attr("class", "pb-16")}
	mods = append(mods,
		Div(Attr("class", "border-b border-line pb-6"),
			P(Attr("class", "text-xs font-semibold uppercase tracking-widest text-accent"), Text(eyebrow)),
			H1(Attr("class", "mt-2 text-3xl font-semibold tracking-tight text-text-heading"), Text(title)),
			P(Attr("class", "mt-3 text-ink-muted leading-relaxed"), Text(lede)),
		),
	)
	for _, s := range sections {
		mods = append(mods, s)
	}
	return Div(mods...)
}

// docSection is a titled slab of the page. The id is what the "on this page" links and
// the tour steps anchor to.
func docSection(id, title string, body ...*VNode) *VNode {
	mods := []Mod{Attr("id", id), Attr("class", "mt-12 scroll-mt-24")}
	mods = append(mods,
		H2(Attr("class", "text-xl font-semibold tracking-tight text-text-heading"), Text(title)),
	)
	for _, b := range body {
		mods = append(mods, b)
	}
	return El("section", mods...)
}

// prose is a paragraph of explanation.
//
// It used to be pinned to a reading measure (max-w-3xl). It is not any more: on a
// documentation page the paragraphs sit directly above demos, tables and code blocks
// that are as wide as the column, and a narrow ribbon of text over a full-width panel
// reads as a mistake rather than as typographic care. The column itself (max-w-6xl, set
// by AppLayout) is the measure now.
func prose(text string) *VNode {
	return P(Attr("class", "mt-3 text-ink-soft leading-relaxed"), Text(text))
}

// ---- code ---------------------------------------------------------------

// code is a Go snippet, captioned with where it comes from.
//
// The caption is a real file path in this example, not a decoration: every snippet on
// these pages is copied from code that actually runs, and saying where from is what
// lets you go and check.
func code(caption, src string) *VNode { return codeLang(caption, "Go", src) }

// codeLang is code() for a block that is not Go — a C header, a shell session, a formula.
// The label in the corner says what you are looking at, and a shell command labelled "Go"
// is worse than no label at all.
//
// The label is ALSO what picks the lexer, so the two cannot disagree: a block cannot be
// labelled C and painted as Go. A language kjol/lexer does not know comes back escaped and
// unpainted, which is what should happen — a shell transcript put through a Go lexer comes
// out with `serving` painted as an identifier and quotes as string literals, and
// highlighting the WRONG language is more distracting than not highlighting at all.
func codeLang(caption, lang, src string) *VNode {
	// Raw, not Text: the lexer returns HTML. It escapes every run of source on the way out
	// — including for a language it does not know — so the snippets that contain markup,
	// and every C snippet, which is all pointers and shifts, stay inert.
	body := El("code", Raw(lexer.Highlight(lang, src)))

	return Div(Attr("class", "mt-4 overflow-hidden rounded-default border border-neutral-800 bg-neutral-900"),
		Div(Attr("class", "flex items-center gap-2 border-b border-neutral-800 px-4 py-2"),
			Span(Attr("class", "text-xs font-medium text-ink-faint font-mono"), Text(caption)),
			Span(Attr("class", "ml-auto rounded-full bg-white/5 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-ink-faint"), Text(lang)),
		),
		Pre(Attr("class", "overflow-x-auto px-4 py-3 text-[13px] leading-relaxed text-neutral-100 font-mono"), body),
	)
}

// ---- demos --------------------------------------------------------------

// demo is the panel a section's example sits in, captioned with what it is showing.
func demo(title string, body ...*VNode) *VNode {
	mods := []Mod{Attr("class", "mt-4 rounded-default border border-line bg-surface shadow-xs")}
	mods = append(mods,
		Div(Attr("class", "border-b border-line px-4 py-2"),
			Span(Attr("class", "text-xs text-ink-muted"), Text(title)),
		),
	)
	inner := []Mod{Attr("class", "p-4")}
	for _, b := range body {
		inner = append(inner, b)
	}
	mods = append(mods, Div(inner...))
	return Div(mods...)
}

// note is an aside — a caveat, a gotcha, the reason something is the way it is.
func note(title, body string) *VNode {
	return Div(Attr("class", "mt-4 rounded-default border border-primary-border bg-primary-subtle px-4 py-3"),
		P(Attr("class", "text-sm font-semibold text-text-heading"), Text(title)),
		P(Attr("class", "mt-1 text-sm text-ink-soft leading-relaxed"), Text(body)),
	)
}

// ---- reference tables ---------------------------------------------------

type apiRow struct{ Name, Desc string }

// apiTable is the reference half of a page: the names, and what each one does.
func apiTable(rows ...apiRow) *VNode {
	body := make([]*VNode, 0, len(rows))
	for _, r := range rows {
		body = append(body, El("tr", Attr("class", "border-t border-line"),
			El("td", Attr("class", "py-2 pr-4 align-top whitespace-nowrap"),
				El("code", Attr("class", "rounded bg-surface-raised px-1.5 py-0.5 text-[13px] font-mono text-ink"), Text(r.Name))),
			El("td", Attr("class", "py-2 text-sm text-ink-soft leading-relaxed"), Text(r.Desc)),
		))
	}
	rowMods := []Mod{}
	for _, b := range body {
		rowMods = append(rowMods, b)
	}
	// Full width, like everything else on the page. A reference table pinned to max-w-5xl
	// inside a max-w-6xl column is not narrower for a reason — it is narrower by an inch,
	// which reads as a misalignment rather than as a decision.
	return Div(Attr("class", "mt-4 overflow-x-auto"),
		El("table", Attr("class", "w-full border-collapse text-left"),
			Tbody(rowMods...),
		),
	)
}

// ---- the docs index -----------------------------------------------------

//gowasm:page /wasm static layout=app
func DocsPage(d Deps) func() *VNode {
	clicks := NewSignal(0)

	// The one measurement on the page: performance.now() when the client's first render
	// commits. Zero until then — which is what the SERVER renders, and what the client
	// renders on its first pass, so the two agree and hydration stays clean.
	hydratedAt := NewSignal(0.0)
	wasmruntime.AfterRender(func() {
		if hydratedAt.Get() == 0 {
			hydratedAt.Set(wasmruntime.Now())
		}
	})

	// demoTree is called TWICE per render below — once for the DOM, once for the HTML.
	// That is the point: the two panes cannot drift, because there is only one of them.
	//
	// This demo used to be on the front page. It does not belong there — it is the Wasm
	// Web engine's single best argument, and the front page is kjøl's, not this engine's.
	// Here it is the first thing the section shows, which is where an argument like this
	// one earns its place.
	demoTree := func() *VNode {
		return Div(Attr("class", "flex items-center gap-3"),
			ui.Button(ui.ButtonProps{
				Color: ui.ButtonPrimary, Text: "Click me",
				OnClick: func() { clicks.Set(clicks.Get() + 1) },
			}),
			Span(Attr("class", "text-ink-soft"), Text("clicked "+itoa(clicks.Get())+" times")),
		)
	}

	return func() *VNode {
		markup := RenderHTML(demoTree())

		var groups []*VNode
		for _, g := range docsNav() {
			grid := []Mod{Attr("class", "mt-3 grid gap-3 sm:grid-cols-2")}
			for _, it := range g.Items {
				if it.Path == "/wasm" {
					continue // don't list this page on itself
				}
				grid = append(grid, docsCard(d, it))
			}
			if len(grid) == 1 {
				continue // the group held nothing but this page
			}
			groups = append(groups,
				Div(Attr("class", "mt-10"),
					H2(Attr("class", "text-sm font-semibold uppercase tracking-widest text-ink-faint"), Text(g.Title)),
					Div(grid...),
				),
			)
		}

		return docPage("Introduction", "Overview",
			"Kjøl Wasm Web is Kjøl's Go→WebAssembly UI engine. You write components as ordinary Go "+
				"functions returning a virtual DOM; the server renders them to HTML and the same code "+
				"hydrates them in the browser. There is no JavaScript build step, and the engine depends "+
				"on nothing outside the standard library.",

			// ---- the demonstration ----
			//
			// The one thing on this site that cannot be faked: the same Go function, rendered
			// twice at once, as live DOM and as the HTML string the server sent.
			docSection("two-runtimes", "One function, two runtimes",
				prose("Below is a single Go function, shown twice. On the left it has been reconciled into "+
					"the DOM and you can use it. On the right is the HTML the same function produces when the "+
					"server renders it — the markup that reached your browser before any WebAssembly had "+
					"loaded. Click the button; both move."),

				Div(Attr("class", "mt-5 border border-line sm:grid sm:grid-cols-2"),
					Div(Attr("class", "border-b border-line sm:border-b-0 sm:border-r"),
						paneLabel("in your browser"),
						Div(Attr("class", "px-4 py-8"), demoTree()),
					),
					Div(
						paneLabel(itoa(len(markup))+" bytes of HTML"),
						Pre(Attr("class", "whitespace-pre-wrap px-4 py-4 font-mono text-[12px] leading-relaxed text-ink-muted"),
							El("code", Text(prettyHTML(markup))),
						),
					),
				),
				P(Attr("class", "mt-3 text-sm leading-relaxed text-ink-muted"),
					Text("The right pane is not a picture of the source. It is vdom.RenderHTML, called on the "+
						"very tree the left pane is showing, recomputed on every click.")),
				P(Attr("class", "mt-2 text-sm text-ink-muted"),
					Text(hydrationNote(hydratedAt.Get()))),
			),

			docSection("what-runs-where", "What runs where",
				prose("A page is Go, compiled twice. On the server it renders to an HTML string, so the first "+
					"paint needs no WebAssembly at all. In the browser the same functions run again, adopt the "+
					"markup that is already there, and from then on a signal write re-renders and reconciles into "+
					"the live DOM."),
				code("app/pages.go", ssrSnippet),
				note("The host API is dual-build",
					"Components measure the DOM — a tooltip has to know where its trigger is. Those calls are "+
						"real under js/wasm and no-ops natively, which is what lets one component both SSR and "+
						"position itself, without a branch in the component."),
			),

			docSection("what-is-in-it", "What is in it",
				Ul(Attr("class", "mt-3 space-y-1.5 leading-relaxed text-ink-soft"),
					item("Server-side rendering and client hydration, from one codebase."),
					item("Server components: mark a function and its code and state stay on the server."),
					item("A component kit — forms, tabs, modals, tooltips, toasts — with dark mode."),
					item("A table with filtering, sorting, column management, formulas, and CSV and PDF export."),
					item("Tailwind, compiled by a Go program that reads your Go."),
				),
				prose("Two commands build it. The first produced the page you are reading; the second serves "+
					"it and rebuilds on save."),
				codeLang("terminal", "sh", buildTranscript),
			),

			appendNodes(Div(Attr("class", "mt-14 border-t border-line pt-2")), groups...),
		)
	}
}

func docsCard(d Deps, it docsItem) *VNode {
	// A component card is a jump into the components page, not a page of its own — so it
	// routes there and scrolls, exactly as the sidebar does.
	click := navigate(d, it.Path)
	if base, frag, ok := strings.Cut(it.Path, "#"); ok {
		click = navigateAnchor(d, base, frag)
	}

	return A(
		Attr("class", "group block rounded-default border border-line bg-surface p-4 no-underline shadow-xs transition hover:border-primary-border hover:shadow-sm"),
		Attr("href", it.Path), click,
		Div(Attr("class", "flex items-center gap-2"),
			Span(Attr("class", "inline-flex h-7 w-7 items-center justify-center rounded-default bg-primary-subtle text-accent"),
				ui.IconInline(it.Icon, 14, "")),
			Span(Attr("class", "font-semibold text-text-heading"), Text(it.Label)),
			Span(Attr("class", "ml-auto text-ink-faint transition group-hover:text-accent"), ui.IconInline("arrow-right", 12, "")),
		),
		P(Attr("class", "mt-2 text-sm text-ink-muted leading-relaxed"), Text(it.Blurb)),
	)
}

// appendNodes adds children to a node after the fact — the shape a few of these pages
// need, where the section list is computed rather than written out.
func appendNodes(parent *VNode, children ...*VNode) *VNode {
	parent.Children = append(parent.Children, children...)
	return parent
}

const ssrSnippet = `//gowasm:page /wasm static layout=app
func DocsPage(d Deps) func() *VNode {
    count := NewSignal(0)          // state lives in the closure

    return func() *VNode {         // the render: pure, called again on every change
        return Div(Attr("class", "space-y-2"),
            H1(Text("Overview")),
            Button(
                Attr("class", "btn"),
                On(EVENT_CLICK, func() { count.Set(count.Get() + 1) }),
                Text("clicked "+itoa(count.Get())+" times"),
            ),
        )
    }
}

// static  => the server pre-renders this route to HTML.
// The same function then hydrates it in the browser.`
