/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { instanceSymbol } from "../../constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { ThreadStyleSheet } from "./stylesheet/thread.mjs";
import { Entry } from "./thread/entry.mjs";
import "./thread/message.mjs";
import { validateInstance, validateString } from "../../types/validate.mjs";
import "./state.mjs";
import { isArray } from "../../types/is.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { ProxyObserver } from "../../types/proxyobserver.mjs";
import { Updater } from "../../dom/updater.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";

export { Thread };

/**
 * @private
 * @type {symbol}
 */
const collapsedStateSymbol = Symbol("collapsedState");
const entriesSymbol = Symbol("entries");
const entryMapSymbol = Symbol("entryMap");
const entryObserverMapSymbol = Symbol("entryObserverMap");
const entryUpdaterMapSymbol = Symbol("entryUpdaterMap");
const entryElementMapSymbol = Symbol("entryElementMap");
const entryTemplateSymbol = Symbol("entryTemplate");
const entriesListSymbol = Symbol("entriesList");
const emptyStateSymbol = Symbol("emptyStateElement");
const idCounterSymbol = Symbol("idCounter");
const timeAgoIntervalSymbol = Symbol("timeAgoInterval");

/**
 * A discussion thread with hierarchical entries.
 *
 * @fragments /fragments/components/state/thread
 *
 * @example /examples/components/state/thread-simple Thread
 * @example /examples/components/state/thread-review Review thread with nested replies
 *
 * @issue https://localhost.alvine.dev:8444/development/issues/open/374.html
 *
 * @since 3.77.0
 * @copyright Volker Schukai
 * @summary A threaded discussion layout for nested replies, comments and conversation-like content.
 **/
class Thread extends CustomElement {
	/**
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initControlReferences.call(this);
		this[entriesSymbol] = [];
		this[collapsedStateSymbol] = new Map();
		this[entryMapSymbol] = new Map();
		this[entryObserverMapSymbol] = new Map();
		this[entryUpdaterMapSymbol] = new Map();
		this[entryElementMapSymbol] = new Map();
		this[idCounterSymbol] = 0;
		initTimeAgoTicker.call(this);
		initEventHandler.call(this);
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/state/thread@@instance");
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Labels
	 * @property {string} labels.nothingToReport Label for empty state
	 * @property {number} updateFrequency Update frequency in milliseconds for the timestamp
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			labels: {
				nothingToReport: "There is nothing to report yet.",
			},

			features: {
				timeAgoMaxHours: 12,
			},

			updateFrequency: 10000,

			entries: [],

			length: 0,

			timestamp: 0,
		});
	}

	/**
	 * @param {string} path
	 * @param {*} defaultValue
	 * @return {*}
	 */
	getOption(path, defaultValue = undefined) {
		if (path === "entries" || path?.startsWith("entries.")) {
			try {
				return new Pathfinder({
					entries: this[entriesSymbol],
				}).getVia(path);
			} catch (e) {
				return defaultValue;
			}
		}

		return super.getOption(path, defaultValue);
	}

	/**
	 * @param {string} path
	 * @param {*} value
	 * @return {Thread}
	 */
	setOption(path, value) {
		if (path === "entries") {
			const prepared = prepareEntries(value);
			this[entriesSymbol] = prepared;
			this[idCounterSymbol] = 0;
			this[collapsedStateSymbol] = new Map();
			renderEntries.call(this, prepared);
			super.setOption("length", countEntries(prepared));
			return this;
		}

		super.setOption(path, value);
		return this;
	}

	/**
	 * @param {object|string} options
	 * @return {Thread}
	 */
	setOptions(options) {
		if (options && typeof options === "object" && options.entries) {
			const { entries, ...rest } = options;
			if (Object.keys(rest).length > 0) {
				super.setOptions(rest);
			}
			this.setOption("entries", entries);
			return this;
		}

		super.setOptions(options);
		return this;
	}

	/**
	 * Clear the thread.
	 *
	 * @return {Thread}
	 */
	clear() {
		this.setOption("entries", []);
		this.setOption("length", 0);
		return this;
	}

