/**
 * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact schukai GmbH.
 */

import { instanceSymbol } from "../../constants.mjs";
import { ATTRIBUTE_PREFIX, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import { CustomElement, getSlottedElements } from "../../dom/customelement.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { SliderStyleSheet } from "./stylesheet/slider.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";

import { getWindow } from "../../dom/util.mjs";
import { isObject, isInteger } from "../../types/is.mjs";

export { Slider };

/**
 * @private
 * @type {symbol}
 */
const sliderElementSymbol = Symbol("sliderElement");

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

/**
 * @private
 * @type {symbol}
 */
const prevElementSymbol = Symbol("prevElement");

/**
 * @private
 * @type {symbol}
 */
const nextElementSymbol = Symbol("nextElement");

/**
 * @private
 * @type {symbol}
 */
const thumbnailElementSymbol = Symbol("thumbnailElement");

/**
 * @private
 * @type {symbol}
 */
const configSymbol = Symbol("config");

/**
 * A Slider
 *
 * @fragments /fragments/components/layout/slider/
 *
 * @example /examples/components/layout/slider-simple
 * @example /examples/components/layout/slider-carousel
 * @example /examples/components/layout/slider-multiple
 *
 * @since 3.74.0
 * @copyright schukai GmbH
 * @summary A beautiful Slider that can make your life easier and also looks good.
 * @fires monster-slider-resized
 * @fires monster-slider-moved
 */
class Slider extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/layout/slider@@instance");
	}

	/**
	 *
	 * @return {Components.Layout.Slider
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		this[configSymbol] = {
			currentIndex: 0,

			isDragging: false,
			draggingPos: 0,
			startPos: 0,
			autoPlayInterval: null,

			eventHandler: {
				mouseOverPause: null,
				mouseout: null,
				touchstart : null,
				touchend: null
			}

		};

		// set --monster-slides-width
		const slides = this.shadowRoot.querySelector(
			`[${ATTRIBUTE_ROLE}="slider"]`,
		);

		const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);
		slides.style.setProperty(
			"--monster-slides-width",
			`${100 / slidesVisible}%`,
		);

		initControlReferences.call(this);
		initEventHandler.call(this);
		initStructure.call(this);

		return this;
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {string} actions.click Callback when clicked
	 * @property {Object} features Features
	 * @property {boolean} features.carousel Carousel feature
	 * @property {boolean} features.autoPlay Auto play feature
	 * @property {boolean} features.thumbnails Thumbnails feature
	 * @property {boolean} features.drag Drag feature
	 * @property {Object} slides Slides configuration, an object with breakpoints and the number of slides to show
	 * @property {Object} slides.0 Number of slides to show at 0px
	 * @property {Object} slides.600 Number of slides to show at 600px @since 3.109.0
	 * @property {Object} slides.1200 Number of slides to show at 1200px @since 3.109.0
	 * @property {Object} slides.1800 Number of slides to show at 1800px @since 3.109.0
	 * @property {Object} carousel Carousel configuration
	 * @property {number} carousel.transition Transition time between a full rotation of the carousel
	 * @property {Object} autoPlay Auto play configuration
	 * @property {number} autoPlay.delay Delay between slides
	 * @property {number} autoPlay.startDelay Start delay
	 * @property {string} autoPlay.direction Direction of the autoplay
	 * @property {boolean} autoPlay.mouseOverPause Pause on mouse over
	 * @property {boolean} autoPlay.touchPause Pause on touch
	 * @property {Object} classes CSS classes
	 * @property {boolean} disabled Disabled state
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			classes: {},
			disabled: false,

			features: {
				carousel: true,
				autoPlay: true,
				thumbnails: true,
				drag: true,
			},

			slides: {
				0: 1,
				600: 2,
				1200: 3,
				1800: 4,
			},

			carousel: {
				transition: 250,
			},

			autoPlay: {
				delay: 1500,
				startDelay: 1000,
				direction: "next",
				mouseOverPause: true,
				touchPause: true,
			},
		});
	}

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

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

	/**
	 * moves the slider to the given index
	 *
	 * @param index
	 * @return {void}
	 */
	moveTo(index) {
		return moveTo.call(this, index);
	}

	/**
	 * shows the previous slide
	 *
	 * @return {void}
	 */
	previous() {
		return prev.call(this);
	}

	/**
	 * shows the next slide
	 *
	 * @return {void}
	 */
	next() {
		return next.call(this);
	}

	/**
	 * stops the auto play
	 *
	 * @return {void}
	 */
	stopAutoPlay() {
		if (this[configSymbol].autoPlayInterval) {
			clearInterval(this[configSymbol].autoPlayInterval);
		}
	}

	/**
	 * starts the auto play
	 *
	 * @return {void}
	 */
	startAutoPlay() {
		initAutoPlay.call(this);
	}
}

