/* Customer Cloud Gallery frontend gallery */
(function () {
	'use strict';

	const ICONS = {
		heart: '<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M12 21s-7.2-4.55-9.7-9.05C.4 8.3 2.9 4 7 4c2.1 0 3.7 1.1 5 2.9C13.3 5.1 14.9 4 17 4c4.1 0 6.6 4.3 4.7 7.95C19.2 16.45 12 21 12 21z"/></svg>',
		check: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="4 12 10 18 20 6"/></svg>',
		download: '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 3v12M6 11l6 6 6-6M4 21h16"/></svg>',
		play: '<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="11" fill="rgba(0,0,0,0.55)"/><path d="M9.5 7.8v8.4L16.5 12z" fill="#fff"/></svg>',
	};

	function init(root) {
		const galleryId = parseInt(root.getAttribute('data-gallery-id'), 10);
		const config = (window.CCGAL_CONFIG && window.CCGAL_CONFIG[galleryId]) || null;
		if (!config) return;

		const state = {
			nextPageToken: '',
			hasMore: true,
			loading: false,
			selected: new Set(),
			items: [],
			lightboxIndex: -1,
			favoritesOnly: false,
			viewedInLightbox: new Set(),
			preloadedLightbox: new Set(),
		};

		const grid = root.querySelector('.ccgal-grid');
		const loadingEl = root.querySelector('.ccgal-loading');
		const endEl = root.querySelector('.ccgal-end');
		const emptyFavsEl = root.querySelector('.ccgal-empty-favs');
		const errorEl = root.querySelector('.ccgal-error');
		const loadMoreBtn = root.querySelector('.ccgal-load-more');
		const selectionControls = root.querySelector('.ccgal-selection-controls');
		const selectionCount = root.querySelector('.ccgal-selection-count');
		const downloadAllBtn = root.querySelector('.ccgal-download-all');
		const downloadSelectedBtn = root.querySelector('.ccgal-download-selected');
		const clearSelectionBtn = root.querySelector('.ccgal-clear-selection');
		const toggleFavsBtn = root.querySelector('.ccgal-toggle-favorites');
		const downloadFavsBtn = root.querySelector('.ccgal-download-favorites');
		const printSelectedBtn = root.querySelector('.ccgal-print-selected');
		const lightbox = root.querySelector('.ccgal-lightbox');
		const lbStage = lightbox ? lightbox.querySelector('.ccgal-lb-stage') : null;
		const lbClose = lightbox ? lightbox.querySelector('.ccgal-lb-close') : null;
		const lbPrev = lightbox ? lightbox.querySelector('.ccgal-lb-prev') : null;
		const lbNext = lightbox ? lightbox.querySelector('.ccgal-lb-next') : null;
		const lbFavorite = lightbox ? lightbox.querySelector('.ccgal-lb-favorite') : null;
		const lbSelect   = lightbox ? lightbox.querySelector('.ccgal-lb-select')   : null;
		const lbDownload = lightbox ? lightbox.querySelector('.ccgal-lb-download') : null;

		if (lbFavorite) lbFavorite.innerHTML = ICONS.heart;
		if (lbSelect)   lbSelect.innerHTML   = ICONS.check;
		if (lbDownload) lbDownload.innerHTML = ICONS.download;

		let thumbObserver = null;
		if ('IntersectionObserver' in window) {
			thumbObserver = new IntersectionObserver(onIntersect, {
				rootMargin: '200px 0px',
				threshold: 0.01,
			});
		}

		let scrollSentinel = null;
		if (config.load_mode === 'scroll') {
			scrollSentinel = document.createElement('div');
			scrollSentinel.className = 'ccgal-scroll-sentinel';
			root.insertBefore(scrollSentinel, root.querySelector('.ccgal-status'));

			if ('IntersectionObserver' in window) {
				const scrollObserver = new IntersectionObserver(
					(entries) => {
						entries.forEach((e) => {
							if (e.isIntersecting) {
								checkSentinel();
							}
						});
					},
					{ rootMargin: '400px 0px' }
				);
				scrollObserver.observe(scrollSentinel);
			}

			let scrollTicking = false;
			const onScrollOrResize = function () {
				if (scrollTicking) return;
				scrollTicking = true;
				requestAnimationFrame(function () {
					scrollTicking = false;
					checkSentinel();
				});
			};
			window.addEventListener('scroll', onScrollOrResize, { passive: true });
			window.addEventListener('resize', onScrollOrResize, { passive: true });
		}

		function checkSentinel() {
			if (!scrollSentinel) return;
			if (state.loading || !state.hasMore || state.favoritesOnly) return;
			const rect = scrollSentinel.getBoundingClientRect();
			const vh = window.innerHeight || document.documentElement.clientHeight || 0;
			if (rect.top < vh + 400) {
				loadBatch();
			}
		}

		if (loadMoreBtn) {
			loadMoreBtn.addEventListener('click', function () {
				loadBatch();
			});
		}

		if (downloadAllBtn) {
			downloadAllBtn.addEventListener('click', function () {
				// Schneller Pfad: WPGBulkDownload entscheidet zwischen FSA-Ordner,
				// Browser-ZIP und (Auto-Fallback) Server-Streaming-ZIP.
				if (window.WPGBulkDownload && typeof window.WPGBulkDownload.start === 'function') {
					window.WPGBulkDownload.start(config.gallery_id);
				} else {
					submitDownload('folder');
				}
			});
		}
		if (downloadSelectedBtn) {
			downloadSelectedBtn.addEventListener('click', function () {
				if (state.selected.size === 0) return;
				const ids = Array.from(state.selected);
				if (window.WPGBulkDownload && typeof window.WPGBulkDownload.start === 'function') {
					window.WPGBulkDownload.start(config.gallery_id, { mode: 'selection', fileIds: ids });
				} else {
					submitDownload('selection', ids);
				}
			});
		}
		if (clearSelectionBtn) {
			clearSelectionBtn.addEventListener('click', clearSelection);
		}
		if (toggleFavsBtn) {
			toggleFavsBtn.addEventListener('click', toggleFavoritesFilter);
		}
		if (printSelectedBtn) {
			printSelectedBtn.addEventListener('click', function () {
				if (!config.print_page_url || state.selected.size === 0) return;
				// Videos vom Druckshop ausschließen
				var ids = state.items
					.filter(function (i) { return !i.is_video && state.selected.has(i.id); })
					.map(function (i) { return i.id; });
				if (!ids.length) return;
				var sep = config.print_page_url.indexOf('?') === -1 ? '?' : '&';
				var url = config.print_page_url + sep +
					'gallery=' + config.gallery_id +
					'&files=' + encodeURIComponent(ids.join(',')) +
					'&from=' + encodeURIComponent(window.location.href);
				window.location.href = url;
			});
		}

		if (downloadFavsBtn) {
			downloadFavsBtn.addEventListener('click', function () {
				const favIds = state.items.filter(function (i) { return i.favorited; }).map(function (i) { return i.id; });
				if (!favIds.length) return;
				// Eigener Modus 'favorites' damit das ZIP einen passenden Dateinamen
				// (Galeriename_Favoriten.zip) bekommt. Backend behandelt es funktional wie selection.
				if (window.WPGBulkDownload && typeof window.WPGBulkDownload.start === 'function') {
					window.WPGBulkDownload.start(config.gallery_id, { mode: 'favorites', fileIds: favIds });
				} else {
					submitDownload('favorites', favIds);
				}
			});
		}

		if (lbClose) lbClose.addEventListener('click', closeLightbox);
		if (lbPrev) lbPrev.addEventListener('click', function () { navigateLightbox(-1); });
		if (lbNext) lbNext.addEventListener('click', function () { navigateLightbox(1); });
		if (lightbox) {
			lightbox.addEventListener('click', function (e) {
				if (e.target === lightbox) closeLightbox();
			});
			document.addEventListener('keydown', function (e) {
				if (lightbox.hidden) return;
				if (e.key === 'Escape') {
					e.preventDefault();
					closeLightbox();
				} else if (e.key === 'ArrowLeft') {
					e.preventDefault();
					navigateLightbox(-1);
				} else if (e.key === 'ArrowRight') {
					e.preventDefault();
					navigateLightbox(1);
				}
			});

			let touchStartX = 0;
			let touchStartY = 0;
			let touchActive = false;
			lightbox.addEventListener('touchstart', function (e) {
				if (e.touches.length !== 1) { touchActive = false; return; }
				if (e.target.closest('button, video, input, [contenteditable]')) {
					touchActive = false;
					return;
				}
				touchStartX = e.touches[0].clientX;
				touchStartY = e.touches[0].clientY;
				touchActive = true;
			}, { passive: true });
			lightbox.addEventListener('touchend', function (e) {
				if (!touchActive) return;
				touchActive = false;
				const t = e.changedTouches[0];
				if (!t) return;
				const dx = t.clientX - touchStartX;
				const dy = t.clientY - touchStartY;
				if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy) * 1.3) return;
				navigateLightbox(dx < 0 ? 1 : -1);
			}, { passive: true });
		}
		if (lbFavorite) {
			lbFavorite.addEventListener('click', function () {
				if (state.lightboxIndex < 0) return;
				toggleFavorite(state.items[state.lightboxIndex]);
			});
		}
		if (lbSelect) {
			lbSelect.addEventListener('click', function () {
				if (state.lightboxIndex < 0) return;
				const item = state.items[state.lightboxIndex];
				const tile = grid.querySelector('[data-file-id="' + cssEscape(item.id) + '"]');
				if (tile) toggleSelection(item.id, tile);
				updateLbSelectState(item.id);
			});
		}
		if (lbDownload) {
			lbDownload.addEventListener('click', function () {
				if (state.lightboxIndex < 0) return;
				downloadSingle(state.items[state.lightboxIndex].id);
			});
		}

		let relayoutTimer = null;
		function scheduleRelayout() {
			if (relayoutTimer) window.clearTimeout(relayoutTimer);
			relayoutTimer = window.setTimeout(function () {
				relayoutTimer = null;
				layoutJustified();
			}, 120);
		}
		window.addEventListener('resize', scheduleRelayout, { passive: true });

		function aspectRatio(item) {
			if (item && item.width > 0 && item.height > 0) {
				return item.width / item.height;
			}
			return item && item.is_video ? 16 / 9 : 1.5;
		}

		function layoutJustified() {
			if (!grid) return;
			const containerWidth = Math.floor(grid.getBoundingClientRect().width);
			if (containerWidth <= 0) return;

			const rootStyles = window.getComputedStyle(root);
			const targetHeight = parseInt(rootStyles.getPropertyValue('--ccgal-img-size'), 10) || 300;
			const gap = parseInt(rootStyles.getPropertyValue('--ccgal-gap'), 10) || 10;

			const tiles = grid.children;
			const entries = [];
			for (let i = 0; i < tiles.length; i++) {
				const tile = tiles[i];
				const idx = parseInt(tile.dataset.index, 10);
				const item = state.items[idx];
				if (!item) continue;
				if (state.favoritesOnly && !item.favorited) continue;
				entries.push({ tile: tile, aspect: aspectRatio(item) });
			}

			if (entries.length === 0) return;

			let row = [];
			let rowAspect = 0;

			function commitRow(list, stretch) {
				const gapsTotal = (list.length - 1) * gap;
				const sumAspect = list.reduce(function (s, r) { return s + r.aspect; }, 0);
				let height;
				let rowPixelWidth;
				if (stretch) {
					rowPixelWidth = containerWidth - gapsTotal;
					height = rowPixelWidth / sumAspect;
				} else {
					height = targetHeight;
					rowPixelWidth = sumAspect * height;
				}
				if (!isFinite(height) || height <= 0) {
					height = targetHeight;
					rowPixelWidth = sumAspect * height;
				}

				const floatWidths = list.map(function (r) { return r.aspect * height; });
				const intWidths = floatWidths.map(function (w) { return Math.floor(w); });

				if (stretch) {
					// Verteile den Rundungs-Rest pixelweise auf die ersten Kacheln,
					// damit die Summe exakt rowPixelWidth ergibt (kein Flex-Wrap der letzten Kachel).
					const targetSum = Math.floor(rowPixelWidth);
					let currentSum = intWidths.reduce(function (a, b) { return a + b; }, 0);
					let remainder = targetSum - currentSum;
					let i = 0;
					while (remainder > 0 && list.length > 0) {
						intWidths[i % list.length] += 1;
						remainder -= 1;
						i += 1;
					}
				}

				const finalHeight = Math.round(height);
				list.forEach(function (r, idx) {
					r.tile.style.width = Math.max(1, intWidths[idx]) + 'px';
					r.tile.style.height = finalHeight + 'px';
				});
			}

			for (let i = 0; i < entries.length; i++) {
				row.push(entries[i]);
				rowAspect += entries[i].aspect;
				// Zeile voll, sobald sie bei Zielhöhe mindestens die Containerbreite füllt.
				const rowWidth = rowAspect * targetHeight + (row.length - 1) * gap;
				if (rowWidth >= containerWidth) {
					commitRow(row, true);
					row = [];
					rowAspect = 0;
				}
			}
			if (row.length) commitRow(row, false);
		}

		notifyGalleryOpen();
		loadBatch();

		function setStatus(s) {
			if (loadingEl) loadingEl.hidden = s !== 'loading';
			if (endEl) endEl.hidden = s !== 'end';
			if (errorEl) errorEl.hidden = s !== 'error';
			if (emptyFavsEl) emptyFavsEl.hidden = s !== 'empty_favs';
			if (loadMoreBtn) {
				loadMoreBtn.hidden = s === 'loading' || s === 'end' || state.favoritesOnly;
				loadMoreBtn.disabled = s === 'loading';
			}
		}

		// Free build emits generic lifecycle events. Any extension module that
		// wants to react to a gallery being opened or an image being viewed (e.g.
		// the Pro statistics module) attaches a `document.addEventListener` to
		// `ccgal:gallery-opened` / `ccgal:image-opened`. No listener in the Free
		// build itself.
		function notifyGalleryOpen() {
			try {
				document.dispatchEvent(new CustomEvent('ccgal:gallery-opened', {
					detail: { galleryId: config.gallery_id }
				}));
			} catch (e) { /* IE11 fallback not needed: WP 6.2+ */ }
		}

		function notifyImageOpen(fileId) {
			// `state.viewedInLightbox` deduplicates fileIds *within the current
			// lightbox session*: rapid swiping back-and-forth A→B→A counts each
			// image only once. The Set is cleared in `closeLightbox()` so the
			// next time the visitor opens the lightbox the counters start fresh
			// and a re-engaged view is recorded as a new event.
			if (!fileId || state.viewedInLightbox.has(fileId)) return;
			state.viewedInLightbox.add(fileId);
			try {
				document.dispatchEvent(new CustomEvent('ccgal:image-opened', {
					detail: { galleryId: config.gallery_id, fileId: fileId }
				}));
			} catch (e) { /* see above */ }
		}

		function loadBatch() {
			if (state.loading || !state.hasMore) return Promise.resolve();
			state.loading = true;
			setStatus('loading');

			const url = new URL(config.rest_url + '/batch', window.location.origin);
			if (state.nextPageToken) url.searchParams.set('page_token', state.nextPageToken);

			return fetch(url.toString(), {
				credentials: 'same-origin',
				headers: {
					'X-WP-Nonce': config.rest_nonce,
					'X-CCGAL-Gallery-Nonce': config.gallery_nonce,
				},
			})
				.then(function (r) {
					if (!r.ok) throw new Error('HTTP ' + r.status);
					return r.json();
				})
				.then(function (data) {
					state.loading = false;
					state.nextPageToken = data.next_page_token || '';
					state.hasMore = !!data.has_more;
					appendItems(data.items || []);
					layoutJustified();
					updateFilterVisibility();
					updateFavoritesDownloadVisibility();
					if (state.favoritesOnly) {
						updateEmptyFavsStatus();
					} else if (!state.hasMore) {
						setStatus('end');
					} else {
						setStatus('idle');
						if (config.load_mode === 'scroll') {
							requestAnimationFrame(checkSentinel);
						}
					}
				})
				.catch(function () {
					state.loading = false;
					if (errorEl) errorEl.textContent = (config.strings && config.strings.error_generic) || 'Error';
					setStatus('error');
				});
		}

		function loadAllRemaining() {
			if (!state.hasMore || state.loading || !state.favoritesOnly) {
				return Promise.resolve();
			}
			return loadBatch().then(function () {
				if (state.hasMore && state.favoritesOnly) return loadAllRemaining();
			});
		}

		function appendItems(items) {
			const frag = document.createDocumentFragment();
			items.forEach(function (item) {
				const idx = state.items.length;
				state.items.push(item);

				const tile = document.createElement('figure');
				tile.className = 'ccgal-tile' + (item.is_video ? ' is-video' : '') + (item.favorited ? ' is-favorited' : '');
				tile.setAttribute('role', 'listitem');
				tile.dataset.fileId = item.id;
				tile.dataset.index = String(idx);

				const img = document.createElement('img');
				img.className = 'ccgal-tile-img';
				img.alt = item.name || '';
				img.loading = 'lazy';
				img.decoding = 'async';
				img.dataset.src = item.thumb_url;
				if (item.thumb_fallback) img.dataset.fallback = item.thumb_fallback;
				if (item.width && item.height) {
					img.width = item.width;
					img.height = item.height;
				}
				img.addEventListener('error', function () {
					if (this.dataset.fallback && this.src !== this.dataset.fallback) {
						this.src = this.dataset.fallback;
					}
				});
				// Fallback für Provider, die keine Bildmaße liefern (z. B. Dropbox bei
				// Dateien ohne EXIF/Media-Info): natürliche Größe nach dem Laden übernehmen
				// und einmalig per Relayout das echte Seitenverhältnis im Raster anwenden.
				img.addEventListener('load', function () {
					if (item.width > 0 && item.height > 0) return;
					const w = this.naturalWidth, h = this.naturalHeight;
					if (w > 0 && h > 0) {
						item.width = w;
						item.height = h;
						scheduleRelayout();
					}
				});

				if (thumbObserver) {
					thumbObserver.observe(img);
				} else {
					img.src = item.thumb_url;
				}

				tile.appendChild(img);

				if (item.is_video) {
					const play = document.createElement('span');
					play.className = 'ccgal-tile-play';
					play.setAttribute('aria-hidden', 'true');
					play.innerHTML = ICONS.play;
					tile.appendChild(play);
				}

				tile.addEventListener('click', function (e) {
					if (e.target.closest('button')) return;
					openLightbox(idx);
				});

				if (config.allow_favorites) {
					const favBtn = document.createElement('button');
					favBtn.type = 'button';
					favBtn.className = 'ccgal-tile-fav' + (item.favorited ? ' is-active' : '');
					favBtn.setAttribute('aria-pressed', item.favorited ? 'true' : 'false');
					favBtn.title = item.favorited ? config.strings.favorite_remove : config.strings.favorite_add;
					favBtn.innerHTML = ICONS.heart;
					favBtn.addEventListener('click', function (e) {
						e.stopPropagation();
						toggleFavorite(item);
						this.blur();
					});
					tile.appendChild(favBtn);
				}

				if (config.allow_zip_selection) {
					const selBtn = document.createElement('button');
					selBtn.type = 'button';
					selBtn.className = 'ccgal-tile-sel';
					selBtn.setAttribute('aria-pressed', 'false');
					selBtn.title = config.strings.selection_label;
					selBtn.innerHTML = ICONS.check;
					selBtn.addEventListener('click', function (e) {
						e.stopPropagation();
						toggleSelection(item.id, tile);
						this.blur();
					});
					tile.appendChild(selBtn);
				}

				if (config.allow_downloads) {
					const dlBtn = document.createElement('button');
					dlBtn.type = 'button';
					dlBtn.className = 'ccgal-tile-dl';
					dlBtn.title = config.strings.download_single;
					dlBtn.innerHTML = ICONS.download;
					dlBtn.addEventListener('click', function (e) {
						e.stopPropagation();
						downloadSingle(item.id);
						this.blur();
					});
					tile.appendChild(dlBtn);
				}

				frag.appendChild(tile);
			});
			grid.appendChild(frag);
		}

		function onIntersect(entries, observer) {
			entries.forEach(function (e) {
				if (e.isIntersecting) {
					const img = e.target;
					if (img.dataset.src && !img.src) {
						img.src = img.dataset.src;
					}
					observer.unobserve(img);
				}
			});
		}

		function toggleFavorite(item) {
			fetch(config.rest_url + '/favorite', {
				method: 'POST',
				credentials: 'same-origin',
				headers: {
					'X-WP-Nonce': config.rest_nonce,
					'X-CCGAL-Gallery-Nonce': config.gallery_nonce,
					'Content-Type': 'application/json',
				},
				body: JSON.stringify({ file_id: item.id }),
			})
				.then(function (r) { return r.json(); })
				.then(function (data) {
					const fav = !!data.favorited;
					item.favorited = fav;
					const tiles = grid.querySelectorAll('[data-file-id="' + cssEscape(item.id) + '"]');
					tiles.forEach(function (tile) {
						tile.classList.toggle('is-favorited', fav);
						const btn = tile.querySelector('.ccgal-tile-fav');
						if (btn) {
							btn.classList.toggle('is-active', fav);
							btn.setAttribute('aria-pressed', fav ? 'true' : 'false');
							btn.title = fav ? config.strings.favorite_remove : config.strings.favorite_add;
						}
					});
					if (lbFavorite && state.lightboxIndex >= 0 && state.items[state.lightboxIndex].id === item.id) {
						lbFavorite.classList.toggle('is-active', fav);
						lbFavorite.setAttribute('aria-pressed', fav ? 'true' : 'false');
					}
					if (state.favoritesOnly) {
						updateEmptyFavsStatus();
					}
					updateFavoritesDownloadVisibility();
				})
				.catch(function () { /* ignore */ });
		}

		function toggleSelection(fileId, tile) {
			if (state.selected.has(fileId)) {
				state.selected.delete(fileId);
				tile.classList.remove('is-selected');
				const b = tile.querySelector('.ccgal-tile-sel');
				if (b) b.setAttribute('aria-pressed', 'false');
			} else {
				state.selected.add(fileId);
				tile.classList.add('is-selected');
				const b = tile.querySelector('.ccgal-tile-sel');
				if (b) b.setAttribute('aria-pressed', 'true');
			}
			updateSelectionUI();
		}

		function clearSelection() {
			state.selected.forEach(function (id) {
				const tile = grid.querySelector('[data-file-id="' + cssEscape(id) + '"]');
				if (tile) {
					tile.classList.remove('is-selected');
					const b = tile.querySelector('.ccgal-tile-sel');
					if (b) b.setAttribute('aria-pressed', 'false');
				}
			});
			state.selected.clear();
			updateSelectionUI();
		}

		function updateLbSelectState(fileId) {
			if (!lbSelect) return;
			const selected = state.selected.has(fileId);
			lbSelect.classList.toggle('is-active', selected);
			lbSelect.setAttribute('aria-pressed', selected ? 'true' : 'false');
			lbSelect.title = selected
				? (config.strings.lb_select_remove || 'Remove from selection')
				: (config.strings.lb_select_add    || 'Add to selection');
		}

		function updateSelectionUI() {
			const n = state.selected.size;
			if (selectionControls) {
				selectionControls.hidden = n === 0;
			}
			if (selectionCount) {
				const tpl = selectionCount.getAttribute('data-template') || '%d';
				selectionCount.textContent = tpl.replace('%d', n);
			}
			if (printSelectedBtn) {
				printSelectedBtn.disabled = n === 0;
			}
		}

		function updateFilterVisibility() {
			if (!state.favoritesOnly) {
				grid.classList.remove('is-filtered-favs');
			} else {
				grid.classList.add('is-filtered-favs');
			}
			layoutJustified();
		}

		function updateFavoritesDownloadVisibility() {
			if (!downloadFavsBtn) return;
			const count = state.items.reduce(function (acc, i) { return acc + (i.favorited ? 1 : 0); }, 0);
			const show = state.favoritesOnly && count > 0;
			downloadFavsBtn.hidden = !show;
			if (show) {
				const tpl = downloadFavsBtn.getAttribute('data-template') || '';
				downloadFavsBtn.textContent = tpl.replace('%d', count);
			}
		}

		function updateEmptyFavsStatus() {
			const hasFav = state.items.some(function (i) { return i.favorited; });
			if (!hasFav) {
				setStatus('empty_favs');
			} else {
				setStatus('idle');
			}
		}

		function toggleFavoritesFilter() {
			if (!toggleFavsBtn) return;
			const next = !state.favoritesOnly;
			state.favoritesOnly = next;
			toggleFavsBtn.setAttribute('aria-pressed', next ? 'true' : 'false');
			toggleFavsBtn.classList.toggle('is-active', next);
			toggleFavsBtn.textContent = next ? config.strings.only_favorites_off : config.strings.only_favorites_on;
			updateFilterVisibility();
			updateFavoritesDownloadVisibility();

			if (next) {
				if (state.hasMore) setStatus('loading');
				loadAllRemaining().then(function () {
					updateFavoritesDownloadVisibility();
					if (state.favoritesOnly) {
						updateFilterVisibility();
						updateEmptyFavsStatus();
					} else if (!state.hasMore) {
						setStatus('end');
					} else {
						setStatus('idle');
					}
				});
			} else {
				if (!state.hasMore) setStatus('end'); else setStatus('idle');
			}
		}

		function downloadSingle(fileId) {
			const form = buildForm('ccgal_download_single');
			appendInput(form, 'file', fileId);
			document.body.appendChild(form);
			form.submit();
			document.body.removeChild(form);
		}

		function submitDownload(mode, ids) {
			const form = buildForm('ccgal_download_zip');
			appendInput(form, 'mode', mode);
			// 'selection' und 'favorites' senden eine Datei-Liste; 'folder' lädt die ganze Galerie.
			if ((mode === 'selection' || mode === 'favorites') && ids && ids.length) {
				ids.forEach(function (id) {
					appendInput(form, 'files[]', id);
				});
			}
			document.body.appendChild(form);
			form.submit();
			document.body.removeChild(form);
		}

		function buildForm(action) {
			const form = document.createElement('form');
			form.method = 'POST';
			form.action = config.download_url;
			// _self statt _blank — _blank öffnet einen kurzen leeren Tab, der
			// sich nach Erhalt des Content-Disposition-Headers wieder schließt;
			// in der Praxis ein irritierender „Tab-Flicker"-Effekt. Mit _self
			// triggert der Browser direkt den Save-Dialog, ohne Tab-Wechsel.
			form.target = '_self';
			form.style.display = 'none';
			appendInput(form, 'action', action);
			appendInput(form, 'gallery', String(config.gallery_id));
			appendInput(form, 'nonce', config.download_nonce);
			return form;
		}

		function appendInput(form, name, value) {
			const i = document.createElement('input');
			i.type = 'hidden';
			i.name = name;
			i.value = value;
			form.appendChild(i);
		}

		function findNextIndex(from, delta) {
			const len = state.items.length;
			if (len === 0) return -1;
			let i = from;
			for (let step = 0; step < len; step++) {
				i += delta;
				if (i < 0 || i >= len) return -1;
				if (!state.favoritesOnly || state.items[i].favorited) return i;
			}
			return -1;
		}

		function openLightbox(index) {
			if (!lightbox || !lbStage) return;
			if (index < 0 || index >= state.items.length) return;
			if (state.favoritesOnly && !state.items[index].favorited) return;

			state.lightboxIndex = index;
			const item = state.items[index];

			lbStage.innerHTML = '';
			if (item.is_video && item.video_url) {
				const video = document.createElement('video');
				video.className = 'ccgal-lb-video';
				video.src = item.video_url;
				video.controls = true;
				video.playsInline = true;
				video.muted = false;
				video.preload = 'auto';
				video.setAttribute('controlsList', 'nodownload');
				lbStage.appendChild(video);
			} else {
				const img = document.createElement('img');
				img.className = 'ccgal-lb-img';
				img.alt = item.name || '';
				if (item.medium_fallback) {
					img.addEventListener('error', function () {
						if (this.src !== item.medium_fallback) {
							this.src = item.medium_fallback;
						}
					});
				}
				img.src = item.medium_url;
				lbStage.appendChild(img);
			}

			lightbox.hidden = false;
			document.body.classList.add('ccgal-lightbox-open');
			if (lbFavorite) {
				lbFavorite.classList.toggle('is-active', !!item.favorited);
				lbFavorite.setAttribute('aria-pressed', item.favorited ? 'true' : 'false');
			}
			if (lbSelect) {
				lbSelect.hidden = !config.allow_downloads;
				updateLbSelectState(item.id);
			}
			if (lbDownload) {
				lbDownload.hidden = !config.allow_downloads;
			}

			notifyImageOpen(item.id);
			preloadLightboxNeighbors(index);
		}

		function preloadLightboxImage(index) {
			if (index < 0 || index >= state.items.length) return;
			const item = state.items[index];
			if (!item || item.is_video) return;
			const url = item.medium_url;
			if (!url || state.preloadedLightbox.has(url)) return;
			state.preloadedLightbox.add(url);
			const pre = new Image();
			if (item.medium_fallback) {
				pre.addEventListener('error', function () {
					if (pre.src !== item.medium_fallback) {
						pre.src = item.medium_fallback;
					}
				});
			}
			pre.src = url;
		}

		function preloadLightboxNeighbors(fromIndex) {
			const nextIdx = findNextIndex(fromIndex, 1);
			const prevIdx = findNextIndex(fromIndex, -1);
			preloadLightboxImage(nextIdx);
			preloadLightboxImage(prevIdx);
			if (nextIdx >= 0) preloadLightboxImage(findNextIndex(nextIdx, 1));
			if (prevIdx >= 0) preloadLightboxImage(findNextIndex(prevIdx, -1));
		}

		function closeLightbox() {
			if (!lightbox) return;
			lightbox.hidden = true;
			state.lightboxIndex = -1;
			document.body.classList.remove('ccgal-lightbox-open');
			if (lbStage) {
				const vid = lbStage.querySelector('video');
				if (vid) {
					try { vid.pause(); } catch (e) { /* ignore */ }
				}
				lbStage.innerHTML = '';
			}
			// Reset the image-view dedup set so the next lightbox session
			// records image-view events again for already-seen images.
			state.viewedInLightbox.clear();
		}

		function navigateLightbox(delta) {
			if (state.lightboxIndex < 0) return;
			let next = findNextIndex(state.lightboxIndex, delta);
			if (next < 0 && delta > 0 && state.hasMore && !state.favoritesOnly) {
				loadBatch().then(function () {
					const again = findNextIndex(state.lightboxIndex, delta);
					if (again >= 0) openLightbox(again);
				});
				return;
			}
			if (next < 0) return;
			openLightbox(next);
		}

		function cssEscape(str) {
			if (window.CSS && window.CSS.escape) return window.CSS.escape(str);
			return String(str).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
		}
	}

	function boot() {
		document.querySelectorAll('.ccgal-gallery').forEach(init);
	}

	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', boot);
	} else {
		boot();
	}
})();
