/*
 * HSCarousel
 * @version: 4.2.0
 * @author: Preline Labs Ltd.
 * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
 * Copyright 2024 Preline Labs Ltd.
 */

import { classToClassList, debounce, htmlToElement } from '../../utils';

import { ICarousel, ICarouselOptions } from './interfaces';
import { TCarouselOptionsSlidesQty } from './types';

import HSBasePlugin from '../base-plugin';
import { BREAKPOINTS } from '../../constants';

class HSCarousel extends HSBasePlugin<ICarouselOptions> implements ICarousel {
	private currentIndex: number;
	private readonly loadingClasses: string | string[];
	private readonly dotsItemClasses: string;
	private readonly isAutoHeight: boolean;
	private readonly isAutoPlay: boolean;
	private readonly isCentered: boolean;
	private readonly isDraggable: boolean;
	private readonly isInfiniteLoop: boolean;
	private readonly isRTL: boolean;
	private readonly isSnap: boolean;
	private readonly hasSnapSpacers: boolean;
	private readonly slidesQty: TCarouselOptionsSlidesQty | number;
	private readonly speed: number;
	private readonly updateDelay: number;

	private readonly loadingClassesRemove: string | string[];
	private readonly loadingClassesAdd: string | string[];
	private readonly afterLoadingClassesAdd: string | string[];

	private readonly container: HTMLElement | null;
	private readonly inner: HTMLElement | null;
	private readonly slides: NodeListOf<HTMLElement> | undefined[];
	private readonly prev: HTMLElement | null;
	private readonly next: HTMLElement | null;
	private readonly dots: HTMLElement | null;
	private dotsItems: NodeListOf<HTMLElement> | undefined[] | null;
	private readonly info: HTMLElement | null;
	private readonly infoTotal: HTMLElement | null;
	private readonly infoCurrent: HTMLElement | null;

	private sliderWidth: number;
	private timer: any;

	// Drag events' help variables
	private isScrolling: ReturnType<typeof setTimeout>;
	private isDragging: boolean;
	private dragStartX: number | null;
	private initialTranslateX: number | null;

	// Touch events' help variables
	private readonly touchX: {
		start: number;
		end: number;
	};
	private readonly touchY: {
		start: number;
		end: number;
	};

	// Resize events' help variables
	private resizeContainer: HTMLElement;
	public resizeContainerWidth: number;

	// Listeners
	private onPrevClickListener: () => void;
	private onNextClickListener: () => void;
	private onContainerScrollListener: () => void;
	private onElementTouchStartListener: (evt: TouchEvent) => void;
	private onElementTouchEndListener: (evt: TouchEvent) => void;
	private onInnerMouseDownListener: (evt: MouseEvent | TouchEvent) => void;
	private onInnerTouchStartListener: (evt: MouseEvent | TouchEvent) => void;
	private onDocumentMouseMoveListener: (evt: MouseEvent | TouchEvent) => void;
	private onDocumentTouchMoveListener: (evt: MouseEvent | TouchEvent) => void;
	private onDocumentMouseUpListener: () => void;
	private onDocumentTouchEndListener: () => void;
	private onDotClickListener: () => void;