/**
 * @private
 * @param name
 */
//function initNavigation(name) {
//const element = this.shadowRoot.querySelector("." + name + "");
//const elementHeight = element.offsetHeight;
//element.style.top = `calc(50% - ${elementHeight / 2}px)`;
//}

/**
 * @private
 */
function initStructure() {
	//initNavigation.call(this, "next");
	//initNavigation.call(this, "prev");

	if (this.getOption("features.thumbnails")) {
		initThumbnails.call(this);
	}

	initShadows.call(this);

	if (this.getOption("features.autoPlay")) {
		initAutoPlay.call(this);
	}
}

/**
 * @private
 */
function initThumbnails() {
	const self = this;
	const thumbnails = this.shadowRoot.querySelector(
		"[data-monster-role='thumbnails']",
	);

	// remove all thumbnails
	while (thumbnails.firstChild) {
		thumbnails.removeChild(thumbnails.firstChild);
	}

	const { originSlides } = getSlidesAndTotal.call(this);

	originSlides.forEach((x, index) => {
		const thumbnail = document.createElement("div");
		thumbnail.classList.add("thumbnail");
		thumbnail.addEventListener("click", () => {
			this.moveTo(index);
		});

		thumbnails.appendChild(thumbnail);
	});

	this.addEventListener("monster-slider-moved", (e) => {
		const index = e.detail.index;
		const thumbnail = thumbnails.children[index];

		if (!thumbnail) {
			return;
		}

		Array.from(thumbnails.children).forEach((thumb) => {
			thumb.classList.remove("current");
		});

		thumbnail.classList.add("current");
	});
}

/**
 * @private
 */
function initAutoPlay() {
	const self = this;
	const autoPlay = this.getOption("autoPlay");
	const delay = autoPlay.delay;
	const startDelay = autoPlay.startDelay;
	const direction = autoPlay.direction;

	function start() {
		if (self[configSymbol].autoPlayInterval) {
			clearInterval(self[configSymbol].autoPlayInterval);
		}

		self[configSymbol].autoPlayInterval = setInterval(() => {
			const { totalOriginSlides } = getSlidesAndTotal.call(self);

			if (direction === "next") {
				if (
					!self.getOption("features.carousel") &&
					self[configSymbol].currentIndex >= totalOriginSlides - 1
				) {
					self[configSymbol].currentIndex = -1;
				}
				self.next();
			} else {
				if (
					!self.getOption("features.carousel") &&
					self[configSymbol].currentIndex <= 0
				) {
					self[configSymbol].currentIndex = totalOriginSlides;
				}
				self.previous();
			}
		}, delay);
	}

	setTimeout(() => {
		start();
	}, startDelay);

	if (autoPlay.mouseOverPause) {

		if(this[configSymbol].eventHandler.mouseOverPause===null) {
			this[configSymbol].eventHandler.mouseOverPause =  () => {
				clearInterval(this[configSymbol].autoPlayInterval);
			}

			this.addEventListener("mouseover",this[configSymbol].eventHandler.mouseOverPause);
		}

		if(this[configSymbol].eventHandler.mouseout===null) {

			this[configSymbol].eventHandler.mouseout = () => {
				if (this[configSymbol].isDragging) {
					return;
				}
				start();
			}

			this.addEventListener("mouseout", this[configSymbol].eventHandler.mouseout);
		}

	}

	if (autoPlay.touchPause) {
		if(this[configSymbol].eventHandler.touchstart===null) {
			this[configSymbol].eventHandler.touchstart =  () => {
				clearInterval(this[configSymbol].autoPlayInterval);
			}

			this.addEventListener("touchstart",this[configSymbol].eventHandler.touchstart);
		}

		if(this[configSymbol].eventHandler.touchend===null) {

			this[configSymbol].eventHandler.touchend = () => {
				if (this[configSymbol].isDragging) {
					return;
				}
				start();
			}

			this.addEventListener("touchend", this[configSymbol].eventHandler.touchend);
		}
	}
}

