// Package app holds the application's pages and components as standalone
// functions (no central App struct). It is platform-neutral, so the SAME code
// renders on the server (SSR) and hydrates on the client.
//
// Directives (processed by cmd/wasmgen at build time):
//
//	//gowasm:page <path> [static] [layout=<name>]
//	                                 marks a page factory as a route. `static`
//	                                 pre-renders it on the server (SSR);
//	                                 `layout=<name>` wraps it in a //gowasm:layout.
//	//gowasm:layout <name>          marks a func(Deps, *VNode) *VNode as a named
//	                                 layout that wraps a page's content.
//	//gowasm:server                 (see server_counter.go) marks a component
//	                                 that runs on the server; the generated
//	                                 client stub makes calling it identical to
//	                                 calling any other component.
//
// The Routes() map, StaticPaths set, and RouteLayout/LayoutFor dispatch are all
// generated from these directives.
package app

//go:generate go run kjol/cmd/wasmgen .

import (
	"strconv"

	. "kjol/vdom"
)

// Deps are the client-only capabilities, injected so the pages stay neutral.
type Deps struct {
	Path     func() string
	Navigate func(string)
}

func itoa(n int) string { return strconv.Itoa(n) }

// Layout wraps a page's rendered content with shared chrome (nav, footer, …).
// Layouts are declared with //gowasm:layout and selected per route via a page's
// `layout=` directive; the generated LayoutFor dispatches by name.
type Layout func(d Deps, content *VNode) *VNode

// Shell renders the current route's page inside its declared layout. `routes` is
// the generated route table; `d.Path()` selects both the page and its layout.
func Shell(d Deps, routes map[string]func() *VNode) *VNode {
	path := d.Path()
	var content *VNode
	if page := routes[path]; page != nil {
		content = page()
	} else {
		content = notFound(path)
	}
	return LayoutFor(d, path, content)
}

func notFound(path string) *VNode {
	return Div(
		H2(Attr("class", "h4 mb-3"), Text("Page not found")),
		P(Attr("class", "text-secondary"), Text("No route matches "+path+".")),
	)
}

// --- layouts (selected per route via `layout=` in //gowasm:page) ---------

// PublicLayout is the chrome for public/marketing pages: a light navbar with a
// call-to-action into the app, and a footer. The func(Deps, *VNode) *VNode shape
// is what //gowasm:layout expects.
//
//gowasm:layout public
func PublicLayout(d Deps, content *VNode) *VNode {
	return Div(
		Nav(Attr("class", "navbar navbar-expand bg-light border-bottom mb-4"),
			Div(Attr("class", "container"),
				A(Attr("class", "navbar-brand fw-bold"), Attr("href", "/"), navigate(d, "/"), Text("gowasm")),
				Ul(Attr("class", "navbar-nav ms-auto align-items-center"),
					navItem(d, "/", "Home"),
					navItem(d, "/about", "About"),
					Li(Attr("class", "nav-item ms-2"),
						A(Attr("class", "btn btn-sm btn-primary"), Attr("href", "/chart"),
							navigate(d, "/chart"), Text("Open app →"))),
				))),
		Main(Attr("class", "container"),
			content,
			Footer(Attr("class", "text-secondary small border-top mt-5 pt-3"),
				Text("gowasm public site — a tiny Blazor-like engine in Go.")),
		),
	)
}

// AppLayout is the chrome for the application itself: a dark app navbar listing
// the app's sections, plus a link back to the public site.
//
//gowasm:layout app
func AppLayout(d Deps, content *VNode) *VNode {
	return Div(
		Nav(Attr("class", "navbar navbar-expand navbar-dark bg-dark mb-4"),
			Div(Attr("class", "container"),
				A(Attr("class", "navbar-brand fw-bold"), Attr("href", "/chart"), navigate(d, "/chart"), Text("gowasm · app")),
				Ul(Attr("class", "navbar-nav me-auto"),
					navItem(d, "/chart", "Chart"),
					navItem(d, "/server", "Server")),
				Ul(Attr("class", "navbar-nav"),
					navItem(d, "/", "Home")),
			)),
		Main(Attr("class", "container"), content),
	)
}

// navItem is a nav link that carries an active state on the current route.
func navItem(d Deps, path, label string) *VNode {
	cls := "nav-link"
	if d.Path() == path {
		cls += " active"
	}
	return Li(Attr("class", "nav-item"),
		A(Attr("class", cls), Attr("href", path), navigate(d, path), Text(label)))
}

// navigate intercepts a link click for client-side SPA navigation. On the server
// Navigate is nil, so the anchor falls back to a normal navigation.
func navigate(d Deps, path string) Mod {
	return OnEvent(EVENT_CLICK, func(e Event) {
		if d.Navigate != nil {
			e.PreventDefault()
			d.Navigate(path)
		}
	})
}

// Counter is a presentational client component; state is owned by the caller.
func Counter(label string, count *Signal[int]) *VNode {
	return Div(Attr("class", "counter card mb-2"),
		Div(Attr("class", "card-body py-2 d-flex align-items-center"),
			Span(Attr("class", "me-2 fw-semibold"), Text(label+": ")),
			Strong(Attr("class", "badge text-bg-primary me-2"), Text(itoa(count.Get()))),
			Div(Attr("class", "btn-group btn-group-sm ms-auto"),
				Button(Attr("class", "btn btn-outline-secondary"),
					On(EVENT_CLICK, func() { count.Update(func(v int) int { return v - 1 }) }), Text("−")),
				Button(Attr("class", "btn btn-outline-primary"),
					On(EVENT_CLICK, func() { count.Update(func(v int) int { return v + 1 }) }), Text("+")),
			)))
}

//gowasm:page / static layout=public
func HomePage(d Deps) func() *VNode {
	a := NewSignal(0)
	b := NewSignal(0)
	return func() *VNode {
		return Div(
			H2(Attr("class", "h4 mb-3"), Text("Home — component composition")),
			P(Attr("class", "text-secondary"), Text("Two counters; the total is derived across them. Server-rendered, then hydrated.")),
			Counter("Apples", a),
			Counter("Bananas", b),
			Div(Attr("class", "alert alert-info d-flex justify-content-between align-items-center mt-3"),
				Span(Text("Combined total: ")),
				Strong(Attr("class", "fs-5"), Text(itoa(a.Get()+b.Get())))),
		)
	}
}

//gowasm:page /about static layout=public
func AboutPage(d Deps) func() *VNode {
	return func() *VNode {
		return Div(
			H2(Attr("class", "h4 mb-3"), Text("About")),
			P(Attr("class", "lead"),
				Text("Components are standalone functions; calling a server component looks "+
					"identical to calling a client one — the //gowasm:server directive and the "+
					"build-time codegen wire up the round-trip. Static routes are SSR'd; the rest "+
					"render on the client.")),
		)
	}
}

//gowasm:page /server layout=app
func ServerPage(d Deps) func() *VNode {
	// ServerCounter is a server component — but calling it is just like calling
	// any component. On the client this resolves to a generated stub that mounts
	// it over /rsc; on the server it's the real function. (Not `static`: the
	// client renders the stub, which round-trips, so there's nothing stable to
	// pre-render + hydrate.)
	counter := ServerCounter()
	return func() *VNode {
		return Div(
			H2(Attr("class", "h4 mb-3"), Text("Server component")),
			P(Attr("class", "text-secondary"),
				Text("This counter runs on the server. Its state lives there; clicks round-trip "+
					"and the returned render merges into the DOM. The call site is identical to a "+
					"client component.")),
			counter(),
		)
	}
}