	constructor(el: HTMLElement, options?: ICarouselOptions) {
		super(el, options);

		const data = el.getAttribute('data-hs-carousel');
		const dataOptions: ICarouselOptions = data ? JSON.parse(data) : {};
		const concatOptions = {
			...dataOptions,
			...options,
		};

		this.currentIndex = concatOptions.currentIndex || 0;
		this.loadingClasses = concatOptions.loadingClasses
			? `${concatOptions.loadingClasses}`.split(',')
			: null;
		this.dotsItemClasses = concatOptions.dotsItemClasses
			? concatOptions.dotsItemClasses
			: null;
		this.isAutoHeight =
			typeof concatOptions.isAutoHeight !== 'undefined'
				? concatOptions.isAutoHeight
				: false;
		this.isAutoPlay =
			typeof concatOptions.isAutoPlay !== 'undefined'
				? concatOptions.isAutoPlay
				: false;
		this.isCentered =
			typeof concatOptions.isCentered !== 'undefined'
				? concatOptions.isCentered
				: false;
		this.isDraggable =
			typeof concatOptions.isDraggable !== 'undefined'
				? concatOptions.isDraggable
				: false;
		this.isInfiniteLoop =
			typeof concatOptions.isInfiniteLoop !== 'undefined'
				? concatOptions.isInfiniteLoop
				: false;
		this.isRTL =
			typeof concatOptions.isRTL !== 'undefined' ? concatOptions.isRTL : false;
		this.isSnap =
			typeof concatOptions.isSnap !== 'undefined'
				? concatOptions.isSnap
				: false;
		this.hasSnapSpacers =
			typeof concatOptions.hasSnapSpacers !== 'undefined'
				? concatOptions.hasSnapSpacers
				: true;
		this.speed = concatOptions.speed || 4000;
		this.updateDelay = concatOptions.updateDelay || 0;
		this.slidesQty = concatOptions.slidesQty || 1;

		this.loadingClassesRemove = this.loadingClasses?.[0]
			? this.loadingClasses[0].split(' ')
			: 'opacity-0';
		this.loadingClassesAdd = this.loadingClasses?.[1]
			? this.loadingClasses[1].split(' ')
			: '';
		this.afterLoadingClassesAdd = this.loadingClasses?.[2]
			? this.loadingClasses[2].split(' ')
			: '';

		this.container = this.el.querySelector('.hs-carousel') || null;
		this.inner = this.el.querySelector('.hs-carousel-body') || null;
		this.slides = this.el.querySelectorAll('.hs-carousel-slide') || [];
		this.prev = this.el.querySelector('.hs-carousel-prev') || null;
		this.next = this.el.querySelector('.hs-carousel-next') || null;
		this.dots = this.el.querySelector('.hs-carousel-pagination') || null;
		this.info = this.el.querySelector('.hs-carousel-info') || null;
		this.infoTotal =
			this?.info?.querySelector('.hs-carousel-info-total') || null;
		this.infoCurrent =
			this?.info?.querySelector('.hs-carousel-info-current') || null;

		this.sliderWidth = this.el.getBoundingClientRect().width;

		// Drag events' help variables
		this.isDragging = false;
		this.dragStartX = null;
		this.initialTranslateX = null;

		// Touch events' help variables
		this.touchX = {
			start: 0,
			end: 0,
		};
		this.touchY = {
			start: 0,
			end: 0,
		};

		// Resize events' help variables
		this.resizeContainer = document.querySelector('body');
		this.resizeContainerWidth = 0;

		this.init();
	}

	private setIsSnap() {
		const containerRect = this.container.getBoundingClientRect();
		const containerCenter = containerRect.left + containerRect.width / 2;

		let closestElement: HTMLElement | null = null;
		let closestElementIndex: number | null = null;
		let closestDistance = Infinity;

		Array.from(this.inner.children).forEach((child: HTMLElement) => {
			const childRect = child.getBoundingClientRect();
			const innerContainerRect = this.inner.getBoundingClientRect();
			const childCenter =
				childRect.left + childRect.width / 2 - innerContainerRect.left;
			const distance = Math.abs(
				containerCenter - (innerContainerRect.left + childCenter),
			);

			if (distance < closestDistance) {
				closestDistance = distance;
				closestElement = child;
			}
		});

		if (closestElement) {
			closestElementIndex = Array.from(this.slides).findIndex(
				(el) => el === closestElement,
			);
		}

		this.setIndex(closestElementIndex);

		if (this.dots) this.setCurrentDot();
	}

	private prevClick() {
		this.goToPrev();
		if (this.isAutoPlay) {
			this.resetTimer();
			this.setTimer();
		}
	}

	private nextClick() {
		this.goToNext();
		if (this.isAutoPlay) {
			this.resetTimer();
			this.setTimer();
		}
	}

	private containerScroll() {
		clearTimeout(this.isScrolling);

		this.isScrolling = setTimeout(() => {
			this.setIsSnap();
		}, 100);
	}

	private elementTouchStart(evt: TouchEvent) {
		this.touchX.start = evt.changedTouches[0].screenX;
		this.touchY.start = evt.changedTouches[0].screenY;
	}

	private elementTouchEnd(evt: TouchEvent) {
		this.touchX.end = evt.changedTouches[0].screenX;
		this.touchY.end = evt.changedTouches[0].screenY;

		this.detectDirection();
	}

	private innerMouseDown(evt: MouseEvent | TouchEvent) {
		this.handleDragStart(evt);
	}

	private innerTouchStart(evt: MouseEvent | TouchEvent) {
		this.handleDragStart(evt);
	}

	private documentMouseMove(evt: MouseEvent | TouchEvent) {
		this.handleDragMove(evt);
	}

