// Minimal server-side DOM for running solid-js/html + solid-js/web inside
// goja.  It implements only the surface Solid's client runtime actually
// touches (enumerated from wwwroot/vendor/solid-js-web.js and
// solid-js-html.js): linked-list tree mutation, template.innerHTML/.content,
// element/text/comment creation, attributes, className/textContent, and
// no-op event wiring.
//
// The tree is the source of truth as a doubly-linked list (firstChild,
// nextSibling, ...) which is how the real DOM models it and what Solid's
// clone-walk assumes.  childNodes is a derived snapshot array so Solid's
// `[...el.childNodes]` spreads work.
//
// HTML *parsing* (innerHTML setter) is the one genuinely hard operation, so
// it is delegated to Go via __parseHTML (x/net/html) which returns a JSON
// tree.  Serialization back to a string is straightforward and lives here.
//   -mta

(function (global) {
	"use strict";

	var ELEMENT_NODE = 1, TEXT_NODE = 3, COMMENT_NODE = 8, FRAGMENT_NODE = 11;

	var VOID = {
		area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1,
		keygen: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1,
	};

	var SVG_NS = "http://www.w3.org/2000/svg";

	function escapeText(s) {
		return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
	}
	function escapeAttr(s) {
		return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
	}

	// ---- Node ----------------------------------------------------------

	class Node {
		constructor(type) {
			this.nodeType = type;
			this.parentNode = null;
			this.firstChild = null;
			this.lastChild = null;
			this.previousSibling = null;
			this.nextSibling = null;
			this._$host = null;
			this.host = null;
		}

		get childNodes() {
			var out = [], n = this.firstChild;
			while (n) { out.push(n); n = n.nextSibling; }
			return out;
		}

		appendChild(child) {
			detach(child);
			child.parentNode = this;
			child.previousSibling = this.lastChild;
			child.nextSibling = null;
			if (this.lastChild) this.lastChild.nextSibling = child;
			else this.firstChild = child;
			this.lastChild = child;
			return child;
		}

		insertBefore(child, ref) {
			if (ref == null) return this.appendChild(child);
			if (ref.parentNode !== this) throw new Error("insertBefore: ref not a child");
			detach(child);
			child.parentNode = this;
			child.nextSibling = ref;
			child.previousSibling = ref.previousSibling;
			if (ref.previousSibling) ref.previousSibling.nextSibling = child;
			else this.firstChild = child;
			ref.previousSibling = child;
			return child;
		}

		removeChild(child) {
			if (child.parentNode !== this) throw new Error("removeChild: not a child");
			detach(child);
			return child;
		}

		replaceChild(newNode, oldNode) {
			this.insertBefore(newNode, oldNode);
			this.removeChild(oldNode);
			return oldNode;
		}

		remove() { detach(this); }

		replaceWith() {
			var args = Array.prototype.slice.call(arguments);
			var parent = this.parentNode, ref = this.nextSibling;
			if (!parent) return;
			detach(this);
			for (var i = 0; i < args.length; i++) {
				var a = args[i];
				if (typeof a === "string") a = new Text(a);
				parent.insertBefore(a, ref);
			}
		}

		cloneNode(deep) {
			var copy = this._shallowClone();
			if (deep) {
				var n = this.firstChild;
				while (n) { copy.appendChild(n.cloneNode(true)); n = n.nextSibling; }
			}
			return copy;
		}

		get textContent() {
			if (this.nodeType === TEXT_NODE || this.nodeType === COMMENT_NODE) return this.data;
			var out = "", n = this.firstChild;
			while (n) {
				if (n.nodeType !== COMMENT_NODE) out += n.textContent;
				n = n.nextSibling;
			}
			return out;
		}
		set textContent(value) {
			if (this.nodeType === TEXT_NODE || this.nodeType === COMMENT_NODE) { this.data = String(value); return; }
			while (this.firstChild) this.removeChild(this.firstChild);
			if (value !== "" && value != null) this.appendChild(new Text(String(value)));
		}

		querySelectorAll(sel) { return querySelectorAll(this, sel); }
		querySelector(sel) { var r = querySelectorAll(this, sel); return r.length ? r[0] : null; }
	}

	function detach(node) {
		var p = node.parentNode;
		if (!p) return;
		if (node.previousSibling) node.previousSibling.nextSibling = node.nextSibling;
		else p.firstChild = node.nextSibling;
		if (node.nextSibling) node.nextSibling.previousSibling = node.previousSibling;
		else p.lastChild = node.previousSibling;
		node.parentNode = null;
		node.previousSibling = null;
		node.nextSibling = null;
	}

	// ---- Text / Comment ------------------------------------------------

	class Text extends Node {
		constructor(data) { super(TEXT_NODE); this.data = data == null ? "" : String(data); this.nodeName = "#text"; }
		_shallowClone() { return new Text(this.data); }
	}

	class Comment extends Node {
		constructor(data) { super(COMMENT_NODE); this.data = data == null ? "" : String(data); this.nodeName = "#comment"; }
		_shallowClone() { return new Comment(this.data); }
	}

	// ---- Element -------------------------------------------------------

	class Element extends Node {
		constructor(tagName, ns) {
			super(ELEMENT_NODE);
			this.tagName = tagName;
			this.localName = String(tagName).toLowerCase();
			this.nodeName = this.localName;
			this.namespaceURI = ns || null;
			this.attributes = {};
			this._style = null;
			this._classList = null;
			if (this.localName === "template") this.content = new Fragment();
		}

		_shallowClone() {
			var copy = new Element(this.tagName, this.namespaceURI);
			for (var k in this.attributes) copy.attributes[k] = this.attributes[k];
			if (this.content) {
				var n = this.content.firstChild;
				while (n) { copy.content.appendChild(n.cloneNode(true)); n = n.nextSibling; }
			}
			return copy;
		}

		setAttribute(name, value) { this.attributes[name] = String(value); }
		setAttributeNS(_ns, name, value) { this.attributes[name] = String(value); }
		getAttribute(name) { return name in this.attributes ? this.attributes[name] : null; }
		hasAttribute(name) { return name in this.attributes; }
		removeAttribute(name) { delete this.attributes[name]; }
		removeAttributeNS(_ns, name) { delete this.attributes[name]; }

		get className() { return this.attributes["class"] || ""; }
		set className(v) { this.attributes["class"] = String(v); }

		get id() { return this.attributes["id"] || ""; }
		set id(v) { this.attributes["id"] = String(v); }

		get innerHTML() { return serializeChildren(this); }
		set innerHTML(htmlStr) {
			var target = this.content ? this.content : this;
			while (target.firstChild) target.removeChild(target.firstChild);
			var json = global.__parseHTML(String(htmlStr));
			var nodes = buildNodes(JSON.parse(json));
			for (var i = 0; i < nodes.length; i++) target.appendChild(nodes[i]);
		}

		get style() {
			if (!this._style) this._style = makeStyle(this);
			return this._style;
		}
		get classList() {
			if (!this._classList) this._classList = makeClassList(this);
			return this._classList;
		}

		// Event wiring is irrelevant to server output.
		addEventListener() {}
		removeEventListener() {}
	}

	class Fragment extends Node {
		constructor() { super(FRAGMENT_NODE); this.nodeName = "#document-fragment"; }
		_shallowClone() { return new Fragment(); }
	}

	// ---- style / classList shims --------------------------------------

	function makeStyle(el) {
		return {
			setProperty: function (k, v) {
				var cur = parseStyle(el.attributes["style"] || "");
				cur[k] = v;
				el.attributes["style"] = stringifyStyle(cur);
			},
			removeProperty: function (k) {
				var cur = parseStyle(el.attributes["style"] || "");
				delete cur[k];
				el.attributes["style"] = stringifyStyle(cur);
			},
			get cssText() { return el.attributes["style"] || ""; },
			set cssText(v) { el.attributes["style"] = String(v); },
		};
	}
	function parseStyle(s) {
		var out = {};
		s.split(";").forEach(function (decl) {
			var i = decl.indexOf(":");
			if (i > -1) out[decl.slice(0, i).trim()] = decl.slice(i + 1).trim();
		});
		return out;
	}
	function stringifyStyle(o) {
		return Object.keys(o).map(function (k) { return k + ":" + o[k]; }).join(";");
	}

	function makeClassList(el) {
		function read() { return (el.attributes["class"] || "").split(/\s+/).filter(Boolean); }
		function write(list) { el.attributes["class"] = list.join(" "); }
		return {
			add: function () { var l = read(); for (var i = 0; i < arguments.length; i++) if (l.indexOf(arguments[i]) < 0) l.push(arguments[i]); write(l); },
			remove: function () { var l = read(), a = Array.prototype.slice.call(arguments); write(l.filter(function (c) { return a.indexOf(c) < 0; })); },
			toggle: function (c, force) { var l = read(), has = l.indexOf(c) > -1; if (force === undefined ? has : !force) write(l.filter(function (x) { return x !== c; })); else if (!has) { l.push(c); write(l); } },
			contains: function (c) { return read().indexOf(c) > -1; },
		};
	}

	// ---- build from parsed JSON ---------------------------------------

	function buildNodes(arr) {
		var out = [];
		for (var i = 0; i < arr.length; i++) out.push(buildNode(arr[i]));
		return out;
	}
	function buildNode(j) {
		if (j.t === "t") return new Text(j.d);
		if (j.t === "c") return new Comment(j.d);
		var el = new Element(j.n, j.ns === "svg" ? SVG_NS : null);
		if (j.a) for (var k in j.a) el.attributes[k] = j.a[k];
		if (j.c) for (var i = 0; i < j.c.length; i++) el.appendChild(buildNode(j.c[i]));
		return el;
	}

	// ---- serialization -------------------------------------------------

	function serializeChildren(node) {
		var out = "", n = node.firstChild;
		while (n) { out += serializeNode(n); n = n.nextSibling; }
		return out;
	}
	function serializeNode(node) {
		if (node.nodeType === TEXT_NODE) return escapeText(node.data);
		// "#" is solid-js/html's template insertion placeholder; any that
		// survive instantiation are framework artifacts, not page content.
		if (node.nodeType === COMMENT_NODE) return node.data === "#" ? "" : "<!--" + node.data + "-->";
		if (node.nodeType === FRAGMENT_NODE) return serializeChildren(node);
		// Output the original-case tag (SVG is case-sensitive: viewBox,
		// linearGradient); use the lowercased localName only for lookups.
		var tag = node.tagName, lname = node.localName;
		var s = "<" + tag;
		for (var k in node.attributes) s += " " + k + '="' + escapeAttr(node.attributes[k]) + '"';
		s += ">";
		if (VOID[lname]) return s;
		if (lname === "template" && node.content) s += serializeChildren(node.content);
		else s += serializeChildren(node);
		return s + "</" + tag + ">";
	}

	// ---- minimal querySelectorAll (only script,style and *[data-hk]) ---

	function querySelectorAll(root, sel) {
		var wantHk = /\[data-hk\]/.test(sel);
		var tags = sel.split(",").map(function (s) { return s.trim().replace(/\[.*\]/, "").replace("*", "").toLowerCase(); }).filter(Boolean);
		var out = [];
		(function walk(n) {
			var c = n.firstChild;
			while (c) {
				if (c.nodeType === ELEMENT_NODE) {
					if (wantHk && c.attributes["data-hk"] != null) out.push(c);
					else if (tags.indexOf(c.localName) > -1) out.push(c);
					walk(c);
				}
				c = c.nextSibling;
			}
		})(root.content || root);
		return out;
	}

	// ---- document ------------------------------------------------------

	var document = {
		createElement: function (tag) { return new Element(tag, null); },
		createElementNS: function (ns, tag) { return new Element(tag, ns); },
		createTextNode: function (data) { return new Text(data); },
		createComment: function (data) { return new Comment(data); },
		createDocumentFragment: function () { return new Fragment(); },
		importNode: function (node, deep) { return node.cloneNode(deep); },
		addEventListener: function () {},
		removeEventListener: function () {},
		nodeType: 9,
	};

	global.document = document;
	global.Node = Node;
	global.Element = Element;
	global.Text = Text;
	global.Comment = Comment;
	global.window = global;

	// Serialize a node's children (innerHTML) — the Go side calls this to
	// extract the rendered markup from the render root.
	global.__serialize = function (node) { return serializeChildren(node); };

})(globalThis);
