/**
 * Karuzela stron odbieranych przez WebSocket.
 *
 * Pamięta wszystkie odebrane strony (html + js). Bieżącą stronę przesuwa się
 * palcem/myszą w bok jak karuzelę — sąsiednie strony jadą pod palcem.
 *
 * Kluczowa zasada: tylko ŚRODKOWY slajd „żyje" (ma prawdziwy #mainContent,
 * odpalony JS, oryginalne id). Sąsiedzi to martwe podglądy (sam HTML, bez JS,
 * ze zdjętymi id) — dzięki temu nie ma kolizji id ani kilku skryptów naraz,
 * a nagrywarka głosu i XPath dalej działają na jednej, konkretnej stronie.
 *
 * Hash strony liczy serwer i przysyła go z treścią — karuzela traktuje go jako
 * identyfikator strony i nigdy sama nie liczy. `replaces` w wiadomości znaczy
 * „to jest nowa treść dla strony o tym hashu”: podmieniamy ją w miejscu, bez
 * przejazdu i bez utraty przewinięcia. Bez `replaces` treść wchodzi jako nowa strona.
 *
 * Użycie:
 *   const c = createPageCarousel({ content: mainContentEl, render: applyPage });
 *   c.push({ html, js, hash });              // nowa strona
 *   c.push({ html, js, hash, replaces });    // podmianka strony `replaces`
 *   c.onLiveChange((hash) => ...);           // przed użytkownikiem stanęła inna strona
 */