	private documentTouchMove(evt: MouseEvent | TouchEvent) {
		this.handleDragMove(evt);
	}

	private documentMouseUp() {
		this.handleDragEnd();
	}

	private documentTouchEnd() {
		this.handleDragEnd();
	}

	private dotClick(ind: number) {
		this.goTo(ind);

		if (this.isAutoPlay) {
			this.resetTimer();
			this.setTimer();
		}
	}

	private init() {
		this.createCollection(window.$hsCarouselCollection, this);

		if (this.inner) {
			this.calculateWidth();

			if (this.isDraggable && !this.isSnap) this.initDragHandling();
		}

		if (this.prev) {
			this.onPrevClickListener = () => this.prevClick();

			this.prev.addEventListener('click', this.onPrevClickListener);
		}

		if (this.next) {
			this.onNextClickListener = () => this.nextClick();

			this.next.addEventListener('click', this.onNextClickListener);
		}

		if (this.dots) this.initDots();
		if (this.info) this.buildInfo();
		if (this.slides.length) {
			this.addCurrentClass();
			if (!this.isInfiniteLoop) this.addDisabledClass();
			if (this.isAutoPlay) this.autoPlay();
		}

		setTimeout(() => {
			if (this.isSnap) this.setIsSnap();

			if (this.loadingClassesRemove) {
				if (typeof this.loadingClassesRemove === 'string') {
					this.inner.classList.remove(this.loadingClassesRemove);
				} else this.inner.classList.remove(...this.loadingClassesRemove);
			}
			if (this.loadingClassesAdd) {
				if (typeof this.loadingClassesAdd === 'string') {
					this.inner.classList.add(this.loadingClassesAdd);
				} else this.inner.classList.add(...this.loadingClassesAdd);
			}

			if (this.inner && this.afterLoadingClassesAdd) {
				setTimeout(() => {
					if (typeof this.afterLoadingClassesAdd === 'string') {
						this.inner.classList.add(this.afterLoadingClassesAdd);
					} else this.inner.classList.add(...this.afterLoadingClassesAdd);
				});
			}
		}, 400);

		if (this.isSnap) {
			this.onContainerScrollListener = () => this.containerScroll();

			this.container.addEventListener('scroll', this.onContainerScrollListener);
		}

		this.el.classList.add('init');

		if (!this.isSnap) {
			this.onElementTouchStartListener = (evt: TouchEvent) =>
				this.elementTouchStart(evt);
			this.onElementTouchEndListener = (evt: TouchEvent) =>
				this.elementTouchEnd(evt);

			this.el.addEventListener('touchstart', this.onElementTouchStartListener);

			this.el.addEventListener('touchend', this.onElementTouchEndListener);
		}

		this.observeResize();
	}

	private initDragHandling(): void {
		const scrollableElement = this.inner;

		this.onInnerMouseDownListener = (evt) => this.innerMouseDown(evt);
		this.onInnerTouchStartListener = (evt) => this.innerTouchStart(evt);
		this.onDocumentMouseMoveListener = (evt) => this.documentMouseMove(evt);
		this.onDocumentTouchMoveListener = (evt) => this.documentTouchMove(evt);
		this.onDocumentMouseUpListener = () => this.documentMouseUp();
		this.onDocumentTouchEndListener = () => this.documentTouchEnd();

		if (scrollableElement) {
			scrollableElement.addEventListener(
				'mousedown',
				this.onInnerMouseDownListener,
			);
			scrollableElement.addEventListener(
				'touchstart',
				this.onInnerTouchStartListener,
				{ passive: true },
			);

			document.addEventListener('mousemove', this.onDocumentMouseMoveListener);
			document.addEventListener('touchmove', this.onDocumentTouchMoveListener, {
				passive: false,
			});

			document.addEventListener('mouseup', this.onDocumentMouseUpListener);
			document.addEventListener('touchend', this.onDocumentTouchEndListener);
		}
	}

	private getTranslateXValue(): number {
		const transformMatrix = window.getComputedStyle(this.inner).transform;

		if (transformMatrix !== 'none') {
			const matrixValues = transformMatrix
				.match(/matrix.*\((.+)\)/)?.[1]
				.split(', ');

			if (matrixValues) {
				let translateX = parseFloat(
					matrixValues.length === 6 ? matrixValues[4] : matrixValues[12],
				);
				if (this.isRTL) translateX = -translateX;

				return isNaN(translateX) || translateX === 0 ? 0 : -translateX;
			}
		}
		return 0;
	}