function getVisibleSlidesFromContainerWidth() {
	const containerWidth = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="slider"]`,
	).offsetWidth;
	const slides = this.getOption("slides");
	let visibleSlides = 1;

	if (!isObject(slides)) {
		return visibleSlides;
	}

	for (const key in slides) {
		if (containerWidth >= key) {
			visibleSlides = slides[key];
		}
	}

	const { originSlides } = getSlidesAndTotal.call(this);
	if (visibleSlides > originSlides.length) {
		visibleSlides = originSlides.length - 1;
	}

	return visibleSlides;
}

/**
 * @private
 */
function initShadows() {
	const { slides, totalSlides } = getSlidesAndTotal.call(this);
	const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);

	if (totalSlides > slidesVisible) {
		let current = slides[0];
		let last = slides[totalSlides - 1];
		for (let i = 0; i < slidesVisible; i++) {
			const clone = current.cloneNode(true);
			clone.setAttribute("data-monster-clone-from", i);
			last.insertAdjacentElement("afterend", clone);
			current = current.nextElementSibling;
			last = clone;
		}

		current = slides[totalSlides - 1];
		let first = slides[0];
		for (let i = 0; i < slidesVisible; i++) {
			const clone = current.cloneNode(true);
			clone.setAttribute("data-monster-clone-from", totalSlides - i);
			first.insertAdjacentElement("beforebegin", clone);
			current = current.previousElementSibling;
			first = clone;
		}

		moveTo.call(this, 0);
	}
}

/**
 * @private
 * @return {{slides: unknown[], totalSlides: number}}
 */
function getSlidesAndTotal() {
	const originSlides = Array.from(
		getSlottedElements.call(
			this,
			":scope:not([data-monster-clone-from])",
			null,
		),
	);
	const totalOriginSlides = originSlides.length;

	const slides = Array.from(getSlottedElements.call(this, ":scope", null));
	const totalSlides = slides.length;

	return { originSlides, totalOriginSlides, slides, totalSlides };
}

/**
 * @private
 * @return {number}
 */
function next() {
	const nextIndex = this[configSymbol].currentIndex + 1;

	queueMicrotask(() => {
		getWindow().requestAnimationFrame(() => {
			getWindow().requestAnimationFrame(() => {
				moveTo.call(this, nextIndex);
			});
		});
	});

	return 0;
}

/**
 * @private
 * @return {number}
 */
function prev() {
	const prevIndex = this[configSymbol].currentIndex - 1;

	queueMicrotask(() => {
		getWindow().requestAnimationFrame(() => {
			getWindow().requestAnimationFrame(() => {
				moveTo.call(this, prevIndex);
			});
		});
	});

	return 0;
}

/**
 * @private
 * @param slides
 * @param index
 */
function setMoveProperties(slides, index) {
	slides.forEach((slide) => {
		slide.classList.remove("current");
	});

	let offset = -(index * 100);
	const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);

	offset = offset / slidesVisible;

	if (offset !== 0) {
		offset += "%";
	}

	this[sliderElementSymbol].style.transform =
		`translateX(calc(${offset} + ${this[configSymbol].draggingPos}px))`;

	if (slides[index]) {
		slides[index].classList.add("current");
	}
	this[configSymbol].lastOffset = offset;
}

/**
 * @private
 * @param {number} index
 * @param {boolean} animation
 * @fires monster-slider-moved
 */
function moveTo(index, animation) {
	const { slides, totalSlides, originSlides, totalOriginSlides } =
		getSlidesAndTotal.call(this);

	if (animation === false) {
		this[sliderElementSymbol].classList.remove("animate");
	} else {
		this[sliderElementSymbol].classList.add("animate");
	}

	if (this.getOption("features.carousel") === true) {
		if (index < 0) {
			index = -1;
		}

		if (index > totalOriginSlides) {
			index = totalOriginSlides;
		}
	} else {
		if (index < 0) {
			index = 0;
		}

		if (index >= totalOriginSlides) {
			index = totalOriginSlides - 1;
		}
	}

	if (!isInteger(index)) {
		return;
	}

	const visibleSlides = getVisibleSlidesFromContainerWidth.call(this);
	if (totalOriginSlides <= visibleSlides) {
		this[prevElementSymbol].classList.add("hidden");
		this[nextElementSymbol].classList.add("hidden");
		this[thumbnailElementSymbol].classList.add("hidden");
		return;
	}
	this[prevElementSymbol].classList.remove("hidden");
	this[nextElementSymbol].classList.remove("hidden");
	this[thumbnailElementSymbol].classList.remove("hidden");

	let slidesIndex = index + visibleSlides;
	this[configSymbol].currentIndex = index;

	if (slidesIndex < 0) {
		slidesIndex = totalSlides - 1 - visibleSlides;
		this[configSymbol].currentIndex = totalOriginSlides - 1;
	} else if (index > totalOriginSlides) {
		slidesIndex = 0;
		this[configSymbol].currentIndex = 0;
	}

	setMoveProperties.call(this, slides, slidesIndex);

	if (index === totalOriginSlides) {
		setTimeout(() => {
			getWindow().requestAnimationFrame(() => {
				moveTo.call(this, 0, false);
			});
		}, this.getOption("carousel.transition"));
	} else if (index === -1) {
		setTimeout(() => {
			getWindow().requestAnimationFrame(() => {
				moveTo.call(this, totalOriginSlides - 1, false);
			});
		}, this.getOption("carousel.transition"));
	}

	fireCustomEvent(this, "monster-slider-moved", {
		index: index,
	});
}

/**
 * @private
 * @return {initEventHandler}
 * @fires monster-slider-resized
 */
function initEventHandler() {
	const self = this;

	const nextElements = this[nextElementSymbol];

	if (nextElements) {
		nextElements.addEventListener("click", () => {
			self.next();
		});
	}

	const prevElements = this[prevElementSymbol];

	if (prevElements) {
		prevElements.addEventListener("click", () => {
			self.previous();
		});
	}

	if (this.getOption("features.drag")) {
		this[sliderElementSymbol].addEventListener("mousedown", (e) =>
			startDragging.call(this, e, "mouse"),
		);

		this[sliderElementSymbol].addEventListener("touchstart", (e) =>
			startDragging.call(this, e, "touch"),
		);
	}


	const initialSize = {
		width: this[sliderElementSymbol]?.offsetWidth || 0,
		height: this[sliderElementSymbol]?.offsetHeight || 0
	};

	const resizeObserver = new ResizeObserver(entries => {
		for (let entry of entries) {
			const {width, height} = entry.contentRect;
			if (width !== initialSize.width || height !== initialSize.height) {

				self.stopAutoPlay();

				if (this.getOption("features.thumbnails")) {
					initThumbnails.call(this);
				}

				const slidesVisible = getVisibleSlidesFromContainerWidth.call(this);
				this[sliderElementSymbol].style.setProperty(
					"--monster-slides-width",
					`${100 / slidesVisible}%`,
				);

				moveTo.call(self,0,false)
				self.startAutoPlay();

				fireCustomEvent(self, "monster-slider-resized", {
					width: width,
					height: height
				});
			}
		}
	});

	resizeObserver.observe(this[sliderElementSymbol]);

	

	return this;
}

/**
 * @private
 * @param e
 * @param type
 */
function startDragging(e, type) {
	const { slides } = getSlidesAndTotal.call(this);

	const widthOfSlider = slides[this[configSymbol].currentIndex]?.offsetWidth;

	this[configSymbol].isDragging = true;
	this[configSymbol].startPos = getPositionX(e, type);
	this[sliderElementSymbol].classList.add("grabbing");
	this[sliderElementSymbol].style.transitionProperty = "none";

	const callbackMousemove = (x) => {
		dragging.call(this, x, type);
	};

	const callbackMouseUp = () => {
		const endEvent = type === "mouse" ? "mouseup" : "touchend";
		const moveEvent = type === "mouse" ? "mousemove" : "touchmove";

		document.body.removeEventListener(endEvent, callbackMouseUp);
		document.body.removeEventListener(moveEvent, callbackMousemove);

		this[configSymbol].isDragging = false;
		this[configSymbol].startPos = 0;
		this[sliderElementSymbol].classList.remove("grabbing");
		this[sliderElementSymbol].style.transitionProperty = "";

		const lastPos = this[configSymbol].draggingPos;
		this[configSymbol].draggingPos = 0;

		let newIndex = this[configSymbol].currentIndex;
		const shift = lastPos / widthOfSlider;
		const shiftIndex = Math.round(shift);

		newIndex += shiftIndex * -1;
		this.moveTo(newIndex);
	};

	document.body.addEventListener("mouseup", callbackMouseUp);
	document.body.addEventListener("mousemove", callbackMousemove);
}

/**
 * @private
 * @param e
 * @param type
 * @return {*|number|number}
 */
function getPositionX(e, type) {
	return type === "mouse" ? e.pageX : e.touches[0].clientX;
}

/**
 * @private
 * @param e
 * @param type
 */
function dragging(e, type) {
	if (!this[configSymbol].isDragging) return;
	this[configSymbol].draggingPos =
		getPositionX(e, type) - this[configSymbol].startPos;

	this[sliderElementSymbol].style.transform =
		`translateX(calc(${this[configSymbol].lastOffset} + ${this[configSymbol].draggingPos}px))`;
}

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

	this[sliderElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="slider"]`,
	);

	this[prevElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="prev"]`,
	);

	this[nextElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="next"]`,
	);

	this[thumbnailElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="thumbnails"]`,
	);

}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div class="prev" data-monster-role="prev" part="prev" part="prev">
                <slot name="prev"></slot>
            </div>
            <div data-monster-role="slider" part="slides">
                <slot></slot>
            </div>
            <div data-monster-role="thumbnails" part="thumbnails"></div>
            <div class="next" data-monster-role="next" part="next" part="next">
                <slot name="next"></slot>
            </div>
        </div>`;
}

registerCustomElement(Slider);