	/**
	 * Add an entry to the thread.
	 *
	 * @param {Entry|Object} entry
	 * @param {string|null} parentId
	 * @return {Thread}
	 */
	addEntry(entry, parentId = null) {
		entry = normalizeEntry(entry);

		let entries = this.getOption("entries");
		if (!isArray(entries)) {
			entries = [];
			this[entriesSymbol] = entries;
		}

		if (parentId) {
			const parent =
				this[entryMapSymbol]?.get(parentId) || findEntryById(entries, parentId);
			if (!parent) {
				throw new Error(`parent entry not found: ${parentId}`);
			}
			applyEntryDefaults(entry, { isTopLevel: false });

			if (parent.collapsed === true) {
				parent.hiddenChildren = isArray(parent.hiddenChildren)
					? parent.hiddenChildren
					: [];
				parent.hiddenChildren.push(entry);
			} else {
				parent.children = isArray(parent.children) ? parent.children : [];
				parent.children.push(entry);

				const parentElement = this[entryElementMapSymbol]?.get(parentId);
				const childrenList = parentElement?.querySelector(
					"[data-monster-role=children]",
				);
				if (childrenList) {
					renderEntry(this, entry, childrenList);
				}
			}

			parent.replyCount = countEntries([
				...parent.children,
				...parent.hiddenChildren,
			]);
			syncEntryField(this, parentId, "replyCount", parent.replyCount);
		} else {
			entries.push(entry);
			applyEntryDefaults(entry, {
				isTopLevel: true,
				newestEntry: null,
			});

			if (this[entriesListSymbol]) {
				renderEntry(this, entry, this[entriesListSymbol]);
			}
			if (this[emptyStateSymbol]) {
				this[emptyStateSymbol].style.display = "none";
			}
		}

		indexEntries(this, [entry]);
		super.setOption("length", countEntries(entries));

		return this;
	}

	/**
	 * Add a message entry to the thread.
	 *
	 * @param {string} message
	 * @param {Date} date
	 * @param {string|null} parentId
	 * @return {Thread}
	 * @throws {TypeError} message is not a string
	 */
	addMessage(message, date, parentId = null) {
		if (!date) {
			date = new Date();
		}

		validateString(message);

		this.addEntry(
			new Entry({
				message: message,
				date: date,
			}),
			parentId,
		);

		return this;
	}

	/**
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-thread";
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [ThreadStyleSheet];
	}

	/**
	 * Toggle the collapsed state of an entry.
	 *
	 * @param {string} entryId
	 * @return {Thread}
	 */
	toggleEntry(entryId) {
		const current = this[collapsedStateSymbol]?.get(entryId) === true;
		const next = !current;
		this[collapsedStateSymbol]?.set(entryId, next);
		setEntryCollapsed(this, entryId, next);
		setCollapsedInDom(this, entryId, next);
		return this;
	}

	/**
	 * Get collapsed state for all entries.
	 *
	 * @return {Object<string, boolean>}
	 */
	getCollapsedState() {
		const state = {};
		if (!(this[collapsedStateSymbol] instanceof Map)) {
			return state;
		}
		for (const [key, value] of this[collapsedStateSymbol].entries()) {
			state[key] = value;
		}
		return state;
	}

	/**
	 * Set collapsed state for entries by id.
	 *
	 * @param {Object<string, boolean>} stateMap
	 * @return {Thread}
	 */
	setCollapsedState(stateMap) {
		if (!stateMap || typeof stateMap !== "object") {
			return this;
		}

		if (!(this[collapsedStateSymbol] instanceof Map)) {
			this[collapsedStateSymbol] = new Map();
		}

		for (const [entryId, value] of Object.entries(stateMap)) {
			const collapsed = Boolean(value);
			this[collapsedStateSymbol].set(entryId, collapsed);
			setEntryCollapsed(this, entryId, collapsed);
			setCollapsedInDom(this, entryId, collapsed);
		}
		return this;
	}

	/**
	 * Get ids of entries that are currently open.
	 *
	 * @return {string[]}
	 */
	getOpenEntries() {
		return collectCollapsedIds(this[collapsedStateSymbol], false);
	}

	/**
	 * Get ids of entries that are currently collapsed.
	 *
	 * @return {string[]}
	 */
	getClosedEntries() {
		return collectCollapsedIds(this[collapsedStateSymbol], true);
	}
}