	private removeClickEventWhileDragging(evt: MouseEvent) {
		evt.preventDefault();
	}

	private handleDragStart(evt: MouseEvent | TouchEvent): void {
		evt.preventDefault();

		this.isDragging = true;
		this.dragStartX = this.getEventX(evt);
		this.initialTranslateX = this.isRTL
			? this.getTranslateXValue()
			: -this.getTranslateXValue();

		this.inner.classList.add('dragging');
	}

	private handleDragMove(evt: MouseEvent | TouchEvent): void {
		if (!this.isDragging) return;

		this.inner.querySelectorAll('a:not(.prevented-click)').forEach((el) => {
			el.classList.add('prevented-click');
			el.addEventListener('click', this.removeClickEventWhileDragging);
		});

		const currentX = this.getEventX(evt);
		let deltaX = currentX - this.dragStartX;
		if (this.isRTL) deltaX = -deltaX;
		const newTranslateX = this.initialTranslateX + deltaX;
		const newTranslateXFunc = () => {
			let calcWidth =
				(this.sliderWidth * this.slides.length) / this.getCurrentSlidesQty() -
				this.sliderWidth;
			const containerWidth = this.sliderWidth;
			const itemWidth = containerWidth / this.getCurrentSlidesQty();
			const centeredOffset = (containerWidth - itemWidth) / 2;
			const limitStart = this.isCentered ? centeredOffset : 0;
			if (this.isCentered) calcWidth = calcWidth + centeredOffset;
			const limitEnd = -calcWidth;

			if (this.isRTL) {
				if (newTranslateX < limitStart) return limitStart;
				if (newTranslateX > calcWidth) return limitEnd;
				else return -newTranslateX;
			} else {
				if (newTranslateX > limitStart) return limitStart;
				else if (newTranslateX < -calcWidth) return limitEnd;
				else return newTranslateX;
			}
		};

		this.setTranslate(newTranslateXFunc());
	}

	private handleDragEnd(): void {
		if (!this.isDragging) return;
		this.isDragging = false;

		const containerWidth = this.sliderWidth;
		const itemWidth = containerWidth / this.getCurrentSlidesQty();
		const currentTranslateX = this.getTranslateXValue();
		let closestIndex = Math.round(currentTranslateX / itemWidth);
		if (this.isRTL) closestIndex = Math.round(currentTranslateX / itemWidth);

		this.inner.classList.remove('dragging');

		setTimeout(() => {
			this.calculateTransform(closestIndex);
			if (this.dots) this.setCurrentDot();

			this.dragStartX = null;
			this.initialTranslateX = null;

			this.inner.querySelectorAll('a.prevented-click').forEach((el) => {
				el.classList.remove('prevented-click');
				el.removeEventListener('click', this.removeClickEventWhileDragging);
			});
		});
	}

	private getEventX(event: MouseEvent | TouchEvent): number {
		return event instanceof MouseEvent
			? event.clientX
			: event.touches[0].clientX;
	}

	private getCurrentSlidesQty(): number {
		if (typeof this.slidesQty === 'object') {
			const windowWidth = document.body.clientWidth;
			let currentRes = 0;

			Object.keys(this.slidesQty).forEach((key: string) => {
				if (
					windowWidth >=
					(typeof key + 1 === 'number'
						? (this.slidesQty as TCarouselOptionsSlidesQty)[key]
						: BREAKPOINTS[key])
				) {
					currentRes = (this.slidesQty as TCarouselOptionsSlidesQty)[key];
				}
			});

			return currentRes;
		} else {
			return this.slidesQty as number;
		}
	}

	private buildSnapSpacers() {
		const existingBefore = this.inner.querySelector('.hs-snap-before');
		const existingAfter = this.inner.querySelector('.hs-snap-after');
		if (existingBefore) existingBefore.remove();
		if (existingAfter) existingAfter.remove();

		const containerWidth = this.sliderWidth;
		const itemWidth = containerWidth / this.getCurrentSlidesQty();
		const spacerWidth = containerWidth / 2 - itemWidth / 2;

		const before = htmlToElement(
			`<div class="hs-snap-before" style="height: 100%; width: ${spacerWidth}px"></div>`,
		);
		const after = htmlToElement(
			`<div class="hs-snap-after" style="height: 100%; width: ${spacerWidth}px"></div>`,
		);

		this.inner.prepend(before);
		this.inner.appendChild(after);
	}

