/**
 * 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 { CustomElement } from "../../dom/customelement.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { CameraCaptureStyleSheet } from "./stylesheet/camera-capture.mjs";
import "../form/button.mjs";
import "../state/state.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { Queue } from "../../types/queue.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";

import "../layout/full-screen.mjs";

export { CameraCapture };

/**
 * @private
 * @type {symbol}
 */
const controlElementSymbol = Symbol("copyElement");

/**
 * @private
 * @type {symbol}
 */
const videoElementSymbol = Symbol("videoElement");

/**
 * @private
 * @type {symbol}
 */
const fullScreenElementSymbol = Symbol("fullScreenElement");

/**
 * @private
 * @type {symbol}
 */
const takePictureButtonElementSymbol = Symbol("takePictureButtonElement");

/**
 * @private
 * @type {symbol}
 */
const canvasElementSymbol = Symbol("canvasElement");

/**
 * @private
 * @type {symbol}
 */
const queueSymbol = Symbol("queue");

/**
 * @private
 * @type {symbol}
 */
const emptyHistoryStateElementSymbol = Symbol("emptyHistoryStateElement");

/**
 * This is a camera capture component.
 *
 * @fragments /fragments/components/content/camera-capture/
 *
 * @example /examples/components/content/camera-capture-simple/
 *
 * @since 3.111.0
 * @copyright Volker Schukai
 * @summary A camera capture component for taking images directly from an available device camera.
 * @fires monster-camera-capture-captured
 */
class CameraCapture extends CustomElement {
	/**
	 * Constructor for the CameraCapture class.
	 * Calls the parent class constructor.
	 */
	constructor() {
		super();

		this[queueSymbol] = new Queue();
	}

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

	/**
	 *
	 * @return {Components.Content.Copy
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initEventHandler.call(this);
		setupLazyCameraInit.call(this);
		return this;
	}

	/**
	 * 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} actions Callbacks
	 * @property {string} actions.click="throw Error" Callback when clicked
	 * @property {Object} features Features
	 * @property {boolean} features.stripTags=true Strip tags from the copied text
	 * @property {boolean} features.preventOpenEventSent=false Prevent open event from being sent
	 * @property {Object} popper Popper configuration
	 * @property {string} popper.placement="top" Popper placement
	 * @property {string[]} popper.middleware=["autoPlacement", "shift", "offset:15", "arrow"] Popper middleware
	 * @property {boolean} disabled=false Disabled state
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			disabled: false,
			features: {},

			labels: getTranslations(),
		});
	}

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

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

	/**
	 * Retrieve the next image from the queue.
	 * If the queue is empty, it returns `null`.
	 * @returns {string|null}
	 */
	getNextImage() {
		if (!this[queueSymbol].isEmpty()) {
			const next = this[queueSymbol].poll();
			if (!next) {
				return null;
			}
			return next;
		}
		return null;
	}

	/**
	 * Capture an image from the camera and add it to the queue.
	 * The image is returned as a data URL.
	 * @returns {string}
	 */
	capture() {
		this[canvasElementSymbol].width = this[videoElementSymbol].videoWidth;
		this[canvasElementSymbol].height = this[videoElementSymbol].videoHeight;
		const ctx = this[canvasElementSymbol].getContext("2d");
		ctx.drawImage(
			this[videoElementSymbol],
			0,
			0,
			this[canvasElementSymbol].width,
			this[canvasElementSymbol].height,
		);
		const dataURL = this[canvasElementSymbol].toDataURL("image/png");
		this[queueSymbol].add(dataURL);
		return dataURL;
	}
}

/**
 * @private
 */
function setupLazyCameraInit() {
	const self = this;

	let initialized = false;

	const observer = new IntersectionObserver(
		(entries) => {
			for (const entry of entries) {
				if (entry.isIntersecting && !initialized) {
					initialized = true;
					observer.disconnect();
					initCameraControl.call(self);
				}
			}
		},
		{
			root: null,
			threshold: 0.1,
		},
	);

	observer.observe(this);
}

/**
 * @private
 */
function initCameraControl() {
	const self = this;

	if (
		!navigator ||
		!navigator.mediaDevices ||
		!navigator.mediaDevices.getUserMedia
	) {
		self[fullScreenElementSymbol].style.display = "none";
		addErrorAttribute(self, "Browser not supported");
		return;
	}

	navigator.mediaDevices
		.getUserMedia({ video: true })
		.then((stream) => {
			stream.getTracks().forEach((track) => track.stop());
			return navigator.mediaDevices.enumerateDevices();
		})
		.then((devices) => {
			const cameras = devices.filter((device) => device.kind === "videoinput");

			if (cameras.length === 0) {
				addErrorAttribute(
					self,
					getTranslations().cameraNotSupportedOrNotAllowed,
				);
				return;
			}

			if (cameras.length > 1) {
				const select = document.createElement("select");
				select.setAttribute("data-monster-role", "cameraSelector");
				select.style.marginBottom = "0.5rem";
				select.style.width = "100%";
				select.style.maxWidth = "100%";
				select.style.height = "1.4rem";
				select.style.marginTop = "0";
				select.style.marginBottom = "0";

				cameras.forEach((camera, index) => {
					const option = document.createElement("option");
					option.value = camera.deviceId;
					option.text = camera.label || `Camera ${index + 1}`;
					select.appendChild(option);
				});

				select.addEventListener("change", () => {
					startCameraWithDeviceId.call(self, select.value);
				});

				self[fullScreenElementSymbol].style.top = "2rem";
				self[controlElementSymbol].insertBefore(
					select,
					self[videoElementSymbol],
				);
			}

			startCameraWithDeviceId.call(self, cameras[0].deviceId);
		})
		.catch((err) => {
			self[fullScreenElementSymbol].style.display = "none";
			addErrorAttribute(self, err);
		});
}

