/**
 * 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 {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import "../notify/notify.mjs";
import { ViewerStyleSheet } from "./stylesheet/viewer.mjs";
import { instanceSymbol } from "../../constants.mjs";
import { isString } from "../../types/is.mjs";
import { getGlobal } from "../../types/global.mjs";
import { MediaType, parseMediaType } from "../../types/mediatype.mjs";
import { MarkdownToHTML } from "../../text/markdown-parser.mjs";
import "../layout/tabs.mjs";
import "./viewer/message.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { Button } from "../form/button.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { sanitizeHtml } from "../../dom/sanitize-html.mjs";

export { Viewer };

const BLOCKED_HTML_RESOURCE_DATA_URL =
	"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";

/**
 * @private
 * @type {symbol}
 */
const viewerElementSymbol = Symbol("viewerElement");

/**
 * @private
 * @type {symbol}
 */
const downloadHandlerSymbol = Symbol("downloadHandler");
/**
 * @private
 * @type {symbol}
 */
const downloadRevokeSymbol = Symbol("downloadRevoke");

/**
 * The Viewer component is used to show a PDF, HTML, or Image.
 *
 * @fragments /fragments/components/content/viewer
 *
 * @example /examples/components/content/pdf-viewer with a PDF
 * @example /examples/components/content/image-viewer with an image
 * @example /examples/components/content/html-viewer with HTML content
 *
 * @copyright Volker Schukai
 * @summary A viewer component for embedded PDFs, HTML documents and images inside Monster layouts.
 */