/**
 * @private
 */
function initEventHandler() {
	const root = this.shadowRoot || this;
	root.addEventListener("click", (event) => {
		const button = event.target.closest("[data-action=toggle]");
		if (!button) {
			return;
		}

		const entryId = button.getAttribute("data-entry-id");
		if (!entryId) {
			return;
		}

		this.toggleEntry(entryId);
		const entry = this[entryMapSymbol]?.get(entryId) || null;
		const collapsed = this[collapsedStateSymbol]?.get(entryId) === true;
		fireCustomEvent(
			this,
			collapsed ? "monster-thread-collapse" : "monster-thread-expand",
			{
				entryId,
				entry,
			},
		);
	});

	root.addEventListener("click", (event) => {
		const button = event.target.closest("button[data-action]");
		if (!button) {
			return;
		}

		const action = button.getAttribute("data-action");
		if (!action || action === "toggle") {
			return;
		}

		const entryId = button.getAttribute("data-entry-id");
		const entry = this[entryMapSymbol]?.get(entryId) || null;
		fireCustomEvent(this, "monster-thread-action", {
			action,
			entryId,
			entry,
		});
	});
}

/**
 * @private
 * @param {Entry|Object} entry
 * @return {Entry}
 */
function normalizeEntry(entry) {
	if (entry instanceof Entry) {
		return entry;
	}

	if (entry && typeof entry === "object") {
		return new Entry(entry);
	}

	validateInstance(entry, Entry);
	return entry;
}

/**
 * @private
 * @param {Entry[]|*} entries
 * @return {Entry[]}
 */
function prepareEntries(entries) {
	const list = isArray(entries) ? entries.map(normalizeEntry) : [];
	const newestEntry = findNewestEntry(list);

	for (const entry of list) {
		applyEntryDefaults(entry, {
			isTopLevel: true,
			newestEntry,
		});
	}

	return list;
}

/**
 * @private
 * @param {Entry} entry
 * @param {{isTopLevel:boolean, newestEntry?:Entry}} context
 * @return {void}
 */
function applyEntryDefaults(entry, { isTopLevel, newestEntry } = {}) {
	entry.children = isArray(entry.children)
		? entry.children.map(normalizeEntry)
		: [];
	entry.hiddenChildren = isArray(entry.hiddenChildren)
		? entry.hiddenChildren.map(normalizeEntry)
		: [];

	if (
		(entry.collapsed === false ||
			entry.collapsed === null ||
			entry.collapsed === undefined) &&
		entry.children.length === 0 &&
		entry.hiddenChildren.length > 0
	) {
		entry.children = entry.hiddenChildren;
		entry.hiddenChildren = [];
	}

	for (let index = 0; index < entry.children.length; index += 1) {
		const child = entry.children[index];
		applyEntryDefaults(child, {
			isTopLevel: false,
		});
	}

	entry.replyCount = countEntries([...entry.children, ...entry.hiddenChildren]);

	if (entry.collapsed === null || entry.collapsed === undefined) {
		if (isTopLevel) {
			entry.collapsed = newestEntry ? entry !== newestEntry : false;
		} else {
			entry.collapsed = false;
		}
	}

	if (entry.collapsed === true && entry.children.length > 0) {
		entry.hiddenChildren = entry.children;
		entry.children = [];
	}
}

/**
 * @private
 * @param {Entry[]} entries
 * @param {string} id
 * @return {Entry|null}
 */
function findEntryById(entries, id) {
	for (const entry of entries) {
		if (entry?.id === id) {
			return entry;
		}

		const children = isArray(entry?.children) ? entry.children : [];
		const match = findEntryById(children, id);
		if (match) {
			return match;
		}

		const hidden = isArray(entry?.hiddenChildren) ? entry.hiddenChildren : [];
		const hiddenMatch = findEntryById(hidden, id);
		if (hiddenMatch) {
			return hiddenMatch;
		}
	}

	return null;
}

/**
 * @private
 * @param {Entry[]} entries
 * @return {number}
 */