function startCameraWithDeviceId(deviceId) {
	const self = this;
	navigator.mediaDevices
		.getUserMedia({ video: { deviceId: { exact: deviceId } } })
		.then(function (stream) {
			self[takePictureButtonElementSymbol].style.display = "block";
			self[videoElementSymbol].style.display = "block";
			self[emptyHistoryStateElementSymbol].style.display = "none";

			self[videoElementSymbol].srcObject = stream;

			self[fullScreenElementSymbol].style.display = "block";
		})
		.catch(function (e) {
			self[fullScreenElementSymbol].style.display = "none";
			addErrorAttribute(self, e);
		});
}

/**
 * @private
 * @returns {{takePicture: string}}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				takePicture: "Bild aufnehmen",
				cameraNotSupportedOrNotAllowed:
					"Die Kamera wird nicht unterstützt oder die Berechtigung wurde nicht erteilt.",
			};

		case "es":
			return {
				takePicture: "Tomar una foto",
				cameraNotSupportedOrNotAllowed:
					"La cámara no es compatible o no se ha otorgado permiso.",
			};

		case "zh":
			return {
				takePicture: "拍照",
				cameraNotSupportedOrNotAllowed: "相机不受支持或未授予权限。",
			};

		case "hi":
			return {
				takePicture: "तस्वीर खींचें",
				cameraNotSupportedOrNotAllowed:
					"कैमरा समर्थित नहीं है या अनुमति नहीं दी गई है।",
			};

		case "bn":
			return {
				takePicture: "ছবি তুলুন",
				cameraNotSupportedOrNotAllowed: "ক্যামেরা সমর্থিত নয় বা অনুমতি দেয়া হয়নি।",
			};

		case "pt":
			return {
				takePicture: "Tirar uma foto",
				cameraNotSupportedOrNotAllowed:
					"A câmera não é suportada ou a permissão não foi concedida.",
			};

		case "ru":
			return {
				takePicture: "Сделать фото",
				cameraNotSupportedOrNotAllowed:
					"Камера не поддерживается или разрешение не предоставлено.",
			};

		case "ja":
			return {
				takePicture: "写真を撮る",
				cameraNotSupportedOrNotAllowed:
					"カメラがサポートされていないか、許可が付与されていません。",
			};

		case "pa":
			return {
				takePicture: "ਤਸਵੀਰ ਖਿੱਚੋ",
				cameraNotSupportedOrNotAllowed:
					"ਕੈਮਰਾ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਹੈ ਜਾਂ ਅਨੁਮਤੀ ਨਹੀਂ ਦਿੱਤੀ ਗਈ ਹੈ।",
			};

		case "mr":
			return {
				takePicture: "फोटो घ्या",
				cameraNotSupportedOrNotAllowed:
					"कॅमेरा समर्थित नाही किंवा परवानगी दिलेली नाही.",
			};

		case "fr":
			return {
				takePicture: "Prendre une photo",
				cameraNotSupportedOrNotAllowed:
					"La caméra n'est pas prise en charge ou l'autorisation n'a pas été accordée.",
			};

		case "it":
			return {
				takePicture: "Scattare una foto",
				cameraNotSupportedOrNotAllowed:
					"La fotocamera non è supportata o l'autorizzazione non è stata concessa.",
			};

		case "nl":
			return {
				takePicture: "Maak een foto",
				cameraNotSupportedOrNotAllowed:
					"De camera wordt niet ondersteund of er is geen toestemming verleend.",
			};

		case "sv":
			return {
				takePicture: "Ta ett foto",
				cameraNotSupportedOrNotAllowed:
					"Kameran stöds inte eller tillståndet har inte beviljats.",
			};

		case "pl":
			return {
				takePicture: "Zrób zdjęcie",
				cameraNotSupportedOrNotAllowed:
					"Kamera nie jest obsługiwana lub nie udzielono zgody.",
			};

		case "da":
			return {
				takePicture: "Tag et billede",
				cameraNotSupportedOrNotAllowed:
					"Kameraen understøttes ikke eller tilladelsen er ikke givet.",
			};

		case "fi":
			return {
				takePicture: "Ota kuva",
				cameraNotSupportedOrNotAllowed:
					"Kameraa ei tueta tai lupaa ei ole myönnetty.",
			};

		case "no":
			return {
				takePicture: "Ta et bilde",
				cameraNotSupportedOrNotAllowed:
					"Kameraen støttes ikke eller tillatelsen er ikke gitt.",
			};

		case "cs":
			return {
				takePicture: "Vyfotit",
				cameraNotSupportedOrNotAllowed:
					"Fotoaparát není podporován nebo povolení nebylo uděleno.",
			};

		case "en":
		default:
			return {
				takePicture: "Take a picture",
				cameraNotSupportedOrNotAllowed:
					"The camera is not supported or permission has not been granted.",
			};
	}
}

/**
 * @private
 * @return {initEventHandler}
 * @fires monster-camera-capture-captured
 */
