import { icon } from '@fortawesome/fontawesome-svg-core';
import { getIcon } from '@scripts/icons';

export default class Lightbox {
	constructor(options = {}) {
		this.album = [];
		this.currentImageIndex = 0;
		this.options = {
			alwaysShowNavOnTouchDevices: false,
			fadeDuration: 600,
			imageFadeDuration: 600,
			positionFromTop: 50,
			resizeDuration: 700,
			wrapAround: false,
			disableScrolling: false,
			sanitizeTitle: false,
			hasVideo: true,
			onMediaChange: () => {},
			onClosing: () => {}
		};
		this.option(options);
		this.gMouseDownOffsetX = 0;
		this.gMouseDownOffsetY = 0;
		this.resizeTimer = null;
		this.boundOnBodyClick = this.onBodyClick.bind(this);
		this.boundOnResize = this.sizeOverlay.bind(this);
		this.boundOnKeyUp = this.keyboardAction.bind(this);
		this.boundOnWheel = this.onWheel.bind(this);
		this.boundOnDragStart = this.onDragStart.bind(this);
		this.boundOnDragMove = this.onDragMove.bind(this);
		this.boundOnDragEnd = this.onDragEnd.bind(this);
		this.init();
	}

	option(options = {}) {
		Object.assign(this.options, options);
	}

	init() {
		if (document.readyState === 'loading') {
			document.addEventListener('DOMContentLoaded', () => {
				this.build();
				this.enable();
			}, { once: true });
		} else {
			this.build();
			this.enable();
		}
	}

	enable() {
		document.body.addEventListener('click', this.boundOnBodyClick);
	}

	disable() {
		document.body.removeEventListener('click', this.boundOnBodyClick);
	}

	onBodyClick(event) {
		const link = event.target.closest('a[data-lightbox], area[data-lightbox]');
		if (!link) return;
		event.preventDefault();
		this.start(link);
	}

	renderIcon(name, sClass=null) {
		return icon(getIcon(name), {classes: ['spot-icon', name, sClass]}).html;
	}

	build() {
		if (!document.getElementById('lightbox')) {
			const wrapper = document.createElement('div');
			wrapper.innerHTML = `
					<div id="lightboxOverlay" tabindex="-1" class="lightboxOverlay"></div>
					<div id="lightbox" tabindex="-1" class="lightbox">
						<div class="lb-outerContainer">
							<div class="lb-container">
								<img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" alt="" />
								<video class="lb-video" controls autoplay></video>
								<div class="lb-nav">
									<div class="lb-prev-area">
										<a class="lb-prev" aria-label="Previous image" href="" role="button">${this.renderIcon('prev')}</a>
									</div>
									<div class="lb-next-area">
										<a class="lb-next" aria-label="Next image" href="" role="button">${this.renderIcon('next')}</a>
									</div>
								</div>
								<div class="lb-loader">
									<a class="lb-cancel" href="#">${this.renderIcon('cancel')}</a>
								</div>
							</div>
							<div class="lb-dataContainer desktop">
								<div class="lb-data">
									<div class="lb-details">
										<span class="lb-caption"></span>
									</div>
									<div class="lb-closeContainer">
										<a class="lb-close" href="#" role="button">${this.renderIcon('close', 'fa-lg')}</a>
									</div>
								</div>
							</div>
						</div>
					</div>
					`;
			document.body.append(...wrapper.children);
		}

		this.overlay = document.getElementById('lightboxOverlay');
		this.lightbox = document.getElementById('lightbox');
		this.outerContainer = this.lightbox.querySelector('.lb-outerContainer');
		this.container = this.lightbox.querySelector('.lb-container');
		this.image = this.lightbox.querySelector('.lb-image');
		this.nav = this.lightbox.querySelector('.lb-nav');
		this.loader = this.lightbox.querySelector('.lb-loader');
		this.caption = this.lightbox.querySelector('.lb-caption');
		this.closeButton = this.lightbox.querySelector('.lb-close');
		this.dataContainer = this.lightbox.querySelector('.lb-dataContainer');
		this.prev = this.lightbox.querySelector('.lb-prev');
		this.next = this.lightbox.querySelector('.lb-next');
		this.video = this.lightbox.querySelector('.lb-video');

		this.setVisible(this.overlay, false);
		this.setVisible(this.lightbox, false);

		this.containerPadding = this.getBoxMetrics(this.container, 'padding');
		this.imageBorderWidth = this.getBoxMetrics(this.image, 'border');
		this.videoBorderWidth = this.getBoxMetrics(this.video, 'border');

		this.overlay.addEventListener('click', () => this.end());
		this.dataContainer.addEventListener('click', () => this.end());
		this.lightbox.addEventListener('click', (event) => {
			if (event.target === this.lightbox) this.end();
		});
		this.outerContainer.addEventListener('click', (event) => {
			if (event.target === this.outerContainer) this.end();
			event.stopPropagation();
		});

		this.prev.addEventListener('click', (event) => {
			event.preventDefault();
			if (this.currentImageIndex === 0) this.changeImage(this.album.length - 1);
			else this.changeImage(this.currentImageIndex - 1);
		});
		this.next.addEventListener('click', (event) => {
			event.preventDefault();
			if (this.currentImageIndex === this.album.length - 1) this.changeImage(0);
			else this.changeImage(this.currentImageIndex + 1);
		});

		this.loader.addEventListener('click', (event) => {
			event.preventDefault();
			this.end();
		});
		this.closeButton.addEventListener('click', (event) => {
			event.preventDefault();
			event.stopPropagation();
			this.end();
		});
		this.closeButton.addEventListener('keyup', (event) => {
			if (event.key === 'Enter' || event.key === ' ') this.end();
		});

		this.nav.addEventListener('wheel', this.boundOnWheel, { passive: false });
		this.nav.addEventListener('mousedown', this.boundOnDragStart);
		window.addEventListener('mouseup', this.boundOnDragEnd);
	}