function countEntries(entries) {
	let count = 0;
	for (const entry of entries) {
		count += 1;
		if (isArray(entry?.children) && entry.children.length > 0) {
			count += countEntries(entry.children);
		}
		if (isArray(entry?.hiddenChildren) && entry.hiddenChildren.length > 0) {
			count += countEntries(entry.hiddenChildren);
		}
	}
	return count;
}

/**
 * @private
 * @param {Entry[]} entries
 * @return {Entry|null}
 */
function findNewestEntry(entries) {
	if (!isArray(entries) || entries.length === 0) {
		return null;
	}

	let newest = entries[entries.length - 1];
	let newestTime = getEntryTimestamp(newest);

	for (const entry of entries) {
		const time = getEntryTimestamp(entry);
		if (time >= newestTime) {
			newest = entry;
			newestTime = time;
		}
	}

	return newest;
}

/**
 * @private
 * @param {Entry} entry
 * @return {number}
 */
function getEntryTimestamp(entry) {
	if (!entry?.date) {
		return Number.NEGATIVE_INFINITY;
	}

	const time = new Date(entry.date).getTime();
	return Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time;
}

/**
 * @private
 * @param {Thread} thread
 * @param {string} entryId
 * @param {boolean} collapsed
 * @return {void}
 */
function setCollapsedInDom(thread, entryId, collapsed) {
	const root = thread.shadowRoot || thread;
	const item = thread[entryElementMapSymbol]?.get(entryId);
	if (!item) {
		return;
	}

	item.setAttribute("data-collapsed", collapsed ? "true" : "false");
	const children = item.querySelector("[data-monster-role=children]");
	if (children) {
		children.setAttribute("data-collapsed", collapsed ? "true" : "false");
	}
}

/**
 * @private
 * @param {Thread} thread
 * @param {string} entryId
 * @param {boolean} collapsed
 * @return {void}
 */
function setEntryCollapsed(thread, entryId, collapsed) {
	const entry = thread[entryMapSymbol]?.get(entryId);
	if (!entry) {
		return;
	}

	entry.collapsed = collapsed;
	syncEntryField(thread, entryId, "collapsed", collapsed);

	if (collapsed) {
		if (entry.children.length > 0) {
			entry.hiddenChildren = entry.children;
			entry.children = [];
		}
	} else if (entry.hiddenChildren.length > 0) {
		entry.children = entry.hiddenChildren;
		entry.hiddenChildren = [];
	}

	const childrenContainer = thread[entryElementMapSymbol]
		?.get(entryId)
		?.querySelector("[data-monster-role=children]");
	if (!childrenContainer) {
		return;
	}

	if (collapsed) {
		clearContainer(childrenContainer);
		removeEntrySubtree(thread, entry.hiddenChildren);
		childrenContainer.style.display = "none";
		return;
	}

	for (const child of entry.children) {
		renderEntry(thread, child, childrenContainer);
	}
	childrenContainer.style.display = "";
}

/**
 * @private
 * @param {Map<string, boolean>} stateMap
 * @param {boolean} collapsed
 * @return {string[]}
 */
function collectCollapsedIds(stateMap, collapsed) {
	if (!(stateMap instanceof Map)) {
		return [];
	}

	const list = [];
	for (const [key, value] of stateMap.entries()) {
		if (Boolean(value) === collapsed) {
			list.push(key);
		}
	}
	return list;
}

/**
 * @private
 * @return {void}
 */
function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[entriesListSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=entries-list]",
	);
	this[emptyStateSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=empty-state]",
	);
	this[entryTemplateSymbol] = this.shadowRoot.querySelector("template#entry");
}

/**
 * @private
 * @return {void}
 */
function initTimeAgoTicker() {
	if (this[timeAgoIntervalSymbol]) {
		return;
	}

	const refresh = () => {
		updateTimeAgo(this);
	};

	refresh();
	this[timeAgoIntervalSymbol] = setInterval(
		refresh,
		this.getOption("updateFrequency"),
	);
}

/**
 * @private
 * @param {Entry[]} entries
 * @return {void}
 */