	private initDots() {
		if (this.el.querySelectorAll('.hs-carousel-pagination-item').length) {
			this.setDots();
		} else this.buildDots();

		if (this.dots) this.setCurrentDot();
	}

	private buildDots() {
		this.dots.innerHTML = '';

		const slidesQty =
			!this.isCentered && this.slidesQty
				? this.slides.length - (this.getCurrentSlidesQty() - 1)
				: this.slides.length;

		for (let i = 0; i < slidesQty; i++) {
			const singleDot = this.buildSingleDot(i);

			this.dots.append(singleDot);
		}
	}

	private setDots() {
		this.dotsItems = this.dots.querySelectorAll('.hs-carousel-pagination-item');

		this.dotsItems.forEach((dot, ind) => {
			const targetIndex = dot.getAttribute(
				'data-carousel-pagination-item-target',
			);

			this.singleDotEvents(dot, targetIndex ? +targetIndex : ind);
		});
	}

	private goToCurrentDot() {
		const container = this.dots;
		const containerRect = container.getBoundingClientRect();
		const containerScrollLeft = container.scrollLeft;
		const containerScrollTop = container.scrollTop;
		const containerWidth = container.clientWidth;
		const containerHeight = container.clientHeight;

		const item = this.dotsItems[this.currentIndex];
		const itemRect = item.getBoundingClientRect();
		const itemLeft = itemRect.left - containerRect.left + containerScrollLeft;
		const itemRight = itemLeft + item.clientWidth;
		const itemTop = itemRect.top - containerRect.top + containerScrollTop;
		const itemBottom = itemTop + item.clientHeight;

		let scrollLeft = containerScrollLeft;
		let scrollTop = containerScrollTop;

		if (
			itemLeft < containerScrollLeft ||
			itemRight > containerScrollLeft + containerWidth
		) {
			scrollLeft = itemRight - containerWidth;
		}

		if (
			itemTop < containerScrollTop ||
			itemBottom > containerScrollTop + containerHeight
		) {
			scrollTop = itemBottom - containerHeight;
		}

		container.scrollTo({
			left: scrollLeft,
			top: scrollTop,
			behavior: 'smooth',
		});
	}

	private buildInfo() {
		if (this.infoTotal) this.setInfoTotal();
		if (this.infoCurrent) this.setInfoCurrent();
	}

	private setInfoTotal() {
		this.infoTotal.innerText = `${this.slides.length}`;
	}

	private setInfoCurrent() {
		this.infoCurrent.innerText = `${this.currentIndex + 1}`;
	}

	private buildSingleDot(ind: number) {
		const singleDot = htmlToElement('<span></span>');
		if (this.dotsItemClasses) classToClassList(this.dotsItemClasses, singleDot);

		this.singleDotEvents(singleDot, ind);

		return singleDot;
	}

	private singleDotEvents(dot: HTMLElement, ind: number) {
		this.onDotClickListener = () => this.dotClick(ind);

		dot.addEventListener('click', this.onDotClickListener);
	}

	private observeResize() {
		const resizeObserver = new ResizeObserver(
			debounce((entries: ResizeObserverEntry[]) => {
				for (let entry of entries) {
					const newWidth = entry.contentRect.width;

					if (newWidth !== this.resizeContainerWidth) {
						this.recalculateWidth();
						if (this.dots) this.initDots();
						this.addCurrentClass();

						this.resizeContainerWidth = newWidth;
					}
				}
			}, this.updateDelay),
		);

		resizeObserver.observe(this.resizeContainer);
	}

	private calculateWidth() {
		if (!this.isSnap) {
			this.inner.style.width = `${
				(this.sliderWidth * this.slides.length) / this.getCurrentSlidesQty()
			}px`;
		}

		this.slides.forEach((el) => {
			el.style.width = `${this.sliderWidth / this.getCurrentSlidesQty()}px`;
		});

		this.calculateTransform();
	}