	getBoxMetrics(element, type) {
		const styles = getComputedStyle(element);
		return {
			top: parseInt(styles[`${type}-top-width`], 10) || 0,
			right: parseInt(styles[`${type}-right-width`], 10) || 0,
			bottom: parseInt(styles[`${type}-bottom-width`], 10) || 0,
			left: parseInt(styles[`${type}-left-width`], 10) || 0
		};
	}

	start(link) {
		this.sizeOverlay();
		this.album = [];
		let imageNumber = 0;
		const setName = link.getAttribute('data-lightbox');

		const links = [...document.querySelectorAll(`${link.tagName}[data-lightbox="${CSS.escape(setName)}"]`)];
		links.forEach((item, index) => {
			this.addToAlbum(item);
			if (item === link) imageNumber = index;
		});

		this.fade(this.overlay, true, this.options.fadeDuration);
		this.fade(this.lightbox, true, this.options.fadeDuration);

		if (this.options.disableScrolling) document.body.classList.add('lb-disable-scrolling');

		window.addEventListener('resize', this.boundOnResize);
		this.changeImage(imageNumber);
	}

	addToAlbum(link) {
		const img = link.querySelector('img');
		this.album.push({
			alt: link.getAttribute('data-alt') || '',
			link: link.getAttribute('href'),
			title: link.getAttribute('data-title') || link.getAttribute('title') || '',
			orientation: parseInt(link.getAttribute('data-orientation') || '0', 10),
			type: link.getAttribute('data-type') || 'image',
			id: link.getAttribute('data-id'),
			width: parseInt(img?.getAttribute('width') || '0', 10),
			height: parseInt(img?.getAttribute('height') || '0', 10),
			set: link.getAttribute('data-lightbox') || ''
		});
	}

	hasMediaAfterCurrent() {
		return this.currentImageIndex < this.album.length - 1;
	}

	refreshAlbum() {
		const current = this.album[this.currentImageIndex];
		if (!current?.set) return;

		const links = [...document.querySelectorAll(`a[data-lightbox="${CSS.escape(current.set)}"], area[data-lightbox="${CSS.escape(current.set)}"]`)];
		if (!links.length) return;

		const existingKeys = new Set(this.album.map((media) => this.getMediaKey(media)));
		links.forEach((link) => {
			const key = this.getLinkMediaKey(link);
			if (existingKeys.has(key)) return;

			this.addToAlbum(link);
			existingKeys.add(key);
		});

		this.updateNav();
	}

	getMediaKey(media) {
		return `${media.set}:${media.id}`;
	}

	getLinkMediaKey(link) {
		return `${link.getAttribute('data-lightbox') || ''}:${link.getAttribute('data-id')}`;
	}

	getMaxSizes(mediaWidth, mediaHeight, mediaType) {
		let maxWidth = window.innerWidth - this.containerPadding.left - this.containerPadding.right;
		let maxHeight = window.innerHeight - this.containerPadding.top - this.containerPadding.bottom - this.options.positionFromTop;
		const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
		maxWidth -= border.left + border.right;
		maxHeight -= border.top + border.bottom;
		maxHeight -= this.getDataContainerHeight(maxWidth + this.containerPadding.left + this.containerPadding.right + border.left + border.right);

		return {
			maxWidth: Math.max(maxWidth, 1),
			maxHeight: Math.max(maxHeight, 1)
		};
	}