function renderEntries(entries) {
	if (!this[entriesListSymbol]) {
		return;
	}

	clearContainer(this[entriesListSymbol]);
	this[entryMapSymbol] = new Map();
	this[entryObserverMapSymbol] = new Map();
	this[entryUpdaterMapSymbol] = new Map();
	this[entryElementMapSymbol] = new Map();

	indexEntries(this, entries);

	const fragment = document.createDocumentFragment();
	for (const entry of entries) {
		renderEntry(this, entry, fragment);
	}
	this[entriesListSymbol].appendChild(fragment);
	updateTimeAgo(this);

	if (this[emptyStateSymbol]) {
		this[emptyStateSymbol].style.display =
			entries.length > 0 ? "none" : "block";
	}
}

/**
 * @private
 * @param {Thread} thread
 * @param {Entry} entry
 * @param {HTMLElement} parentList
 * @return {void}
 */
function renderEntry(thread, entry, parentList) {
	if (!entry.id) {
		thread[idCounterSymbol] += 1;
		entry.id = `entry-${thread[idCounterSymbol]}`;
	}

	const template = thread[entryTemplateSymbol];
	if (!template) {
		return;
	}

	const fragment = template.content.cloneNode(true);
	const item = fragment.querySelector("[data-monster-role=entry]");
	if (!item) {
		return;
	}

	item.setAttribute("data-entry-id", entry.id);
	item.setAttribute("data-collapsed", entry.collapsed ? "true" : "false");

	parentList.appendChild(item);

	const observer = new ProxyObserver({ entry });
	const updater = new Updater(item, observer);
	updater.run().catch(() => {});

	thread[entryObserverMapSymbol].set(entry.id, observer);
	thread[entryUpdaterMapSymbol].set(entry.id, updater);
	thread[entryElementMapSymbol].set(entry.id, item);

	const childrenContainer = item.querySelector("[data-monster-role=children]");
	if (!childrenContainer) {
		return;
	}

	const timeAgo = item.querySelector("[data-monster-role=time-ago]");
	if (timeAgo) {
		timeAgo.dataset.entryId = entry.id;
	}

	const toggleButton = item.querySelector("[data-action=toggle]");
	if (toggleButton) {
		toggleButton.setAttribute("data-entry-id", entry.id);
	}

	const children = entry.collapsed ? [] : entry.children;
	if (children.length === 0 && entry.hiddenChildren.length === 0) {
		childrenContainer.style.display = "none";
	} else {
		childrenContainer.style.display = "";
	}
	for (const child of children) {
		renderEntry(thread, child, childrenContainer);
	}
}

/**
 * @private
 * @param {Thread} thread
 * @return {void}
 */
function updateTimeAgo(thread) {
	const locale = getLocaleOfDocument().toString();
	const maxHours = Number(thread.getOption("features.timeAgoMaxHours", 12));
	const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
	for (const [entryId, element] of thread[entryElementMapSymbol].entries()) {
		const entry = thread[entryMapSymbol]?.get(entryId);
		if (!entry?.date) {
			continue;
		}
		const timeElement = element.querySelector("[data-monster-role=time-ago]");
		if (!timeElement) {
			continue;
		}
		try {
			timeElement.textContent = formatRelativeTime(
				new Date(entry.date),
				locale,
				maxHours,
				rtf,
			);
		} catch (e) {}
	}
}

/**
 * @private
 * @param {Date} date
 * @param {string} locale
 * @param {number} maxHours
 * @param {Intl.RelativeTimeFormat} rtf
 * @return {string}
 */
function formatRelativeTime(date, locale, maxHours, rtf) {
	let diffSeconds = Math.floor((Date.now() - date.getTime()) / 1000);
	if (!Number.isFinite(diffSeconds) || diffSeconds < 0) {
		diffSeconds = 0;
	}

	if (diffSeconds < 5) {
		return "just now";
	}

	if (diffSeconds < 60) {
		return rtf.format(-diffSeconds, "second");
	}

	const diffMinutes = Math.floor(diffSeconds / 60);
	if (diffMinutes < 60) {
		return rtf.format(-diffMinutes, "minute");
	}

	const diffHours = Math.floor(diffMinutes / 60);
	if (diffHours < maxHours) {
		return rtf.format(-diffHours, "hour");
	}

	return date.toLocaleDateString(locale, {
		year: "numeric",
		month: "2-digit",
		day: "2-digit",
	});
}

