/**
 * 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 { internalSymbol } from "../../constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import {
	findElementWithSelectorUpwards,
	getDocument,
	getWindow,
} from "../../dom/util.mjs";
import { Formatter } from "../../text/formatter.mjs";
import { diff } from "../../data/diff.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { buildTree } from "../../data/buildtree.mjs";
import { ID } from "../../types/id.mjs";
import { NodeRecursiveIterator } from "../../types/noderecursiveiterator.mjs";
import { Observer } from "../../types/observer.mjs";
import { clone } from "../../util/clone.mjs";
import { isArray, isFunction, isObject, isString } from "../../types/is.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { CommonStyleSheet } from "../stylesheet/common.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { FileManagerStyleSheet } from "./stylesheet/file-manager.mjs";
import "../content/image-editor.mjs";
import { ATTRIBUTE_BUTTON_LABEL } from "../form/constants.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";

import "../layout/split-panel.mjs";
import "../layout/panel.mjs";
import "../layout/tabs.mjs";
import { State } from "../state/state.mjs";
import "../tree-menu/tree-menu.mjs";
import "../form/message-state-button.mjs";
import "../datatable/datasource/rest.mjs";

export { FileManager };

void State;

/**
 * @private
 * @type {symbol}
 */
const splitPanelSymbol = Symbol("splitPanel");

/**
 * @private
 * @type {symbol}
 */
const treeMenuSymbol = Symbol("treeMenu");

/**
 * @private
 * @type {symbol}
 */
const tabsSymbol = Symbol("tabs");
const tabsContainerSymbol = Symbol("tabsContainer");
const tabsResizeObserverSymbol = Symbol("tabsResizeObserver");
const hostResizeObserverSymbol = Symbol("hostResizeObserver");
const editorSizeRafSymbol = Symbol("editorSizeRaf");

/**
 * @private
 * @type {symbol}
 */
const saveButtonSymbol = Symbol("saveButton");
const titleSymbol = Symbol("title");

/**
 * @private
 * @type {symbol}
 */
const emptyStateSymbol = Symbol("emptyState");

/**
 * @private
 * @type {symbol}
 */
const listDatasourceSymbol = Symbol("listDatasource");

/**
 * @private
 * @type {symbol}
 */
const fileDatasourceSymbol = Symbol("fileDatasource");
const listDatasourceOwnedSymbol = Symbol("listDatasourceOwned");
const fileDatasourceOwnedSymbol = Symbol("fileDatasourceOwned");

/**
 * @private
 * @type {symbol}
 */
const tabsByKeySymbol = Symbol("tabsByKey");

/**
 * @private
 * @type {symbol}
 */
const tabsByReferenceSymbol = Symbol("tabsByReference");

/**
 * @private
 * @type {symbol}
 */
const entryByValueSymbol = Symbol("entryByValue");
const treeEntriesSymbol = Symbol("treeEntries");
const loadedParentsSymbol = Symbol("loadedParents");
const listRequestParentSymbol = Symbol("listRequestParent");
const listRequestInFlightSymbol = Symbol("listRequestInFlight");
const queuedParentSymbol = Symbol("queuedParent");

/**
 * @private
 * @type {symbol}
 */
const saveAllButtonSymbol = Symbol("saveAllButton");

/**
 * A file manager for navigating and editing files.
 *
 * @fragments /fragments/components/files/file-manager/
 * @example /examples/components/files/file-manager-basic Basic file manager workflow
 *
 * @since 4.44.0
 * @summary A file manager that loads a remote file tree and opens selected files in tabbed views.
 */
class FileManager extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/files/file-manager");
	}

	/**
	 * 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}
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Label definitions
	 * @property {string} labels.title Title label
	 * @property {string} labels.save Save button label
	 * @property {string} labels.empty Empty state label
	 * @property {Object} layout Layout configuration
	 * @property {Object} layout.split Split panel options
	 * @property {Object} mapping Mapping for list and content
	 * @property {string} mapping.data Path to list data in the datasource response
	 * @property {string} mapping.id Template or key for entry id
	 * @property {string} mapping.parent Template or key for entry parent
	 * @property {string} mapping.label Template or key for entry label
	 * @property {string} mapping.value Template or key for entry value
	 * @property {string} mapping.path Template or key for entry path
	 * @property {string} mapping.type Template or key for entry type
	 * @property {string} mapping.icon Template or key for entry icon id
	 * @property {string} mapping.contentPath Path to content in file read response
	 * @property {Object} datasource Datasource definitions
	 * @property {Object} datasource.list List datasource options
	 * @property {string} datasource.list.selector Selector for existing datasource
	 * @property {Object} datasource.list.rest Options for monster-datasource-rest
	 * @property {Object} datasource.file File datasource options
	 * @property {string} datasource.file.selector Selector for existing datasource
	 * @property {Object} datasource.file.rest Options for monster-datasource-rest
	 * @property {Object} editor Editor configuration
	 * @property {string} editor.tag Tag name for editor element
	 * @property {Object} editor.adapter Adapter with getValue/setValue/bindChange
	 * @property {Object} tabs Tabs configuration
	 * @property {string} tabs.dirtyMarker Marker appended to tab labels
	 * @property {Object} icons Icon mapping configuration
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			layout: {
				split: {
					type: "vertical",
					initial: "20%",
					min: "15%",
					max: "45%",
				},
			},
			mapping: {
				data: "dataset",
				id: "id",
				parent: "parent",
				label: "${name}",
				value: "${path}",
				path: "${path}",
				type: "${type}",
				icon: null,
				contentPath: "content",
			},
			datasource: {
				list: {
					selector: null,
					rest: {
						read: {
							url: null,
						},
					},
				},
				file: {
					selector: null,
					rest: {
						read: {
							url: null,
						},
						write: {
							url: null,
						},
					},
				},
			},
			editor: {
				readOnly: false,
				defaultTag: "textarea",
				byMimeType: {
					"image/png": "monster-image-editor",
					"image/jpeg": "monster-image-editor",
					"image/gif": "monster-image-editor",
					"image/svg+xml": "monster-image-editor",
					"image/webp": "monster-image-editor",
					"image/bmp": "monster-image-editor",
					"image/x-icon": "monster-image-editor",
					"text/plain": "textarea",
					"text/markdown": "textarea",
					"text/css": "textarea",
					"text/html": "textarea",
					"text/javascript": "textarea",
					"application/json": "textarea",
				},
			},
			tabs: {
				dirtyMarker: "*",
			},
			persistence: {
				enabled: true,
				keyPrefix: "monster-file-manager-diff",
				debounceMs: 300,
			},
			lazy: {
				enabled: false,
				parameter: "path",
				root: null,
			},
			icons: {
				default: "monster-file-icon-default",
				code: "monster-file-icon-code",
				image: "monster-file-icon-image",
				sound: "monster-file-icon-sound",
				pdf: "monster-file-icon-pdf",
				css: "monster-file-icon-css",
				csv: "monster-file-icon-csv",
				doc: "monster-file-icon-doc",
				jpg: "monster-file-icon-jpg",
				html: "monster-file-icon-html",
				gif: "monster-file-icon-gif",
				mp4: "monster-file-icon-mp4",
				mov: "monster-file-icon-mov",
				mp3: "monster-file-icon-mp3",
				txt: "monster-file-icon-txt",
				png: "monster-file-icon-png",
				byExtension: {
					js: "code",
					ts: "code",
					json: "code",
					css: "css",
					scss: "css",
					html: "html",
					htm: "html",
					txt: "txt",
					md: "txt",
					csv: "csv",
					pdf: "pdf",
					png: "png",
					jpg: "jpg",
					jpeg: "jpg",
					gif: "gif",
					svg: "image",
					mp3: "mp3",
					wav: "sound",
					ogg: "sound",
					mp4: "mp4",
					m4p: "mp4",
					mov: "mov",
				},
			},
			debug: false,
		});
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [CommonStyleSheet, ThemeStyleSheet, FileManagerStyleSheet];
	}

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

	/**
	 * @param {object} options
	 * @return {FileManager}
	 */
	setOptions(options) {
		super.setOptions(options);
		queueMicrotask(() => {
			syncDatasources.call(this);
			refreshLabels.call(this);
		});
		return this;
	}

	/**
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		this[tabsByKeySymbol] = new Map();
		this[tabsByReferenceSymbol] = new Map();
		this[entryByValueSymbol] = new Map();
		this[treeEntriesSymbol] = [];
		this[loadedParentsSymbol] = new Set();

		queueMicrotask(() => {
			initControlReferences.call(this);
			initSplitPanel.call(this);
			initTabs.call(this);
			initSaveButton.call(this);
			initTreeMenu.call(this);
			initDatasources.call(this);
		});

		this[internalSymbol].attachObserver(
			new Observer(() => {
				syncDatasources.call(this);
				refreshLabels.call(this);
			}),
		);
	}
}

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

	this[titleSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=title]",
	);
	this[splitPanelSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=split-panel]",
	);
	this[treeMenuSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=tree]",
	);
	this[tabsSymbol] = this.shadowRoot.querySelector("[data-monster-role=tabs]");
	this[tabsContainerSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=tabs-container]",
	);
	this[saveButtonSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=save-button]",
	);
	this[saveAllButtonSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=save-all-button]",
	);
	this[emptyStateSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=empty]",
	);

	return this;
}

/**
 * @private
 * @return {void}
 */
function initSplitPanel() {
	if (!this[splitPanelSymbol]) {
		return;
	}

	const split = this.getOption("layout.split", {});
	const initial = split.initial || "20%";
	this[splitPanelSymbol].setOptions({
		splitType: split.type || "vertical",
		dimension: {
			initial: initial,
			min: split.min || "15%",
			max: split.max || "45%",
		},
	});
	this[splitPanelSymbol].setDimension(initial);
}