	getDataContainerHeight(width = null) {
		if (!this.dataContainer) return 0;

		const currentWidth = this.dataContainer.style.width;
		if (width !== null) this.dataContainer.style.width = `${width}px`;
		const height = Math.ceil(this.dataContainer.getBoundingClientRect().height || this.dataContainer.offsetHeight || 0);
		this.dataContainer.style.width = currentWidth;

		return height;
	}

	getMediaSize(media, maxWidth, maxHeight) {
		if (media.width <= maxWidth && media.height <= maxHeight) {
			return {
				width: media.width,
				height: media.height
			};
		}

		const widthRatio = media.width / maxWidth;
		const heightRatio = media.height / maxHeight;

		if (widthRatio > heightRatio) {
			return {
				width: maxWidth,
				height: Math.round(media.height / widthRatio)
			};
		}

		return {
			width: Math.round(media.width / heightRatio),
			height: maxHeight
		};
	}

	fitSizeWithDataContainer(size, mediaType) {
		const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
		const maxOuterHeight = Math.max(window.innerHeight - this.options.positionFromTop, 1);
		let fittedSize = size;

		for (let i = 0; i < 5; i++) {
			const containerWidth = fittedSize.width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
			const containerHeight = fittedSize.height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
			const dataHeight = this.getDataContainerHeight(containerWidth);
			const overflow = Math.ceil(containerHeight + dataHeight - maxOuterHeight);
			if (overflow <= 0 || fittedSize.height <= 1) break;

			const height = Math.max(fittedSize.height - overflow, 1);
			fittedSize = {
				width: Math.max(Math.round(fittedSize.width * (height / fittedSize.height)), 1),
				height
			};
		}

		return fittedSize;
	}

	updateSize(index) {
		const media = this.album[index];
		const maxSizes = this.getMaxSizes(media.width, media.height, media.type);
		const maxWidth = this.options.maxWidth ? Math.min(this.options.maxWidth, maxSizes.maxWidth) : maxSizes.maxWidth;
		const maxHeight = this.options.maxHeight ? Math.min(this.options.maxHeight, maxSizes.maxHeight) : maxSizes.maxHeight;
		const size = this.fitSizeWithDataContainer(this.getMediaSize(media, maxWidth, maxHeight), media.type);

		const target = media.type === 'video' ? this.video : this.image;
		target.width = size.width;
		target.height = size.height;
		this.sizeContainer(size.width, size.height, media.type);
	}

	changeImage(index) {
		const media = this.album[index];
		if (!media) return;

		this.updateDetails(media, false);
		this.hideElements([this.dataContainer]);
		this.disableKeyboardNav();
		this.fade(this.overlay, true, this.options.fadeDuration);
		this.fade(this.loader, true, 200);
		this.hideElements([this.image, this.video, this.nav, this.prev, this.next]);
		this.resetImageTransform();
		this.outerContainer.classList.add('animating');
		this.container.classList.remove('moveable', 'moving', 'lb-video-nav');
		this.currentImageIndex = index;

		this.options.onMediaChange(media);

		if (media.type === 'video') {
			this.image.removeAttribute('src');
			this.container.classList.add('lb-video-nav');
			this.video.onloadedmetadata = () => {
				media.width = this.video.videoWidth;
				media.height = this.video.videoHeight;
				this.video.onloadedmetadata = null;
				this.updateSize(index);
			};
			this.video.src = media.link;
		} else {
			this.video.pause();
			this.video.removeAttribute('src');
			this.image.onload = () => {
				this.image.alt = media.alt;
				let width = this.image.naturalWidth;
				let height = this.image.naturalHeight;
				if (Math.abs(media.orientation) === 90 && width > height) {
					const tmp = width;
					width = height;
					height = tmp;
				}
				media.width = width;
				media.height = height;
				this.image.onload = null;
				this.updateSize(index);
			};
			this.image.src = media.link;
		}
	}

	sizeOverlay() {
		if (this.resizeTimer) clearTimeout(this.resizeTimer);
		if (!this.album.length) return;

		this.resizeTimer = window.setTimeout(() => {
			const current = this.album[this.currentImageIndex];
			if (!current) return;
			if (current.type === 'image') this.changeImage(this.currentImageIndex);
			else this.updateSize(this.currentImageIndex);
		}, 200);
	}

