/**
 * 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 { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import {
	findTargetElementFromEvent,
	fireCustomEvent,
} from "../../dom/events.mjs";
import { addErrorAttribute, resetErrorAttribute } from "../../dom/error.mjs";
import { getDocument } from "../../dom/util.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { isFunction, isObject, isString } from "../../types/is.mjs";
import { DropzoneStyleSheet } from "./stylesheet/dropzone.mjs";

import "./button.mjs";

export { Dropzone };

/**
 * @private
 * @type {symbol}
 */
const dropzoneElementSymbol = Symbol("dropzoneElement");

/**
 * @private
 * @type {symbol}
 */
const inputElementSymbol = Symbol("inputElement");

/**
 * @private
 * @type {symbol}
 */
const buttonElementSymbol = Symbol("buttonElement");

/**
 * @private
 * @type {symbol}
 */
const statusElementSymbol = Symbol("statusElement");

/**
 * @private
 * @type {symbol}
 */
const dragCounterSymbol = Symbol("dragCounter");

/**
 * @private
 * @type {symbol}
 */
const listElementSymbol = Symbol("listElement");

/**
 * @private
 * @type {symbol}
 */
const fileItemMapSymbol = Symbol("fileItemMap");

/**
 * @private
 * @type {symbol}
 */
const fileRequestMapSymbol = Symbol("fileRequestMap");

/**
 * @private
 * @type {symbol}
 */
const fileTimeoutMapSymbol = Symbol("fileTimeoutMap");

/**
 * A Dropzone control
 *
 * @fragments /fragments/components/form/dropzone/
 *
 * @example /examples/components/form/dropzone-simple Simple dropzone
 * @example /examples/components/form/dropzone-avatar Profile image upload
 *
 * @since 4.40.0
 * @copyright Volker Schukai
 * @summary A file upload dropzone for drag-and-drop or click-based document and media uploads.
 *
 * @fires monster-dropzone-selected
 * @fires monster-dropzone-file-added
 * @fires monster-dropzone-file-removed
 * @fires monster-dropzone-file-retry
 * @fires monster-dropzone-file-upload-start
 * @fires monster-dropzone-file-upload-success
 * @fires monster-dropzone-file-upload-error
 * @fires monster-dropzone-upload-start
 * @fires monster-dropzone-upload-success
 * @fires monster-dropzone-upload-error
 */