/**
 * @private
 * @return {void}
 */
function initSaveButton() {
	if (!this[saveButtonSymbol]) {
		return;
	}

	this[saveButtonSymbol].setOption(
		"labels.button",
		this.getOption("labels.save"),
	);
	this[saveButtonSymbol].setOption("disabled", true);

	this[saveButtonSymbol].setOption("actions.click", () => {
		saveActiveTab.call(this);
	});

	if (this[saveAllButtonSymbol]) {
		this[saveAllButtonSymbol].setOption(
			"labels.button",
			this.getOption("labels.saveAll"),
		);
		this[saveAllButtonSymbol].setOption("disabled", true);
		this[saveAllButtonSymbol].setOption("actions.click", () => {
			saveAllTabs.call(this);
		});
	}
}

/**
 * @private
 * @return {void}
 */
function initTabs() {
	if (!this[tabsSymbol]) {
		return;
	}

	this[tabsSymbol].setOption("debug", this.getOption("debug") === true);
	this[tabsSymbol].setOption("features.openFirst", false);

	this[tabsSymbol].addEventListener("monster-tab-changed", (event) => {
		const reference = event?.detail?.reference;
		if (!reference) {
			return;
		}

		updateEmptyState.call(this);
		updateSaveButtonState.call(this);
		scheduleUpdateEditorSizes.call(this);
	});

	this[tabsSymbol].addEventListener("monster-tab-remove", (event) => {
		const reference = event?.detail?.reference;
		if (!reference) {
			return;
		}

		const tabData = this[tabsByReferenceSymbol].get(reference);
		if (tabData?.unbindChange) {
			tabData.unbindChange();
		}

		this[tabsByReferenceSymbol].delete(reference);
		if (tabData?.key) {
			this[tabsByKeySymbol].delete(tabData.key);
		}

		updateEmptyState.call(this);
		updateSaveButtonState.call(this);
		scheduleUpdateEditorSizes.call(this);
	});

	initHostResizeObserver.call(this);
	scheduleUpdateEditorSizes.call(this);
}

/**
 * @private
 * @return {void}
 */
function initTreeMenu() {
	if (!this[treeMenuSymbol]) {
		return;
	}

	this[treeMenuSymbol].setOptions({
		mapping: getTreeMenuMapping.call(this),
		actions: {
			select: (entry) => {
				const key = entry?.value;
				if (!key) {
					return;
				}

				const fileEntry = this[entryByValueSymbol].get(key);
				if (!fileEntry) {
					return;
				}

				openFile.call(this, fileEntry);
			},
			open: (entry) => {
				handleTreeOpen.call(this, entry);
			},
		},
	});
}

/**
 * @private
 * @return {void}
 */
function initDatasources() {
	initListDatasource.call(this);
	initFileDatasource.call(this);
}

/**
 * @private
 * @return {void}
 */
function initListDatasource() {
	const selector = this.getOption("datasource.list.selector");
	if (isString(selector) && selector.trim() !== "") {
		const element = findElementWithSelectorUpwards(this, selector.trim());
		if (element instanceof HTMLElement) {
			customElements
				.whenDefined(element.tagName.toLowerCase())
				.then(() => {
					logDebug.call(this, "list datasource found via selector", selector);
					this[listDatasourceSymbol] = element;
					attachListDatasourceListeners.call(this);
					requestList.call(this, this.getOption("lazy.root", null));
				})
				.catch((err) => {
					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, err.message);
				});
			return;
		}

		addAttributeToken(
			this,
			ATTRIBUTE_ERRORMESSAGE,
			"list datasource selector not found",
		);
	}

	const restConfig = this.getOption("datasource.list.rest", {});
	if (!isObject(restConfig)) {
		return;
	}

	const rest = getDocument().createElement("monster-datasource-rest");
	rest.setOptions(restConfig);
	rest.setOption("features.autoInit", true);
	rest.setOption("autoInit.oneTime", true);
	this.appendChild(rest);
	this[listDatasourceSymbol] = rest;
	this[listDatasourceOwnedSymbol] = true;
	logDebug.call(this, "list datasource created", restConfig?.read?.url);

	attachListDatasourceListeners.call(this);
	requestList.call(this, this.getOption("lazy.root", null));
}

/**
 * @private
 * @return {void}
 */
function initFileDatasource() {
	const selector = this.getOption("datasource.file.selector");
	if (isString(selector) && selector.trim() !== "") {
		const element = findElementWithSelectorUpwards(this, selector.trim());
		if (element instanceof HTMLElement) {
			customElements
				.whenDefined(element.tagName.toLowerCase())
				.then(() => {
					this[fileDatasourceSymbol] = element;
				})
				.catch((err) => {
					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, err.message);
				});
			return;
		}

		addAttributeToken(
			this,
			ATTRIBUTE_ERRORMESSAGE,
			"file datasource selector not found",
		);
	}

	const restConfig = this.getOption("datasource.file.rest", {});
	if (!isObject(restConfig)) {
		return;
	}

	const rest = getDocument().createElement("monster-datasource-rest");
	rest.setOptions(restConfig);
	this.appendChild(rest);
	this[fileDatasourceSymbol] = rest;
	this[fileDatasourceOwnedSymbol] = true;
}

/**
 * @private
 * @return {void}
 */
function attachListDatasourceListeners() {
	const datasource = this[listDatasourceSymbol];
	if (!(datasource instanceof HTMLElement)) {
		return;
	}

	const hasObserver = !!datasource.datasource?.attachObserver;
	if (!hasObserver) {
		datasource.addEventListener("monster-datasource-fetched", () => {
			logDebug.call(this, "list datasource fetched");
			applyListData.call(this);
		});
	}
	datasource.addEventListener("monster-datasource-error", (event) => {
		logDebug.call(this, "list datasource error", event?.detail?.error?.message);
		clearListRequest.call(this);
	});

	if (datasource.datasource && hasObserver) {
		datasource.datasource.attachObserver(
			new Observer(() => {
				logDebug.call(this, "list datasource fetched");
				applyListData.call(this);
			}),
		);
	}
}

/**
 * @private
 * @return {void}
 */
function applyListData() {
	const datasource = this[listDatasourceSymbol];
	if (!datasource || !datasource.data) {
		logDebug.call(this, "list datasource has no data");
		return;
	}

	let data = datasource.data;
	const dataPath = this.getOption("mapping.data");

	if (isString(dataPath) && dataPath.trim() !== "") {
		const pathfinder = new Pathfinder(data);
		if (pathfinder.exists(dataPath)) {
			data = pathfinder.getVia(dataPath);
		} else {
			logDebug.call(
				this,
				"list datasource path not found, falling back to root data",
				dataPath,
			);
		}
	}

	if (isObject(data)) {
		data = Object.values(data);
	}

	if (!isArray(data)) {
		data = [];
	}

	const lazyEnabled = this.getOption("lazy.enabled", false) === true;
	const listParent = lazyEnabled
		? (this[listRequestParentSymbol] ?? this.getOption("lazy.root", null))
		: null;

	const entries = [];
	const entryByValue = new Map();

	for (const entry of data) {
		const normalized = normalizeEntry.call(this, entry);
		if (lazyEnabled) {
			if (!matchesParent(normalized.parent, listParent)) {
				continue;
			}
		}
		entries.push(normalized);
		entryByValue.set(normalized.value, normalized);
	}

	logDebug.call(
		this,
		"list entries normalized",
		lazyEnabled
			? `${entries.length} (parent ${listParent ?? "root"})`
			: entries.length,
	);

	if (lazyEnabled) {
		mergeLazyEntries.call(this, entries, listParent);
	} else {
		this[entryByValueSymbol] = entryByValue;
		const treeEntries = buildTreeEntries.call(this, entries);
		this[treeEntriesSymbol] = treeEntries;
		if (this[treeMenuSymbol]) {
			this[treeMenuSymbol].setOption("entries", treeEntries);
		}
	}

	clearListRequest.call(this);
	updateEmptyState.call(this);
}

/**
 * @private
 * @param {object} entry
 * @return {object}
 */
function normalizeEntry(entry) {
	const mapping = this.getOption("mapping", {});
	const formatted = {
		id: formatEntryValue(entry, mapping.id),
		parent: formatEntryValue(entry, mapping.parent),
		label: formatEntryValue(entry, mapping.label),
		value: formatEntryValue(entry, mapping.value),
		path: formatEntryValue(entry, mapping.path),
		type: formatEntryValue(entry, mapping.type),
		icon: formatEntryValue(entry, mapping.icon),
		hasChildren:
			entry?.hasChildren === true ||
			entry?.["has-children"] === true ||
			entry?.has_children === true,
		raw: entry,
	};

	if (!formatted.path && isString(formatted.value)) {
		formatted.path = formatted.value;
	}

	if (!formatted.value && isString(formatted.path)) {
		formatted.value = formatted.path;
	}

	if (!formatted.id && isString(formatted.path)) {
		formatted.id = formatted.path;
	}

	if (!formatted.parent && isString(formatted.path)) {
		const parent = formatted.path.split("/").slice(0, -1).join("/");
		formatted.parent = parent === "" ? null : parent;
	}

	if (!formatted.label) {
		formatted.label = formatted.path || formatted.id || "file";
	}

	if (!formatted.type && isString(formatted.path)) {
		formatted.type = getTypeFromPath(formatted.path);
	}

	if (formatted.hasChildren !== true) {
		formatted.hasChildren = formatted.type === "folder";
	}

	if (!formatted.icon) {
		formatted.icon = getIconIdForEntry.call(this, formatted);
	}

	return formatted;
}

/**
 * @private
 * @param {object} entry
 * @return {string}
 */