	sizeContainer(width, height, mediaType = 'image') {
		const border = mediaType === 'image' ? this.imageBorderWidth : this.videoBorderWidth;
		const newWidth = width + this.containerPadding.left + this.containerPadding.right + border.left + border.right;
		const newHeight = height + this.containerPadding.top + this.containerPadding.bottom + border.top + border.bottom;
		const dataHeight = this.getDataContainerHeight(newWidth);

		this.outerContainer.style.transition = `width ${this.options.resizeDuration}ms, height ${this.options.resizeDuration}ms`;
		this.outerContainer.style.width = `${newWidth}px`;
		this.outerContainer.style.height = `${newHeight + dataHeight}px`;
		this.container.style.height = `${newHeight}px`;

		window.setTimeout(() => {
			this.overlay.focus();
			this.showImage();
			this.outerContainer.style.transition = '';
		}, this.options.resizeDuration);
	}

	showImage() {
		this.fade(this.loader, false, 0);
		if (this.options.hasVideo && this.album[this.currentImageIndex].type === 'video') this.fade(this.video, true, this.options.imageFadeDuration);
		else this.fade(this.image, true, this.options.imageFadeDuration);

		this.updateNav();
		this.updateDetails();
		this.preloadNeighboringImages();
		this.enableKeyboardNav();
	}

	updateNav() {
		this.setVisible(this.nav, true);
		this.setVisible(this.prev, false);
		this.setVisible(this.next, false);

		const alwaysShowNav = ('ontouchstart' in window) && this.options.alwaysShowNavOnTouchDevices;
		if (this.album.length <= 1) return;

		if (this.options.wrapAround) {
			this.setVisible(this.prev, true);
			this.setVisible(this.next, true);
		} else {
			if (this.currentImageIndex > 0) this.setVisible(this.prev, true);
			if (this.currentImageIndex < this.album.length - 1) this.setVisible(this.next, true);
		}

		if (alwaysShowNav) {
			this.prev.style.opacity = '1';
			this.next.style.opacity = '1';
		} else {
			this.prev.style.opacity = '';
			this.next.style.opacity = '';
		}
	}

	updateDetails(media = this.album[this.currentImageIndex], show = true) {
		if (!media) return;

		if (media.title) {
			if (this.options.sanitizeTitle) this.caption.textContent = media.title;
			else this.caption.innerHTML = media.title;
			if (show) this.fade(this.caption, true, 200);
			else this.setVisible(this.caption, true);
		} else {
			this.caption.textContent = '';
			this.setVisible(this.caption, false);
		}

		if (show) {
			this.fade(this.closeButton, true, 200);
			this.outerContainer.classList.remove('animating');
			this.fade(this.dataContainer, true, this.options.resizeDuration);
		} else {
			this.setVisible(this.closeButton, true);
			this.setVisible(this.dataContainer, false);
			this.dataContainer.style.transition = '';
			this.dataContainer.style.opacity = '0';
		}
	}

	preloadNeighboringImages() {
		const next = this.album[this.currentImageIndex + 1];
		const prev = this.album[this.currentImageIndex - 1];
		if (next && next.type === 'image') {
			const preloadNext = new Image();
			preloadNext.src = next.link;
		}
		if (prev && prev.type === 'image') {
			const preloadPrev = new Image();
			preloadPrev.src = prev.link;
		}
	}

	enableKeyboardNav() {
		this.disableKeyboardNav();
		this.lightbox.addEventListener('keyup', this.boundOnKeyUp);
		this.overlay.addEventListener('keyup', this.boundOnKeyUp);
	}

	disableKeyboardNav() {
		this.lightbox?.removeEventListener('keyup', this.boundOnKeyUp);
		this.overlay?.removeEventListener('keyup', this.boundOnKeyUp);
	}

	keyboardAction(event) {
		switch (event.key) {
			case 'Escape':
				event.stopPropagation();
				this.end();
				break;
			case 'ArrowLeft':
				if (this.currentImageIndex !== 0) this.changeImage(this.currentImageIndex - 1);
				else if (this.options.wrapAround && this.album.length > 1) this.changeImage(this.album.length - 1);
				break;
			case 'ArrowRight':
				if (this.currentImageIndex !== this.album.length - 1) this.changeImage(this.currentImageIndex + 1);
				else if (this.options.wrapAround && this.album.length > 1) this.changeImage(0);
				break;
		}
	}

