import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
import { extractArtifactToCanvas, extractBtnCss } from "./extract-artifact";

const styles = css`
	:host {
		background: var(--rs-bg-surface, #fff);
		color: var(--rs-text-primary, #1e293b);
		border-radius: 8px;
		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
		min-width: 380px;
		min-height: 480px;
	}

	.header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 8px 12px;
		background: linear-gradient(135deg, #ea580c, #f59e0b);
		color: white;
		border-radius: 8px 8px 0 0;
		font-size: 12px;
		font-weight: 600;
		cursor: move;
	}

	.header-title {
		display: flex;
		align-items: center;
		gap: 6px;
	}

	.header-actions {
		display: flex;
		gap: 4px;
	}

	.header-actions button {
		background: transparent;
		border: none;
		color: white;
		cursor: pointer;
		padding: 2px 6px;
		border-radius: 4px;
		font-size: 14px;
	}

	.header-actions button:hover {
		background: rgba(255, 255, 255, 0.2);
	}

	.wrapper {
		height: 100%;
		overflow: hidden;
		display: flex;
		flex-direction: column;
	}

	.content {
		display: flex;
		flex-direction: column;
		flex: 1;
		min-height: 0;
		overflow: hidden;
	}

	.prompt-area {
		padding: 12px;
		border-bottom: 1px solid #e2e8f0;
	}

	.prompt-input {
		width: 100%;
		padding: 10px 12px;
		border: 2px solid var(--rs-input-border, #e2e8f0);
		border-radius: 8px;
		font-size: 13px;
		resize: none;
		outline: none;
		font-family: inherit;
		background: var(--rs-input-bg, #fff);
		color: var(--rs-input-text, inherit);
	}

	.prompt-input:focus {
		border-color: #ea580c;
	}

	.controls {
		display: flex;
		gap: 8px;
		margin-top: 8px;
	}

	.generate-btn {
		flex: 1;
		padding: 8px 16px;
		background: linear-gradient(135deg, #ea580c, #f59e0b);
		color: white;
		border: none;
		border-radius: 6px;
		font-size: 13px;
		font-weight: 600;
		cursor: pointer;
		transition: opacity 0.2s;
	}

	.generate-btn:hover {
		opacity: 0.9;
	}

	.generate-btn:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}

	.tabs {
		display: flex;
		border-bottom: 1px solid #e2e8f0;
	}

	.tab {
		flex: 1;
		padding: 8px;
		text-align: center;
		font-size: 12px;
		font-weight: 600;
		color: #64748b;
		border: none;
		background: none;
		cursor: pointer;
		border-bottom: 2px solid transparent;
		transition: all 0.2s;
	}

	.tab:hover {
		color: #ea580c;
	}

	.tab.active {
		color: #ea580c;
		border-bottom-color: #ea580c;
	}

	.preview-area {
		flex: 1;
		min-height: 0;
		overflow: hidden;
		display: flex;
		flex-direction: column;
	}

	.render-preview {
		flex: 1;
		display: flex;
		align-items: center;
		justify-content: center;
		overflow: hidden;
		min-height: 0;
		position: relative;
		touch-action: none;
		cursor: grab;
		user-select: none;
	}

	.render-preview.dragging { cursor: grabbing; }

	.render-preview img {
		transform-origin: 0 0;
		border-radius: 8px;
		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
		pointer-events: none;
		max-width: none;
		max-height: none;
	}

	.render-preview .zoom-hint {
		position: absolute;
		bottom: 8px;
		right: 8px;
		font-size: 10px;
		color: var(--rs-text-tertiary, #94a3b8);
		opacity: 0.7;
		pointer-events: none;
	}

	.render-preview .zoom-reset {
		position: absolute;
		top: 8px;
		right: 8px;
		font-size: 11px;
		padding: 3px 8px;
		border-radius: 4px;
		border: 1px solid var(--rs-border, #e2e8f0);
		background: var(--rs-bg-surface, #fff);
		color: var(--rs-text-secondary, #64748b);
		cursor: pointer;
		opacity: 0;
		transition: opacity 0.15s;
	}

	.render-preview:hover .zoom-reset { opacity: 1; }
	.render-preview .zoom-reset:hover { background: var(--rs-surface-hover, #f1f5f9); }

	${extractBtnCss}

	.code-area {
		flex: 1;
		overflow: auto;
		padding: 12px;
	}

	.code-area pre {
		margin: 0;
		padding: 12px;
		background: #1e293b;
		color: #e2e8f0;
		border-radius: 6px;
		font-size: 11px;
		line-height: 1.5;
		overflow-x: auto;
		white-space: pre-wrap;
		word-break: break-all;
	}

	.download-row {
		display: flex;
		gap: 8px;
		padding: 8px 12px;
		border-top: 1px solid #e2e8f0;
	}

	.download-btn {
		padding: 6px 12px;
		background: #334155;
		color: white;
		border: none;
		border-radius: 6px;
		font-size: 12px;
		cursor: pointer;
		text-decoration: none;
	}

	.download-btn:hover {
		background: #475569;
	}

	.placeholder {
		flex: 1;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		color: #94a3b8;
		text-align: center;
		gap: 8px;
	}

	.placeholder-icon {
		font-size: 48px;
		opacity: 0.5;
	}

	.loading {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		padding: 24px;
		gap: 12px;
	}

	.spinner {
		width: 32px;
		height: 32px;
		border: 3px solid #e2e8f0;
		border-top-color: #ea580c;
		border-radius: 50%;
		animation: spin 1s linear infinite;
	}

	@keyframes spin {
		to { transform: rotate(360deg); }
	}

	.error {
		color: #ef4444;
		padding: 12px;
		background: #fef2f2;
		border-radius: 6px;
		font-size: 13px;
		margin: 12px;
	}

	.multiplayer-area {
		flex: 1;
		overflow: auto;
		padding: 12px;
		display: none;
	}

	.mp-status {
		display: flex;
		align-items: center;
		gap: 8px;
		margin-bottom: 12px;
		font-size: 13px;
		font-weight: 600;
	}

	.mp-dot {
		width: 10px;
		height: 10px;
		border-radius: 50%;
		background: #94a3b8;
	}

	.mp-dot.online { background: #22c55e; }
	.mp-dot.offline { background: #ef4444; }

	.mp-field {
		margin-bottom: 10px;
	}

	.mp-field label {
		display: block;
		font-size: 11px;
		font-weight: 600;
		color: #64748b;
		margin-bottom: 4px;
		text-transform: uppercase;
		letter-spacing: 0.5px;
	}

	.mp-conn {
		display: flex;
		gap: 6px;
	}

	.mp-conn input {
		flex: 1;
		padding: 7px 10px;
		border: 1px solid #e2e8f0;
		border-radius: 6px;
		font-size: 13px;
		font-family: monospace;
		background: #f8fafc;
		color: #1e293b;
	}

	.mp-copy-btn {
		padding: 7px 12px;
		border: 1px solid #e2e8f0;
		border-radius: 6px;
		background: #f8fafc;
		cursor: pointer;
		font-size: 13px;
	}

	.mp-copy-btn:hover { background: #e2e8f0; }

	.mp-instructions {
		font-size: 12px;
		color: #64748b;
		line-height: 1.6;
		margin-top: 12px;
		padding: 10px;
		background: #f8fafc;
		border-radius: 6px;
		border: 1px solid #e2e8f0;
	}

	.mp-instructions ol {
		margin: 4px 0 0 16px;
		padding: 0;
	}

	.mp-instructions a {
		color: #ea580c;
		text-decoration: underline;
	}
`;