function getIconIdForEntry(entry) {
	const icons = this.getOption("icons", {});
	const byExtension = icons.byExtension || {};
	const type = isString(entry.type) ? entry.type.toLowerCase() : "";
	const ext = getExtension(entry.path || "");
	let iconKey = "";

	if (ext && byExtension[ext]) {
		iconKey = byExtension[ext];
	} else if (type && icons[type]) {
		iconKey = type;
	}

	if (!iconKey || !icons[iconKey]) {
		return icons.default;
	}

	return icons[iconKey];
}

/**
 * @private
 * @param {string} path
 * @return {string}
 */
function getTypeFromPath(path) {
	const ext = getExtension(path);
	if (!ext) {
		return "file";
	}
	return ext.toLowerCase();
}

/**
 * @private
 * @param {string} path
 * @return {string}
 */
function getExtension(path) {
	if (!isString(path)) {
		return "";
	}

	const last = path.split("/").pop() || "";
	const index = last.lastIndexOf(".");
	if (index === -1) {
		return "";
	}

	return last.substring(index + 1).toLowerCase();
}

/**
 * @private
 * @param {object} entry
 * @param {string} template
 * @return {string}
 */
function formatEntryValue(entry, template) {
	if (!isString(template) || template.trim() === "") {
		return "";
	}

	if (template.includes("${")) {
		try {
			return new Formatter(entry).format(template);
		} catch (e) {
			return "";
		}
	}

	if (entry && Object.prototype.hasOwnProperty.call(entry, template)) {
		return entry[template];
	}

	return "";
}

/**
 * @private
 * @return {object}
 */
function getTreeMenuMapping() {
	return {
		rootReferences: [null, undefined, 0, "0", ""],
		idTemplate: "id",
		parentKey: "parent",
		selector: "*",
		labelTemplate: "${label}",
		valueTemplate: "${value}",
		iconTemplate: "${icon}",
	};
}

/**
 * @private
 * @param {object} entry
 * @return {void}
 */
function handleTreeOpen(entry) {
	const lazyEnabled = this.getOption("lazy.enabled", false) === true;
	if (!lazyEnabled) {
		return;
	}
	const key = entry?.value;
	if (!key) {
		return;
	}
	const normalized = this[entryByValueSymbol].get(key);
	if (!normalized || normalized.type !== "folder") {
		return;
	}
	if (normalized.hasChildren !== true) {
		return;
	}

	if (this[loadedParentsSymbol].has(normalized.path)) {
		return;
	}

	requestList.call(this, normalized.path);
}

/**
 * @private
 * @param {Array} entries
 * @return {Array}
 */
function buildTreeEntries(entries) {
	const nodes = buildTree(entries, "*", "id", "parent", {
		rootReferences: [null, undefined, ""],
	});

	const options = [];
	for (const node of nodes) {
		const iterator = new NodeRecursiveIterator(node);
		for (const n of iterator) {
			const entry = n.value || {};
			const intend = n.level;
			const visibility = intend > 0 ? "hidden" : "visible";
			options.push({
				value: entry.value,
				label: entry.label,
				icon: buildIconMarkup(entry.icon),
				intend,
				state: "close",
				visibility,
				["has-children"]: n.hasChildNodes(),
			});
		}
	}

	return options;
}

/**
 * @private
 * @param {string} iconId
 * @return {string}
 */
function buildIconMarkup(iconId) {
	const id = isString(iconId) ? iconId : "";
	return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><use xlink:href="#${id}"></use></svg>`;
}

/**
 * @private
 * @param {Array} entries
 * @param {string|null} parentValue
 * @return {void}
 */
function mergeLazyEntries(entries, parentValue) {
	let treeEntries = Array.isArray(this[treeEntriesSymbol])
		? this[treeEntriesSymbol].slice()
		: [];
	const loadedParents = this[loadedParentsSymbol] || new Set();

	if (parentValue === null || parentValue === undefined || parentValue === "") {
		treeEntries = [];
		this[entryByValueSymbol] = new Map();
		loadedParents.clear();
	}

	const parentIndex = findTreeEntryIndex(treeEntries, parentValue);
	const parentIntend = parentIndex >= 0 ? treeEntries[parentIndex].intend : -1;
	const parentState =
		parentIndex >= 0 ? treeEntries[parentIndex].state : "open";
	const baseVisibility = parentState === "open" ? "visible" : "hidden";

	const insertIndex =
		parentIndex >= 0
			? getSubtreeEndIndex(treeEntries, parentIndex)
			: treeEntries.length;

	const newEntries = [];
	let hasChildren = entries.length > 0;

	for (const entry of entries) {
		if (!entry?.value) {
			continue;
		}
		if (this[entryByValueSymbol].has(entry.value)) {
			continue;
		}

		this[entryByValueSymbol].set(entry.value, entry);

		newEntries.push({
			value: entry.value,
			label: entry.label,
			icon: buildIconMarkup(entry.icon),
			intend: parentIntend + 1,
			state: "close",
			visibility: parentIndex >= 0 ? baseVisibility : "visible",
			["has-children"]: entry.hasChildren === true,
		});
	}

	const nextEntries = [
		...treeEntries.slice(0, insertIndex),
		...newEntries,
		...treeEntries.slice(insertIndex),
	];

	if (parentIndex >= 0) {
		nextEntries[parentIndex] = Object.assign({}, nextEntries[parentIndex], {
			["has-children"]: hasChildren,
			state: hasChildren ? nextEntries[parentIndex].state : "close",
		});
	}

	this[treeEntriesSymbol] = nextEntries;
	loadedParents.add(parentValue ?? null);
	this[loadedParentsSymbol] = loadedParents;

	if (this[treeMenuSymbol]) {
		this[treeMenuSymbol].setOption("entries", nextEntries);
	}
}

/**
 * @private
 * @param {Array} entries
 * @param {string|null} value
 * @return {number}
 */
function findTreeEntryIndex(entries, value) {
	if (!isString(value) || value.trim() === "") {
		return -1;
	}
	return entries.findIndex((entry) => entry.value === value);
}

/**
 * @private
 * @param {Array} entries
 * @param {number} index
 * @return {number}
 */
function getSubtreeEndIndex(entries, index) {
	if (index < 0 || index >= entries.length) {
		return entries.length;
	}
	const baseIntend = entries[index].intend;
	let i = index + 1;
	while (i < entries.length && entries[i].intend > baseIntend) {
		i += 1;
	}
	return i;
}

/**
 * @private
 * @param {string|null|undefined} entryParent
 * @param {string|null|undefined} expectedParent
 * @return {boolean}
 */
function matchesParent(entryParent, expectedParent) {
	const normalized = entryParent ?? null;
	const expected = expectedParent ?? null;
	if (expected === null) {
		return normalized === null || normalized === "" || normalized === undefined;
	}
	return normalized === expected;
}

/**
 * @private
 * @param {string|null} parentValue
 * @return {void}
 */
function requestList(parentValue) {
	const datasource = this[listDatasourceSymbol];
	if (!datasource?.read) {
		return;
	}

	const isRestDatasource =
		datasource?.tagName?.toLowerCase?.() === "monster-datasource-rest";
	if (isRestDatasource) {
		const readUrl = getListReadUrl.call(this, parentValue);
		if (!isString(readUrl) || readUrl.trim() === "") {
			logDebug.call(this, "list datasource read url missing");
			return;
		}
		datasource.setOption("read.url", readUrl);
	}

	if (this[listRequestInFlightSymbol] === true) {
		this[queuedParentSymbol] = parentValue;
		return;
	}

	const paramName = this.getOption("lazy.parameter", "path");
	const params = {};
	if (isString(paramName) && paramName.trim() !== "") {
		params[paramName] = parentValue ?? "";
	}

	this[listRequestInFlightSymbol] = true;
	this[listRequestParentSymbol] = parentValue ?? null;
	logDebug.call(this, "list request", parentValue ?? "root");
	datasource.setOption("read.parameters", params);
	datasource.read();
}

/**
 * @private
 * @param {string|null} parentValue
 * @return {string|null}
 */
function getListReadUrl(parentValue) {
	const baseUrl = this.getOption("datasource.list.rest.read.url");
	const fallbackUrl = this[listDatasourceSymbol]?.getOption?.("read.url");
	const url =
		isString(baseUrl) && baseUrl.trim() !== "" ? baseUrl : fallbackUrl;

	if (!isString(url) || url.trim() === "") {
		return null;
	}

	const trimmed = url.trim();
	if (trimmed.includes("${")) {
		return trimmed;
	}

	const lazyEnabled = this.getOption("lazy.enabled", false) === true;
	if (!lazyEnabled) {
		return trimmed;
	}

	const paramName = this.getOption("lazy.parameter", "path");
	if (!isString(paramName) || paramName.trim() === "") {
		return trimmed;
	}

	const encoded = encodeURIComponent(parentValue ?? "");
	const escaped = escapeRegExp(paramName);
	const hasParam = new RegExp(`([?&])${escaped}=`).test(trimmed);
	const glue = trimmed.includes("?") ? "&" : "?";

	if (hasParam) {
		return trimmed.replace(
			new RegExp(`([?&])${escaped}=[^&]*`),
			`$1${paramName}=${encoded}`,
		);
	}

	return `${trimmed}${glue}${paramName}=${encoded}`;
}

/**
 * @private
 * @param {string} value
 * @return {string}
 */
function escapeRegExp(value) {
	return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
 * @private
 * @return {void}
 */
function clearListRequest() {
	this[listRequestInFlightSymbol] = false;
	this[listRequestParentSymbol] = null;
	const queued = this[queuedParentSymbol];
	this[queuedParentSymbol] = null;
	if (queued !== null && queued !== undefined) {
		requestList.call(this, queued);
	}
}

/**
 * @private
 * @param {object} entry
 * @return {void}
 */
function openFile(entry) {
	if (entry?.type === "folder") {
		return;
	}
	const key = entry.value || entry.id;
	if (!key) {
		return;
	}

	const existing = this[tabsByKeySymbol].get(key);
	if (existing) {
		this[tabsSymbol]?.activeTab(existing.reference);
		return;
	}

	const editor = createEditorElement.call(this, entry);
	editor.setAttribute("data-monster-role", "editor");

	const tabReference = new ID("file-tab").toString();
	this[tabsSymbol]?.addTab(editor, {
		tabId: tabReference,
		label: entry.label,
		active: true,
		removable: true,
	});

	const tabElement = this[tabsSymbol]?.querySelector(
		`#${CSS.escape(tabReference)}`,
	);
	if (!(tabElement instanceof HTMLElement)) {
		throw new Error("tab element not found");
	}
	tabElement.setAttribute("data-monster-role", "tab-content");

	const adapter = getEditorAdapter.call(this);
	adapter.setReadOnly(editor, this.getOption("editor.readOnly", false));
	const tabData = {
		key,
		reference: tabReference,
		entry: entry,
		editor: editor,
		adapter,
		original: "",
		dirty: false,
		tabElement,
		label: entry.label,
		unbindChange: null,
	};

	tabData.unbindChange = adapter.bindChange(editor, () => {
		handleEditorChange.call(this, tabData);
	});

	this[tabsByKeySymbol].set(key, tabData);
	this[tabsByReferenceSymbol].set(tabReference, tabData);

	this[tabsSymbol]?.activeTab(tabReference);
	updateEmptyState.call(this);
	updateSaveButtonState.call(this);
	scheduleUpdateEditorSizes.call(this);

	loadFileContent.call(this, tabData).catch((err) => {
		addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, err.message);
	});
}

