package app

import (
	"bytes"
	"io"
	"math/rand"

	chart "github.com/wcharczuk/go-chart/v2"

	. "kjol/vdom"
	"kjol/wasmruntime"
	ui "kjol/webui"
)

var chartLabels = []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}

// fixed initial data so the server SSR and the client's first render match.
func fixedChartData() []int { return []int{42, 17, 63, 28, 55, 9, 71} }

func randomValues() []int {
	v := make([]int, len(chartLabels))
	for i := range v {
		v[i] = rand.Intn(95) + 5
	}
	return v
}

func renderSVG(c interface {
	Render(chart.RendererProvider, io.Writer) error
}) string {
	var buf bytes.Buffer
	if c.Render(chart.SVG, &buf) != nil {
		return "<p class=\"text-danger m-0\">chart error</p>"
	}
	return buf.String()
}

func barSVG(values []int) string {
	bars := make([]chart.Value, len(values))
	for i, v := range values {
		bars[i] = chart.Value{Value: float64(v), Label: chartLabels[i%len(chartLabels)]}
	}
	return renderSVG(&chart.BarChart{
		Title:      "Weekly values (bar)",
		TitleStyle: chart.Style{FontSize: 15},
		Background: chart.Style{Padding: chart.Box{Top: 48, Left: 16, Right: 16, Bottom: 16}},
		Height:     320, BarWidth: 48, Bars: bars,
	})
}

func pieSVG(values []int) string {
	vs := make([]chart.Value, len(values))
	for i, v := range values {
		vs[i] = chart.Value{Value: float64(v), Label: chartLabels[i%len(chartLabels)]}
	}
	return renderSVG(&chart.PieChart{
		Title:      "Share by day (pie)",
		TitleStyle: chart.Style{FontSize: 15},
		Background: chart.Style{Padding: chart.Box{Top: 48}},
		Width:      320, Height: 320, Values: vs,
	})
}

// chartSkeleton is what the SERVER puts where a chart is going to be: a box of the right
// height, so nothing jumps when the real one arrives.
func chartSkeleton(height string) *VNode {
	return Div(Attr("class", "flex animate-pulse items-center justify-center rounded-default bg-surface-muted "+height),
		Span(Attr("class", "text-xs text-ink-faint"), Text("drawing…")),
	)
}

// newChartDrawing returns a signal that is FALSE on the server and on the client's first
// render, and true from the moment the WebAssembly has committed that first render.
//
// It is what keeps the charts CLIENT-DRAWN. go-chart is ordinary Go and would run just as
// happily on the server — it used to, and this page's markup carried two finished SVGs.
// Two reasons not to:
//
//   - It is work the server does on every single request for a picture that only matters
//     once the page is alive. Drawing it in the browser costs the server nothing and the
//     reader nothing they can see.
//   - It is the more honest demonstration. A Go charting library, compiled to WebAssembly,
//     drawing an SVG in the browser is the thing this layer claims it can do. Shipping a
//     server-rendered picture of one proves the opposite point.
//
// The false-on-first-render part is not optional: hydration walks the server's DOM
// alongside the client's first tree, so that tree has to be the SAME tree. Draw the charts
// on the client's first pass and the two disagree, and the reconciler has to rebuild what
// it should have adopted.
func newChartDrawing() *Signal[bool] {
	drawn := NewSignal(false)
	wasmruntime.AfterRender(func() {
		if !drawn.Get() {
			drawn.Set(true) // a write re-renders; the second pass draws for real
		}
	})
	return drawn
}

// chartBox renders one chart, or the placeholder standing in for it. draw is a closure so
// that on the server go-chart is never called at all — not called and discarded, but never
// entered.
func chartBox(class, height string, drawn bool, draw func() string) *VNode {
	if !drawn {
		return Div(Attr("class", class), chartSkeleton(height))
	}
	return Div(Attr("class", class), Raw(draw()))
}