class Dropzone extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/dropzone@@instance");
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Label definitions
	 * @property {string} labels.title Title text
	 * @property {string} labels.hint Hint text
	 * @property {string} labels.button Button label
	 * @property {string} labels.statusIdle Status text for idle state
	 * @property {string} labels.statusUploading Status text for uploading state
	 * @property {string} labels.statusSuccess Status text for success state
	 * @property {string} labels.statusError Status text for error state
	 * @property {string} labels.statusMissingUrl Status text for missing URL
	 * @property {Object} classes Class definitions
	 * @property {string} classes.dropzone Dropzone CSS class
	 * @property {string} classes.button Monster button class
	 * @property {string} url Upload URL
	 * @property {string} fieldName="files" FormData field name for files
	 * @property {string} accept File input accept attribute
	 * @property {boolean} multiple Allow multiple file selection
	 * @property {boolean} disabled Disable interaction
	 * @property {Object} data Additional data appended to the FormData
	 * @property {Object} features Feature flags
	 * @property {boolean} features.autoUpload Automatically upload after selection
	 * @property {boolean} features.previewImages Show image previews
	 * @property {boolean} features.disappear Enable auto-removal of finished items
	 * @property {Object} disappear Disappear settings
	 * @property {number} disappear.time Delay before auto-removal (ms)
	 * @property {number} disappear.duration Delay before auto-removal (ms)
	 * @property {Object} actions Action definitions for custom event handling
	 * @property {Function} actions.fileAdded Called after a file is added to the list
	 * @property {Function} actions.fileRemoved Called after a file is removed
	 * @property {Function} actions.fileRetry Called before retrying a failed upload
	 * @property {Function} actions.uploadStart Called when uploads start
	 * @property {Function} actions.uploadSuccess Called after successful upload
	 * @property {Function} actions.uploadError Called when upload fails
	 * @property {Function} actions.beforeUpload Called before upload, return false to cancel
	 * @property {Object} fetch Fetch options
	 * @property {string} fetch.method="POST"
	 * @property {string} fetch.redirect="error"
	 * @property {string} fetch.mode="same-origin"
	 * @property {string} fetch.credentials="same-origin"
	 * @property {Object} fetch.headers={"accept":"application/json"}
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			classes: {
				dropzone: "monster-dropzone",
				button: "monster-button-outline-primary",
			},
			url: "",
			fieldName: "files",
			accept: "",
			multiple: true,
			disabled: false,
			data: {},
			features: {
				autoUpload: true,
				previewImages: true,
				disappear: true,
			},
			disappear: {
				duration: 3000,
			},
			actions: {
				fileAdded: null,
				fileRemoved: null,
				fileRetry: null,
				uploadStart: null,
				uploadSuccess: null,
				uploadError: null,
				beforeUpload: null,
			},
			fetch: {
				method: "POST",
				redirect: "error",
				mode: "same-origin",
				credentials: "same-origin",
				headers: {
					accept: "application/json",
				},
			},
		});
	}

	/**
	 *
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initEventHandler.call(this);
		this[fileItemMapSymbol] = new Map();
		this[fileRequestMapSymbol] = new Map();
		this[fileTimeoutMapSymbol] = new Map();
		setStatus.call(this, this.getOption("labels.statusIdle"));
	}

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

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

	/**
	 * Open the native file picker.
	 *
	 * @return {void}
	 */
	open() {
		if (this.getOption("disabled") === true) {
			return;
		}

		const input = this[inputElementSymbol];
		if (input && typeof input.click === "function") {
			input.click();
		}
	}

	/**
	 * Upload files programmatically.
	 *
	 * @param {FileList|File[]} files
	 * @return {Promise<void>}
	 */
	upload(files) {
		const normalized = normalizeFiles(files);
		if (normalized.length === 0) {
			return Promise.resolve();
		}

		return uploadFiles.call(this, normalized);
	}
}

/**
 * @private
 */