/**
 * @private
 * @return {void}
 */
function initTabsResizeObserver() {
	if (this[tabsResizeObserverSymbol]) {
		return;
	}
	if (!this[tabsSymbol]) {
		return;
	}
	if (typeof ResizeObserver === "undefined") {
		return;
	}

	this[tabsResizeObserverSymbol] = new ResizeObserver(() => {
		scheduleUpdateEditorSizes.call(this);
	});
	this[tabsResizeObserverSymbol].observe(this[tabsSymbol]);
}

/**
 * @private
 * @return {void}
 */
function initHostResizeObserver() {
	if (this[hostResizeObserverSymbol]) {
		return;
	}
	if (typeof ResizeObserver === "undefined") {
		return;
	}

	this[hostResizeObserverSymbol] = new ResizeObserver(() => {
		scheduleUpdateEditorSizes.call(this);
	});
	this[hostResizeObserverSymbol].observe(this);
}

/**
 * @private
 * @return {void}
 */
function scheduleUpdateEditorSizes() {
	const win = getWindow();
	if (!win) {
		return;
	}
	if (this[editorSizeRafSymbol]) {
		win.cancelAnimationFrame(this[editorSizeRafSymbol]);
	}
	this[editorSizeRafSymbol] = win.requestAnimationFrame(() => {
		this[editorSizeRafSymbol] = null;
		updateEditorSizes.call(this);
	});
}

/**
 * @private
 * @return {void}
 */
function updateEditorSizes() {
	if (!this[tabsSymbol]) {
		return;
	}

	const tabsElement = this[tabsSymbol];
	const container = this[tabsContainerSymbol] || tabsElement;
	const containerRect = container.getBoundingClientRect();
	if (containerRect.height <= 0) {
		return;
	}

	const nav = tabsElement.shadowRoot?.querySelector?.(
		"[data-monster-role=nav]",
	);
	const navHeight = nav?.getBoundingClientRect?.().height || 0;
	const available = Math.max(0, containerRect.height - navHeight);

	for (const tabData of this[tabsByReferenceSymbol].values()) {
		const editor = tabData?.editor;
		const tabElement = tabData?.tabElement;
		if (tabElement instanceof HTMLElement) {
			tabElement.style.height = `${available}px`;
			tabElement.style.minHeight = `${available}px`;
		}
		if (!(editor instanceof HTMLElement)) {
			continue;
		}
		if (!isCanvasElement(editor)) {
			editor.style.height = "100%";
			editor.style.minHeight = "0";
		}
		if (isCanvasElement(editor)) {
			editor.width = Math.max(1, Math.floor(containerRect.width));
			editor.height = Math.max(1, Math.floor(available));
			renderCanvasFromValue(editor, editor.dataset.monsterValue || "");
		}
	}
}

/**
 * @private
 * @param {object} tabData
 * @return {Promise<void>}
 */
async function loadFileContent(tabData) {
	const datasource = this[fileDatasourceSymbol];
	if (!datasource?.read) {
		const content = tabData.entry?.raw?.content || "";
		const patched = applyPersistedDiff.call(this, content, tabData.entry);
		tabData.adapter.setValue(tabData.editor, patched.value);
		tabData.original = content;
		updateTabDirty.call(this, tabData, patched.dirty);
		return;
	}

	const params = buildParameters(tabData.entry);
	const readUrl = datasource.getOption?.("read.url");

	if (!isString(readUrl) || readUrl.trim() === "") {
		const content = tabData.entry?.raw?.content || "";
		const patched = applyPersistedDiff.call(this, content, tabData.entry);
		tabData.adapter.setValue(tabData.editor, patched.value);
		tabData.original = content;
		updateTabDirty.call(this, tabData, patched.dirty);
		return;
	}

	datasource.setOption("read.parameters", params);

	await datasource.read();

	const data = datasource.data;
	const content = extractContent.call(this, data);
	const patched = applyPersistedDiff.call(this, content, tabData.entry);
	tabData.adapter.setValue(tabData.editor, patched.value);
	tabData.original = content;
	updateTabDirty.call(this, tabData, patched.dirty);
}

/**
 * @private
 * @param {object} data
 * @return {string}
 */
function extractContent(data) {
	if (isString(data)) {
		return data;
	}

	const path = this.getOption("mapping.contentPath");
	if (isString(path) && path.trim() !== "") {
		try {
			const value = new Pathfinder(data).getVia(path);
			if (isString(value)) {
				return value;
			}
		} catch (e) {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
		}
	}

	if (isString(data?.content)) {
		return data.content;
	}

	if (isString(data?.file?.content)) {
		return data.file.content;
	}

	return "";
}

/**
 * @private
 * @param {object} tabData
 * @return {void}
 */
function handleEditorChange(tabData) {
	const value = tabData.adapter.getValue(tabData.editor);
	const isDirty = value !== tabData.original;
	updateTabDirty.call(this, tabData, isDirty);
	schedulePersistDiff.call(this, tabData);
	updateSaveButtonState.call(this);
}

/**
 * @private
 * @param {object} tabData
 * @param {boolean} dirty
 * @return {void}
 */
function updateTabDirty(tabData, dirty) {
	const marker = this.getOption("tabs.dirtyMarker", "*");
	const label = dirty ? `${tabData.label}${marker}` : tabData.label;
	const current = tabData.tabElement?.getAttribute?.(ATTRIBUTE_BUTTON_LABEL);
	if (tabData.dirty === dirty && current === label) {
		return;
	}
	tabData.dirty = dirty;
	if (current !== label) {
		tabData.tabElement.setAttribute(ATTRIBUTE_BUTTON_LABEL, label);
	}
}

/**
 * @private
 * @return {void}
 */
function updateEmptyState() {
	if (!this[emptyStateSymbol]) {
		return;
	}

	const hasTabs = this[tabsByReferenceSymbol].size > 0;
	if (hasTabs) {
		this[emptyStateSymbol].classList.add("hidden");
	} else {
		this[emptyStateSymbol].classList.remove("hidden");
	}
}

/**
 * @private
 * @return {void}
 */
function updateSaveButtonState() {
	if (!this[saveButtonSymbol]) {
		return;
	}

	const active = getActiveTabData.call(this);
	const enabled = active?.dirty === true;
	this[saveButtonSymbol].setOption("disabled", !enabled);

	if (this[saveAllButtonSymbol]) {
		const anyDirty = Array.from(this[tabsByReferenceSymbol].values()).some(
			(tab) => tab.dirty === true,
		);
		this[saveAllButtonSymbol].setOption("disabled", !anyDirty);
	}
}

/**
 * @private
 * @return {object|null}
 */
function getActiveTabData() {
	if (!this[tabsSymbol]) {
		return null;
	}

	const reference = this[tabsSymbol].getActiveTab?.();
	if (!reference) {
		return null;
	}

	return this[tabsByReferenceSymbol].get(reference) || null;
}

/**
 * @private
 * @return {void}
 */
function saveActiveTab() {
	const tabData = getActiveTabData.call(this);
	if (!tabData || tabData.dirty !== true) {
		return;
	}

	if (this[saveButtonSymbol]) {
		this[saveButtonSymbol].setState("activity");
	}

	saveTab
		.call(this, tabData)
		.then(() => {
			if (this[saveButtonSymbol]) {
				this[saveButtonSymbol].setState("successful", 1200);
			}
		})
		.catch((err) => {
			if (this[saveButtonSymbol]) {
				this[saveButtonSymbol].setState("failed", 1600);
			}
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, err.message);
		})
		.finally(() => {
			updateSaveButtonState.call(this);
		});
}

/**
 * @private
 * @param {object} tabData
 * @return {Promise<void>}
 */
async function saveTab(tabData) {
	const datasource = this[fileDatasourceSymbol];
	if (!datasource?.write) {
		throw new Error("file datasource not available");
	}

	const payload = buildWritePayload.call(this, tabData);
	const params = buildParameters(tabData.entry);
	datasource.data = payload;
	datasource.setOption("write.parameters", params);

	await datasource.write();

	tabData.original = tabData.adapter.getValue(tabData.editor);
	updateTabDirty.call(this, tabData, false);
	clearPersistedDiff.call(this, tabData.entry);
}