class Viewer extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/content/viewer@@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 {string} content Content to be displayed in the viewer
	 * @property {Object} classes Css classes
	 * @property {string} classes.viewer Css class for the viewer
	 * @property {Object} renderers Renderers for different media types
	 * @property {function} renderers.image Function to render image content
	 * @property {function} renderers.html Function to render HTML content
	 * @property {function} renderers.pdf Function to render PDF content
	 * @property {function} renderers.plaintext Function to render plain text content
	 * @property {function} renderers.markdown Function to render Markdown content
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			content: "<slot></slot>",
			classes: {
				viewer: "",
			},
			labels: getLabels(),
			renderers: {
				image: this.setImage,
				html: this.setHTML,
				pdf: this.setPDF,
				download: this.setDownload,
				plaintext: this.setPlainText,
				markdown: this.setMarkdown,
				audio: this.setAudio,
				video: this.setVideo,
				message: this.setMessage,
			},
		});
	}

	/**
	 * @private
	 * @type {symbol}
	 * @memberof Viewer
	 * @static
	 * @description downloadHandlerSymbol is used to store the download handler function.
	 * This is necessary to remove the event listener when the element is disconnected.
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener}
	 */
	disconnectedCallback() {
		super.disconnectedCallback?.();
		if (this[downloadHandlerSymbol]) {
			this.removeEventListener("click", this[downloadHandlerSymbol]);
			this[downloadHandlerSymbol] = null;
		}
		if (this[downloadRevokeSymbol]) {
			try {
				this[downloadRevokeSymbol]();
			} catch {}
			this[downloadRevokeSymbol] = null;
		}
	}

	/**
	 * Sets the content of an element based on the provided content and media type.
	 *
	 * Supported forms:
	 * - setContent(contentString, mediaType)
	 * - setContent({ mediaType, data, encoding })
	 *
	 * The declarative form requires an explicit `encoding` when `data` is a string.
	 * Use `encoding: "url"` for URLs and `encoding: "base64"` for base64 payloads.
	 *
	 * @param {string|object} content - The content to be set or a declarative payload.
	 * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified.
	 * @return {void} This method does not return a value.
	 * @throws {Error} Throws an error if shadowRoot is not defined.
	 */
	setContent(content, mediaType = "text/plain") {
		if (!this.shadowRoot) {
			throw new Error("no shadow-root is defined");
		}

		let declarativePayload = null;
		// Declarative form: { mediaType, data, encoding }
		if (
			content &&
			typeof content === "object" &&
			!isBlob(content) &&
			!(content instanceof URL)
		) {
			const { mediaType: mt, data, encoding } = content;
			mediaType = mt ?? mediaType ?? "text/plain";
			declarativePayload = { data, encoding };

			if (isString(data)) {
				if (!encoding) {
					this.dispatchEvent(
						new CustomEvent("viewer-error", {
							detail: "String content requires explicit encoding",
						}),
					);
					return;
				}
				if (encoding === "base64") {
					content = data;
				} else if (encoding === "url") {
					content = data;
				} else {
					this.dispatchEvent(
						new CustomEvent("viewer-error", {
							detail: "Invalid encoding",
						}),
					);
					return;
				}
			} else {
				content = data;
			}
		}

		const renderers = this.getOption("renderers");

		const isDataURL = (value) => {
			return (
				(typeof value === "string" && value.startsWith("data:")) ||
				(value instanceof URL && value.protocol === "data:")
			);
		};

		if (isDataURL(content)) {
			try {
				const dataUrl = content.toString();
				const [header] = dataUrl.split(",");
				const [typeSegment] = header.split(";");
				mediaType = typeSegment.replace("data:", "") || "text/plain";
			} catch (error) {
				this.dispatchEvent(
					new CustomEvent("viewer-error", {
						detail: "Invalid data URL format",
					}),
				);
				return;
			}
		}

		if (mediaType === undefined || mediaType === null || mediaType === "") {
			mediaType = "text/plain";
		}

		let mediaTypeObject;

		try {
			mediaTypeObject = new parseMediaType(mediaType);
			if (!(mediaTypeObject instanceof MediaType)) {
				this.dispatchEvent(
					new CustomEvent("viewer-error", { detail: "Invalid MediaType" }),
				);
				return;
			}
		} catch (error) {
			this.dispatchEvent(new CustomEvent("viewer-error", { detail: error }));
			return;
		}

		const checkRenderer = (renderer, contentType) => {
			if (renderers && typeof renderers[renderer] === "function") {
				return true;
			} else {
				this.dispatchEvent(
					new CustomEvent("viewer-error", {
						detail: `Renderer for ${contentType} not found`,
					}),
				);
				return false;
			}
		};

		switch (mediaTypeObject.type) {
			case "text":
				switch (mediaTypeObject.subtype) {
					case "html":
						if (checkRenderer("html", mediaTypeObject.toString())) {
							renderers.html.call(this, content);
						}
						break;
					case "plain":
						if (checkRenderer("plaintext", mediaTypeObject.toString())) {
							renderers.plaintext.call(this, content);
						}
						break;
					case "markdown":
						if (checkRenderer("markdown", mediaTypeObject.toString())) {
							this.setMarkdown(content);
						}
						break;
					default:
						if (checkRenderer("plaintext", mediaTypeObject.toString())) {
							renderers.plaintext.call(this, content);
						}
						break;
				}
				break;

			case "application":
				switch (mediaTypeObject.subtype) {
					case "pdf":
						if (checkRenderer("pdf", mediaTypeObject.toString())) {
							renderers.pdf.call(
								this,
								declarativePayload ? declarativePayload : content,
							);
						}
						break;

					default:
						// Handle octet-stream as a generic binary data type
						if (checkRenderer("download", mediaTypeObject.toString())) {
							renderers.download.call(this, content);
							break;
						}

						this.setOption("content", content);
						break;
				}
				break;

			case "message":
				switch (mediaTypeObject.subtype) {
					case "rfc822":
						if (checkRenderer("message", mediaTypeObject.toString())) {
							renderers.message.call(this, content);
						}
						break;

					default:
						this.setOption("content", content);
						break;
				}
				break;

			case "audio":
				if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
					renderers[mediaTypeObject.type].call(this, content);
				}
				break;

			case "video":
				if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
					renderers[mediaTypeObject.type].call(this, content);
				}
				break;

			case "image":
				if (checkRenderer("image", mediaTypeObject.toString())) {
					renderers.image.call(this, content);
				}
				break;

			default:
				this.setOption("content", content);
				this.dispatchEvent(
					new CustomEvent("viewer-error", {
						detail: `Unsupported media type: ${mediaTypeObject.toString()}`,
					}),
				); // Notify about unsupported media type
				return;
		}
	}

	/**
	 * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it
	 * to configure audio playback within the viewer. Throws an error if the input type is invalid.
	 *
	 * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string.
	 * @return {void} No return value.
	 */
	setAudio(data) {
		if (isBlob(data)) {
			data = URL.createObjectURL(data);
		} else if (isURL(data)) {
			// nothing to do
		} else if (isString(data)) {
			// nothing to do
		} else {
			this.dispatchEvent(
				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
			);
			throw new Error("Blob or URL expected");
		}

		this.setOption(
			"content",
			`
                   <audio controls part="audio" style="max-width: 100%">
                       <source src="${data}">
                   </audio>`,
		);
	}

	/**
	 * Sets the video content for the viewer. The method accepts a Blob, URL, or string,
	 * verifies its type, and updates the viewer's content accordingly.
	 *
	 * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string.
	 * @return {void} This method does not return a value. It updates the viewer's state.
	 * @throws {Error} Throws an error if the provided data is not a Blob or URL.
	 */
	setVideo(data) {
		if (isBlob(data)) {
			data = URL.createObjectURL(data);
		} else if (isURL(data)) {
			// nothing to do
		} else if (isString(data)) {
			// nothing to do
		} else {
			this.dispatchEvent(
				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
			);
			throw new Error("Blob or URL expected");
		}

		this.setOption(
			"content",
			`
                   <video controls part="video" style="max-width: 100%">
                     <source src="${data}">
                   </video>`,
		);
	}

	/**
	 * Renders Markdown content using built-in or custom Markdown parser.
	 * Overrideable via `customRenderers['text/markdown']`.
	 *
	 * @param {string|Blob} data
	 */
	setMarkdown(data) {
		if (isBlob(data)) {
			blobToText(data)
				.then((markdownText) => {
					try {
						const html = MarkdownToHTML.convert(markdownText);
						this.setHTML(html);
					} catch (error) {
						this.setPlainText(markdownText); // Fallback to plain text if conversion fails
						this.dispatchEvent(
							new CustomEvent("viewer-error", { detail: error }),
						);
					}
				})
				.catch((error) => {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: error }),
					);
					throw new Error(error);
				});
			return;
		} else if (isURL(data)) {
			getGlobal()
				.fetch(data)
				.then((response) => {
					return response.text();
				})
				.then((markdownText) => {
					try {
						const html = MarkdownToHTML.convert(markdownText);
						this.setHTML(html);
					} catch (error) {
						this.setPlainText(markdownText); // Fallback to plain text if conversion fails
						this.dispatchEvent(
							new CustomEvent("viewer-error", { detail: error }),
						);
					}
				})
				.catch((error) => {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: error }),
					);
					throw new Error(error);
				});
			return;
		} else if (isString(data)) {
			try {
				const html = MarkdownToHTML.convert(data);
				this.setHTML(html);
			} catch (error) {
				this.setPlainText(data); // Fallback to plain text if conversion fails
				this.dispatchEvent(new CustomEvent("viewer-error", { detail: error }));
			}
			return;
		}

		this.dispatchEvent(
			new CustomEvent("viewer-error", { detail: "Blob or string expected" }),
		);
		throw new Error("Blob or string expected");
	}

	/**
	 * Configures and embeds a PDF document into the application with customizable display settings.
	 *
	 * Supported forms:
	 * - setPDF(blobOrUrlOrBase64String, navigation, toolbar, scrollbar)
	 * - setPDF({ data, encoding, navigation, toolbar, scrollbar })
	 *
	 * The declarative form requires an explicit `encoding` when `data` is a string.
	 * Use `encoding: "url"` for URLs and `encoding: "base64"` for base64 payloads.
	 *
	 * @param {Blob|URL|string|object} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string,
	 *                                    or a declarative payload: { data, encoding, navigation, toolbar, scrollbar }.
	 * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer.
	 * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer.
	 * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer.
	 * @return {void} This method returns nothing but sets the embedded PDF as the content.
	 */
	setPDF(data, navigation = true, toolbar = true, scrollbar = false) {
		let stringEncoding = null;
		// Declarative form: { data, encoding, navigation, toolbar, scrollbar }
		if (
			data &&
			typeof data === "object" &&
			!isBlob(data) &&
			!(data instanceof URL)
		) {
			const payload = data;
			data = payload.data;
			stringEncoding = payload.encoding ?? null;
			if (typeof payload.navigation === "boolean") {
				navigation = payload.navigation;
			}
			if (typeof payload.toolbar === "boolean") {
				toolbar = payload.toolbar;
			}
			if (typeof payload.scrollbar === "boolean") {
				scrollbar = payload.scrollbar;
			}
			if (isString(data)) {
				if (!payload.encoding) {
					this.dispatchEvent(
						new CustomEvent("viewer-error", {
							detail: "String PDF data requires explicit encoding",
						}),
					);
					return;
				}
				if (payload.encoding === "base64") {
					// handled below as string/base64
				} else if (payload.encoding === "url") {
					// handled below as string/url
				} else {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: "Invalid encoding" }),
					);
					return;
				}
			}
		}

		const hashes =
			"#toolbar=" +
			(toolbar ? "1" : "0") +
			"&navpanes=" +
			(navigation ? "1" : "0") +
			"&scrollbar=" +
			(scrollbar ? "1" : "0");
		let pdfURL = "";
		if (isBlob(data)) {
			pdfURL = URL.createObjectURL(data);
			pdfURL += hashes;
		} else if (isURL(data)) {
			// check if the url already contains the hashes
			if (data?.hash?.indexOf("#") === -1) {
				pdfURL = data.toString() + hashes;
			} else {
				pdfURL = data.toString();
			}
		} else if (isString(data)) {
			if (stringEncoding === "url") {
				if (data.indexOf("#") === -1) {
					pdfURL = data + hashes;
				} else {
					pdfURL = data;
				}
			} else {
				// Default legacy behavior: treat string as base64
				const blobObj = new Blob([atob(data)], { type: "application/pdf" });
				const url = window.URL.createObjectURL(blobObj);
				pdfURL = url + hashes;
			}
		} else {
			this.dispatchEvent(
				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
			);
			throw new Error("Blob or URL expected");
		}

		const html =
			'<object part="pdf" data="' +
			pdfURL +
			'" width="100%" height="100%" type="application/pdf"></object>';

		this.setOption("content", html);
	}

	/**
	 * Sets the download functionality for the viewer.
	 * @param data
	 * @param filename
	 */
	setDownload(data, filename = "download") {
		let href = "";
		let revoke = null;
		let suggestedName = filename;

		if (data instanceof File) {
			href = URL.createObjectURL(data);
			suggestedName = data.name || filename;
			revoke = () => URL.revokeObjectURL(href);
		} else if (isBlob(data)) {
			href = URL.createObjectURL(data);
			revoke = () => URL.revokeObjectURL(href);
		} else if (
			isURL(data) ||
			(isString(data) && /^(data:|blob:|https?:)/.test(data))
		) {
			href = data.toString();
			try {
				const u = new URL(href, location.href);
				const last = u.pathname.split("/").pop();
				if ((!filename || filename === "download") && last) {
					suggestedName = decodeURIComponent(last);
				}
			} catch {}
		} else if (isString(data)) {
			const blob = new Blob([data], { type: "application/octet-stream" });
			href = URL.createObjectURL(blob);
			revoke = () => URL.revokeObjectURL(href);
		} else {
			const msg = "Blob, URL oder String erwartet";
			this.dispatchEvent(new CustomEvent("viewer-error", { detail: msg }));
			throw new Error(msg);
		}

		const button =
			`<monster-button data-monster-role="download">` +
			this.getOption("labels.download") +
			`</monster-button>`;
		this.setOption("content", button);

		if (this[downloadHandlerSymbol]) {
			this.removeEventListener("click", this[downloadHandlerSymbol]);
		}
		if (this[downloadRevokeSymbol]) {
			try {
				this[downloadRevokeSymbol]();
			} catch {}
		}

		this[downloadHandlerSymbol] = (event) => {
			const el = findTargetElementFromEvent(
				event,
				"data-monster-role",
				"download",
			);
			if (el instanceof Button) {
				const a = document.createElement("a");
				a.href = href;
				a.download = suggestedName;
				a.rel = "noopener";
				a.style.display = "none";
				document.body.appendChild(a);
				a.click();
				document.body.removeChild(a);
			}
		};
		this[downloadRevokeSymbol] = revoke;

		this.addEventListener("click", this[downloadHandlerSymbol]);
	}

	/**
	 * Sets the content for displaying an email message.
	 * The data is expected to be an object with a structure containing
	 * 'to', 'from', 'subject', 'parts', and 'headers'.
	 * The parts are processed to display plain text and HTML in separate tabs,
	 * and attachments are listed.
	 *
	 * @param {object} emailData - The structured email data.
	 */
	setMessage(emailData) {
		if (!emailData || typeof emailData !== "object") {
			this.dispatchEvent(
				new CustomEvent("viewer-error", {
					detail: "Invalid email data provided",
				}),
			);
			return;
		}

		this.setOption(
			"content",
			'<monster-message-content part="message"></monster-message-content>',
		);

		setTimeout(() => {
			const messageContent = this.shadowRoot.querySelector(
				"monster-message-content",
			);
			if (!messageContent) {
				this.dispatchEvent(
					new CustomEvent("viewer-error", {
						detail: "Message content element not found",
					}),
				);
				return;
			}

			messageContent.setMessage(emailData);
		}, 100);
	}

	/**
	 * Sets an image for the target by accepting a blob, URL, or string representation of the image.
	 *
	 * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image.
	 * @return {void} Does not return a value.
	 */
	setImage(data) {
		if (isBlob(data)) {
			data = URL.createObjectURL(data);
		} else if (isURL(data)) {
			// nothing to do
		} else if (isString(data)) {
			// nothing to do
		} else {
			this.dispatchEvent(
				new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
			);
			throw new Error("Blob or URL expected");
		}

		this.setOption(
			"content",
			`<img style="max-width: 100%" src="${data}" alt="image" part="image" onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));">`,
		);
	}

	/**
	 *
	 * if the data is a string, it is interpreted as HTML.
	 * if the data is a URL, the HTML is loaded from the url and set as content.
	 * if the data is an HTMLElement, the outerHTML is used as content.
	 *
	 * @param {HTMLElement|URL|string|Blob} data
	 */
	setHTML(data) {
		if (data instanceof Blob) {
			blobToText(data)
				.then((html) => {
					this.setOption("content", sanitizeViewerHTMLContent(html));
				})
				.catch((error) => {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: error }),
					);
					throw new Error(error);
				});

			return;
		} else if (data instanceof HTMLElement) {
			data = data.outerHTML;
		} else if (isString(data)) {
			// nothing to do
		} else if (isURL(data)) {
			// fetch element
			getGlobal()
				.fetch(data)
				.then((response) => {
					return response.text();
				})
				.then((html) => {
					this.setOption("content", sanitizeViewerHTMLContent(html));
				})
				.catch((error) => {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: error }),
					);
					throw new Error(error);
				});
		} else {
			this.dispatchEvent(
				new CustomEvent("viewer-error", {
					detail: "HTMLElement or string expected",
				}),
			);
			throw new Error("HTMLElement or string expected");
		}

		this.setOption("content", sanitizeViewerHTMLContent(data));
	}

	/**
	 * Sets the plain text content by processing the input data, which can be of various types, including Blob,
	 * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option.
	 *
	 * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement,
	 * a plain string, or a string formatted as a valid URL. The method determines
	 * the data type and processes it accordingly.
	 * @return {void} - This method does not return any value. It processes the content and updates the relevant option
	 * property.
	 */
	setPlainText(data) {
		const mkPreSpan = (text) => {
			const pre = document.createElement("pre");
			pre.textContent = text;
			pre.setAttribute("part", "text");
			return pre.outerHTML;
		};

		if (data instanceof Blob) {
			blobToText(data)
				.then((text) => {
					this.setOption("content", mkPreSpan(text));
				})
				.catch((error) => {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: error }),
					);
					throw new Error(error);
				});

			return;
		} else if (data instanceof HTMLElement) {
			data = data.outerText;
		} else if (isString(data)) {
			// text/plain should never be interpreted as HTML
		} else if (isURL(data)) {
			getGlobal()
				.fetch(data)
				.then((response) => {
					return response.text();
				})
				.then((text) => {
					this.setOption("content", mkPreSpan(text));
				})
				.catch((error) => {
					this.dispatchEvent(
						new CustomEvent("viewer-error", { detail: error }),
					);
					throw new Error(error);
				});
		} else {
			this.dispatchEvent(
				new CustomEvent("viewer-error", {
					detail: "HTMLElement or string expected",
				}),
			);
			throw new Error("HTMLElement or string expected");
		}

		this.setOption("content", mkPreSpan(data));
	}

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

		initControlReferences.call(this);
		initEventHandler.call(this);
	}

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

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