declare global {
	interface HTMLElementTagNameMap {
		"folk-blender": FolkBlender;
	}
}

export class FolkBlender extends FolkShape {
	static override tagName = "folk-blender";

	static {
		const sheet = new CSSStyleSheet();
		const parentRules = Array.from(FolkShape.styles.cssRules)
			.map((r) => r.cssText)
			.join("\n");
		const childRules = Array.from(styles.cssRules)
			.map((r) => r.cssText)
			.join("\n");
		sheet.replaceSync(`${parentRules}\n${childRules}`);
		this.styles = sheet;
	}

	#isLoading = false;
	#error: string | null = null;
	#prompt: string | null = null;
	#renderUrl: string | null = null;
	#script: string | null = null;
	#blendUrl: string | null = null;
	#activeTab: "preview" | "code" | "multiplayer" = "preview";
	#promptInput: HTMLTextAreaElement | null = null;
	#generateBtn: HTMLButtonElement | null = null;
	#previewArea: HTMLElement | null = null;
	#codeArea: HTMLElement | null = null;
	#multiplayerArea: HTMLElement | null = null;
	#downloadRow: HTMLElement | null = null;

	override createRenderRoot() {
		const root = super.createRenderRoot();

		const wrapper = document.createElement("div");
		wrapper.className = "wrapper";
		wrapper.innerHTML = html`
			<div class="header">
				<span class="header-title">
					<span>🧊</span>
					<span>3D Blender</span>
				</span>
				<div class="header-actions">
					<button class="close-btn" title="Close">×</button>
				</div>
			</div>
			<div class="content">
				<div class="prompt-area">
					<textarea class="prompt-input" placeholder="Describe the 3D scene to generate...\ne.g. 'A low-poly fox sitting on a tree stump'" rows="3"></textarea>
					<div class="controls">
						<button class="generate-btn">🧊 Generate 3D</button>
					</div>
				</div>
				<div class="tabs">
					<button class="tab active" data-tab="preview">🖼️ Render</button>
					<button class="tab" data-tab="code">📜 Script</button>
					<button class="tab" data-tab="multiplayer">🌐 Multiplayer</button>
				</div>
				<div class="preview-area">
					<div class="render-preview">
						<div class="placeholder">
							<span class="placeholder-icon">🧊</span>
							<span>Describe a 3D scene and click Generate</span>
						</div>
					</div>
					<div class="code-area" style="display:none">
						<pre>// Blender Python script will appear here</pre>
					</div>
					<div class="multiplayer-area">
						<div class="mp-status">
							<span class="mp-dot"></span>
							<span class="mp-status-text">Checking...</span>
						</div>
						<div class="mp-field">
							<label>Connection</label>
							<div class="mp-conn">
								<input class="mp-conn-input" readonly value="" />
								<button class="mp-copy-btn" title="Copy">📋</button>
							</div>
						</div>
						<div class="mp-instructions">
							<strong>How to connect:</strong>
							<ol>
								<li>Install <a href="https://extensions.blender.org/add-ons/multi-user/" target="_blank" rel="noopener">Multi-User addon</a> in Blender 4.3+</li>
								<li>Open sidebar → Multi-User tab</li>
								<li>Paste the connection address above</li>
								<li>Click Connect — you're in the shared session!</li>
							</ol>
						</div>
					</div>
				</div>
				<div class="download-row" style="display:none">
					<a class="download-btn blend-dl" href="#" download>⬇️ .blend</a>
				</div>
			</div>
		`;

		const slot = root.querySelector("slot");
		const containerDiv = slot?.parentElement as HTMLElement;
		if (containerDiv) {
			containerDiv.replaceWith(wrapper);
		}

		this.#promptInput = wrapper.querySelector(".prompt-input");
		this.#generateBtn = wrapper.querySelector(".generate-btn");
		this.#previewArea = wrapper.querySelector(".render-preview");
		this.#codeArea = wrapper.querySelector(".code-area");
		this.#multiplayerArea = wrapper.querySelector(".multiplayer-area");
		this.#downloadRow = wrapper.querySelector(".download-row");
		const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;

		// Tab switching
		wrapper.querySelectorAll(".tab").forEach((tab) => {
			tab.addEventListener("click", (e) => {
				e.stopPropagation();
				const tabName = (tab as HTMLElement).dataset.tab as "preview" | "code" | "multiplayer";
				this.#activeTab = tabName;
				wrapper.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
				tab.classList.add("active");
				if (this.#previewArea) this.#previewArea.style.display = tabName === "preview" ? "flex" : "none";
				if (this.#codeArea) this.#codeArea.style.display = tabName === "code" ? "block" : "none";
				if (this.#multiplayerArea) this.#multiplayerArea.style.display = tabName === "multiplayer" ? "block" : "none";
				if (tabName === "multiplayer") this.#fetchMultiplayerStatus();
			});
		});

		this.#generateBtn?.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#generate();
		});

		this.#promptInput?.addEventListener("keydown", (e) => {
			if (e.key === "Enter" && !e.shiftKey) {
				e.preventDefault();
				this.#generate();
			}
		});
		this.#promptInput?.addEventListener("pointerdown", (e) => e.stopPropagation());

		closeBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.dispatchEvent(new CustomEvent("close"));
		});

		// Restore persisted state
		if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt;
		if (this.#renderUrl || this.#script) {
			this.#renderResult();
		}

		// Health check — only disable if real error (sidecar starts on demand)
		fetch("/api/blender-gen/health").then(r => r.json()).then((h: any) => {
			if (this.#generateBtn) {
				if (!h.available && h.issues?.length) {
					this.#generateBtn.disabled = true;
					this.#generateBtn.title = h.issues.join(", ");
				} else if (h.warnings?.length) {
					this.#generateBtn.title = h.warnings.join(", ");
				}
			}
		}).catch(() => {});

		// Multiplayer copy button
		const copyBtn = wrapper.querySelector(".mp-copy-btn") as HTMLButtonElement;
		const connInput = wrapper.querySelector(".mp-conn-input") as HTMLInputElement;
		copyBtn?.addEventListener("click", (e) => {
			e.stopPropagation();
			if (connInput?.value) {
				navigator.clipboard.writeText(connInput.value).then(() => {
					copyBtn.textContent = "✅";
					setTimeout(() => { copyBtn.textContent = "📋"; }, 1500);
				});
			}
		});

		return root;
	}

	async #fetchMultiplayerStatus() {
		const dot = this.#multiplayerArea?.querySelector(".mp-dot");
		const statusText = this.#multiplayerArea?.querySelector(".mp-status-text");
		const connInput = this.#multiplayerArea?.querySelector(".mp-conn-input") as HTMLInputElement | null;

		try {
			const res = await fetch("/api/blender-multiuser/status");
			const data = await res.json() as { available: boolean; host: string; port: number };
			if (dot) {
				dot.classList.toggle("online", data.available);
				dot.classList.toggle("offline", !data.available);
			}
			if (statusText) statusText.textContent = data.available ? "Server Online" : "Server Offline";
			if (connInput) connInput.value = `${data.host}:${data.port}`;
		} catch {
			if (dot) { dot.classList.remove("online"); dot.classList.add("offline"); }
			if (statusText) statusText.textContent = "Unable to check";
		}
	}

	async #generate() {
		const prompt = this.#promptInput?.value.trim();
		if (!prompt || this.#isLoading) return;

		this.#prompt = prompt;
		this.#isLoading = true;
		this.#error = null;
		if (this.#generateBtn) this.#generateBtn.disabled = true;

		if (this.#previewArea) {
			this.#previewArea.innerHTML = '<div class="loading"><div class="spinner"></div><span>Generating 3D scene...</span></div>';
		}

		try {
			const res = await fetch("/api/blender-gen", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ prompt }),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({ error: res.statusText }));
				throw new Error(err.error || "Generation failed");
			}

			const data = await res.json();
			this.#renderUrl = data.render_url || null;
			this.#script = data.script || null;
			this.#blendUrl = data.blend_url || null;

			this.#renderResult();
		} catch (err) {
			this.#error = err instanceof Error ? err.message : "Generation failed";
			if (this.#previewArea) {
				this.#previewArea.innerHTML = `<div class="error">${this.#escapeHtml(this.#error)}</div>`;
			}
		} finally {
			this.#isLoading = false;
			if (this.#generateBtn) this.#generateBtn.disabled = false;
		}
	}

	// Pan/zoom state
	#viewX = 0;
	#viewY = 0;
	#viewScale = 1;
	#pointers = new Map<number, { x: number; y: number }>();
	#lastPinchDist = 0;
	#viewCleanup: (() => void) | null = null;

	#renderResult() {
		if (this.#previewArea) {
			// Clean up previous listeners
			this.#viewCleanup?.();
			this.#viewCleanup = null;

			if (this.#renderUrl) {
				this.#previewArea.innerHTML = `<button class="extract-btn" data-url="${this.#escapeHtml(this.#renderUrl)}" data-title="${this.#escapeHtml(this.#prompt || "3D Render")}" title="Extract to canvas">↗</button><img src="${this.#escapeHtml(this.#renderUrl)}" alt="3D Render" /><button class="zoom-reset" title="Reset zoom">Reset</button><span class="zoom-hint">Scroll to zoom · drag to pan</span>`;
				const img = this.#previewArea.querySelector("img") as HTMLImageElement;
				const resetBtn = this.#previewArea.querySelector(".zoom-reset") as HTMLButtonElement;

				// Reset view state and fit image once loaded
				this.#viewScale = 1;
				this.#viewX = 0;
				this.#viewY = 0;

				const fitImage = () => {
					if (!this.#previewArea) return;
					const cw = this.#previewArea.clientWidth;
					const ch = this.#previewArea.clientHeight;
					const iw = img.naturalWidth || img.width;
					const ih = img.naturalHeight || img.height;
					if (!iw || !ih) return;
					this.#viewScale = Math.min(cw / iw, ch / ih, 1);
					this.#viewX = (cw - iw * this.#viewScale) / 2;
					this.#viewY = (ch - ih * this.#viewScale) / 2;
					this.#applyTransform(img);
				};

				if (img.complete) fitImage();
				else img.addEventListener("load", fitImage);

				this.#wireViewerEvents(this.#previewArea, img);
				resetBtn?.addEventListener("click", (e) => {
					e.stopPropagation();
					fitImage();
				});

				// Extract button
				const extractBtn = this.#previewArea.querySelector(".extract-btn") as HTMLElement;
				extractBtn?.addEventListener("click", (e) => {
					e.stopPropagation();
					extractArtifactToCanvas({ url: extractBtn.dataset.url!, mediaType: "image", title: extractBtn.dataset.title, sourceShape: this });
				});
			} else {
				this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">✅</span><span>Script generated (see Script tab)</span></div>';
			}
		}

		if (this.#codeArea && this.#script) {
			this.#codeArea.innerHTML = `<pre>${this.#escapeHtml(this.#script)}</pre>`;
		}

		if (this.#downloadRow && this.#blendUrl) {
			this.#downloadRow.style.display = "flex";
			const blendLink = this.#downloadRow.querySelector(".blend-dl") as HTMLAnchorElement;
			if (blendLink) blendLink.href = this.#blendUrl;
		}
	}

	#applyTransform(img: HTMLImageElement) {
		img.style.transform = `translate(${this.#viewX}px, ${this.#viewY}px) scale(${this.#viewScale})`;
	}

	#wireViewerEvents(container: HTMLElement, img: HTMLImageElement) {
		let dragging = false;
		let dragStartX = 0;
		let dragStartY = 0;
		let startViewX = 0;
		let startViewY = 0;

		const onPointerDown = (e: PointerEvent) => {
			e.stopPropagation();
			this.#pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
			container.setPointerCapture(e.pointerId);

			if (this.#pointers.size === 1) {
				dragging = true;
				dragStartX = e.clientX;
				dragStartY = e.clientY;
				startViewX = this.#viewX;
				startViewY = this.#viewY;
				container.classList.add("dragging");
			} else if (this.#pointers.size === 2) {
				// Start pinch
				const pts = [...this.#pointers.values()];
				this.#lastPinchDist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
			}
		};

		const onPointerMove = (e: PointerEvent) => {
			if (!this.#pointers.has(e.pointerId)) return;
			this.#pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });

			if (this.#pointers.size === 2) {
				// Pinch zoom
				const pts = [...this.#pointers.values()];
				const dist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
				if (this.#lastPinchDist > 0) {
					const ratio = dist / this.#lastPinchDist;
					const cx = (pts[0].x + pts[1].x) / 2;
					const cy = (pts[0].y + pts[1].y) / 2;
					const rect = container.getBoundingClientRect();
					const px = cx - rect.left;
					const py = cy - rect.top;
					this.#zoomAt(px, py, ratio, img);
				}
				this.#lastPinchDist = dist;
			} else if (dragging && this.#pointers.size === 1) {
				this.#viewX = startViewX + (e.clientX - dragStartX);
				this.#viewY = startViewY + (e.clientY - dragStartY);
				this.#applyTransform(img);
			}
		};

		const onPointerUp = (e: PointerEvent) => {
			this.#pointers.delete(e.pointerId);
			if (this.#pointers.size < 2) this.#lastPinchDist = 0;
			if (this.#pointers.size === 0) {
				dragging = false;
				container.classList.remove("dragging");
			}
		};

		const onWheel = (e: WheelEvent) => {
			e.preventDefault();
			e.stopPropagation();
			const rect = container.getBoundingClientRect();
			const px = e.clientX - rect.left;
			const py = e.clientY - rect.top;
			const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
			this.#zoomAt(px, py, factor, img);
		};

		const onDblClick = (e: MouseEvent) => {
			e.stopPropagation();
			// Double-click to reset
			const cw = container.clientWidth;
			const ch = container.clientHeight;
			const iw = img.naturalWidth || img.width;
			const ih = img.naturalHeight || img.height;
			if (!iw || !ih) return;
			this.#viewScale = Math.min(cw / iw, ch / ih, 1);
			this.#viewX = (cw - iw * this.#viewScale) / 2;
			this.#viewY = (ch - ih * this.#viewScale) / 2;
			this.#applyTransform(img);
		};

		container.addEventListener("pointerdown", onPointerDown);
		container.addEventListener("pointermove", onPointerMove);
		container.addEventListener("pointerup", onPointerUp);
		container.addEventListener("pointercancel", onPointerUp);
		container.addEventListener("wheel", onWheel, { passive: false });
		container.addEventListener("dblclick", onDblClick);

		this.#viewCleanup = () => {
			container.removeEventListener("pointerdown", onPointerDown);
			container.removeEventListener("pointermove", onPointerMove);
			container.removeEventListener("pointerup", onPointerUp);
			container.removeEventListener("pointercancel", onPointerUp);
			container.removeEventListener("wheel", onWheel);
			container.removeEventListener("dblclick", onDblClick);
		};
	}

	#zoomAt(px: number, py: number, factor: number, img: HTMLImageElement) {
		const newScale = Math.min(Math.max(this.#viewScale * factor, 0.1), 10);
		const ratio = newScale / this.#viewScale;
		this.#viewX = px - ratio * (px - this.#viewX);
		this.#viewY = py - ratio * (py - this.#viewY);
		this.#viewScale = newScale;
		this.#applyTransform(img);
	}

	#escapeHtml(text: string): string {
		const div = document.createElement("div");
		div.textContent = text;
		return div.innerHTML;
	}

	static override fromData(data: Record<string, any>): FolkBlender {
		const shape = FolkShape.fromData(data) as FolkBlender;
		if (data.prompt) shape.#prompt = data.prompt;
		if (data.renderUrl) shape.#renderUrl = data.renderUrl;
		if (data.script) shape.#script = data.script;
		if (data.blendUrl) shape.#blendUrl = data.blendUrl;
		return shape;
	}

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-blender",
			prompt: this.#prompt,
			renderUrl: this.#renderUrl,
			script: this.#script,
			blendUrl: this.#blendUrl,
		};
	}

	override applyData(data: Record<string, any>): void {
		super.applyData(data);
		if ("prompt" in data) this.#prompt = data.prompt;
		if ("renderUrl" in data) this.#renderUrl = data.renderUrl;
		if ("script" in data) this.#script = data.script;
		if ("blendUrl" in data) this.#blendUrl = data.blendUrl;
		if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt;
		if (this.#renderUrl || this.#script) {
			this.#renderResult();
		}
	}
}