/**
 * @private
 * @return {void}
 */
function saveAllTabs() {
	if (!this[saveAllButtonSymbol]) {
		return;
	}

	const dirtyTabs = Array.from(this[tabsByReferenceSymbol].values()).filter(
		(tab) => tab.dirty === true,
	);

	if (dirtyTabs.length === 0) {
		return;
	}

	this[saveAllButtonSymbol].setState("activity");

	Promise.all(dirtyTabs.map((tab) => saveTab.call(this, tab)))
		.then(() => {
			this[saveAllButtonSymbol].setState("successful", 1200);
		})
		.catch((err) => {
			this[saveAllButtonSymbol].setState("failed", 1600);
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, err.message);
		})
		.finally(() => {
			updateSaveButtonState.call(this);
		});
}

/**
 * @private
 * @param {object} tabData
 * @return {object}
 */
function buildWritePayload(tabData) {
	const custom = this.getOption("actions.buildPayload");
	if (isFunction(custom)) {
		return custom(tabData);
	}

	const entry = tabData.entry || {};
	const content = tabData.adapter.getValue(tabData.editor);
	return {
		id: entry.id,
		path: entry.path,
		name: entry.label,
		type: entry.type,
		content: content,
	};
}

/**
 * @private
 * @param {object} entry
 * @return {object}
 */
function buildParameters(entry) {
	const payload = clone(entry);
	payload.id = entry.id;
	payload.path = entry.path;
	payload.name = entry.label;
	payload.type = entry.type;
	return payload;
}

/**
 * @private
 * @return {object}
 */
function getEditorAdapter() {
	const custom = this.getOption("editor.adapter");
	const base = {
		getValue: (element) => {
			if (isImageEditorElement(element)) {
				if (typeof element.getImageDataUrl === "function") {
					return element.getImageDataUrl(
						element.dataset.monsterOutputType || undefined,
					);
				}
				return element.dataset.monsterValue || "";
			}
			if (isCanvasElement(element)) {
				return element.dataset.monsterValue || "";
			}
			if (element && "value" in element) {
				return element.value ?? "";
			}
			return element?.textContent ?? "";
		},
		setValue: (element, value) => {
			const next = value ?? "";
			if (isImageEditorElement(element)) {
				element.dataset.monsterValue = next;
				if (typeof element.setImage === "function") {
					element.setImage(next, {
						contentType: element.dataset.monsterContentType,
						storeOriginal: true,
					});
				}
				return;
			}
			if (isCanvasElement(element)) {
				renderCanvasFromValue(element, next);
				return;
			}
			if (element && "value" in element) {
				element.value = next;
				return;
			}
			if (element) {
				element.textContent = next;
			}
		},
		bindChange: (element, callback) => {
			if (!element || typeof callback !== "function") {
				return () => {};
			}
			if (isCanvasElement(element)) {
				return () => {};
			}
			if (isImageEditorElement(element)) {
				const handler = () => callback();
				element.addEventListener("monster-image-editor-changed", handler);
				return () => {
					element.removeEventListener("monster-image-editor-changed", handler);
				};
			}

			if (isCanvasElement(element)) {
				return () => {};
			}

			const handler = () => callback();
			element.addEventListener("input", handler);
			element.addEventListener("change", handler);
			return () => {
				element.removeEventListener("input", handler);
				element.removeEventListener("change", handler);
			};
		},
		setReadOnly: (element, readOnly) => {
			if (!element) {
				return;
			}
			if (isImageEditorElement(element)) {
				if (readOnly === true) {
					element.setAttribute("data-monster-readonly", "");
				} else {
					element.removeAttribute("data-monster-readonly");
				}
				return;
			}
			if (isCanvasElement(element)) {
				return;
			}
			if ("readOnly" in element) {
				element.readOnly = readOnly === true;
				return;
			}
			if (readOnly === true) {
				element.setAttribute("data-monster-readonly", "");
			} else {
				element.removeAttribute("data-monster-readonly");
			}
		},
	};

	if (!isObject(custom)) {
		return base;
	}

	const adapter = Object.assign({}, base);
	if (isFunction(custom.getValue)) {
		adapter.getValue = custom.getValue;
	}
	if (isFunction(custom.setValue)) {
		adapter.setValue = custom.setValue;
	}
	if (isFunction(custom.bindChange)) {
		adapter.bindChange = custom.bindChange;
	}
	if (isFunction(custom.setReadOnly)) {
		adapter.setReadOnly = custom.setReadOnly;
	}

	return adapter;
}

/**
 * @private
 * @param {object} entry
 * @return {HTMLElement}
 */
function createEditorElement(entry) {
	const tag = resolveEditorTag.call(this, entry);
	const editor = getDocument().createElement(tag);
	const mimeType = getMimeTypeForEntry(entry);
	if (isImageEditorElement(editor) && isString(mimeType)) {
		editor.dataset.monsterContentType = mimeType;
		editor.dataset.monsterOutputType = mimeType;
		if (typeof editor.setOption === "function") {
			editor.setOption("source.contentType", mimeType);
			editor.setOption("output.type", mimeType);
		}
	}
	return editor;
}

/**
 * @private
 * @param {object} entry
 * @return {boolean}
 */
function resolveEditorTag(entry) {
	const mimeType = getMimeTypeForEntry(entry);
	const map = this.getOption("editor.byMimeType", {});
	const tag = map?.[mimeType];
	if (isString(tag) && tag.trim() !== "") {
		return tag.trim();
	}
	const fallback = this.getOption("editor.defaultTag", "textarea");
	if (isString(fallback) && fallback.trim() !== "") {
		return fallback.trim();
	}
	return "textarea";
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {boolean}
 */
function isCanvasElement(element) {
	return element instanceof HTMLCanvasElement;
}

/**
 * @private
 * @param {HTMLElement} element
 * @return {boolean}
 */
function isImageEditorElement(element) {
	return (
		element instanceof HTMLElement &&
		element.tagName.toLowerCase() === "monster-image-editor"
	);
}

/**
 * @private
 * @param {HTMLCanvasElement} canvas
 * @param {string} value
 * @return {void}
 */
function renderCanvasFromValue(canvas, value) {
	if (!isCanvasElement(canvas)) {
		return;
	}

	const next = value ?? "";
	canvas.dataset.monsterValue = next;

	const ctx = canvas.getContext("2d");
	if (!ctx) {
		return;
	}

	const source = normalizeImageSource(next);
	if (!source) {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		return;
	}

	const win = getWindow();
	if (!win) {
		return;
	}

	const img = new win.Image();
	img.onload = () => {
		const targetWidth = canvas.width || img.naturalWidth;
		const targetHeight = canvas.height || img.naturalHeight;

		if (canvas.width !== targetWidth) {
			canvas.width = targetWidth;
		}
		if (canvas.height !== targetHeight) {
			canvas.height = targetHeight;
		}

		const scale = Math.min(
			canvas.width / img.naturalWidth,
			canvas.height / img.naturalHeight,
		);
		const drawWidth = img.naturalWidth * scale;
		const drawHeight = img.naturalHeight * scale;
		const offsetX = (canvas.width - drawWidth) / 2;
		const offsetY = (canvas.height - drawHeight) / 2;

		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
	};
	img.onerror = () => {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
	};
	img.src = source;
}

/**
 * @private
 * @param {string} value
 * @return {string}
 */
function normalizeImageSource(value) {
	if (!isString(value)) {
		return "";
	}
	const trimmed = value.trim();
	if (trimmed === "") {
		return "";
	}
	if (trimmed.startsWith("data:image/")) {
		return trimmed;
	}
	if (trimmed.startsWith("<svg")) {
		return `data:image/svg+xml;utf8,${encodeURIComponent(trimmed)}`;
	}
	return trimmed;
}

/**
 * @private
 * @param {object} entry
 * @return {string}
 */
function getMimeTypeForEntry(entry) {
	if (isString(entry?.mimeType) && entry.mimeType.trim() !== "") {
		return entry.mimeType.trim().toLowerCase();
	}
	const ext = (entry?.type || "").toString().toLowerCase();
	switch (ext) {
		case "png":
			return "image/png";
		case "jpg":
		case "jpeg":
			return "image/jpeg";
		case "gif":
			return "image/gif";
		case "svg":
			return "image/svg+xml";
		case "webp":
			return "image/webp";
		case "bmp":
			return "image/bmp";
		case "ico":
			return "image/x-icon";
		case "md":
			return "text/markdown";
		case "css":
			return "text/css";
		case "html":
		case "htm":
			return "text/html";
		case "js":
		case "mjs":
			return "text/javascript";
		case "json":
			return "application/json";
		case "txt":
			return "text/plain";
		default:
			return "text/plain";
	}
}

/**
 * @private
 * @param {object} tabData
 * @return {void}
 */
function schedulePersistDiff(tabData) {
	const debounceMs = this.getOption("persistence.debounceMs", 300);
	if (!(tabData.persistDebounce instanceof DeadMansSwitch)) {
		tabData.persistDebounce = new DeadMansSwitch(debounceMs, () => {
			persistDiff.call(this, tabData);
		});
		return;
	}

	try {
		tabData.persistDebounce.touch();
	} catch (_e) {
		tabData.persistDebounce = new DeadMansSwitch(debounceMs, () => {
			persistDiff.call(this, tabData);
		});
	}
}

/**
 * @private
 * @return {void}
 */
function syncDatasources() {
	if (this[listDatasourceOwnedSymbol] === true && this[listDatasourceSymbol]) {
		const restConfig = this.getOption("datasource.list.rest", {});
		if (isObject(restConfig)) {
			logDebug.call(this, "sync list datasource", restConfig?.read?.url);
			this[listDatasourceSymbol].setOptions(restConfig);
			this[listDatasourceSymbol].setOption("features.autoInit", true);
			this[listDatasourceSymbol].setOption("autoInit.oneTime", true);
			requestList.call(this, this.getOption("lazy.root", null));
		}
	} else if (!this[listDatasourceSymbol]) {
		initListDatasource.call(this);
	}

	if (this[fileDatasourceOwnedSymbol] === true && this[fileDatasourceSymbol]) {
		const restConfig = this.getOption("datasource.file.rest", {});
		if (isObject(restConfig)) {
			this[fileDatasourceSymbol].setOptions(restConfig);
		}
	} else if (!this[fileDatasourceSymbol]) {
		initFileDatasource.call(this);
	}
}

/**
 * @private
 * @param {string} message
 * @param {*=} data
 * @return {void}
 */
function logDebug(message, data) {
	if (this.getOption("debug") !== true) {
		return;
	}
	if (data !== undefined) {
		console.log(`[monster-file-manager] ${message}`, data);
	} else {
		console.log(`[monster-file-manager] ${message}`);
	}
}

/**
 * @private
 * @return {void}
 */
function refreshLabels() {
	if (this[titleSymbol]) {
		this[titleSymbol].textContent = this.getOption("labels.title");
	}
	if (this[saveButtonSymbol]) {
		this[saveButtonSymbol].setOption(
			"labels.button",
			this.getOption("labels.save"),
		);
	}
	if (this[saveAllButtonSymbol]) {
		this[saveAllButtonSymbol].setOption(
			"labels.button",
			this.getOption("labels.saveAll"),
		);
	}
	if (this[emptyStateSymbol]) {
		const emptyLabel = this.getOption("labels.empty");
		if (isFunction(this[emptyStateSymbol].setOption)) {
			this[emptyStateSymbol].setOption("content.visual", getEmptyStateIcon());
			this[emptyStateSymbol].setOption("content.content", emptyLabel);
		} else {
			this[emptyStateSymbol].textContent = emptyLabel;
		}
	}
}

/**
 * @private
 * @return {string}
 */
function getEmptyStateIcon() {
	return `<svg width="4rem" height="4rem" viewBox="0 -12 512.00032 512" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><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><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><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"></path></svg>`;
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				title: "Dateien",
				save: "Speichern",
				saveAll: "Alle speichern",
				empty: "Datei aus dem Baum auswaehlen.",
			};
		case "fr":
			return {
				title: "Fichiers",
				save: "Enregistrer",
				saveAll: "Tout enregistrer",
				empty: "Selectionnez un fichier dans l'arborescence.",
			};
		case "sp":
			return {
				title: "Archivos",
				save: "Guardar",
				saveAll: "Guardar todo",
				empty: "Seleccione un archivo del arbol.",
			};
		case "it":
			return {
				title: "File",
				save: "Salva",
				saveAll: "Salva tutto",
				empty: "Seleziona un file dall'albero.",
			};
		case "pl":
			return {
				title: "Pliki",
				save: "Zapisz",
				saveAll: "Zapisz wszystko",
				empty: "Wybierz plik z drzewa.",
			};
		case "no":
		case "dk":
			return {
				title: "Filer",
				save: "Lagre",
				saveAll: "Lagre alle",
				empty: "Velg en fil fra treet.",
			};
		case "sw":
			return {
				title: "Filer",
				save: "Spara",
				saveAll: "Spara alla",
				empty: "Valj en fil fran tradet.",
			};
		default:
		case "en":
			return {
				title: "Files",
				save: "Save",
				saveAll: "Save All",
				empty: "Select a file from the tree to open it.",
			};
	}
}