/**
 * @private
 * @param variable
 * @return {boolean}
 */
function isURL(variable) {
	try {
		new URL(variable);
		return true;
	} catch (error) {
		return false;
	}
}

/**
 * @private
 * @param variable
 * @return {boolean}
 */
function isBlob(variable) {
	return variable instanceof Blob;
}

/**
 * @private
 * @param blob
 * @return {Promise<unknown>}
 */
function blobToText(blob) {
	return new Promise((resolve, reject) => {
		const reader = new FileReader();
		reader.onloadend = () => resolve(reader.result);
		reader.onerror = reject;
		reader.readAsText(blob);
	});
}

function sanitizeViewerHTMLContent(html) {
	const sanitizedHTML = sanitizeHtml(html);
	const parser = new DOMParser();
	const doc = parser.parseFromString(sanitizedHTML, "text/html");

	doc.querySelectorAll("*").forEach((element) => {
		removeCrossOriginResourceAttribute(element, "src");
		removeCrossOriginResourceAttribute(element, "srcset");
		removeCrossOriginResourceAttribute(element, "poster");

		if (element.hasAttribute("style")) {
			const safeStyle = sanitizeStyleAttribute(element.getAttribute("style"));
			if (safeStyle) {
				element.setAttribute("style", safeStyle);
			} else {
				element.removeAttribute("style");
			}
		}
	});

	return doc.body.innerHTML;
}