function initControlReferences() {
	this[dropzoneElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=dropzone]`,
	);
	this[inputElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=input]`,
	);
	this[buttonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=button]`,
	);
	this[statusElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=status]`,
	);
	this[listElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=list]`,
	);
}

/**
 * @private
 */
function initEventHandler() {
	this[dragCounterSymbol] = 0;

	const dropzone = this[dropzoneElementSymbol];
	const input = this[inputElementSymbol];
	const button = this[buttonElementSymbol];

	if (dropzone) {
		dropzone.addEventListener("dragenter", (event) => {
			if (this.getOption("disabled") === true) {
				return;
			}
			event.preventDefault();
			this[dragCounterSymbol] += 1;
			setDropActive.call(this, true);
		});

		dropzone.addEventListener("dragover", (event) => {
			if (this.getOption("disabled") === true) {
				return;
			}
			event.preventDefault();
			setDropActive.call(this, true);
		});

		dropzone.addEventListener("dragleave", (event) => {
			if (this.getOption("disabled") === true) {
				return;
			}
			event.preventDefault();
			this[dragCounterSymbol] = Math.max(0, this[dragCounterSymbol] - 1);
			if (this[dragCounterSymbol] === 0) {
				setDropActive.call(this, false);
			}
		});

		dropzone.addEventListener("drop", (event) => {
			if (this.getOption("disabled") === true) {
				return;
			}
			event.preventDefault();
			this[dragCounterSymbol] = 0;
			setDropActive.call(this, false);
			const files = event.dataTransfer?.files;
			handleFiles.call(this, files);
		});

		dropzone.addEventListener("keydown", (event) => {
			if (this.getOption("disabled") === true) {
				return;
			}
			if (event.key === "Enter" || event.key === " ") {
				event.preventDefault();
				this.open();
			}
		});
	}

	if (input) {
		input.addEventListener("change", (event) => {
			const files = event.target?.files;
			handleFiles.call(this, files);
		});
	}

	if (button) {
		button.addEventListener("monster-button-clicked", (event) => {
			if (this.getOption("disabled") === true) {
				return;
			}

			const element = findTargetElementFromEvent(
				event,
				ATTRIBUTE_ROLE,
				"button",
			);

			if (!(element instanceof Node && this.hasNode(element))) {
				return;
			}

			this.open();
		});
	}
}

/**
 * @private
 * @param {FileList|File[]} files
 */
function handleFiles(files) {
	const normalized = normalizeFiles(files);
	if (normalized.length === 0) {
		return;
	}

	for (const file of normalized) {
		addFileItem.call(this, file);
		fireCustomEvent(this, "monster-dropzone-file-added", { file });
		triggerAction.call(this, "fileAdded", { file });
	}

	fireCustomEvent(this, "monster-dropzone-selected", {
		files: normalized,
	});

	if (this.getOption("features.autoUpload") === true) {
		uploadFiles.call(this, normalized);
	}
}

/**
 * @private
 * @param {FileList|File[]} files
 * @return {File[]}
 */
function normalizeFiles(files) {
	if (!files) {
		return [];
	}

	if (Array.isArray(files)) {
		return files.filter((file) => file instanceof File);
	}

	if (files instanceof FileList) {
		return Array.from(files);
	}

	return [];
}

/**
 * @private
 * @param {File[]} files
 * @return {Promise<void>}
 */
function uploadFiles(files) {
	let url = this.getOption("url");
	if (!isString(url) || url === "") {
		const message = this.getOption("labels.statusMissingUrl");
		setStatus.call(this, message);
		addErrorAttribute(this, message);
		fireCustomEvent(this, "monster-dropzone-upload-error", {
			files,
			error: new Error(message),
		});
		return Promise.reject(new Error(message));
	}

	try {
		url = new URL(url, getDocument().location).toString();
	} catch (error) {
		addErrorAttribute(this, error);
		setStatus.call(this, this.getOption("labels.statusError"));
		fireCustomEvent(this, "monster-dropzone-upload-error", { files, error });
		return Promise.reject(error);
	}

	setStatus.call(this, this.getOption("labels.statusUploading"));
	fireCustomEvent(this, "monster-dropzone-upload-start", { files, url });
	triggerAction.call(this, "uploadStart", { files, url });

	const uploads = files.map((file) => {
		const item = this[fileItemMapSymbol]?.get(file);
		return uploadSingleFile.call(this, file, url, item);
	});

	return Promise.all(uploads)
		.then((responses) => {
			resetErrorAttribute(this);
			setStatus.call(this, this.getOption("labels.statusSuccess"));
			fireCustomEvent(this, "monster-dropzone-upload-success", {
				files,
				url,
				response: responses,
			});
			triggerAction.call(this, "uploadSuccess", {
				files,
				url,
				response: responses,
			});
		})
		.catch((error) => {
			addErrorAttribute(this, error);
			setStatus.call(this, this.getOption("labels.statusError"));
			fireCustomEvent(this, "monster-dropzone-upload-error", {
				files,
				url,
				error,
			});
			triggerAction.call(this, "uploadError", { files, url, error });

			throw error;
		});
}

/**
 * @private
 * @param {File} file
 * @param {string} url
 * @param {HTMLElement|null} item
 * @return {Promise<*>}
 */
function uploadSingleFile(file, url, item) {
	let formData = new FormData();
	const fieldName = this.getOption("fieldName") || "files";
	formData.append(fieldName, file, file.name);

	const extraData = this.getOption("data");
	if (isObject(extraData)) {
		for (const [key, value] of Object.entries(extraData)) {
			if (value === undefined || value === null) {
				continue;
			}
			formData.append(key, `${value}`);
		}
	}

	const beforeUpload = this.getOption("actions.beforeUpload");
	if (isFunction(beforeUpload)) {
		const result = beforeUpload.call(this, { file, formData, url });
		if (result === false) {
			return Promise.resolve(null);
		}
		if (result instanceof FormData) {
			formData = result;
		}
	}

	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest();
		const fetchOptions = Object.assign({}, this.getOption("fetch", {}));
		const method = fetchOptions.method || "POST";

		xhr.open(method, url);

		if (fetchOptions.headers && isObject(fetchOptions.headers)) {
			for (const [key, value] of Object.entries(fetchOptions.headers)) {
				if (key.toLowerCase() === "content-type") {
					continue;
				}
				xhr.setRequestHeader(key, String(value));
			}
		}

		const credentials = fetchOptions.credentials || "same-origin";
		xhr.withCredentials = credentials === "include";

		xhr.upload.addEventListener("progress", (event) => {
			if (event.lengthComputable) {
				const percent = Math.round((event.loaded / event.total) * 100);
				updateFileProgress.call(this, item, percent);
			}
		});

		xhr.addEventListener("load", () => {
			const status = xhr.status;
			const response = parseXhrResponse(xhr);
			this[fileRequestMapSymbol]?.delete(file);
			if (status >= 200 && status < 300) {
				updateFileProgress.call(this, item, 100);
				setItemState.call(this, item, "success");
				fireCustomEvent(this, "monster-dropzone-file-upload-success", {
					file,
					url,
					response,
				});
				triggerAction.call(this, "uploadSuccess", {
					files: [file],
					url,
					response,
				});
				resolve(response);
			} else {
				setItemState.call(this, item, "error");
				fireCustomEvent(this, "monster-dropzone-file-upload-error", {
					file,
					url,
					error: new Error(
						`upload failed (${status} ${xhr.statusText || "error"})`,
					),
				});
				triggerAction.call(this, "uploadError", {
					files: [file],
					url,
					error: new Error(
						`upload failed (${status} ${xhr.statusText || "error"})`,
					),
				});
				reject(
					new Error(`upload failed (${status} ${xhr.statusText || "error"})`),
				);
			}
		});

		xhr.addEventListener("error", () => {
			this[fileRequestMapSymbol]?.delete(file);
			setItemState.call(this, item, "error");
			fireCustomEvent(this, "monster-dropzone-file-upload-error", {
				file,
				url,
				error: new Error("upload failed"),
			});
			triggerAction.call(this, "uploadError", {
				files: [file],
				url,
				error: new Error("upload failed"),
			});
			reject(new Error("upload failed"));
		});

		xhr.addEventListener("abort", () => {
			this[fileRequestMapSymbol]?.delete(file);
			setItemState.call(this, item, "error");
			fireCustomEvent(this, "monster-dropzone-file-upload-error", {
				file,
				url,
				error: new Error("upload aborted"),
			});
			triggerAction.call(this, "uploadError", {
				files: [file],
				url,
				error: new Error("upload aborted"),
			});
			reject(new Error("upload aborted"));
		});

		setItemState.call(this, item, "uploading");
		fireCustomEvent(this, "monster-dropzone-file-upload-start", { file, url });
		triggerAction.call(this, "uploadStart", { files: [file], url });
		this[fileRequestMapSymbol].set(file, xhr);
		xhr.send(formData);
	});
}

/**
 * @private
 * @param {XMLHttpRequest} xhr
 * @return {*}
 */
function parseXhrResponse(xhr) {
	const contentType = xhr.getResponseHeader("content-type") || "";
	if (contentType.includes("application/json")) {
		try {
			return JSON.parse(xhr.responseText || "{}");
		} catch {
			return {};
		}
	}
	return xhr.responseText;
}

/**
 * @private
 * @param {boolean} active
 */
function setDropActive(active) {
	const dropzone = this[dropzoneElementSymbol];
	if (!dropzone) {
		return;
	}
	dropzone.classList.toggle("is-dragover", active);
}

/**
 * @private
 * @param {string} text
 */
function setStatus(text) {
	const status = this[statusElementSymbol];
	if (!status) {
		return;
	}
	status.textContent = isString(text) ? text : "";
}

/**
 * @private
 * @param {File} file
 */
function addFileItem(file) {
	const list = this[listElementSymbol];
	if (!list || !file) {
		return;
	}

	const item = document.createElement("li");
	item.setAttribute("data-monster-role", "item");

	const preview = document.createElement("div");
	preview.setAttribute("data-monster-role", "preview");

	const meta = document.createElement("div");
	meta.setAttribute("data-monster-role", "meta");

	const name = document.createElement("div");
	name.setAttribute("data-monster-role", "name");
	name.textContent = file.name;

	const info = document.createElement("div");
	info.setAttribute("data-monster-role", "info");
	info.textContent = `${formatFileType(file)} | ${formatFileSize(file.size)}`;

	const progress = document.createElement("div");
	progress.setAttribute("data-monster-role", "progress");

	const bar = document.createElement("div");
	bar.setAttribute("data-monster-role", "bar");
	progress.appendChild(bar);

	const percent = document.createElement("div");
	percent.setAttribute("data-monster-role", "percent");
	percent.textContent = "0%";

	const stateIcon = document.createElement("div");
	stateIcon.setAttribute("data-monster-role", "state-icon");

	const removeButton = document.createElement("button");
	removeButton.setAttribute("type", "button");
	removeButton.setAttribute("data-monster-role", "remove");
	removeButton.setAttribute("aria-label", "remove");
	removeButton.innerHTML = getIconMarkup("cancel");

	const retryButton = document.createElement("button");
	retryButton.setAttribute("type", "button");
	retryButton.setAttribute("data-monster-role", "retry");
	retryButton.setAttribute("aria-label", "retry");
	retryButton.innerHTML = getIconMarkup("reload");

	removeButton.addEventListener("click", () => {
		handleItemRemove.call(this, file);
	});

	retryButton.addEventListener("click", () => {
		handleItemRetry.call(this, file);
	});

	meta.appendChild(name);
	meta.appendChild(info);

	item.appendChild(preview);
	item.appendChild(meta);
	item.appendChild(progress);
	item.appendChild(percent);
	item.appendChild(stateIcon);
	item.appendChild(retryButton);
	item.appendChild(removeButton);
	list.appendChild(item);

	const showPreview =
		this.getOption("features.previewImages") === true &&
		isString(file.type) &&
		file.type.startsWith("image/");

	if (showPreview) {
		const img = document.createElement("img");
		img.setAttribute("data-monster-role", "preview-image");
		img.alt = file.name;
		const url = URL.createObjectURL(file);
		img.src = url;
		img.addEventListener(
			"load",
			() => {
				URL.revokeObjectURL(url);
			},
			{ once: true },
		);
		preview.appendChild(img);
	} else {
		const icon = document.createElement("div");
		icon.setAttribute("data-monster-role", "preview-icon");
		icon.textContent = formatFileExtension(file);
		preview.appendChild(icon);
	}

	this[fileItemMapSymbol].set(file, item);
}

/**
 * @private
 * @param {HTMLElement|null} item
 * @param {number} percent
 */
function updateFileProgress(item, percent) {
	if (!item) {
		return;
	}
	const bar = item.querySelector(`[${ATTRIBUTE_ROLE}=bar]`);
	if (bar instanceof HTMLElement) {
		bar.style.width = `${percent}%`;
	}
	const label = item.querySelector(`[${ATTRIBUTE_ROLE}=percent]`);
	if (label instanceof HTMLElement) {
		label.textContent = `${percent}%`;
	}
}

/**
 * @private
 * @param {HTMLElement|null} item
 * @param {string} icon
 */
/**
 * @private
 * @param {HTMLElement|null} item
 * @param {string} state
 */
function setItemState(item, state) {
	if (!item) {
		return;
	}
	item.setAttribute("data-monster-state", state);
	setStateIcon.call(this, item, state);
	if (state === "success" || state === "error") {
		scheduleDisappear.call(this, item);
	}
}

/**
 * @private
 * @param {HTMLElement|null} item
 * @param {string} state
 */
function setStateIcon(item, state) {
	if (!item) {
		return;
	}
	const target = item.querySelector(`[${ATTRIBUTE_ROLE}=state-icon]`);
	if (!(target instanceof HTMLElement)) {
		return;
	}
	if (state === "success") {
		target.innerHTML = getIconMarkup("success");
		return;
	}
	if (state === "error") {
		target.innerHTML = getIconMarkup("error");
		return;
	}
	target.innerHTML = "";
}

/**
 * @private
 * @param {File} file
 */
function handleItemRemove(file) {
	const item = this[fileItemMapSymbol]?.get(file);
	const xhr = this[fileRequestMapSymbol]?.get(file);
	const timeout = this[fileTimeoutMapSymbol]?.get(file);

	if (xhr instanceof XMLHttpRequest) {
		xhr.abort();
		this[fileRequestMapSymbol]?.delete(file);
	}

	if (timeout) {
		clearTimeout(timeout);
		this[fileTimeoutMapSymbol]?.delete(file);
	}

	if (item && item.parentElement) {
		item.parentElement.removeChild(item);
	}

	this[fileItemMapSymbol]?.delete(file);
	fireCustomEvent(this, "monster-dropzone-file-removed", { file });
	triggerAction.call(this, "fileRemoved", { file });
}

/**
 * @private
 * @param {File} file
 */
function handleItemRetry(file) {
	const item = this[fileItemMapSymbol]?.get(file);
	const timeout = this[fileTimeoutMapSymbol]?.get(file);
	if (!item) {
		return;
	}
	if (timeout) {
		clearTimeout(timeout);
		this[fileTimeoutMapSymbol]?.delete(file);
	}
	updateFileProgress.call(this, item, 0);
	setItemState.call(this, item, "uploading");
	fireCustomEvent(this, "monster-dropzone-file-retry", { file });
	triggerAction.call(this, "fileRetry", { file });
	uploadSingleFile
		.call(this, file, this.getOption("url"), item)
		.catch(() => {});
}

/**
 * @private
 * @param {string} name
 * @param {object} payload
 */
function triggerAction(name, payload) {
	const action = this.getOption(`actions.${name}`);
	if (isFunction(action)) {
		action.call(this, payload);
	}
}

/**
 * @private
 * @param {HTMLElement} item
 */
function scheduleDisappear(item) {
	if (this.getOption("features.disappear") !== true) {
		return;
	}

	const delay =
		this.getOption("disappear.duration") ??
		this.getOption("disappear.time") ??
		3000;

	const file = [...this[fileItemMapSymbol].entries()].find(
		([, value]) => value === item,
	)?.[0];

	if (!file) {
		return;
	}

	if (this[fileTimeoutMapSymbol].has(file)) {
		clearTimeout(this[fileTimeoutMapSymbol].get(file));
	}

	const timeout = setTimeout(() => {
		item.classList.add("is-disappearing");
		setTimeout(() => {
			handleItemRemove.call(this, file);
		}, 250);
	}, delay);

	this[fileTimeoutMapSymbol].set(file, timeout);
}
/**
 * @private
 * @param {File} file
 * @return {string}
 */
function formatFileType(file) {
	if (isString(file.type) && file.type !== "") {
		return file.type;
	}
	return "unknown";
}

/**
 * @private
 * @param {File} file
 * @return {string}
 */
function formatFileExtension(file) {
	const parts = file.name.split(".");
	if (parts.length < 2) {
		return "file";
	}
	const ext = parts.pop() || "file";
	return ext.slice(0, 4).toLowerCase();
}

/**
 * @private
 * @param {number} bytes
 * @return {string}
 */
function formatFileSize(bytes) {
	if (!Number.isFinite(bytes)) {
		return "0 B";
	}
	const units = ["B", "KB", "MB", "GB"];
	let size = bytes;
	let unitIndex = 0;
	while (size >= 1024 && unitIndex < units.length - 1) {
		size /= 1024;
		unitIndex += 1;
	}
	return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`;
}