/**
 * @private
 * @param {object} entry
 * @return {string|null}
 */
function getStorageKey(entry) {
	const prefix = this.getOption("persistence.keyPrefix", "");
	const value = entry?.path || entry?.id;
	if (!isString(prefix) || prefix.trim() === "") {
		return null;
	}
	if (!isString(value) || value.trim() === "") {
		return null;
	}
	return `${prefix}:${value}`;
}

/**
 * @private
 * @return {Storage|null}
 */
function getStorage() {
	try {
		return getWindow()?.localStorage ?? null;
	} catch (_e) {
		return null;
	}
}

/**
 * @private
 * @param {string} text
 * @return {number}
 */
function hashText(text) {
	let hash = 0;
	for (let i = 0; i < text.length; i++) {
		hash = (hash + text.charCodeAt(i) * (i + 1)) % 2147483647;
	}
	return hash;
}

/**
 * @private
 * @param {string} original
 * @param {object} entry
 * @return {{value: string, dirty: boolean}}
 */
function applyPersistedDiff(original, entry) {
	const enabled = this.getOption("persistence.enabled", true);
	if (enabled !== true) {
		return { value: original, dirty: false };
	}

	const storage = getStorage.call(this);
	if (!storage) {
		return { value: original, dirty: false };
	}

	const key = getStorageKey.call(this, entry);
	if (!key) {
		return { value: original, dirty: false };
	}

	const raw = storage.getItem(key);
	if (!raw) {
		return { value: original, dirty: false };
	}

	let payload;
	try {
		payload = JSON.parse(raw);
	} catch (_e) {
		storage.removeItem(key);
		return { value: original, dirty: false };
	}

	if (!payload || payload.baseHash !== hashText(original)) {
		storage.removeItem(key);
		return { value: original, dirty: false };
	}

	const updated = applyDiffToLines(
		original.split("\n"),
		payload.diff || [],
	).join("\n");
	return { value: updated, dirty: updated !== original };
}

/**
 * @private
 * @param {string[]} lines
 * @param {Array} diffOps
 * @return {string[]}
 */
function applyDiffToLines(lines, diffOps) {
	const out = lines.slice();
	const updates = [];
	const deletes = [];
	const adds = [];

	for (const change of diffOps) {
		const index = change?.path?.[0];
		if (!Number.isInteger(index)) {
			continue;
		}

		switch (change?.operator) {
			case "update":
				updates.push({
					index,
					value: change?.second?.value ?? "",
				});
				break;
			case "delete":
				deletes.push(index);
				break;
			case "add":
				adds.push({
					index,
					value: change?.second?.value ?? "",
				});
				break;
			default:
				break;
		}
	}

	updates
		.sort((a, b) => a.index - b.index)
		.forEach(({ index, value }) => {
			out[index] = value;
		});

	deletes
		.sort((a, b) => b - a)
		.forEach((index) => {
			if (index >= 0 && index < out.length) {
				out.splice(index, 1);
			}
		});

	adds
		.sort((a, b) => a.index - b.index)
		.forEach(({ index, value }) => {
			const target = Math.max(0, Math.min(index, out.length));
			out.splice(target, 0, value);
		});

	return out;
}

/**
 * @private
 * @param {object} tabData
 * @return {void}
 */
function persistDiff(tabData) {
	const enabled = this.getOption("persistence.enabled", true);
	if (enabled !== true) {
		return;
	}

	const storage = getStorage.call(this);
	if (!storage) {
		return;
	}

	const key = getStorageKey.call(this, tabData.entry);
	if (!key) {
		return;
	}

	const current = tabData.adapter.getValue(tabData.editor);
	const original = tabData.original;
	if (current === original) {
		storage.removeItem(key);
		return;
	}

	const diffResult = diff(original.split("\n"), current.split("\n"));
	const payload = {
		v: 1,
		baseHash: hashText(original),
		diff: diffResult,
	};

	try {
		storage.setItem(key, JSON.stringify(payload));
	} catch (_e) {
		// ignore storage errors (quota, private mode)
	}
}

/**
 * @private
 * @param {object} entry
 * @return {void}
 */