function removeCrossOriginResourceAttribute(element, attributeName) {
	const value = element.getAttribute(attributeName);
	if (!value) {
		return;
	}

	if (attributeName === "srcset") {
		const sameOriginEntries = value
			.split(",")
			.map((entry) => entry.trim())
			.filter(Boolean)
			.filter((entry) => {
				const [candidateUrl] = entry.split(/\s+/, 1);
				return isSameOriginResourceUrl(candidateUrl);
			});

		if (sameOriginEntries.length > 0) {
			element.setAttribute(attributeName, sameOriginEntries.join(", "));
			return;
		}
	} else if (isSameOriginResourceUrl(value)) {
		return;
	}

	if (
		attributeName === "src" &&
		element.tagName.toLowerCase() === "img" &&
		!element.classList.contains("privacyImage")
	) {
		element.setAttribute("src", BLOCKED_HTML_RESOURCE_DATA_URL);
		element.classList.add("privacyImage");
		return;
	}

	element.removeAttribute(attributeName);
}

function sanitizeStyleAttribute(styleValue) {
	return styleValue.replace(/url\(([^)]+)\)/gi, (match, rawUrl) => {
		const cleanedUrl = rawUrl.trim().replace(/^['"]|['"]$/g, "");
		return isSameOriginResourceUrl(cleanedUrl) ? match : "";
	});
}

function isSameOriginResourceUrl(value) {
	if (!value) {
		return true;
	}

	const trimmedValue = value.trim();
	if (!trimmedValue || trimmedValue.startsWith("#")) {
		return true;
	}

	try {
		const url = new URL(trimmedValue, document.location.href);
		return (
			url.origin === document.location.origin ||
			url.protocol === "data:" ||
			url.protocol === "blob:"
		);
	} catch {
		return true;
	}
}

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

	this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
}

