/*
 * HSDatepicker
 * @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 { dispatch } from '../../utils';
import { Calendar, DatesArr, Range } from 'vanilla-calendar-pro';

import CustomVanillaCalendar from './vanilla-datepicker-pro';

import { templates } from './templates';
import { templatesBasedOnUtility } from './templates-utility';

import { todayTranslations } from './locale';
import { classToClassList, htmlToElement } from '../../utils';
import HSSelect from '../select/core';
import { ISelectOptions } from '../select/interfaces';

import {
	ICustomDatepickerOptions,
	IDatepicker,
	ITemplates,
} from './interfaces';

import HSBasePlugin from '../base-plugin';
import { ICollectionItem } from '../../interfaces';

declare var _: any;

class HSDatepicker extends HSBasePlugin<{}> implements IDatepicker {
	private dataOptions: ICustomDatepickerOptions;
	private concatOptions: ICustomDatepickerOptions;
	private updatedStyles: ICustomDatepickerOptions['styles'];
	private applyUtilityClasses: boolean;
	private templatesByType: ITemplates;

	private vanillaCalendar: Calendar;

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

		const dataOptions: ICustomDatepickerOptions = el.getAttribute(
			'data-hs-datepicker',
		)
			? JSON.parse(el.getAttribute('data-hs-datepicker')!)
			: {};

		this.dataOptions = {
			...dataOptions,
			...options,
		};
		this.applyUtilityClasses =
			typeof this.dataOptions?.applyUtilityClasses !== 'undefined'
				? this.dataOptions?.applyUtilityClasses
				: false;

		this.templatesByType = this.applyUtilityClasses
			? templates
			: templatesBasedOnUtility;

		const removeDefaultStyles =
			typeof this.dataOptions?.removeDefaultStyles !== 'undefined'
				? this.dataOptions?.removeDefaultStyles
				: false;

		this.updatedStyles = _.mergeWith(
			removeDefaultStyles ? {} : CustomVanillaCalendar.defaultStyles,
			this.dataOptions?.styles || {},
			(a: any, b: any) => {
				if (typeof a === 'string' && typeof b === 'string') {
					return `${a} ${b}`;
				}
			},
		);

		const today = new Date();
		const defaults = {
			selectedTheme: this.dataOptions.selectedTheme ?? '',
			styles: this.updatedStyles,
			dateMin: this.dataOptions.dateMin ?? today.toISOString().split('T')[0],
			dateMax: this.dataOptions.dateMax ?? '2470-12-31',
			mode: this.dataOptions.mode ?? 'default',
			inputMode:
				typeof this.dataOptions.inputMode !== 'undefined'
					? this.dataOptions.inputMode
					: true,
		};

		const chainCallbacks =
			(superCallback?: Function, customCallback?: (self: Calendar) => void) =>
			(self: Calendar) => {
				superCallback?.(self);
				customCallback?.(self);
			};
		const initTime = (self: Calendar) => {
			if (this.hasTime(self)) this.initCustomTime(self);
		};
		const _options = {
			layouts: {
				month: this.templatesByType.month(defaults.selectedTheme),
			},
			onInit: chainCallbacks(this.dataOptions.onInit, (self) => {
				if (defaults.mode === 'custom-select' && !this.dataOptions.inputMode) {
					initTime(self);
				}
			}),
			onShow: chainCallbacks(this.dataOptions.onShow, (self) => {
				if (defaults.inputMode) {
					requestAnimationFrame(() => {
						requestAnimationFrame(() => {
							window.dispatchEvent(new Event('resize'));
						});
					});
				}

				if (defaults.mode === 'custom-select') {
					this.updateCustomSelects(self);
					initTime(self);
				}
			}),
			onHide: chainCallbacks(this.dataOptions.onHide, (self) => {
				if (defaults.mode === 'custom-select') {
					this.destroySelects(self.context.mainElement);
				}
			}),
			onUpdate: chainCallbacks(this.dataOptions.onUpdate, (self) => {
				this.updateCalendar(self.context.mainElement);
			}),
			onCreateDateEls: chainCallbacks(
				this.dataOptions.onCreateDateEls,
				(self) => {
					if (defaults.mode === 'custom-select') this.updateCustomSelects(self);
				},
			),
			onChangeToInput: chainCallbacks(
				this.dataOptions.onChangeToInput,
				(self) => {
					if (!self.context.inputElement) return;

					this.setInputValue(
						self.context.inputElement,
						self.context.selectedDates,
					);

					const data = {
						selectedDates: self.context.selectedDates,
						selectedTime: self.context.selectedTime,
						rest: self.context,
					};

					this.fireEvent('change', data);
					dispatch('change.hs.datepicker', this.el, data);
				},
			),
			onChangeTime: chainCallbacks(this.dataOptions.onChangeTime, initTime),
			onClickYear: chainCallbacks(this.dataOptions.onClickYear, initTime),
			onClickMonth: chainCallbacks(this.dataOptions.onClickMonth, initTime),
			onClickArrow: chainCallbacks(this.dataOptions.onClickArrow, (self) => {
				if (defaults.mode === 'custom-select') {
					setTimeout(() => {
						this.disableNav();
						this.disableOptions();
						this.updateCalendar(self.context.mainElement);
					});
				}
			}),
		};

		this.concatOptions = _.merge(_options, this.dataOptions);

		const processedOptions = {
			...defaults,
			layouts: {
				default: this.processCustomTemplate(
					this.templatesByType.default(defaults.selectedTheme),
					'default',
				),
				multiple: this.processCustomTemplate(
					this.templatesByType.multiple(defaults.selectedTheme),
					'multiple',
				),
				year: this.processCustomTemplate(
					this.templatesByType.year(defaults.selectedTheme),
					'default',
				),
			},
		};

		this.concatOptions = _.merge(this.concatOptions, processedOptions);
		this.vanillaCalendar = new CustomVanillaCalendar(
			this.el,
			this.concatOptions,
		);

		this.init();
	}

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

		this.vanillaCalendar.init();

		if (this.dataOptions?.selectedDates) {
			this.setInputValue(
				this.vanillaCalendar.context.inputElement,
				this.formatDateArrayToIndividualDates(this.dataOptions?.selectedDates),
			);
		}
	}

	private getTimeParts(time: string) {
		const [_time, meridiem] = time.split(' ');
		const [hours, minutes] = _time.split(':');

		return [hours, minutes, meridiem];
	}

	private getCurrentMonthAndYear(el: HTMLElement) {
		const currentMonthHolder = el.querySelector('[data-vc="month"]');
		const currentYearHolder = el.querySelector('[data-vc="year"]');

		return {
			month: +currentMonthHolder.getAttribute('data-vc-month'),
			year: +currentYearHolder.getAttribute('data-vc-year'),
		};
	}

	private extractSeparatorFromFormat(format: string): string {
		const match = format.match(/[^A-Za-z0-9]/);

		return match ? match[0] : '.';
	}

	private setInputValue(target: HTMLInputElement, dates: DatesArr) {
		const dateFormat = this.dataOptions?.dateFormat;
		const extractedSeparator = dateFormat
			? this.extractSeparatorFromFormat(dateFormat)
			: null;
		const dateSeparator =
			extractedSeparator ??
			this.dataOptions?.inputModeOptions?.dateSeparator ??
			'.';
		const itemsSeparator =
			this.dataOptions?.inputModeOptions?.itemsSeparator ?? ', ';
		const selectionDatesMode = this.dataOptions?.selectionDatesMode ?? 'single';

		if (dates.length && dates.length > 1) {
			if (selectionDatesMode === 'multiple') {
				const temp: string[] = [];
				dates.forEach((date) =>
					temp.push(
						dateFormat
							? this.formatDate(date, dateFormat)
							: this.changeDateSeparator(date, dateSeparator),
					),
				);

				target.value = temp.join(itemsSeparator);
			} else {
				const formattedStart = dateFormat
					? this.formatDate(dates[0], dateFormat)
					: this.changeDateSeparator(dates[0], dateSeparator);
				const formattedEnd = dateFormat
					? this.formatDate(dates[1], dateFormat)
					: this.changeDateSeparator(dates[1], dateSeparator);

				target.value = [formattedStart, formattedEnd].join(itemsSeparator);
			}
		} else if (dates.length && dates.length === 1) {
			target.value = dateFormat
				? this.formatDate(dates[0], dateFormat)
				: this.changeDateSeparator(dates[0], dateSeparator);
		} else target.value = '';
	}

	private getLocalizedTodayText(locale?: string): string {
		return todayTranslations[locale] || 'Today';
	}

	private changeDateSeparator(
		date: string | number | Date,
		separator = '.',
		defaultSeparator = '-',
	) {
		const dateObj = new Date(date);

		if (this.dataOptions?.replaceTodayWithText) {
			const today = new Date();
			const isToday = dateObj.toDateString() === today.toDateString();

			if (isToday) {
				const dateLocale = this.dataOptions?.dateLocale;

				return this.getLocalizedTodayText(dateLocale);
			}
		}

		const newDate = (date as string).split(defaultSeparator);
		return newDate.join(separator);
	}

	private formatDateArrayToIndividualDates(dates: DatesArr): string[] {
		const selectionDatesMode = this.dataOptions?.selectionDatesMode ?? 'single';
		const expandDateRange = (start: string, end: string): string[] => {
			const startDate = new Date(start);
			const endDate = new Date(end);
			const result: string[] = [];

			while (startDate <= endDate) {
				result.push(startDate.toISOString().split('T')[0]);
				startDate.setDate(startDate.getDate() + 1);
			}

			return result;
		};
		const formatDate = (date: string | number | Date): string[] => {
			if (typeof date === 'string') {
				if (date.toLowerCase() === 'today') {
					const today = new Date();

					return [today.toISOString().split('T')[0]];
				}

				const rangeMatch = date.match(
					/^(\d{4}-\d{2}-\d{2})\s*[^a-zA-Z0-9]*\s*(\d{4}-\d{2}-\d{2})$/,
				);

				if (rangeMatch) {
					const [_, start, end] = rangeMatch;

					return selectionDatesMode === 'multiple-ranged'
						? [start, end]
						: expandDateRange(start.trim(), end.trim());
				}

				return [date];
			} else if (typeof date === 'number') {
				return [new Date(date).toISOString().split('T')[0]];
			} else if (date instanceof Date) {
				return [date.toISOString().split('T')[0]];
			}

			return [];
		};

		return dates.flatMap(formatDate);
	}

	private hasTime(el: Calendar) {
		const { mainElement } = el.context;
		const hours = mainElement.querySelector(
			'[data-hs-select].--hours',
		) as HTMLElement;
		const minutes = mainElement.querySelector(
			'[data-hs-select].--minutes',
		) as HTMLElement;
		const meridiem = mainElement.querySelector(
			'[data-hs-select].--meridiem',
		) as HTMLElement;

		return hours && minutes && meridiem;
	}

	private createArrowFromTemplate(
		template: string,
		classes: string | boolean = false,
	) {
		if (!classes) return template;

		const temp = htmlToElement(template);
		classToClassList(classes as string, temp);

		return temp.outerHTML;
	}

	private concatObjectProperties<
		T extends ISelectOptions,
		U extends ISelectOptions,
	>(shared: T, other: U): Partial<T & U> {
		const result: Partial<T & U> = {};
		const allKeys = new Set<keyof T | keyof U>([
			...Object.keys(shared || {}),
			...Object.keys(other || {}),
		] as Array<keyof T | keyof U>);

		allKeys.forEach((key) => {
			const sharedValue = shared[key as keyof T] || '';
			const otherValue = other[key as keyof U] || '';

			result[key as keyof T & keyof U] =
				`${sharedValue} ${otherValue}`.trim() as T[keyof T & keyof U] &
					U[keyof T & keyof U];
		});

		return result;
	}

	private updateTemplate(
		template: string,
		shared: ISelectOptions,
		specific: ISelectOptions,
	) {
		if (!shared) return template;

		const defaultOptions = JSON.parse(
			template.match(/data-hs-select='([^']+)'/)[1],
		);
		const concatOptions = this.concatObjectProperties(shared, specific);
		const mergedOptions = _.merge(defaultOptions, concatOptions);
		const updatedTemplate = template.replace(
			/data-hs-select='[^']+'/,
			`data-hs-select='${JSON.stringify(mergedOptions)}'`,
		);

		return updatedTemplate;
	}

	private initCustomTime(self: Calendar) {
		const { mainElement } = self.context;
		const timeParts = this.getTimeParts(self.selectedTime ?? '12:00 PM');
		const selectors = {
			hours: mainElement.querySelector(
				'[data-hs-select].--hours',
			) as HTMLElement,
			minutes: mainElement.querySelector(
				'[data-hs-select].--minutes',
			) as HTMLElement,
			meridiem: mainElement.querySelector(
				'[data-hs-select].--meridiem',
			) as HTMLElement,
		};

		Object.entries(selectors).forEach(([key, element]) => {
			if (!HSSelect.getInstance(element, true)) {
				const instance = new HSSelect(element);

				instance.setValue(
					timeParts[key === 'meridiem' ? 2 : key === 'minutes' ? 1 : 0],
				);
				instance.el.addEventListener('change.hs.select', (evt: CustomEvent) => {
					this.destroySelects(mainElement);
					const updatedTime = {
						hours: key === 'hours' ? evt.detail.payload : timeParts[0],
						minutes: key === 'minutes' ? evt.detail.payload : timeParts[1],
						meridiem: key === 'meridiem' ? evt.detail.payload : timeParts[2],
					};

					self.set(
						{
							selectedTime: `${updatedTime.hours}:${updatedTime.minutes} ${updatedTime.meridiem}`,
						},
						{
							dates: false,
							year: false,
							month: false,
						},
					);
				});
			}
		});
	}

	private initCustomMonths(self: Calendar) {
		const { mainElement } = self.context;
		const columns = Array.from(mainElement.querySelectorAll('.--single-month'));

		if (columns.length) {
			columns.forEach((column: HTMLElement, idx: number) => {
				const _month = column.querySelector(
					'[data-hs-select].--month',
				) as HTMLElement;
				const isInstanceExists = HSSelect.getInstance(_month, true);

				if (isInstanceExists) return false;

				const instance = new HSSelect(_month);
				const { month, year } = this.getCurrentMonthAndYear(column);

				instance.setValue(`${month}`);

				instance.el.addEventListener('change.hs.select', (evt: CustomEvent) => {
					this.destroySelects(mainElement);
					self.set(
						{
							selectedMonth: (+evt.detail.payload - idx < 0
								? 11
								: +evt.detail.payload - idx) as Range<12>,
							selectedYear: (+evt.detail.payload - idx < 0
								? +year - 1
								: year) as number,
						},
						{
							dates: false,
							time: false,
						},
					);
				});
			});
		}
	}

	private initCustomYears(self: Calendar) {
		const { mainElement } = self.context;
		const columns = Array.from(mainElement.querySelectorAll('.--single-month'));

		if (columns.length) {
			columns.forEach((column: HTMLElement) => {
				const _year = column.querySelector(
					'[data-hs-select].--year',
				) as HTMLElement;
				const isInstanceExists = HSSelect.getInstance(_year, true);

				if (isInstanceExists) return false;

				const instance = new HSSelect(_year);
				const { month, year } = this.getCurrentMonthAndYear(column);

				instance.setValue(`${year}`);

				instance.el.addEventListener('change.hs.select', (evt: CustomEvent) => {
					const { dateMax, displayMonthsCount } = this.vanillaCalendar.context;
					const maxYear = new Date(dateMax).getFullYear();
					const maxMonth = new Date(dateMax).getMonth();

					this.destroySelects(mainElement);
					self.set(
						{
							selectedMonth: (month > maxMonth - displayMonthsCount &&
							+evt.detail.payload === maxYear
								? maxMonth - displayMonthsCount + 1
								: month) as Range<12>,
							selectedYear: evt.detail.payload,
						},
						{
							dates: false,
							time: false,
						},
					);
				});
			});
		}
	}

	private generateCustomTimeMarkup() {
		const customSelectOptions = this.updatedStyles?.customSelect;
		const hours = customSelectOptions
			? this.updateTemplate(
					this.templatesByType.hours(this.concatOptions.selectedTheme),
					customSelectOptions?.shared || ({} as ISelectOptions),
					customSelectOptions?.hours || ({} as ISelectOptions),
				)
			: this.templatesByType.hours(this.concatOptions.selectedTheme);
		const minutes = customSelectOptions
			? this.updateTemplate(
					this.templatesByType.minutes(this.concatOptions.selectedTheme),
					customSelectOptions?.shared || ({} as ISelectOptions),
					customSelectOptions?.minutes || ({} as ISelectOptions),
				)
			: this.templatesByType.minutes(this.concatOptions.selectedTheme);
		const meridiem = customSelectOptions
			? this.updateTemplate(
					this.templatesByType.meridiem(this.concatOptions.selectedTheme),
					customSelectOptions?.shared || ({} as ISelectOptions),
					customSelectOptions?.meridiem || ({} as ISelectOptions),
				)
			: this.templatesByType.meridiem(this.concatOptions.selectedTheme);
		const time =
			this?.dataOptions?.templates?.time ??
			`
			<div class="pt-3 flex justify-center items-center gap-x-2">
        ${hours}
        <span class="text-gray-800 ${
					this.concatOptions.selectedTheme !== 'light' ? 'dark:text-white' : ''
				}">:</span>
        ${minutes}
        ${meridiem}
      </div>
		`;

		return `<div class="--time">${time}</div>`;
	}

	private generateCustomMonthMarkup() {
		const mode = this?.dataOptions?.mode ?? 'default';
		const customSelectOptions = this.updatedStyles?.customSelect;
		const updatedTemplate = customSelectOptions
			? this.updateTemplate(
					this.templatesByType.months(this.concatOptions.selectedTheme),
					customSelectOptions?.shared || ({} as ISelectOptions),
					customSelectOptions?.months || ({} as ISelectOptions),
				)
			: this.templatesByType.months(this.concatOptions.selectedTheme);
		const month = mode === 'custom-select' ? updatedTemplate : '<#Month />';

		return month;
	}

	private generateCustomYearMarkup() {
		const mode = this?.dataOptions?.mode ?? 'default';

		if (mode === 'custom-select') {
			const today = new Date();
			const dateMin =
				this?.dataOptions?.dateMin ?? today.toISOString().split('T')[0];
			const tempDateMax = this?.dataOptions?.dateMax ?? '2470-12-31';
			const dateMax = tempDateMax;
			const startDate = new Date(dateMin);
			const endDate = new Date(dateMax);
			const startDateYear = startDate.getFullYear();
			const endDateYear = endDate.getFullYear();
			const generateOptions = () => {
				let result = '';

				for (let i = startDateYear; i <= endDateYear; i++) {
					result += `<option value="${i}">${i}</option>`;
				}

				return result;
			};
			const years = this.templatesByType.years(
				generateOptions(),
				this.concatOptions.selectedTheme,
			);
			const customSelectOptions = this.updatedStyles?.customSelect;
			const updatedTemplate = customSelectOptions
				? this.updateTemplate(
						years,
						customSelectOptions?.shared || ({} as ISelectOptions),
						customSelectOptions?.years || ({} as ISelectOptions),
					)
				: years;

			return updatedTemplate;
		} else {
			return '<#Year />';
		}
	}

	private generateCustomArrowPrevMarkup() {
		const arrowPrev = this?.dataOptions?.templates?.arrowPrev
			? this.createArrowFromTemplate(
					this.dataOptions.templates.arrowPrev,
					this.updatedStyles.arrowPrev,
				)
			: '<#ArrowPrev [month] />';

		return arrowPrev;
	}

	private generateCustomArrowNextMarkup() {
		const arrowNext = this?.dataOptions?.templates?.arrowNext
			? this.createArrowFromTemplate(
					this.dataOptions.templates.arrowNext,
					this.updatedStyles.arrowNext,
				)
			: '<#ArrowNext [month] />';

		return arrowNext;
	}

	private parseCustomTime(template: string) {
		template = template.replace(
			/<#CustomTime\s*\/>/g,
			this.generateCustomTimeMarkup(),
		);

		return template;
	}

	private parseCustomMonth(template: string) {
		template = template.replace(
			/<#CustomMonth\s*\/>/g,
			this.generateCustomMonthMarkup(),
		);

		return template;
	}

	private parseCustomYear(template: string) {
		template = template.replace(
			/<#CustomYear\s*\/>/g,
			this.generateCustomYearMarkup(),
		);

		return template;
	}

	private parseArrowPrev(template: string) {
		template = template.replace(
			/<#CustomArrowPrev\s*\/>/g,
			this.generateCustomArrowPrevMarkup(),
		);

		return template;
	}

	private parseArrowNext(template: string) {
		template = template.replace(
			/<#CustomArrowNext\s*\/>/g,
			this.generateCustomArrowNextMarkup(),
		);

		return template;
	}

	private processCustomTemplate(
		template: string,
		type: 'default' | 'multiple',
	): string {
		const templateAccordingToType =
			type === 'default'
				? this?.dataOptions?.layouts?.default
				: this?.dataOptions?.layouts?.multiple;
		const processedCustomMonth = this.parseCustomMonth(
			templateAccordingToType ?? template,
		);
		const processedCustomYear = this.parseCustomYear(processedCustomMonth);
		const processedCustomTime = this.parseCustomTime(processedCustomYear);
		const processedCustomArrowPrev = this.parseArrowPrev(processedCustomTime);
		const processedCustomTemplate = this.parseArrowNext(
			processedCustomArrowPrev,
		);

		return processedCustomTemplate;
	}

	private disableOptions() {
		const { mainElement, dateMax, displayMonthsCount } =
			this.vanillaCalendar.context;
		const maxDate = new Date(dateMax);
		const columns = Array.from(mainElement.querySelectorAll('.--single-month'));

		columns.forEach((column, idx) => {
			const year = +column
				.querySelector('[data-vc="year"]')
				?.getAttribute('data-vc-year')!;
			const monthOptions = column.querySelectorAll(
				'[data-hs-select].--month option',
			);
			const pseudoOptions = column.querySelectorAll(
				'[data-hs-select-dropdown] [data-value]',
			);
			const isDisabled = (option: HTMLOptionElement | HTMLElement) => {
				const value = +option.getAttribute('data-value')!;

				return (
					value > maxDate.getMonth() - displayMonthsCount + idx + 1 &&
					year === maxDate.getFullYear()
				);
			};

			Array.from(monthOptions).forEach((option: HTMLOptionElement) =>
				option.toggleAttribute('disabled', isDisabled(option)),
			);
			Array.from(pseudoOptions).forEach((option: HTMLOptionElement) =>
				option.classList.toggle('disabled', isDisabled(option)),
			);
		});
	}

	private disableNav() {
		const {
			mainElement,
			dateMax,
			selectedYear,
			selectedMonth,
			displayMonthsCount,
		} = this.vanillaCalendar.context;
		const maxYear = new Date(dateMax).getFullYear();
		const next = mainElement.querySelector(
			'[data-vc-arrow="next"]',
		) as HTMLElement;

		if (selectedYear === maxYear && selectedMonth + displayMonthsCount > 11) {
			next.style.visibility = 'hidden';
		} else next.style.visibility = '';
	}

	private destroySelects(container: HTMLElement) {
		const selects = Array.from(container.querySelectorAll('[data-hs-select]'));

		selects.forEach((select: HTMLElement) => {
			const instance = HSSelect.getInstance(
				select,
				true,
			) as ICollectionItem<HSSelect>;

			if (instance) instance.element.destroy();
		});
	}

	private updateSelect(el: HTMLElement, value: string) {
		const instance = HSSelect.getInstance(
			el,
			true,
		) as ICollectionItem<HSSelect>;

		if (instance) instance.element.setValue(value);
	}

	private updateCalendar(calendar: HTMLElement) {
		const columns = calendar.querySelectorAll('.--single-month');

		if (columns.length) {
			columns.forEach((column: HTMLElement) => {
				const { month, year } = this.getCurrentMonthAndYear(column);

				this.updateSelect(
					column.querySelector('[data-hs-select].--month'),
					`${month}`,
				);
				this.updateSelect(
					column.querySelector('[data-hs-select].--year'),
					`${year}`,
				);
			});
		}
	}

	private updateCustomSelects(el: Calendar) {
		setTimeout(() => {
			this.disableOptions();
			this.disableNav();

			this.initCustomMonths(el);
			this.initCustomYears(el);
		});
	}

	// Public methods
	public getCurrentState() {
		return {
			selectedDates: this.vanillaCalendar.selectedDates,
			selectedTime: this.vanillaCalendar.selectedTime,
		};
	}

	public formatDate(date: string | number | Date, format?: string): string {
		const dateFormat = format || this.dataOptions?.dateFormat;
		const dateLocale = this.dataOptions?.dateLocale || undefined;

		if (!dateFormat) {
			const dateSeparator =
				this.dataOptions?.inputModeOptions?.dateSeparator ?? '.';

			return this.changeDateSeparator(date, dateSeparator);
		}

		const dateObj = new Date(date);

		if (isNaN(dateObj.getTime())) {
			return this.changeDateSeparator(date as string);
		}

		let result = '';
		let i = 0;

		while (i < dateFormat.length) {
			if (dateFormat.slice(i, i + 4) === 'YYYY') {
				result += dateObj.getFullYear().toString();
				i += 4;
			} else if (dateFormat.slice(i, i + 4) === 'dddd') {
				const dayName = dateObj.toLocaleDateString(dateLocale, {
					weekday: 'long',
				});

				if (this.dataOptions?.replaceTodayWithText) {
					const today = new Date();
					const isToday = dateObj.toDateString() === today.toDateString();

					if (isToday) {
						result += this.getLocalizedTodayText(dateLocale);
					} else {
						result += dayName;
					}
				} else {
					result += dayName;
				}
				i += 4;
			} else if (dateFormat.slice(i, i + 4) === 'MMMM') {
				result += dateObj.toLocaleDateString(dateLocale, { month: 'long' });
				i += 4;
			} else if (dateFormat.slice(i, i + 3) === 'ddd') {
				const dayName = dateObj.toLocaleDateString(dateLocale, {
					weekday: 'short',
				});

				if (this.dataOptions?.replaceTodayWithText) {
					const today = new Date();
					const isToday = dateObj.toDateString() === today.toDateString();

					if (isToday) {
						result += this.getLocalizedTodayText(dateLocale);
					} else {
						result += dayName;
					}
				} else {
					result += dayName;
				}
				i += 3;
			} else if (dateFormat.slice(i, i + 3) === 'MMM') {
				result += dateObj.toLocaleDateString(dateLocale, { month: 'short' });
				i += 3;
			} else if (dateFormat.slice(i, i + 2) === 'YY') {
				result += dateObj.getFullYear().toString().slice(-2);
				i += 2;
			} else if (dateFormat.slice(i, i + 2) === 'MM') {
				result += String(dateObj.getMonth() + 1).padStart(2, '0');
				i += 2;
			} else if (dateFormat.slice(i, i + 2) === 'DD') {
				result += String(dateObj.getDate()).padStart(2, '0');
				i += 2;
			} else if (dateFormat.slice(i, i + 2) === 'HH') {
				result += String(dateObj.getHours()).padStart(2, '0');
				i += 2;
			} else if (dateFormat.slice(i, i + 2) === 'mm') {
				result += String(dateObj.getMinutes()).padStart(2, '0');
				i += 2;
			} else if (dateFormat.slice(i, i + 2) === 'ss') {
				result += String(dateObj.getSeconds()).padStart(2, '0');
				i += 2;
			} else if (dateFormat[i] === 'Y') {
				result += dateObj.getFullYear().toString();
				i += 1;
			} else if (dateFormat[i] === 'M') {
				result += String(dateObj.getMonth() + 1);
				i += 1;
			} else if (dateFormat[i] === 'D') {
				result += String(dateObj.getDate());
				i += 1;
			} else if (dateFormat[i] === 'H') {
				result += String(dateObj.getHours());
				i += 1;
			} else if (dateFormat[i] === 'm') {
				result += String(dateObj.getMinutes());
				i += 1;
			} else if (dateFormat[i] === 's') {
				result += String(dateObj.getSeconds());
				i += 1;
			} else {
				result += dateFormat[i];
				i += 1;
			}
		}

		return result;
	}

	public destroy() {
		const inputElement = this.el;
		const elementId = inputElement.id;
		const parent = inputElement.parentElement;
		const nextSibling = inputElement.nextElementSibling;
		const attributes: { [key: string]: string } = {};

		Array.from(inputElement.attributes).forEach((attr) => {
			attributes[attr.name] = attr.value;
		});

		const className = inputElement.className;
		const value = (inputElement as HTMLInputElement).value;

		if (this.vanillaCalendar) {
			this.vanillaCalendar.destroy();
			this.vanillaCalendar = null;
		}

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

		const isElementInDOM = document.body.contains(inputElement);
		const elementById = elementId ? document.getElementById(elementId) : null;

		if (!isElementInDOM && !elementById && parent) {
			const newInput = document.createElement('input');

			Object.keys(attributes).forEach((attrName) => {
				newInput.setAttribute(attrName, attributes[attrName]);
			});

			newInput.className = className;
			(newInput as HTMLInputElement).value = value;

			if (nextSibling && nextSibling.parentElement === parent) {
				parent.insertBefore(newInput, nextSibling);
			} else {
				parent.appendChild(newInput);
			}

			this.el = newInput;
		}
	}

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

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

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

		document
			.querySelectorAll('.hs-datepicker:not(.--prevent-on-load-init)')
			.forEach((el: HTMLElement) => {
				if (
					!window.$hsDatepickerCollection.find(
						(elC) => (elC?.element?.el as HTMLElement) === el,
					)
				) {
					new HSDatepicker(el);
				}
			});
	}
}

export default HSDatepicker;