(function () {
	'use strict';

	const TRANSITION = 'transform 0.3s ease';
	const SWIPE_RATIO = 0.25;   // ile szerokości ekranu trzeba przeciągnąć, by zmienić stronę
	const DECIDE_PX = 8;        // próg, po którym decydujemy: poziom czy pion

	function createPageCarousel({ content, render }) {
		// --- budowa struktury wokół istniejącego #mainContent ---
		const container = content.parentNode;
		const viewport = div('carousel-viewport');
		const track = div('carousel-track');
		const prevSlide = div('carousel-slide');
		const currentSlide = div('carousel-slide');
		const nextSlide = div('carousel-slide');
		const prevContent = div('main-content');   // podgląd (martwy)
		const nextContent = div('main-content');   // podgląd (martwy)

		container.insertBefore(viewport, content);
		currentSlide.appendChild(content);          // #mainContent = środkowy, żywy slajd
		prevSlide.appendChild(prevContent);
		nextSlide.appendChild(nextContent);
		track.append(prevSlide, currentSlide, nextSlide);
		viewport.appendChild(track);

		const indicator = createIndicator();

		// --- stan ---
		const pages = [];
		const queue = [];
		const liveListeners = [];   // powiadamiani, gdy przed użytkownikiem staje inna strona
		let index = -1;
		let isAnimating = false;
		let currentTranslate = 0;

		const W = () => viewport.clientWidth;
		const atHead = () => index === pages.length - 1;

		// natychmiastowe ustawienie pozycji toru (bez animacji)
		function setTransform(px) {
			track.style.transition = 'none';
			track.style.transform = `translateX(${px}px)`;
			currentTranslate = px;
		}

		// animowany przejazd toru do pozycji px (niezawodne włączenie transition)
		function animateTransform(px) {
			track.style.transition = TRANSITION;
			void track.offsetWidth;   // reflush: transition wystartuje od bieżącej wartości
			track.style.transform = `translateX(${px}px)`;
			currentTranslate = px;
		}

		// wypełnij podgląd sąsiada treścią i jego zapamiętanym scrollem
		function fillNeighbor(el, j) {
			if (j >= 0 && j < pages.length) {
				el.innerHTML = inertHtml(pages[j].html);
				el.scrollTop = pages[j].scrollTop || 0;
			} else {
				el.innerHTML = '';
			}
		}

		// ustaw history[i] jako żywą stronę środkową i wyśrodkuj tor
		function showLive(i) {
			// zapamiętaj scroll pionowy strony, którą opuszczamy
			if (index >= 0 && index !== i) {
				pages[index].scrollTop = content.scrollTop;
			}

			index = i;
			render(pages[i]);

			// podglądy sąsiadów z ICH własnym scrollem (przejście bez skoku)
			fillNeighbor(prevContent, i - 1);
			fillNeighbor(nextContent, i + 1);

			// przywróć scroll tej strony; nowa (bez zapisu) startuje od góry
			content.scrollTop = pages[i].scrollTop || 0;

			setTransform(-W());
			indicator.update(i + 1, pages.length, atHead());
			notifyLive();
		}

		// Zawołane, gdy przed użytkownikiem staje strona o innym hashu — bez znaczenia,
		// czy przewinął ją sam, czy przysłał ją agent. Nagrywarka głosu zapisuje z tego
		// ścieżkę ekranów, po których użytkownik chodził w trakcie jednego polecenia.
		function notifyLive() {
			const hash = index >= 0 ? (pages[index].hash ?? null) : null;
			for (const listener of liveListeners) {
				try {
					listener(hash);
				} catch (e) {
					console.error('carousel: błąd słuchacza zmiany strony', e);
				}
			}
		}

		// Podmianka treści strony BEZ zmiany pozycji w karuzeli: użytkownik zostaje tam,
		// gdzie jest, i nie traci przewinięcia. Nowa treść ma nowy hash — od teraz to on
		// jedzie do serwera przy nagraniu.
		function replaceAt(j, page) {
			const keptScroll = j === index ? content.scrollTop : (pages[j].scrollTop || 0);
			pages[j] = { html: page.html, js: page.js, hash: page.hash, scrollTop: keptScroll };

			// ten sam hash to ta sama treść, więc dwie takie strony to jeden ekran pokazany
			// dwa razy. Zwijamy je, bo inaczej hash przestaje jednoznacznie wskazywać stronę
			// i kolejna podmianka mogłaby trafić w kopię, której użytkownik nie ma przed sobą.
			const twin = pages.findIndex((p, i) => i !== j && p.hash === page.hash);
			if (twin !== -1) {
				let live = twin === index ? j : index;   // patrzył na bliźniaka -> zostaje podmieniony
				pages.splice(twin, 1);
				if (twin < j) j -= 1;
				if (twin < live) live -= 1;
				index = live;
			}

			if (j === index) {
				render(pages[j]);
				content.scrollTop = keptScroll;
			}

			fillNeighbor(prevContent, index - 1);
			fillNeighbor(nextContent, index + 1);
			indicator.update(index + 1, pages.length, atHead());

			if (j === index) {
				notifyLive();   // ta sama pozycja, ale inna strona przed oczami
			}
		}

		// uruchom `run` po zakończeniu przejazdu toru.
		// Filtrujemy zdarzenie (tylko transform samego toru, bez bąbelków z treści)
		// i mamy bezpiecznik czasowy — isAnimating nigdy nie utknie na true.
		function afterSettle(run) {
			let done = false;
			const finish = () => {
				if (done) return;
				done = true;
				track.removeEventListener('transitionend', onEnd);
				clearTimeout(timer);
				run();
			};
			const onEnd = (e) => {
				if (e.target === track && e.propertyName === 'transform') finish();
			};
			track.addEventListener('transitionend', onEnd);
			const timer = setTimeout(finish, 450);
		}

		// animowany przejazd o jeden slajd (dir: +1 następna, -1 poprzednia)
		function animateTo(dir) {
			const target = index + dir;
			if (target < 0 || target >= pages.length) { snapBack(); return; }
			isAnimating = true;
			afterSettle(() => {
				showLive(target);     // podmiana podglądu na żywą stronę (te same piksele -> bez mrugnięcia)
				isAnimating = false;
				drain();
			});
			animateTransform(dir > 0 ? -2 * W() : 0);
		}

		function snapBack() {
			if (currentTranslate === -W()) return;
			isAnimating = true;
			afterSettle(() => { isAnimating = false; drain(); });
			animateTransform(-W());
		}

		// --- kolejka wiadomości (serializuje push-e względem animacji) ---
		function push(page) {
			queue.push(page);
			drain();
		}

		// przewiń do strony o indeksie j (sąsiednia -> z animacją, dalsza -> od razu)
		function goTo(j) {
			if (j === index) { snapBack(); drain(); return; }
			if (j === index + 1) { animateTo(+1); return; }   // animateTo -> afterSettle -> drain
			if (j === index - 1) { animateTo(-1); return; }
			showLive(j);   // daleki skok bez przejazdu
			drain();
		}

		function drain() {
			if (isAnimating || queue.length === 0) return;
			const page = queue.shift();

			if (index === -1) {           // pierwsza strona — bez animacji
				pages.push(page);
				showLive(0);
				drain();
				return;
			}

			// podmianka wskazanej strony: serwer przysłał nową treść (page.hash) razem
			// z hashem strony, którą ma zastąpić (page.replaces)
			if (page.replaces != null) {
				const target = pages.findIndex((p) => p.hash === page.replaces);
				if (target !== -1) {
					replaceAt(target, page);
					drain();
					return;
				}
				// strony do podmiany nie ma — np. przeglądarkę przeładowano po tym, jak
				// agent dostał jej hash. Zgłosić tego agentowi nie mamy jak, a nie pokazać
				// treści wcale byłoby gorsze, więc wchodzi jako nowa strona.
				console.warn(`carousel: brak strony ${page.replaces} do podmiany — wstawiam jako nową`);
			}

			// deduplikacja po hashu: znana strona -> przewiń do istniejącej zamiast wstawiać
			const known = page.hash != null ? pages.findIndex((p) => p.hash === page.hash) : -1;
			if (known !== -1) {
				goTo(known);
				return;
			}

			// nowa strona: wstaw zaraz po bieżącej i od razu przejedź na nią
			pages.splice(index + 1, 0, page);
			nextContent.innerHTML = inertHtml(page.html);
			animateTo(+1);
		}

		// --- gest poziomy (palec/mysz), z rozróżnieniem od scrolla pionowego ---
		let startX = 0, startY = 0, dragging = false, decided = false, horizontal = false;
		let justSwiped = false;   // ostatni gest był poziomy — click zaraz po nim to zmiana ekranu

		// wspólna logika: rozstrzygnij kierunek i przesuń tor za palcem
		// zwraca true, jeśli to gest poziomy (i należy zablokować natywny scroll)
		function onMove(dx, dy) {
			if (!dragging) return false;
			if (!decided) {
				if (Math.abs(dx) < DECIDE_PX && Math.abs(dy) < DECIDE_PX) return false;
				decided = true;
				horizontal = Math.abs(dx) > Math.abs(dy);
				if (!horizontal) { dragging = false; return false; }   // pion -> natywny scroll
			}
			if (!horizontal) return false;

			let d = dx;
			if (d > 0 && index <= 0) d *= 0.3;                  // opór: brak poprzedniej
			if (d < 0 && index >= pages.length - 1) d *= 0.3;   // opór: brak następnej
			let tx = Math.max(-2 * W(), Math.min(0, -W() + d));
			setTransform(tx);
			return true;
		}

		function onEndMove(dx) {
			const wasHorizontal = horizontal;
			dragging = false;
			horizontal = false;
			if (!wasHorizontal) return;

			// click leci zaraz po mouseup, więc flaga musi żyć do końca tej tury zdarzeń
			justSwiped = true;
			setTimeout(() => { justSwiped = false; }, 0);

			const threshold = W() * SWIPE_RATIO;
			if (dx <= -threshold && index < pages.length - 1) animateTo(+1);
			else if (dx >= threshold && index > 0) animateTo(-1);
			else snapBack();
		}

		function beginDrag(x, y) {
			if (isAnimating) return;
			startX = x;
			startY = y;
			dragging = true;
			decided = false;
			horizontal = false;
		}

		// DOTYK — preventDefault dla gestu poziomego twardo blokuje natywny scroll,
		// dzięki czemu dostajemy cały ruch palca (a nie urwany przez pointercancel)
		viewport.addEventListener('touchstart', (e) => {
			beginDrag(e.touches[0].clientX, e.touches[0].clientY);
		}, { passive: true });

		viewport.addEventListener('touchmove', (e) => {
			if (!dragging) return;
			const t = e.touches[0];
			if (onMove(t.clientX - startX, t.clientY - startY)) {
				e.preventDefault();
			}
		}, { passive: false });

		viewport.addEventListener('touchend', (e) => {
			onEndMove(e.changedTouches[0].clientX - startX);
		});

		viewport.addEventListener('touchcancel', () => {
			if (horizontal) snapBack();
			dragging = false;
			horizontal = false;
		});

		// MYSZ (desktop)
		let mouseDown = false;
		viewport.addEventListener('mousedown', (e) => {
			mouseDown = true;
			beginDrag(e.clientX, e.clientY);
		});

		window.addEventListener('mousemove', (e) => {
			if (!mouseDown) return;
			onMove(e.clientX - startX, e.clientY - startY);
		});

		window.addEventListener('mouseup', (e) => {
			if (!mouseDown) return;
			mouseDown = false;
			onEndMove(e.clientX - startX);
		});

		window.addEventListener('resize', () => { if (!isAnimating) setTransform(-W()); });

		setTransform(-W());   // wyśrodkuj środkowy (żywy) slajd od startu

		return {
			push,
			next: () => { if (!isAnimating && index < pages.length - 1) animateTo(+1); },
			prev: () => { if (!isAnimating && index > 0) animateTo(-1); },
			// zapisz się na zmiany żywej strony (hash tej, która stanęła przed użytkownikiem)
			onLiveChange: (listener) => { liveListeners.push(listener); },
			get index()  { return index; },
			get length() { return pages.length; },
			get currentHash() { return index >= 0 ? (pages[index].hash ?? null) : null; },
			get justSwiped() { return justSwiped; },
		};
	}

	// --- pomocnicze ---

	function div(className) {
		const el = document.createElement('div');
		el.className = className;
		return el;
	}

	// martwy podgląd: ten sam HTML, ale bez id (żeby nie kolidowały z żywą stroną)
	function inertHtml(html) {
		const tmp = document.createElement('div');
		tmp.innerHTML = html;
		tmp.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
		return tmp.innerHTML;
	}

	function createIndicator() {
		const el = div('carousel-indicator');
		// celowo NIE wpinamy do GUI — logika gotowa, w przyszłości wystarczy
		// wstawić `el` w wybrane miejsce (np. document.body.appendChild(el)).
		return {
			update(pos, total, live) {
				if (total <= 1) { el.style.display = 'none'; return; }
				el.style.display = 'block';
				el.textContent = `${pos} / ${total} · ${live ? 'na żywo' : 'historia'}`;
				el.classList.toggle('is-history', !live);
			},
		};
	}

	window.createPageCarousel = createPageCarousel;
})();