/**
 * @private
 */
function initEventHandler() {
	return this;
}

function getLabels() {
	switch (getLocaleOfDocument().language) {
		case "de": // German
			return {
				download: "Herunterladen",
			};

		case "es": // Spanish
			return {
				download: "Descargar",
			};

		case "zh": // Mandarin
			return {
				download: "下载",
			};

		case "hi": // Hindi
			return {
				download: "下载",
			};

		case "bn": // Bengali
			return {
				download: "ডাউনলোড",
			};

		case "pt": // Portuguese
			return {
				download: "Baixar",
			};

		case "ru": // Russian
			return {
				download: "Скачать",
			};

		case "ja": // Japanese
			return {
				download: "ダウンロード",
			};

		case "pa": // Western Punjabi
			return {
				download: "ਡਾਊਨਲੋਡ",
			};

		case "mr": // Marathi
			return {
				download: "डाउनलोड",
			};

		case "fr": // French
			return {
				download: "Télécharger",
			};

		case "it": // Italian
			return {
				download: "Scarica",
			};

		case "nl": // Dutch
			return {
				download: "Downloaden",
			};

		case "sv": // Swedish
			return {
				download: "Ladda ner",
			};

		case "pl": // Polish
			return {
				download: "Ściągnij",
			};

		case "da": // Danish
			return {
				download: "Lad ned",
			};

		case "fi": // Finnish
			return {
				download: "Lataa",
			};

		case "no": // Norwegian
			return {
				download: "Laste ned",
			};

		case "cs": // Czech
			return {
				download: "Stáhnout",
			};

		default:
			return {
				download: "Download",
			};
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div id="viewer" data-monster-role="viewer" part="viewer"
             data-monster-replace="path:content"
             data-monster-attributes="class path:classes.viewer">
        </div>`;
}

registerCustomElement(Viewer);