	private addCurrentClass() {
		if (this.isSnap) {
			const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2);

			for (let i = 0; i < this.slides.length; i++) {
				const slide = this.slides[i];

				if (
					i <= this.currentIndex + itemsQty &&
					i >= this.currentIndex - itemsQty
				) {
					slide.classList.add('active');
				} else slide.classList.remove('active');
			}
		} else {
			const maxIndex = this.isCentered
				? this.currentIndex +
					this.getCurrentSlidesQty() +
					(this.getCurrentSlidesQty() - 1)
				: this.currentIndex + this.getCurrentSlidesQty();

			this.slides.forEach((el, i) => {
				if (i >= this.currentIndex && i < maxIndex) {
					el.classList.add('active');
				} else {
					el.classList.remove('active');
				}
			});
		}
	}

	private setCurrentDot() {
		const toggleDotActive = (el: HTMLElement | Element, i: number) => {
			let statement = false;
			const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2);

			if (this.isSnap && !this.hasSnapSpacers) {
				statement =
					i ===
					(this.getCurrentSlidesQty() % 2 === 0
						? this.currentIndex - itemsQty + 1
						: this.currentIndex - itemsQty);
			} else statement = i === this.currentIndex;

			if (statement) el.classList.add('active');
			else el.classList.remove('active');
		};

		if (this.dotsItems) {
			this.dotsItems.forEach((el, i) => toggleDotActive(el, i));
		} else {
			this.dots
				.querySelectorAll(':scope > *')
				.forEach((el, i) => toggleDotActive(el, i));
		}
	}

	private setElementToDisabled(el: HTMLElement) {
		el.classList.add('disabled');
		if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
			el.setAttribute('disabled', 'disabled');
		}
	}

	private unsetElementToDisabled(el: HTMLElement) {
		el.classList.remove('disabled');
		if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
			el.removeAttribute('disabled');
		}
	}

	private addDisabledClass() {
		if (!this.prev || !this.next) return false;

		const gapValue = getComputedStyle(this.inner).getPropertyValue('gap');
		const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2);
		let currentIndex = 0;
		let maxIndex = 0;
		let statementPrev = false;
		let statementNext = false;

		if (this.isSnap) {
			currentIndex = this.currentIndex;
			maxIndex = this.hasSnapSpacers
				? this.slides.length - 1
				: this.slides.length - itemsQty - 1;
			statementPrev = this.hasSnapSpacers
				? currentIndex === 0
				: this.getCurrentSlidesQty() % 2 === 0
					? currentIndex - itemsQty < 0
					: currentIndex - itemsQty === 0;
			statementNext =
				currentIndex >= maxIndex &&
				this.container.scrollLeft +
					this.container.clientWidth +
					(parseFloat(gapValue) || 0) >=
					this.container.scrollWidth;
		} else {
			currentIndex = this.currentIndex;
			maxIndex = this.isCentered
				? this.slides.length -
					this.getCurrentSlidesQty() +
					(this.getCurrentSlidesQty() - 1)
				: this.slides.length - this.getCurrentSlidesQty();
			statementPrev = currentIndex === 0;
			statementNext = currentIndex >= maxIndex;
		}

		if (statementPrev) {
			this.unsetElementToDisabled(this.next);
			this.setElementToDisabled(this.prev);
		} else if (statementNext) {
			this.unsetElementToDisabled(this.prev);
			this.setElementToDisabled(this.next);
		} else {
			this.unsetElementToDisabled(this.prev);
			this.unsetElementToDisabled(this.next);
		}
	}

	private autoPlay() {
		this.setTimer();
	}

	private setTimer() {
		this.timer = setInterval(() => {
			if (this.currentIndex === this.slides.length - 1) this.goTo(0);
			else this.goToNext();
		}, this.speed);
	}

	private resetTimer() {
		clearInterval(this.timer);
	}

	private detectDirection() {
		const deltaX = this.touchX.end - this.touchX.start;
		const deltaY = this.touchY.end - this.touchY.start;
		const absDeltaX = Math.abs(deltaX);
		const absDeltaY = Math.abs(deltaY);
		const SWIPE_THRESHOLD = 30;

		if (absDeltaX < SWIPE_THRESHOLD || absDeltaX < absDeltaY) return;

		const isSwipeToNext = this.isRTL ? deltaX > 0 : deltaX < 0;

		if (!this.isInfiniteLoop) {
			if (
				isSwipeToNext &&
				this.currentIndex < this.slides.length - this.getCurrentSlidesQty()
			) {
				this.goToNext();
			}
			if (!isSwipeToNext && this.currentIndex > 0) {
				this.goToPrev();
			}
		} else {
			if (isSwipeToNext) this.goToNext();
			else this.goToPrev();
		}
	}

	private calculateTransform(currentIdx?: number | undefined): void {
		if (currentIdx !== undefined) this.currentIndex = currentIdx;

		const containerWidth = this.sliderWidth;
		const itemWidth = containerWidth / this.getCurrentSlidesQty();
		let translateX = this.currentIndex * itemWidth;

		if (this.isSnap && !this.isCentered) {
			if (
				this.container.scrollLeft < containerWidth &&
				this.container.scrollLeft + itemWidth / 2 > containerWidth
			) {
				this.container.scrollLeft = this.container.scrollWidth;
			}
		}

		if (this.isCentered && !this.isSnap) {
			const centeredOffset = (containerWidth - itemWidth) / 2;

			if (this.currentIndex === 0) translateX = -centeredOffset;
			else if (
				this.currentIndex >=
				this.slides.length -
					this.getCurrentSlidesQty() +
					(this.getCurrentSlidesQty() - 1)
			) {
				const totalSlideWidth = this.slides.length * itemWidth;

				translateX = totalSlideWidth - containerWidth + centeredOffset;
			} else translateX = this.currentIndex * itemWidth - centeredOffset;
		}

		if (!this.isSnap) this.setTransform(translateX);

		if (this.isAutoHeight) {
			this.inner.style.height = `${
				this.slides[this.currentIndex].clientHeight
			}px`;
		}

		if (this.dotsItems) this.goToCurrentDot();

		this.addCurrentClass();
		if (!this.isInfiniteLoop) this.addDisabledClass();
		if (this.isSnap && this.hasSnapSpacers) this.buildSnapSpacers();
		if (this.infoCurrent) this.setInfoCurrent();
	}

	private setTransform(val: number) {
		if (this.slides.length > this.getCurrentSlidesQty()) {
			this.inner.style.transform = this.isRTL
				? `translate(${val}px, 0px)`
				: `translate(${-val}px, 0px)`;
		} else this.inner.style.transform = 'translate(0px, 0px)';
	}

	private setTranslate(val: number) {
		this.inner.style.transform = this.isRTL
			? `translate(${-val}px, 0px)`
			: `translate(${val}px, 0px)`;
	}

	private setIndex(i: number) {
		this.currentIndex = i;

		this.addCurrentClass();
		if (!this.isInfiniteLoop) this.addDisabledClass();
	}

	// Public methods
	public recalculateWidth() {
		this.sliderWidth = this.inner.parentElement.getBoundingClientRect().width;

		this.calculateWidth();

		if (
			this.sliderWidth !==
			this.inner.parentElement.getBoundingClientRect().width
		) {
			this.recalculateWidth();
		}
	}

	public goToPrev() {
		if (this.currentIndex > 0) {
			this.currentIndex--;
		} else {
			this.currentIndex = this.slides.length - this.getCurrentSlidesQty();
		}

		this.fireEvent('update', this.currentIndex);

		if (this.isSnap) {
			const itemWidth = this.sliderWidth / this.getCurrentSlidesQty();

			this.container.scrollBy({
				left: Math.max(-this.container.scrollLeft, -itemWidth),
				behavior: 'smooth',
			});

			this.addCurrentClass();
			if (!this.isInfiniteLoop) this.addDisabledClass();
		} else this.calculateTransform();

		if (this.dots) this.setCurrentDot();
	}

	public goToNext() {
		const statement = this.isCentered
			? this.slides.length -
				this.getCurrentSlidesQty() +
				(this.getCurrentSlidesQty() - 1)
			: this.slides.length - this.getCurrentSlidesQty();

		if (this.currentIndex < statement) {
			this.currentIndex++;
		} else {
			this.currentIndex = 0;
		}

		this.fireEvent('update', this.currentIndex);

		if (this.isSnap) {
			const itemWidth = this.sliderWidth / this.getCurrentSlidesQty();
			const maxScrollLeft =
				this.container.scrollWidth - this.container.clientWidth;

			this.container.scrollBy({
				left: Math.min(itemWidth, maxScrollLeft - this.container.scrollLeft),
				behavior: 'smooth',
			});

			this.addCurrentClass();
			if (!this.isInfiniteLoop) this.addDisabledClass();
		} else this.calculateTransform();

		if (this.dots) this.setCurrentDot();
	}

	public goTo(i: number) {
		const currentIndex = this.currentIndex;
		this.currentIndex = i;

		this.fireEvent('update', this.currentIndex);

		if (this.isSnap) {
			const itemWidth = this.sliderWidth / this.getCurrentSlidesQty();
			const index =
				currentIndex > this.currentIndex
					? currentIndex - this.currentIndex
					: this.currentIndex - currentIndex;
			const width =
				currentIndex > this.currentIndex
					? -(itemWidth * index)
					: itemWidth * index;

			this.container.scrollBy({
				left: width,
				behavior: 'smooth',
			});

			this.addCurrentClass();
			if (!this.isInfiniteLoop) this.addDisabledClass();
		} else this.calculateTransform();

		if (this.dots) this.setCurrentDot();
	}

	public destroy() {
		// Remove classes
		if (this.loadingClassesAdd) {
			if (typeof this.loadingClassesAdd === 'string') {
				this.inner.classList.remove(this.loadingClassesAdd);
			} else this.inner.classList.remove(...this.loadingClassesAdd);
		}
		if (this.inner && this.afterLoadingClassesAdd) {
			setTimeout(() => {
				if (typeof this.afterLoadingClassesAdd === 'string') {
					this.inner.classList.remove(this.afterLoadingClassesAdd);
				} else this.inner.classList.remove(...this.afterLoadingClassesAdd);
			});
		}
		this.el.classList.remove('init');
		this.inner.classList.remove('dragging');
		this.slides.forEach((el) => el.classList.remove('active'));
		if (this?.dotsItems?.length) {
			this.dotsItems.forEach((el) => el.classList.remove('active'));
		}
		this.prev.classList.remove('disabled');
		this.next.classList.remove('disabled');

		// Remove styles
		this.inner.style.width = '';
		this.slides.forEach((el) => (el.style.width = ''));
		if (!this.isSnap) this.inner.style.transform = '';
		if (this.isAutoHeight) this.inner.style.height = '';

		// Remove listeners
		this.prev.removeEventListener('click', this.onPrevClickListener);
		this.next.removeEventListener('click', this.onNextClickListener);
		this.container.removeEventListener(
			'scroll',
			this.onContainerScrollListener,
		);
		this.el.removeEventListener('touchstart', this.onElementTouchStartListener);
		this.el.removeEventListener('touchend', this.onElementTouchEndListener);
		this.inner.removeEventListener('mousedown', this.onInnerMouseDownListener);
		this.inner.removeEventListener(
			'touchstart',
			this.onInnerTouchStartListener,
		);
		document.removeEventListener('mousemove', this.onDocumentMouseMoveListener);
		document.removeEventListener('touchmove', this.onDocumentTouchMoveListener);
		document.removeEventListener('mouseup', this.onDocumentMouseUpListener);
		document.removeEventListener('touchend', this.onDocumentTouchEndListener);
		this.inner.querySelectorAll('a:not(.prevented-click)').forEach((el) => {
			el.classList.remove('prevented-click');
			el.removeEventListener('click', this.removeClickEventWhileDragging);
		});
		if (
			this?.dotsItems?.length ||
			this.dots.querySelectorAll(':scope > *').length
		) {
			const dots = this?.dotsItems || this.dots.querySelectorAll(':scope > *');

			dots.forEach((el) =>
				el.removeEventListener('click', this.onDotClickListener),
			);

			this.dots.innerHTML = null;
		}

		// Remove elements
		if (this.isSnap && this.hasSnapSpacers) {
			this.inner.querySelector('.hs-snap-before').remove();
			this.inner.querySelector('.hs-snap-after').remove();
		}

		this.dotsItems = null;

		this.isDragging = false;
		this.dragStartX = null;
		this.initialTranslateX = null;

		window.$hsCarouselCollection = window.$hsCarouselCollection.filter(
			({ element }) => element.el !== this.el,
		);
	}

	// Static methods
	static getInstance(target: HTMLElement | string, isInstance?: boolean) {
		const elInCollection = window.$hsCarouselCollection.find(
			(el) =>
				el.element.el ===
				(typeof target === 'string' ? document.querySelector(target) : target),
		);

		return elInCollection
			? isInstance
				? elInCollection
				: elInCollection.element
			: null;
	}

	static autoInit() {
		if (!window.$hsCarouselCollection) window.$hsCarouselCollection = [];

		if (window.$hsCarouselCollection) {
			window.$hsCarouselCollection = window.$hsCarouselCollection.filter(
				({ element }) => document.contains(element.el),
			);
		}

		document
			.querySelectorAll('[data-hs-carousel]:not(.--prevent-on-load-init)')
			.forEach((el: HTMLElement) => {
				if (
					!window.$hsCarouselCollection.find(
						(elC) => (elC?.element?.el as HTMLElement) === el,
					)
				) {
					new HSCarousel(el);
				}
			});
	}
}

export default HSCarousel;