/**
 * @private
 * @param {Thread} thread
 * @param {Entry[]} entries
 * @return {void}
 */
function indexEntries(thread, entries) {
	for (const entry of entries) {
		if (!entry.id) {
			thread[idCounterSymbol] += 1;
			entry.id = `entry-${thread[idCounterSymbol]}`;
		}

		thread[entryMapSymbol].set(entry.id, entry);
		thread[collapsedStateSymbol].set(entry.id, Boolean(entry.collapsed));

		const children = [
			...(isArray(entry.children) ? entry.children : []),
			...(isArray(entry.hiddenChildren) ? entry.hiddenChildren : []),
		];

		if (children.length > 0) {
			indexEntries(thread, children);
		}
	}
}

/**
 * @private
 * @param {Thread} thread
 * @param {string} entryId
 * @param {string} key
 * @param {*} value
 * @return {void}
 */
function syncEntryField(thread, entryId, key, value) {
	const observer = thread[entryObserverMapSymbol]?.get(entryId);
	if (!observer) {
		return;
	}

	const subject = observer.getSubject();
	if (!subject?.entry) {
		return;
	}

	subject.entry[key] = value;
}

/**
 * @private
 * @param {Thread} thread
 * @param {Entry[]} entries
 * @return {void}
 */
function removeEntrySubtree(thread, entries) {
	for (const entry of entries) {
		if (entry?.id) {
			thread[entryObserverMapSymbol]?.delete(entry.id);
			thread[entryUpdaterMapSymbol]?.delete(entry.id);
			thread[entryElementMapSymbol]?.delete(entry.id);
		}
		if (isArray(entry?.children) && entry.children.length > 0) {
			removeEntrySubtree(thread, entry.children);
		}
		if (isArray(entry?.hiddenChildren) && entry.hiddenChildren.length > 0) {
			removeEntrySubtree(thread, entry.hiddenChildren);
		}
	}
}

/**
 * @private
 * @param {HTMLElement} container
 * @return {void}
 */
