// Package vdom is a platform-neutral virtual DOM shared by the server (renders
// to an HTML string) and the client (reconciles into the real DOM). It compiles
// on BOTH native and js/wasm, so the same component code runs in both places —
// which is what makes server-side rendering + client hydration possible.
package vdom

import (
	"html"
	"sort"
	"strings"
)

// Event is a DOM event passed to handlers. The client provides a concrete
// implementation; on the server events are never invoked.
type Event interface {
	PreventDefault()
	Value() string // target.value (for inputs)
}

// VNode is a virtual DOM node. Tag == "" is a text node (content in Text). HTML,
// if set on an element, is raw innerHTML (children ignored).
type VNode struct {
	Tag      string
	Text     string
	HTML     string
	Attrs    map[string]string
	Props    map[string]string
	Events   map[string]func(Event)
	Children []*VNode

	// Runtime holds the wasm reconciler's per-node bookkeeping (DOM handle,
	// listener wrappers). It's `any` so this package stays platform-neutral; it
	// is nil on the server.
	Runtime any
}

// Mod configures a VNode while it is built.
type Mod interface{ apply(*VNode) }

// El builds an element VNode.
func El(tag string, mods ...Mod) *VNode {
	n := &VNode{Tag: tag, Attrs: map[string]string{}, Props: map[string]string{}, Events: map[string]func(Event){}}
	for _, m := range mods {
		m.apply(n)
	}
	return n
}

// Text builds a text VNode.
func Text(s string) *VNode { return &VNode{Text: s} }

func (n *VNode) apply(parent *VNode) { parent.Children = append(parent.Children, n) }

type attrMod struct{ k, v string }

func (a attrMod) apply(n *VNode) { n.Attrs[a.k] = a.v }

// Attr sets an HTML attribute.
func Attr(k, v string) Mod { return attrMod{k, v} }

type propMod struct{ k, v string }

func (p propMod) apply(n *VNode) { n.Props[p.k] = p.v }

// Prop sets a live DOM property (e.g. an input's value).
func Prop(k, v string) Mod { return propMod{k, v} }

type htmlMod struct{ html string }

func (h htmlMod) apply(n *VNode) { n.HTML = h.html }

// Raw sets inner HTML verbatim (children ignored) — e.g. a server-computed SVG.
func Raw(html string) Mod { return htmlMod{html} }

type eventMod struct {
	name string
	h    func(Event)
}

func (e eventMod) apply(n *VNode) { n.Events[e.name] = e.h }

// On registers an event handler that ignores the event object.
func On(event string, h func()) Mod { return eventMod{event, func(Event) { h() }} }

// OnEvent registers an event handler that receives the Event.
func OnEvent(event string, h func(Event)) Mod { return eventMod{event, h} }

// --- server-side HTML rendering (used for SSR; runs on any platform) ---

var voidTags = map[string]bool{"br": true, "hr": true, "img": true, "input": true, "meta": true, "link": true}

// RenderHTML serializes a VNode tree to HTML with NO extra whitespace, so the
// browser's parsed childNodes line up 1:1 with the VNode children on hydration.
func RenderHTML(n *VNode) string {
	var b strings.Builder
	writeNode(&b, n)
	return b.String()
}

func writeNode(b *strings.Builder, n *VNode) {
	if n.Tag == "" {
		b.WriteString(html.EscapeString(n.Text))
		return
	}
	b.WriteByte('<')
	b.WriteString(n.Tag)
	writeAttrs(b, n.Attrs)
	writeAttrs(b, n.Props) // props like input value show up as attributes in SSR
	b.WriteByte('>')
	if voidTags[n.Tag] {
		return
	}
	if n.HTML != "" {
		b.WriteString(n.HTML) // raw
	} else {
		for _, c := range n.Children {
			writeNode(b, c)
		}
	}
	b.WriteString("</")
	b.WriteString(n.Tag)
	b.WriteByte('>')
}

func writeAttrs(b *strings.Builder, m map[string]string) {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		b.WriteByte(' ')
		b.WriteString(k)
		b.WriteString(`="`)
		b.WriteString(html.EscapeString(m[k]))
		b.WriteByte('"')
	}
}