	onWheel(event) {
		const media = this.album[this.currentImageIndex];
		if (!media || media.type === 'video') return;
		event.preventDefault();

		const rect = this.image.getBoundingClientRect();
		const oldTransform = this.getImageTransform();
		const oldZoom = oldTransform.scale;
		const maxZoom = Math.max(media.width / Math.max(this.image.width, 1), media.height / Math.max(this.image.height, 1), 1);
		const newZoom = Math.min(Math.max(oldZoom + (-Math.sign(event.deltaY) / 10), 1), maxZoom);

		const imageCenterX = rect.left + rect.width / 2 - oldTransform.translateX;
		const imageCenterY = rect.top + rect.height / 2 - oldTransform.translateY;
		const cursorX = event.clientX - imageCenterX;
		const cursorY = event.clientY - imageCenterY;
		const zoomRatio = newZoom / oldZoom;
		const transform = this.clampImageTransform({
			scale: newZoom,
			translateX: cursorX - zoomRatio * (cursorX - oldTransform.translateX),
			translateY: cursorY - zoomRatio * (cursorY - oldTransform.translateY)
		});

		this.container.classList.toggle('moveable', newZoom > 1);
		this.setImageTransform(transform);
	}

	onDragStart(event) {
		const scale = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
		if (scale <= 1) return;

		this.gMouseDownOffsetX = event.clientX - parseFloat(this.image.style.getPropertyValue('--translate-x') || '0');
		this.gMouseDownOffsetY = event.clientY - parseFloat(this.image.style.getPropertyValue('--translate-y') || '0');
		this.container.classList.add('moving');
		window.addEventListener('mousemove', this.boundOnDragMove);
	}

	onDragMove(event) {
		const zoom = parseFloat(this.image.style.getPropertyValue('--scale') || '1');
		const transform = this.clampImageTransform({
			scale: zoom,
			translateX: event.clientX - this.gMouseDownOffsetX,
			translateY: event.clientY - this.gMouseDownOffsetY
		});

		this.setImageTransform(transform);
	}

	onDragEnd() {
		window.removeEventListener('mousemove', this.boundOnDragMove);
		this.container?.classList.remove('moving');
	}

	resetImageTransform() {
		this.setImageTransform({scale: 1, translateX: 0, translateY: 0});
	}

	getImageTransform() {
		return {
			scale: parseFloat(this.image.style.getPropertyValue('--scale') || '1'),
			translateX: parseFloat(this.image.style.getPropertyValue('--translate-x') || '0'),
			translateY: parseFloat(this.image.style.getPropertyValue('--translate-y') || '0')
		};
	}

	clampImageTransform(transform) {
		const maxTranslateX = (transform.scale - 1) * this.image.width / 2;
		const maxTranslateY = (transform.scale - 1) * this.image.height / 2;

		return {
			scale: transform.scale,
			translateX: Math.max(Math.min(transform.translateX, maxTranslateX), -maxTranslateX),
			translateY: Math.max(Math.min(transform.translateY, maxTranslateY), -maxTranslateY)
		};
	}

	setImageTransform(transform) {
		if (!this.image) return;
		this.image.style.setProperty('--scale', String(transform.scale));
		this.image.style.setProperty('--translate-x', `${transform.translateX}px`);
		this.image.style.setProperty('--translate-y', `${transform.translateY}px`);
	}

	hideElements(elements) {
		elements.forEach((element) => {
			this.setVisible(element, false);
		});
	}

	setVisible(element, visible) {
		if (!element) return;
		element.style.visibility = visible ? 'visible' : 'hidden';
		element.style.pointerEvents = visible ? '' : 'none';
	}

	fade(element, show, duration, done) {
		if (!element) return;

		const safeDuration = duration || 0;
		element.style.transition = `opacity ${safeDuration}ms`;
		if (show) {
			this.setVisible(element, true);
			requestAnimationFrame(() => {
				element.style.opacity = element === this.overlay ? '0.8' : '1';
			});
		} else {
			element.style.opacity = '0';
			element.style.pointerEvents = 'none';
			window.setTimeout(() => {
				this.setVisible(element, false);
			}, safeDuration);
		}

		if (typeof done === 'function') {
			window.setTimeout(done, safeDuration);
		}
	}

	end(dispose = false) {
		this.disableKeyboardNav();
		this.video?.pause();
		this.video?.removeAttribute('src');
		this.container?.classList.remove('lb-video-nav', 'moveable', 'moving');
		window.removeEventListener('resize', this.boundOnResize);
		window.removeEventListener('mousemove', this.boundOnDragMove);
		
		if(dispose){
			this.disable();
			if(this.resizeTimer) clearTimeout(this.resizeTimer);
			window.removeEventListener('mouseup', this.boundOnDragEnd);
			this.lightbox?.remove();
			this.overlay?.remove();
			this.album = [];
		}
		else {
			this.fade(this.lightbox, false, this.options.fadeDuration);
			this.fade(this.overlay, false, this.options.fadeDuration);
			this.options.onClosing();
		}

		if (this.options.disableScrolling) document.body.classList.remove('lb-disable-scrolling');
	}
}