function clearPersistedDiff(entry) {
	const storage = getStorage.call(this);
	if (!storage) {
		return;
	}

	const key = getStorageKey.call(this, entry);
	if (key) {
		storage.removeItem(key);
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
		<div data-monster-role="control" part="control">
			<div data-monster-role="toolbar" part="toolbar">
				<div data-monster-role="title" part="title" data-monster-replace="path:labels.title"></div>
					<div data-monster-role="actions" part="actions" class="monster-button-bar">
					<monster-message-state-button
						data-monster-role="save-button"
						data-monster-replace="path:labels.save"></monster-message-state-button>
					<monster-message-state-button
						data-monster-role="save-all-button"
						data-monster-replace="path:labels.saveAll"></monster-message-state-button>
					</div>
				</div>
			<monster-split-panel data-monster-role="split-panel">
				<div slot="start" data-monster-role="nav-panel" part="nav-panel">
					<monster-panel data-monster-role="tree-panel" part="tree-panel">
						<monster-tree-menu data-monster-role="tree">
							${getIconSprite()}
						</monster-tree-menu>
					</monster-panel>
				</div>
				<div slot="end" data-monster-role="editor-panel" part="editor-panel">
					<div data-monster-role="tabs-container" part="tabs-container">
						<monster-tabs data-monster-role="tabs"></monster-tabs>
					</div>
					<monster-state data-monster-role="empty" part="empty"></monster-state>
				</div>
			</monster-split-panel>
		</div>
	`;
}

/**
 * @private
 * @return {string}
 */
function getIconSprite() {
	// language=HTML
	return `
		<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
			<symbol id="monster-file-icon-default" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
			</symbol>
			<symbol id="monster-file-icon-code" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
				<path d="M8.646 6.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 9 8.646 7.354a.5.5 0 0 1 0-.708m-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 9l1.647-1.646a.5.5 0 0 0 0-.708"/>
			</symbol>
			<symbol id="monster-file-icon-image" viewBox="0 0 16 16">
				<path d="M6.502 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>
				<path d="M14 14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zM4 1a1 1 0 0 0-1 1v10l2.224-2.224a.5.5 0 0 1 .61-.075L8 11l2.157-3.02a.5.5 0 0 1 .76-.063L13 10V4.5h-2A1.5 1.5 0 0 1 9.5 3V1z"/>
			</symbol>
			<symbol id="monster-file-icon-sound" viewBox="0 0 16 16">
				<path d="M11 6.64a1 1 0 0 0-1.243-.97l-1 .25A1 1 0 0 0 8 6.89v4.306A2.6 2.6 0 0 0 7 11c-.5 0-.974.134-1.338.377-.36.24-.662.628-.662 1.123s.301.883.662 1.123c.364.243.839.377 1.338.377s.974-.134 1.338-.377c.36-.24.662-.628.662-1.123V8.89l2-.5z"/>
				<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
			</symbol>
			<symbol id="monster-file-icon-pdf" viewBox="0 0 16 16">
				<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
				<path d="M4.603 14.087a.8.8 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.7 7.7 0 0 1 1.482-.645 20 20 0 0 0 1.062-2.227 7.3 7.3 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.188-.012.396-.047.614-.084.51-.27 1.134-.52 1.794a11 11 0 0 0 .98 1.686 5.8 5.8 0 0 1 1.334.05c.364.066.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.86.86 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.7 5.7 0 0 1-.911-.95 11.7 11.7 0 0 0-1.997.406 11.3 11.3 0 0 1-1.02 1.51c-.292.35-.609.656-.927.787a.8.8 0 0 1-.58.029m1.379-1.901q-.25.115-.459.238c-.328.194-.541.383-.647.547-.094.145-.096.25-.04.361q.016.032.026.044l.035-.012c.137-.056.355-.235.635-.572a8 8 0 0 0 .45-.606m1.64-1.33a13 13 0 0 1 1.01-.193 12 12 0 0 1-.51-.858 21 21 0 0 1-.5 1.05zm2.446.45q.226.245.435.41c.24.19.407.253.498.256a.1.1 0 0 0 .07-.015.3.3 0 0 0 .094-.125.44.44 0 0 0 .059-.2.1.1 0 0 0-.026-.063c-.052-.062-.2-.152-.518-.209a4 4 0 0 0-.612-.053zM8.078 7.8a7 7 0 0 0 .2-.828q.046-.282.038-.465a.6.6 0 0 0-.032-.198.5.5 0 0 0-.145.04c-.087.035-.158.106-.196.283-.04.192-.03.469.046.822q.036.167.09.346z"/>
			</symbol>
			<symbol id="monster-file-icon-css" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M3.397 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.44.187-.284.187-.656 0-.336-.134-.56a1 1 0 0 0-.375-.357 2 2 0 0 0-.566-.21l-.621-.144a1 1 0 0 1-.404-.176.37.37 0 0 1-.144-.299q0-.234.185-.384.188-.152.512-.152.214 0 .37.068a.6.6 0 0 1 .246.181.56.56 0 0 1 .12.258h.75a1.1 1.1 0 0 0-.2-.566 1.2 1.2 0 0 0-.5-.41 1.8 1.8 0 0 0-.78-.152q-.439 0-.776.15-.337.149-.527.421-.19.273-.19.639 0 .302.122.524.124.223.352.367.228.143.539.213l.618.144q.31.073.463.193a.39.39 0 0 1 .152.326.5.5 0 0 1-.085.29.56.56 0 0 1-.255.193q-.167.07-.413.07-.175 0-.32-.04a.8.8 0 0 1-.248-.115.58.58 0 0 1-.255-.384zM.806 13.693q0-.373.102-.633a.87.87 0 0 1 .302-.399.8.8 0 0 1 .475-.137q.225 0 .398.097a.7.7 0 0 1 .272.26.85.85 0 0 1 .12.381h.765v-.072a1.33 1.33 0 0 0-.466-.964 1.4 1.4 0 0 0-.489-.272 1.8 1.8 0 0 0-.606-.097q-.534 0-.911.223-.375.222-.572.632-.195.41-.196.979v.498q0 .568.193.976.197.407.572.626.375.217.914.217.439 0 .785-.164t.55-.454a1.27 1.27 0 0 0 .226-.674v-.076h-.764a.8.8 0 0 1-.118.363.7.7 0 0 1-.272.25.9.9 0 0 1-.401.087.85.85 0 0 1-.478-.132.83.83 0 0 1-.299-.392 1.7 1.7 0 0 1-.102-.627zM6.78 15.29a1.2 1.2 0 0 1-.111-.449h.764a.58.58 0 0 0 .255.384q.106.073.25.114.142.041.319.041.245 0 .413-.07a.56.56 0 0 0 .255-.193.5.5 0 0 0 .085-.29.39.39 0 0 0-.153-.326q-.152-.12-.463-.193l-.618-.143a1.7 1.7 0 0 1-.539-.214 1 1 0 0 1-.351-.367 1.1 1.1 0 0 1-.123-.524q0-.366.19-.639.19-.272.527-.422t.777-.149q.456 0 .779.152.326.153.5.41.18.255.2.566h-.75a.56.56 0 0 0-.12-.258.6.6 0 0 0-.246-.181.9.9 0 0 0-.37-.068q-.324 0-.512.152a.47.47 0 0 0-.184.384q0 .18.143.3a1 1 0 0 0 .404.175l.621.143q.326.075.566.211t.375.358.135.56q0 .37-.188.656a1.2 1.2 0 0 1-.539.439q-.351.158-.858.158-.381 0-.665-.09a1.4 1.4 0 0 1-.478-.252 1.1 1.1 0 0 1-.29-.375"/>
			</symbol>
			<symbol id="monster-file-icon-csv" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M3.517 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.44.187-.284.187-.656 0-.336-.134-.56a1 1 0 0 0-.375-.357 2 2 0 0 0-.566-.21l-.621-.144a1 1 0 0 1-.404-.176.37.37 0 0 1-.144-.299q0-.234.185-.384.188-.152.512-.152.214 0 .37.068a.6.6 0 0 1 .246.181.56.56 0 0 1 .12.258h.75a1.1 1.1 0 0 0-.2-.566 1.2 1.2 0 0 0-.5-.41 1.8 1.8 0 0 0-.78-.152q-.439 0-.776.15-.337.149-.527.421-.19.273-.19.639 0 .302.122.524.124.223.352.367.228.143.539.213l.618.144q.31.073.463.193a.39.39 0 0 1 .152.326.5.5 0 0 1-.085.29.56.56 0 0 1-.255.193q-.167.07-.413.07-.175 0-.32-.04a.8.8 0 0 1-.248-.115.58.58 0 0 1-.255-.384zM.806 13.693q0-.373.102-.633a.87.87 0 0 1 .302-.399.8.8 0 0 1 .475-.137q.225 0 .398.097a.7.7 0 0 1 .272.26.85.85 0 0 1 .12.381h.765v-.072a1.33 1.33 0 0 0-.466-.964 1.4 1.4 0 0 0-.489-.272 1.8 1.8 0 0 0-.606-.097q-.534 0-.911.223-.375.222-.572.632-.195.41-.196.979v.498q0 .568.193.976.197.407.572.626.375.217.914.217.439 0 .785-.164t.55-.454a1.27 1.27 0 0 0 .226-.674v-.076h-.764a.8.8 0 0 1-.118.363.7.7 0 0 1-.272.25.9.9 0 0 1-.401.087.85.85 0 0 1-.478-.132.83.83 0 0 1-.299-.392 1.7 1.7 0 0 1-.102-.627zm8.239 2.238h-.953l-1.338-3.999h.917l.896 3.138h.038l.888-3.138h.879z"/>
			</symbol>
			<symbol id="monster-file-icon-doc" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M6.161 13.666v.522q0 .384-.117.641a.86.86 0 0 1-.322.387.9.9 0 0 1-.469.126.9.9 0 0 1-.471-.126.87.87 0 0 1-.32-.386 1.55 1.55 0 0 1-.117-.642v-.522q0-.386.117-.641a.87.87 0 0 1 .32-.387.87.87 0 0 1 .471-.129q.264 0 .469.13a.86.86 0 0 1 .322.386q.117.255.117.641m.803.519v-.513q0-.565-.205-.972a1.46 1.46 0 0 0-.589-.63q-.381-.22-.917-.22-.533 0-.92.22a1.44 1.44 0 0 0-.589.627q-.204.406-.205.975v.513q0 .563.205.973.205.406.59.627.386.216.92.216.535 0 .916-.216.383-.22.59-.627.204-.41.204-.973M0 11.926v4h1.459q.603 0 .999-.238a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.59-.68q-.395-.234-1.004-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082H.79V12.57Zm7.422.483a1.7 1.7 0 0 0-.103.633v.495q0 .369.103.627a.83.83 0 0 0 .298.393.85.85 0 0 0 .478.131.9.9 0 0 0 .401-.088.7.7 0 0 0 .273-.248.8.8 0 0 0 .117-.364h.765v.076a1.27 1.27 0 0 1-.226.674q-.205.29-.55.454a1.8 1.8 0 0 1-.786.164q-.54 0-.914-.216a1.4 1.4 0 0 1-.571-.627q-.194-.408-.194-.976v-.498q0-.568.197-.978.195-.411.571-.633.378-.223.911-.223.328 0 .607.097.28.093.489.272a1.33 1.33 0 0 1 .466.964v.073H9.78a.85.85 0 0 0-.12-.38.7.7 0 0 0-.273-.261.8.8 0 0 0-.398-.097.8.8 0 0 0-.475.138.87.87 0 0 0-.301.398"/>
			</symbol>
			<symbol id="monster-file-icon-jpg" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M9.66 12.632q.114.23.14.492h-.776a.8.8 0 0 0-.097-.249.7.7 0 0 0-.17-.19.7.7 0 0 0-.237-.126 1 1 0 0 0-.299-.044q-.428 0-.665.302-.234.301-.234.85v.498q0 .351.097.615a.9.9 0 0 0 .304.413.87.87 0 0 0 .519.146 1 1 0 0 0 .457-.096.67.67 0 0 0 .272-.264q.09-.164.091-.363v-.255H8.24v-.59h1.576v.798q0 .29-.097.55a1.3 1.3 0 0 1-.293.458 1.4 1.4 0 0 1-.495.313q-.296.111-.697.111a2 2 0 0 1-.753-.132 1.45 1.45 0 0 1-.533-.377 1.6 1.6 0 0 1-.32-.58 2.5 2.5 0 0 1-.105-.745v-.506q0-.543.2-.95.201-.406.582-.633.384-.228.926-.228.357 0 .636.1.28.1.48.275t.314.407ZM0 14.786q0 .246.082.465.083.22.243.39.165.17.407.267.246.093.569.093.63 0 .984-.345.357-.346.358-1.005v-2.725h-.791v2.745q0 .303-.138.466t-.422.164a.5.5 0 0 1-.454-.246.6.6 0 0 1-.073-.27H0Zm4.92-2.86H3.322v4h.791v-1.343h.803q.43 0 .732-.172.305-.177.463-.475.162-.302.161-.677 0-.374-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179Zm.546 1.333a.8.8 0 0 1-.085.381.57.57 0 0 1-.238.24.8.8 0 0 1-.375.082H4.11v-1.406h.66q.327 0 .512.182.185.181.185.521Z"/>
			</symbol>
			<symbol id="monster-file-icon-html" viewBox="0 0 16 16">
				<path d="M14 4.5V11h-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M4.264 11.85v3.999h-.791v-1.714H1.79v1.714H1V11.85h.791v1.626h1.682V11.85h.79Zm2.251.662v3.337h-.794v-3.337H4.588v-.662h3.064v.662zm2.176 3.337v-2.66h.038l.952 2.159h.516l.946-2.16h.038v2.661h.715V11.85h-.8l-1.14 2.596H9.93L8.79 11.85h-.805v3.999zm4.71-.674h1.696v.674H12.61V11.85h.79v3.325Z"/>
			</symbol>
			<symbol id="monster-file-icon-gif" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2H9v-1h3a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M3.278 13.124a1.4 1.4 0 0 0-.14-.492 1.3 1.3 0 0 0-.314-.407 1.5 1.5 0 0 0-.48-.275 1.9 1.9 0 0 0-.636-.1q-.542 0-.926.229a1.5 1.5 0 0 0-.583.632 2.1 2.1 0 0 0-.199.95v.506q0 .408.105.745.105.336.32.58.213.243.533.377.323.132.753.132.402 0 .697-.111a1.29 1.29 0 0 0 .788-.77q.097-.261.097-.551v-.797H1.717v.589h.823v.255q0 .199-.09.363a.67.67 0 0 1-.273.264 1 1 0 0 1-.457.096.87.87 0 0 1-.519-.146.9.9 0 0 1-.305-.413 1.8 1.8 0 0 1-.096-.615v-.499q0-.547.234-.85.237-.3.665-.301a1 1 0 0 1 .3.044q.136.044.236.126a.7.7 0 0 1 .17.19.8.8 0 0 1 .097.25zm1.353 2.801v-3.999H3.84v4h.79Zm1.493-1.59v1.59h-.791v-3.999H7.88v.653H6.124v1.117h1.605v.638z"/>
			</symbol>
			<symbol id="monster-file-icon-mp4" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M.706 15.849v-2.66h.038l.952 2.16h.516l.946-2.16h.038v2.66h.715V11.85h-.8l-1.14 2.596h-.026L.805 11.85H0v3.999zm5.237-3.999q-.393.65-.79 1.3t-.748 1.31v.648h1.937v.741h.74v-.741h.49v-.639h-.49V11.85H5.944Zm-.82 2.62v-.021q.27-.51.571-1.017.304-.507.607-.984h.04v2.021H5.124Zm2.893-2.62h1.6q.434 0 .732.179.302.175.46.477t.158.677-.16.677q-.159.298-.464.474a1.45 1.45 0 0 1-.732.173h-.803v1.342h-.79zm2.06 1.714a.8.8 0 0 0 .085-.381q0-.34-.185-.521-.184-.183-.513-.182h-.659v1.406h.66a.8.8 0 0 0 .374-.082.57.57 0 0 0 .238-.24"/>
			</symbol>
			<symbol id="monster-file-icon-mov" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M7.086 13.666v.522q0 .384-.117.641a.86.86 0 0 1-.323.387.9.9 0 0 1-.468.126.9.9 0 0 1-.472-.126.87.87 0 0 1-.32-.386 1.55 1.55 0 0 1-.117-.642v-.522q0-.386.118-.641a.87.87 0 0 1 .319-.387.87.87 0 0 1 .472-.129q.263 0 .468.13a.86.86 0 0 1 .323.386q.117.255.117.641m.802.519v-.513q0-.565-.205-.972a1.46 1.46 0 0 0-.588-.63q-.381-.22-.917-.22-.534 0-.92.22a1.44 1.44 0 0 0-.59.627q-.204.406-.204.975v.513q0 .563.205.973.205.406.589.627.386.216.92.216.536 0 .917-.216.383-.22.588-.627.205-.41.205-.973m-7.182 1.74v-2.66h.038l.952 2.16h.516l.946-2.16h.038v2.66h.715v-3.999h-.8l-1.14 2.596h-.026l-1.14-2.596H0v4zm9.54 0h-.952l-1.34-3.999h.918l.896 3.138h.038l.888-3.138h.879z"/>
			</symbol>
			<symbol id="monster-file-icon-mp3" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M9.089 14.17h-.443v-.609h.422a.7.7 0 0 0 .322-.073.56.56 0 0 0 .22-.2.5.5 0 0 0 .076-.284.49.49 0 0 0-.176-.392.65.65 0 0 0-.442-.15.7.7 0 0 0-.252.041.6.6 0 0 0-.193.112.5.5 0 0 0-.179.349H7.71q.009-.235.102-.437.094-.202.27-.352.176-.152.428-.237.255-.085.583-.088.418-.003.723.132.304.135.472.372a.9.9 0 0 1 .173.539.83.83 0 0 1-.12.478.96.96 0 0 1-.619.439v.041a1 1 0 0 1 .718.434.9.9 0 0 1 .144.521q.003.285-.117.507a1.1 1.1 0 0 1-.329.378q-.21.152-.486.234-.273.08-.583.08-.451 0-.77-.153a1.2 1.2 0 0 1-.487-.41 1.1 1.1 0 0 1-.178-.563h.726a.46.46 0 0 0 .106.258.7.7 0 0 0 .249.179 1 1 0 0 0 .357.067.9.9 0 0 0 .384-.076.6.6 0 0 0 .252-.217.56.56 0 0 0 .088-.319.56.56 0 0 0-.334-.522.8.8 0 0 0-.372-.079ZM.706 15.925v-2.66h.038l.952 2.16h.516l.946-2.16h.038v2.66h.715v-3.999h-.8l-1.14 2.596h-.026l-1.14-2.596H0v4zm5.458-3.999h-1.6v4h.792v-1.342h.803q.43 0 .732-.173.304-.177.463-.475a1.4 1.4 0 0 0 .161-.677q0-.374-.158-.677a1.2 1.2 0 0 0-.46-.477 1.4 1.4 0 0 0-.733-.179m.545 1.333a.8.8 0 0 1-.085.381.57.57 0 0 1-.237.24.8.8 0 0 1-.375.082h-.66v-1.406h.66q.328 0 .513.182.184.181.184.521"/>
			</symbol>
			<symbol id="monster-file-icon-txt" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2h-2v-1h2a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M1.928 15.849v-3.337h1.136v-.662H0v.662h1.134v3.337zm4.689-3.999h-.894L4.9 13.289h-.035l-.832-1.439h-.932l1.228 1.983-1.24 2.016h.862l.853-1.415h.035l.85 1.415h.907l-1.253-1.992zm1.93.662v3.337h-.794v-3.337H6.619v-.662h3.064v.662H8.546Z"/>
			</symbol>
			<symbol id="monster-file-icon-png" viewBox="0 0 16 16">
				<path d="M14 4.5V14a2 2 0 0 1-2 2v-1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5z"/>
				<path d="M10.24 12.632q.114.23.14.492h-.776a.8.8 0 0 0-.097-.249.7.7 0 0 0-.17-.19.7.7 0 0 0-.237-.126 1 1 0 0 0-.299-.044q-.427 0-.665.302-.234.301-.234.85v.498q0 .351.097.615a.9.9 0 0 0 .304.413.87.87 0 0 0 .519.146 1 1 0 0 0 .457-.096.67.67 0 0 0 .272-.264q.09-.164.091-.363v-.255H8.82v-.59h1.576v.798q0 .29-.097.55a1.3 1.3 0 0 1-.293.458 1.4 1.4 0 0 1-.495.313q-.296.111-.697.111a2 2 0 0 1-.753-.132 1.45 1.45 0 0 1-.533-.377 1.6 1.6 0 0 1-.32-.58 2.5 2.5 0 0 1-.105-.745v-.506q0-.543.2-.95.201-.406.582-.633.384-.228.926-.228.357 0 .636.1.281.1.48.275.2.176.314.407Zm-8.64-.706H0v4h.791v-1.343h.803q.43 0 .732-.172.305-.177.463-.475a1.4 1.4 0 0 0 .161-.677q0-.374-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.381.57.57 0 0 1-.238.24.8.8 0 0 1-.375.082H.788v-1.406h.66q.327 0 .512.182.185.181.185.521m1.964 2.666V13.25h.032l1.761 2.675h.656v-3.999h-.75v2.66h-.032l-1.752-2.66h-.662v4z"/>
			</symbol>
		</svg>
	`;
}

registerCustomElement(FileManager);