function initEventHandler() {
	const self = this;

	this[takePictureButtonElementSymbol].setOption("actions.click", function () {
		self.capture();

		fireCustomEvent(self, "monster-camera-capture-captured", {
			element: self,
		});
	});

	return this;
}

/**
 * @private
 * @return {void}
 */
function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="control"]`,
	);

	this[takePictureButtonElementSymbol] = this.shadowRoot.querySelector(
		`[data-monster-role="takePicture"]`,
	);

	this[videoElementSymbol] = this.shadowRoot.querySelector(`video`);

	this[canvasElementSymbol] = this.shadowRoot.querySelector(`canvas`);

	this[emptyHistoryStateElementSymbol] = this.shadowRoot.querySelector(
		`[data-monster-role="emptyHistoryState"]`,
	);

	this[fullScreenElementSymbol] = this.shadowRoot.querySelector(
		`[data-monster-role="full-screen"]`,
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <monster-full-screen part="full-screen" style="display: none" data-monster-role="full-screen"
                                 data-monster-option-selector="[data-monster-role=control]"></monster-full-screen>
            <monster-state data-monster-role="emptyHistoryState">
                <svg slot="visual" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                     width="350"
                     height="350" viewBox="0 0 92.604 92.604">
                    <defs>
                        <linearGradient id="a" x1="336.587" x2="372.879" y1="218.625" y2="218.625"
                                        gradientUnits="userSpaceOnUse" spreadMethod="pad">
                            <stop offset="0" style="stop-color:#fefe00"/>
                            <stop offset="1" style="stop-color:#ff43c6"/>
                        </linearGradient>
                        <linearGradient xlink:href="#a" id="p" x1="-2894.157" x2="-805.215" y1="-4285.143"
                                        y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
                                        gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
                        <linearGradient xlink:href="#a" id="q" x1="-2894.157" x2="-805.215" y1="-4285.143"
                                        y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
                                        gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
                        <linearGradient xlink:href="#a" id="o" x1="-2894.157" x2="-805.215" y1="-4285.143"
                                        y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
                                        gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
                    </defs>
                    <g style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#o);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1">
                        <path d="M8.983 34.64a9.33 9.33 0 0 1 9.33-9.33H74.29a9.33 9.33 0 0 1 9.33 9.33v13.995a9.33 9.33 0 0 1-9.33 9.33h-22.07c.673.755 1.54 1.51 2.478 2.215a32.7 32.7 0 0 0 4.23 2.66l.066.027.014.01a2.332 2.332 0 0 1-1.045 4.417H34.64a2.332 2.332 0 0 1-1.045-4.417l.014-.01.065-.033a23 23 0 0 0 1.25-.69 33 33 0 0 0 2.981-1.965c.933-.7 1.806-1.46 2.477-2.215h-22.07a9.33 9.33 0 0 1-9.329-9.33Zm9.33-4.665a4.665 4.665 0 0 0-4.665 4.665v13.995a4.665 4.665 0 0 0 4.665 4.664H74.29a4.665 4.665 0 0 0 4.665-4.664V34.64a4.665 4.665 0 0 0-4.665-4.665z"
                              style="font-variation-settings:normal;vector-effect:none;fill:url(#p);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/>
                        <path d="M46.302 36.972a4.665 4.665 0 1 0 0 9.33 4.665 4.665 0 0 0 0-9.33m-9.33 4.665a9.33 9.33 0 1 1 18.66 0 9.33 9.33 0 0 1-18.66 0m32.655 0a2.332 2.332 0 1 1-4.665 0 2.332 2.332 0 0 1 4.665 0"
                              style="font-variation-settings:normal;vector-effect:none;fill:url(#q);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/>
                    </g>
                </svg>

                <p class="camera-not-supported-text"
                   data-monster-replace="path:labels.cameraNotSupportedOrNotAllowed | default:there was an error:string"></p>
            </monster-state>


            <video autoplay style="display:none"></video>
            <canvas style="display:none;"></canvas>
            <div>
                <monster-button part="takePictureButton" style="display:none"
                                data-monster-role="takePicture" data-monster-attributes="classes.takePictureButton"
                                data-monster-replace="path:labels.takePicture"></monster-button>
            </div>
        </div>`;
}

registerCustomElement(CameraCapture);
