//go:build js && wasm

// Package wasmruntime is the wasm client runtime for the neutral vdom package: it
// reconciles vdom.VNode trees into the real DOM (fresh mount or hydration of
// server-rendered DOM), and drives re-renders when signals change.
package wasmruntime

import (
	"syscall/js"

	"kjol/vdom"
)

var document = js.Global().Get("document")

// nodeRT is the per-node bookkeeping stored in VNode.Runtime (wasm only).
type nodeRT struct {
	dom     js.Value
	jsFuncs map[string]js.Func
	refs    map[string]*handlerRef
}

type handlerRef struct{ fn func(vdom.Event) }

func rt(n *vdom.VNode) *nodeRT {
	if n.Runtime == nil {
		n.Runtime = &nodeRT{jsFuncs: map[string]js.Func{}, refs: map[string]*handlerRef{}}
	}
	return n.Runtime.(*nodeRT)
}

// clientEvent adapts a DOM event to vdom.Event.
type clientEvent struct{ js js.Value }

func (e clientEvent) PreventDefault() { e.js.Call("preventDefault") }

func (e clientEvent) Value() string {
	t := e.js.Get("target")
	if !t.Truthy() {
		return ""
	}
	v := t.Get("value")
	if !v.Truthy() {
		return "" // e.g. a button has no value
	}
	return v.String()
}

func one(n *vdom.VNode) []*vdom.VNode {
	if n == nil {
		return nil
	}
	return []*vdom.VNode{n}
}

// ---- fresh create + diff ----

func createDOM(n *vdom.VNode) js.Value {
	if n.Tag == "" {
		d := document.Call("createTextNode", n.Text)
		rt(n).dom = d
		return d
	}
	el := document.Call("createElement", n.Tag)
	rt(n).dom = el
	for k, v := range n.Attrs {
		el.Call("setAttribute", k, v)
	}
	for k, v := range n.Props {
		el.Set(k, v)
	}
	for name, h := range n.Events {
		addListener(n, name, h)
	}
	if n.HTML != "" {
		el.Set("innerHTML", n.HTML)
		return el
	}
	for _, c := range n.Children {
		el.Call("appendChild", createDOM(c))
	}
	return el
}

func patchChildren(parent js.Value, old, next []*vdom.VNode) {
	n := max(len(old), len(next))
	for i := range n {
		var o, x *vdom.VNode
		if i < len(old) {
			o = old[i]
		}
		if i < len(next) {
			x = next[i]
		}
		patch(parent, o, x)
	}
}

func patch(parent js.Value, o, x *vdom.VNode) {
	switch {
	case o == nil && x == nil:
		return
	case o == nil:
		parent.Call("appendChild", createDOM(x))
	case x == nil:
		parent.Call("removeChild", rt(o).dom)
		release(o)
	case o.Tag != x.Tag:
		parent.Call("replaceChild", createDOM(x), rt(o).dom)
		release(o)
	default:
		x.Runtime = o.Runtime // adopt dom + listeners
		dom := rt(x).dom
		if x.Tag == "" {
			if x.Text != o.Text {
				dom.Set("nodeValue", x.Text)
			}
			return
		}
		updateAttrs(o, x)
		updateProps(o, x)
		updateEvents(o, x)
		if x.HTML != "" {
			if x.HTML != o.HTML {
				dom.Set("innerHTML", x.HTML)
			}
			return
		}
		patchChildren(dom, o.Children, x.Children)
	}
}

func updateAttrs(o, x *vdom.VNode) {
	dom := rt(x).dom
	for k := range o.Attrs {
		if _, ok := x.Attrs[k]; !ok {
			dom.Call("removeAttribute", k)
		}
	}
	for k, v := range x.Attrs {
		if o.Attrs[k] != v {
			dom.Call("setAttribute", k, v)
		}
	}
}

func updateProps(o, x *vdom.VNode) {
	dom := rt(x).dom
	for k, v := range x.Props {
		if o.Props[k] != v && dom.Get(k).String() != v {
			dom.Set(k, v)
		}
	}
}

func updateEvents(o, x *vdom.VNode) {
	r := rt(x) // same nodeRT as o (adopted above)
	for name, fn := range r.jsFuncs {
		if _, ok := x.Events[name]; !ok {
			r.dom.Call("removeEventListener", name, fn)
			fn.Release()
			delete(r.jsFuncs, name)
			delete(r.refs, name)
		}
	}
	for name, h := range x.Events {
		if ref, ok := r.refs[name]; ok {
			ref.fn = h
		} else {
			addListener(x, name, h)
		}
	}
}

func addListener(n *vdom.VNode, name string, handler func(vdom.Event)) {
	r := rt(n)
	ref := &handlerRef{fn: handler}
	fn := js.FuncOf(func(this js.Value, args []js.Value) any {
		var ev js.Value
		if len(args) > 0 {
			ev = args[0]
		}
		ref.fn(clientEvent{js: ev})
		return nil
	})
	r.dom.Call("addEventListener", name, fn)
	r.jsFuncs[name] = fn
	r.refs[name] = ref
}

func release(n *vdom.VNode) {
	if n.Runtime != nil {
		for _, fn := range rt(n).jsFuncs {
			fn.Release()
		}
	}
	for _, c := range n.Children {
		release(c)
	}
}

// ---- hydration: adopt server-rendered DOM instead of creating it ----

func hydrateNode(dom js.Value, n *vdom.VNode) {
	if !dom.Truthy() {
		return // structural mismatch; leave a hole (a later re-render will fix)
	}
	rt(n).dom = dom
	if n.Tag == "" {
		if dom.Get("nodeValue").String() != n.Text {
			dom.Set("nodeValue", n.Text)
		}
		return
	}
	for name, h := range n.Events {
		addListener(n, name, h)
	}
	for k, v := range n.Props {
		dom.Set(k, v)
	}
	if n.HTML != "" {
		return // trust server-rendered HTML
	}
	childNodes := dom.Get("childNodes")
	for i, c := range n.Children {
		hydrateNode(childNodes.Index(i), c)
	}
}