//gowasm:page /wasm/chart static layout=app
func ChartPage(d Deps) func() *VNode {
	data := NewSignal(fixedChartData())
	drawn := newChartDrawing()

	return func() *VNode {
		values := data.Get()

		return docPage("Rendering", "SSR & hydration",
			"A static route is rendered to HTML by the server, so the page is complete before any "+
				"WebAssembly has downloaded. The same component then runs in the browser, adopts the markup "+
				"that is already there, and takes over. One function, two runtimes.",

			docSection("the-directive", "Marking a route static",
				prose("static on the page directive is what puts a route in the server's pre-render set. Leave "+
					"it off and the route renders on the client only — which is the right choice when the page "+
					"is behind a login, or its content depends on something only the browser knows."),
				code("app/chart.go", chartSnippet),
				note("Hydration adopts, it does not rebuild",
					"The client renders the same tree the server did and walks the existing DOM alongside it, "+
						"wiring event handlers to the nodes that are already on the page. If the two trees "+
						"disagree, the CLIENT wins — a stale server binary should not be able to pin a wrong "+
						"class onto the page forever."),
			),

			docSection("charts", "A worked example: charts",
				prose("These charts are SVG produced by go-chart — a plain Go library that knows nothing "+
					"about the browser. They are drawn by the WEBASSEMBLY, in your browser, and never by the "+
					"server: what the server sends is the two placeholders you may have seen for a moment, "+
					"and the WebAssembly replaces them on its first commit."),
				prose("That is the demonstration. A Go charting library, compiled to wasm, drawing an SVG in "+
					"the browser is exactly what this layer claims it can do — and a server-rendered picture "+
					"of a chart would prove the opposite point while looking identical. Shuffle redraws them, "+
					"and no request is made."),
				prose("The rest of the page IS server-rendered — the headings, the prose, the code you are "+
					"reading. Static and client-drawn are not opposites: a route can be pre-rendered and still "+
					"leave the expensive, browser-only parts of itself for the client."),

				Div(Attr("class", "mt-4"),
					ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Icon: "chart-column", Text: "Shuffle data",
						OnClick: func() { data.Set(randomValues()) }}),
				),
				Div(Attr("class", "mt-4 grid gap-4 lg:grid-cols-12"),
					chartBox("lg:col-span-7 rounded-default border border-line bg-surface p-3 shadow-xs overflow-auto",
						"h-[260px]", drawn.Get(), func() string { return barSVG(values) }),
					chartBox("lg:col-span-5 rounded-default border border-line bg-surface p-3 shadow-xs overflow-auto",
						"h-[320px]", drawn.Get(), func() string { return pieSVG(values) }),
				),

				note("go-chart lives in the EXAMPLE, not in kjol",
					"The engine is standard-library-only. This example is its own Go module precisely so a "+
						"charting dependency it happens to want does not become a dependency of everyone who "+
						"uses the framework."),
			),

			docSection("api", "Reference",
				apiTable(
					apiRow{"//gowasm:page /path static", "Pre-render this route on the server, then hydrate it."},
					apiRow{"vdom.RenderHTML", "Render a tree to an HTML string. This is what the server calls."},
					apiRow{"wasmruntime.Hydrate", "Adopt server-rendered DOM instead of building it. The client's entry point for a static route."},
					apiRow{"vdom.Raw", "Insert markup verbatim — how the SVG the WebAssembly drew gets in. The reconciler clears it correctly when the element is reused."},
				),
			),
		)
	}
}

const chartSnippet = `//gowasm:page /wasm/chart static layout=app
func ChartPage(d Deps) func() *VNode {
    data  := NewSignal(fixedChartData())
    drawn := NewSignal(false)          // false on the server AND on the first client render

    // AfterRender is the post-commit hook. It fires once the WebAssembly has put its
    // first tree on the page — the earliest moment at which drawing is a client act.
    wasmruntime.AfterRender(func() {
        if !drawn.Get() {
            drawn.Set(true)            // a write re-renders; the second pass draws
        }
    })

    return func() *VNode {
        return Div(
            ui.Button(ui.ButtonProps{
                Text:    "Shuffle data",
                OnClick: func() { data.Set(randomValues()) },
            }),

            // The server never enters barSVG: chartBox takes a CLOSURE, and calls it only
            // once drawn is true. It renders the placeholder instead, and the WebAssembly
            // swaps in the real chart on its first commit.
            //
            // drawn must be FALSE on the client's first render too. Hydration walks the
            // server's DOM alongside the client's first tree, so the two have to BE the
            // same tree; draw on that first pass and the reconciler rebuilds what it
            // should have adopted.
            chartBox("", "h-[260px]", drawn.Get(),
                func() string { return barSVG(data.Get()) }),
        )
    }
}

// chartBox is the whole trick, and it is four lines.
func chartBox(class, height string, drawn bool, draw func() string) *VNode {
    if !drawn {
        return Div(Attr("class", class), chartSkeleton(height))
    }
    return Div(Attr("class", class), Raw(draw()))
}`