/**
 * @private
 * @param {string} kind
 * @return {string}
 */
function getIconMarkup(kind) {
	switch (kind) {
		case "reload":
			return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
  <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
  <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
</svg>`;
		case "success":
			return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
  <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
  <path d="m10.97 4.97-.02.022-3.473 4.425-2.093-2.094a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/>
</svg>`;
		case "error":
			return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
  <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
  <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
</svg>`;
		case "cancel":
		default:
			return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
  <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
  <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708"/>
</svg>`;
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div data-monster-role="dropzone" part="dropzone" tabindex="0"
                 data-monster-attributes="
                 class path:classes.dropzone, 
                 data-monster-disabled path:disabled | if:true">
                <div data-monster-role="content" part="content">
                    <div data-monster-role="title" part="title"
                         data-monster-replace="path:labels.title"></div>
                    <div data-monster-role="hint" part="hint"
                         data-monster-replace="path:labels.hint"></div>
                </div>
                <monster-button data-monster-role="button" part="button"
                                data-monster-attributes="
                                data-monster-button-class path:classes.button,
                                disabled path:disabled | if:true">
                    <span data-monster-replace="path:labels.button"></span>
                </monster-button>
                <input data-monster-role="input" part="input" type="file"
                       data-monster-attributes="
                       accept path:accept, 
                       multiple path:multiple | if:true, 
                       disabled path:disabled | if:true">
            </div>
            <div data-monster-role="status" part="status"></div>
            <ul data-monster-role="list" part="list"></ul>
        </div>`;
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				title: "Dokumente hochladen",
				hint: "oder Dateien hierhin ziehen",
				button: "Dateien wählen",
				statusIdle: "",
				statusUploading: "Upload läuft ...",
				statusSuccess: "Upload abgeschlossen.",
				statusError: "Upload fehlgeschlagen.",
				statusMissingUrl: "Keine Upload-URL konfiguriert.",
			};
		case "es":
			return {
				title: "Subir documentos",
				hint: "o arrastre archivos aquí",
				button: "Seleccionar archivos",
				statusIdle: "",
				statusUploading: "Cargando ...",
				statusSuccess: "Subida completa.",
				statusError: "Error al subir.",
				statusMissingUrl: "No se ha configurado la URL de carga.",
			};
		case "zh":
			return {
				title: "上传文档",
				hint: "或将文件拖到此处",
				button: "选择文件",
				statusIdle: "",
				statusUploading: "正在上传...",
				statusSuccess: "上传完成。",
				statusError: "上传失败。",
				statusMissingUrl: "未配置上传URL。",
			};
		case "hi":
			return {
				title: "दस्तावेज़ अपलोड करें",
				hint: "या फ़ाइलें यहाँ खींचें",
				button: "फ़ाइलें चुनें",
				statusIdle: "",
				statusUploading: "अपलोड हो रहा है...",
				statusSuccess: "अपलोड पूर्ण हुआ।",
				statusError: "अपलोड विफल।",
				statusMissingUrl: "अपलोड URL कॉन्फ़िगर नहीं है।",
			};
		case "bn":
			return {
				title: "ডকুমেন্ট আপলোড করুন",
				hint: "অথবা এখানে ফাইল টেনে আনুন",
				button: "ফাইল নির্বাচন করুন",
				statusIdle: "",
				statusUploading: "আপলোড হচ্ছে...",
				statusSuccess: "আপলোড সম্পন্ন।",
				statusError: "আপলোড ব্যর্থ।",
				statusMissingUrl: "আপলোড URL কনফিগার করা নেই।",
			};
		case "pt":
			return {
				title: "Enviar documentos",
				hint: "ou solte os arquivos aqui",
				button: "Selecionar arquivos",
				statusIdle: "",
				statusUploading: "Enviando...",
				statusSuccess: "Envio concluído.",
				statusError: "Falha no envio.",
				statusMissingUrl: "URL de envio não configurada.",
			};
		case "ru":
			return {
				title: "Загрузить документы",
				hint: "или перетащите файлы сюда",
				button: "Выбрать файлы",
				statusIdle: "",
				statusUploading: "Загрузка...",
				statusSuccess: "Загрузка завершена.",
				statusError: "Ошибка загрузки.",
				statusMissingUrl: "URL загрузки не настроен.",
			};
		case "ja":
			return {
				title: "ドキュメントをアップロード",
				hint: "またはここにファイルをドロップ",
				button: "ファイルを選択",
				statusIdle: "",
				statusUploading: "アップロード中...",
				statusSuccess: "アップロード完了。",
				statusError: "アップロードに失敗しました。",
				statusMissingUrl: "アップロードURLが設定されていません。",
			};
		case "pa":
			return {
				title: "ਦਸਤਾਵੇਜ਼ ਅਪਲੋਡ ਕਰੋ",
				hint: "ਜਾਂ ਫਾਈਲਾਂ ਇੱਥੇ ਖਿੱਚੋ",
				button: "ਫਾਈਲਾਂ ਚੁਣੋ",
				statusIdle: "",
				statusUploading: "ਅਪਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...",
				statusSuccess: "ਅਪਲੋਡ ਪੂਰਾ ਹੋ ਗਿਆ।",
				statusError: "ਅਪਲੋਡ ਫੇਲ੍ਹ ਹੋ ਗਿਆ।",
				statusMissingUrl: "ਅਪਲੋਡ URL ਸੰਰਚਿਤ ਨਹੀਂ ਹੈ।",
			};
		case "mr":
			return {
				title: "दस्तऐवज अपलोड करा",
				hint: "किंवा फायली येथे ओढा",
				button: "फायली निवडा",
				statusIdle: "",
				statusUploading: "अपलोड होत आहे...",
				statusSuccess: "अपलोड पूर्ण झाले.",
				statusError: "अपलोड अयशस्वी.",
				statusMissingUrl: "अपलोड URL संरचीत केलेले नाही.",
			};
		case "fr":
			return {
				title: "Téléverser des documents",
				hint: "ou déposer les fichiers ici",
				button: "Choisir des fichiers",
				statusIdle: "",
				statusUploading: "Téléversement en cours...",
				statusSuccess: "Téléversement terminé.",
				statusError: "Échec du téléversement.",
				statusMissingUrl: "URL de téléversement non configurée.",
			};
		case "it":
			return {
				title: "Carica documenti",
				hint: "oppure trascina i file qui",
				button: "Seleziona file",
				statusIdle: "",
				statusUploading: "Caricamento in corso...",
				statusSuccess: "Caricamento completato.",
				statusError: "Caricamento non riuscito.",
				statusMissingUrl: "URL di caricamento non configurata.",
			};
		case "nl":
			return {
				title: "Documenten uploaden",
				hint: "of sleep bestanden hierheen",
				button: "Bestanden kiezen",
				statusIdle: "",
				statusUploading: "Uploaden...",
				statusSuccess: "Upload voltooid.",
				statusError: "Upload mislukt.",
				statusMissingUrl: "Upload-URL niet geconfigureerd.",
			};
		case "sv":
			return {
				title: "Ladda upp dokument",
				hint: "eller dra filer hit",
				button: "Välj filer",
				statusIdle: "",
				statusUploading: "Uppladdning pågår...",
				statusSuccess: "Uppladdning klar.",
				statusError: "Uppladdning misslyckades.",
				statusMissingUrl: "Ingen uppladdnings-URL konfigurerad.",
			};
		case "pl":
			return {
				title: "Prześlij dokumenty",
				hint: "lub upuść pliki tutaj",
				button: "Wybierz pliki",
				statusIdle: "",
				statusUploading: "Przesyłanie...",
				statusSuccess: "Przesyłanie zakończone.",
				statusError: "Przesyłanie nieudane.",
				statusMissingUrl: "Brak skonfigurowanego URL przesyłania.",
			};
		case "da":
			return {
				title: "Upload dokumenter",
				hint: "eller træk filer her",
				button: "Vælg filer",
				statusIdle: "",
				statusUploading: "Uploader...",
				statusSuccess: "Upload fuldført.",
				statusError: "Upload mislykkedes.",
				statusMissingUrl: "Ingen upload-URL konfigureret.",
			};
		case "fi":
			return {
				title: "Lataa asiakirjoja",
				hint: "tai pudota tiedostot tähän",
				button: "Valitse tiedostot",
				statusIdle: "",
				statusUploading: "Siirretään...",
				statusSuccess: "Siirto valmis.",
				statusError: "Siirto epäonnistui.",
				statusMissingUrl: "Siirto-URL ei ole määritetty.",
			};
		case "no":
			return {
				title: "Last opp dokumenter",
				hint: "eller dra filer hit",
				button: "Velg filer",
				statusIdle: "",
				statusUploading: "Laster opp...",
				statusSuccess: "Opplasting fullført.",
				statusError: "Opplasting feilet.",
				statusMissingUrl: "Ingen opplastings-URL konfigurert.",
			};
		case "cs":
			return {
				title: "Nahrát dokumenty",
				hint: "nebo přetáhněte soubory sem",
				button: "Vybrat soubory",
				statusIdle: "",
				statusUploading: "Nahrávání...",
				statusSuccess: "Nahrávání dokončeno.",
				statusError: "Nahrávání selhalo.",
				statusMissingUrl: "URL pro nahrávání není nastavena.",
			};
		default:
		case "en":
			return {
				title: "Upload documents",
				hint: "or drop files here",
				button: "Choose files",
				statusIdle: "",
				statusUploading: "Uploading ...",
				statusSuccess: "Upload complete.",
				statusError: "Upload failed.",
				statusMissingUrl: "No upload URL configured.",
			};
	}
}

registerCustomElement(Dropzone);