function clearContainer(container) {
	while (container.firstChild) {
		container.removeChild(container.firstChild);
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <template id="entry">
            <li data-monster-role="entry">
                <div data-monster-role="entry-card">
                    <div data-monster-role="meta">
                        <span data-monster-replace="path:entry.user"
                              data-monster-attributes="class path:entry.user | ?:user:hidden"></span>
                        <span data-monster-replace="path:entry.title"
                              data-monster-attributes="class path:entry.title | ?:title:hidden"></span>
                        <span data-monster-role="time-ago"
                              data-monster-replace="path:entry.date | time-ago"
                              data-monster-attributes="title path:entry.date | datetime"></span>
                    </div>
                    <monster-thread-message data-monster-role="message"
                         data-monster-attributes="data-monster-option-content path:entry.message | default: , class path:entry.message | ?:message:hidden"></monster-thread-message>
                    <div data-monster-role="thread-controls">
                        <button type="button"
                                class="monster-button-outline-secondary"
                                data-action="toggle"
                                data-monster-attributes="data-entry-id path:entry.id, data-reply-count path:entry.replyCount">
                            Replies
                            <span data-monster-role="badge"
                                  data-monster-replace="path:entry.replyCount"
                                  data-monster-attributes="data-reply-count path:entry.replyCount"></span>
                        </button>
                        <div data-monster-role="actions"
                             data-monster-replace="path:entry.actions"
                             data-monster-attributes="class path:entry.actions | ?:actions:hidden"></div>
                    </div>
                </div>
                <ul data-monster-role="children"></ul>
            </li>
        </template>

        <div part="control" data-monster-role="control">
            <div data-monster-role="empty-state">
                <monster-state>
                    <div part="visual">
                        <svg width="4rem" height="4rem" viewBox="0 -12 512.00032 512"
                             xmlns="http://www.w3.org/2000/svg">
                            <path d="m455.074219 172.613281 53.996093-53.996093c2.226563-2.222657 3.273438-5.367188 2.828126-8.480469-.441407-3.113281-2.328126-5.839844-5.085938-7.355469l-64.914062-35.644531c-4.839844-2.65625-10.917969-.886719-13.578126 3.953125-2.65625 4.84375-.890624 10.921875 3.953126 13.578125l53.234374 29.230469-46.339843 46.335937-166.667969-91.519531 46.335938-46.335938 46.839843 25.722656c4.839844 2.65625 10.921875.890626 13.578125-3.953124 2.660156-4.839844.890625-10.921876-3.953125-13.578126l-53.417969-29.335937c-3.898437-2.140625-8.742187-1.449219-11.882812 1.695313l-54 54-54-54c-3.144531-3.144532-7.988281-3.832032-11.882812-1.695313l-184.929688 101.546875c-2.757812 1.515625-4.644531 4.238281-5.085938 7.355469-.445312 3.113281.601563 6.257812 2.828126 8.480469l53.996093 53.996093-53.996093 53.992188c-2.226563 2.226562-3.273438 5.367187-2.828126 8.484375.441407 3.113281 2.328126 5.839844 5.085938 7.351562l55.882812 30.6875v102.570313c0 3.652343 1.988282 7.011719 5.1875 8.769531l184.929688 101.542969c1.5.824219 3.15625 1.234375 4.8125 1.234375s3.3125-.410156 4.8125-1.234375l184.929688-101.542969c3.199218-1.757812 5.1875-5.117188 5.1875-8.769531v-102.570313l55.882812-30.683594c2.757812-1.515624 4.644531-4.242187 5.085938-7.355468.445312-3.113282-.601563-6.257813-2.828126-8.480469zm-199.074219 90.132813-164.152344-90.136719 164.152344-90.140625 164.152344 90.140625zm-62.832031-240.367188 46.332031 46.335938-166.667969 91.519531-46.335937-46.335937zm-120.328125 162.609375 166.667968 91.519531-46.339843 46.339844-166.671875-91.519531zm358.089844 184.796875-164.929688 90.5625v-102.222656c0-5.523438-4.476562-10-10-10s-10 4.476562-10 10v102.222656l-164.929688-90.5625v-85.671875l109.046876 59.878907c1.511718.828124 3.167968 1.234374 4.808593 1.234374 2.589844 0 5.152344-1.007812 7.074219-2.929687l54-54 54 54c1.921875 1.925781 4.484375 2.929687 7.074219 2.929687 1.640625 0 3.296875-.40625 4.808593-1.234374l109.046876-59.878907zm-112.09375-46.9375-46.339844-46.34375 166.667968-91.515625 46.34375 46.335938zm0 0"/>
                            <path d="m404.800781 68.175781c2.628907 0 5.199219-1.070312 7.070313-2.933593 1.859375-1.859376 2.929687-4.4375 2.929687-7.066407 0-2.632812-1.070312-5.210937-2.929687-7.070312-1.859375-1.863281-4.441406-2.929688-7.070313-2.929688-2.640625 0-5.210937 1.066407-7.070312 2.929688-1.871094 1.859375-2.929688 4.4375-2.929688 7.070312 0 2.628907 1.058594 5.207031 2.929688 7.066407 1.859375 1.863281 4.441406 2.933593 7.070312 2.933593zm0 0"/>
                            <path d="m256 314.925781c-2.628906 0-5.210938 1.066407-7.070312 2.929688-1.859376 1.867187-2.929688 4.4375-2.929688 7.070312 0 2.636719 1.070312 5.207031 2.929688 7.078125 1.859374 1.859375 4.441406 2.921875 7.070312 2.921875s5.210938-1.0625 7.070312-2.921875c1.859376-1.871094 2.929688-4.441406 2.929688-7.078125 0-2.632812-1.070312-5.203125-2.929688-7.070312-1.859374-1.863281-4.441406-2.929688-7.070312-2.929688zm0 0"/>
                        </svg>
                    </div>
                    <div part="content" data-monster-replace="path:labels.nothingToReport">
                        There is nothing to report yet.
                    </div>
                </monster-state>
            </div>
            <div data-monster-role="entries">
                <ul data-monster-role="entries-list"></ul>
            </div>
            <div part="editor" data-monster-role="editor">
                <slot name="editor"></slot>
            </div>
        </div>
    `;
}

registerCustomElement(Thread);
