/**
 * Copyright © Volker Schukai 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 Volker Schukai.
 */

import { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
	CustomElement,
	getSlottedElements,
	initMethodSymbol,
} from "../../dom/customelement.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isFunction, isString } from "../../types/is.mjs";

import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { MonthCalendarStyleSheet } from "./stylesheet/month-calendar.mjs";
import { AccessibilityStyleSheet } from "../stylesheet/accessibility.mjs";
import {
	datasourceLinkedElementSymbol,
	handleDataSourceChanges,
} from "../datatable/util.mjs";
import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
import { Datasource } from "../datatable/datasource.mjs";
import { Observer } from "../../types/observer.mjs";
import { positionPopper } from "../form/util/floating-ui.mjs";
import { Segment as AppointmentSegment } from "./timeline/segment.mjs";
import { Appointment as AppointmentAppointment } from "./timeline/appointment.mjs";

export { MonthCalendar };

/**
 * @private
 * @type {symbol}
 */
const calendarElementSymbol = Symbol("calendarElement");

/**
 * @private
 * @type {symbol}
 */
const calendarBodyElementSymbol = Symbol("calendarBodyElement");

/**
 * A Calendar
 *
 * @fragments /fragments/components/time/calendar/
 *
 * @example /examples/components/time/calendar-simple
 *
 * @since 3.112.0
 * @copyright Volker Schukai
 * @summary A month calendar for appointments and date-based planning, with optional datasource-driven event loading.
 */
class MonthCalendar extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/time/calendar@@instance");
	}

	[initMethodSymbol]() {
		super[initMethodSymbol]();

		const def = generateCalendarData.call(this);
		this.setOption("calendarDays", def.calendarDays);
		this.setOption("calendarWeekdays", def.calendarWeekdays);
	}

	/**
	 *
	 * @return {Components.Time.Calendar
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		setTimeout(() => {
			initControlReferences.call(this);
			initEventHandler.call(this);
			// refresh the labels for updater
			this.setOption("labels.__update", true);
			initDataSource.call(this);
		}, 0);
		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 {Object} labels Labels for the control
	 * @property {Object} classes Classes for the control
	 * @property {boolean} disabled Disables the control
	 * @property {Object} features Features for the control
	 * @property {boolean} features.showWeekend Show the weekend
	 * @property {boolean} features.monthOneLine Show the month in one line
	 * @property {Object} actions Actions for the control
	 * @property {Object} locale Locale for the control
	 * @property {string} locale.weekdayFormat Weekday format
	 * @property {string} startDate Start date
	 * @property {string} endDate End date
	 * @property {Object} datasource Datasource
	 * @property {string} datasource.selector Datasource selector
	 */
	get defaults() {
		const startDate = new Date();
		const startDateString =
			startDate.getFullYear() +
			"-" +
			("00" + (startDate.getMonth() + 1)).slice(-2) +
			"-" +
			("00" + startDate.getDate()).slice(-2);

		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			classes: {},

			disabled: false,
			features: {
				showWeekend: true,
				monthOneLine: false,
			},

			actions: {},

			locale: {
				weekdayFormat: "short",
			},

			startDate: startDateString,
			endDate: "",
			calendarDays: [],
			calendarWeekdays: [],

			data: [],

			datasource: {
				selector: null,
			},
		});
	}

	/**
	 * This method is called when the component is created.
	 * @return {Promise}
	 */
	refresh() {
		// makes sure that handleDataSourceChanges is called
		return new Promise((resolve) => {
			this.setOption("data", {});
			queueMicrotask(() => {
				handleDataSourceChanges.call(this);
				placeAppointments.call(this);
				resolve();
			});
		});
	}

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

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [MonthCalendarStyleSheet, AccessibilityStyleSheet];
	}
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				more: "weitere Termin",
			};
		default:
			return {
				more: "more appointments",
			};
	}
}

/**
 * Calculates how many days of an appointment are distributed across calendar rows (weeks).
 * Uses the start date of the calendar grid (e.g., from generateCalendarData()) as a reference.
 *
 * @param {Date} appointmentStart - Start date of the appointment.
 * @param {Date} appointmentEnd - End date of the appointment (inclusive).
 * @param {Date} calendarGridStart - The first day of the calendar grid (e.g., the Monday from generateCalendarData()).
 * @returns {number[]} Array indicating how many days the appointment spans per row.
 *
 * Example:
 * - Appointment: 01.03.2025 (Saturday) to 01.03.2025:
 *   -> getAppointmentRowsUsingCalendar(new Date("2025-03-01"), new Date("2025-03-01"), calendarGridStart)
 *   returns: [1] (since it occupies only one day in the first row, starting at column 6).
 *
 * - Appointment: 01.03.2025 to 03.03.2025:
 *   -> returns: [2, 1] (first row: Saturday and Sunday, second row: Monday).
 */
function getAppointmentRowsUsingCalendar(
	appointmentStart,
	appointmentEnd,
	calendarGridStart,
) {
	const oneDayMs = 24 * 60 * 60 * 1000;
	// Calculate the offset (in days) from the calendar start to the appointment start
	const offset = Math.floor((appointmentStart - calendarGridStart) / oneDayMs);
	// Determine the column index in the calendar row (Monday = 0, ..., Sunday = 6)
	let startColumn = offset % 7;
	if (startColumn < 0) {
		startColumn += 7;
	}
	// Calculate the total number of days for the appointment (including start and end date)
	const totalDays =
		Math.floor((appointmentEnd - appointmentStart) / oneDayMs) + 1;

	// The first calendar block can accommodate at most (7 - startColumn) days.
	const firstRowDays = Math.min(totalDays, 7 - startColumn);
	const rows = [firstRowDays];

	let remainingDays = totalDays - firstRowDays;
	// Handle full weeks (7 days per row)
	while (remainingDays > 7) {
		rows.push(7);
		remainingDays -= 7;
	}
	// Handle the last row if there are any remaining days
	if (remainingDays > 0) {
		rows.push(remainingDays);
	}

	return rows;
}

/**
 * @private
 * @param format
 * @returns {*[]}
 */
function getWeekdays(format = "long") {
	const locale = getLocaleOfDocument();

	const weekdays = [];
	for (let i = 1; i < 8; i++) {
		const date = new Date(1970, 0, 4 + i); // 4. Jan. 1970 = Sonntag
		weekdays.push(
			new Intl.DateTimeFormat(locale, { weekday: format }).format(date),
		);
	}
	return weekdays;
}

/**
 * Assigns a "line" property to the provided segments (with "startIndex" and "columns").
 * It checks for horizontal overlaps within each calendar row (7 boxes).
 * Always assigns the lowest available "line".
 *
 * @private
 *
 * @param {Array} segments - Array of segments, e.g.
 *   [
 *     {"columns":6,"label":"03/11/2025 - 04/05/2025","start":"2025-03-11","startIndex":15},
 *     {"columns":7,"label":"03/11/2025 - 04/05/2025","start":"2025-03-17","startIndex":21},
 *     {"columns":7,"label":"03/11/2025 - 04/05/2025","start":"2025-03-24","startIndex":28},
 *     {"columns":6,"label":"03/11/2025 - 04/05/2025","start":"2025-03-31","startIndex":35}
 *   ]
 * @returns {Array} The segments with assigned "line" property
 */
function assignLinesToSegments(segments) {
	const groups = {};
	segments.forEach((segment) => {
		const week = Math.floor(segment.startIndex / 7);
		if (!groups[week]) {
			groups[week] = [];
		}
		groups[week].push(segment);
	});

	Object.keys(groups).forEach((weekKey) => {
		const weekSegments = groups[weekKey];

		weekSegments.sort((a, b) => a.startIndex - b.startIndex);

		const lineEnds = [];

		weekSegments.forEach((segment) => {
			const segStart = segment.startIndex;
			const segEnd = segment.startIndex + segment.columns - 1;
			let placed = false;

			for (let line = 0; line < lineEnds.length; line++) {
				if (segStart >= lineEnds[line] + 1) {
					segment.line = line;
					lineEnds[line] = segEnd;
					placed = true;
					break;
				}
			}

			if (!placed) {
				segment.line = lineEnds.length;
				lineEnds.push(segEnd);
			}
		});
	});

	return segments;
}

/**
 * @private
 */
function initDataSource() {
	setTimeout(() => {
		if (!this[datasourceLinkedElementSymbol]) {
			const selector = this.getOption("datasource.selector");

			if (isString(selector)) {
				const element = findElementWithSelectorUpwards(this, selector);
				if (element === null) {
					addErrorAttribute(
						this,
						"the selector must match exactly one element",
					);
					return;
				}

				if (!(element instanceof Datasource)) {
					addErrorAttribute(this, "the element must be a datasource");
					return;
				}

				this[datasourceLinkedElementSymbol] = element;
				element.datasource.attachObserver(
					new Observer(handleDataSourceChanges.bind(this)),
				);

				handleDataSourceChanges.call(this);
				placeAppointments.call(this);
			} else {
				addErrorAttribute(
					this,
					"the datasource selector is missing or invalid",
				);
			}
		}
	}, 10);
}

/**
 * Places and organizes appointments within a calendar grid according to their start and end dates,
 * ensuring proper alignment and rendering within the calendar day containers.
 *
 * Appointments are evaluated to determine their appropriate positions, lengths, and styles based on calendar
 * configuration and available rendering space.
 *
 * Errors are logged for invalid appointment data or when
 * rendering issues are encountered.
 *
 * @private
 * @return {void} No return value; the method operates on the DOM and internal state of the calendar element to render appointments visually.
 */
function placeAppointments() {
	const self = this;

	const currentWithOfGridCell =
		this[calendarElementSymbol].getBoundingClientRect().width / 7;
	const appointments = this.getOption("data");

	const segments = [];
	let maxLineHeight = 0;

	const calendarDays = this.getOption("calendarDays");

	const calenderStartDate = new Date(calendarDays[0].date);
	const calenderEndDate = new Date(calendarDays[calendarDays.length - 1].date);

	const app = getAppointmentsPerDay(
		appointments,
		calenderStartDate,
		calenderEndDate,
	);

	calendarDays.forEach((day) => {
		const k =
			day.date.getFullYear() +
			"-" +
			("00" + (day.date.getMonth() + 1)).slice(-2) +
			"-" +
			("00" + day.date.getDate()).slice(-2);
		day.appointments = app[k];
	});

	appointments.forEach((appointment) => {
		if (!appointment?.startDate || !appointment?.endDate) {
			addErrorAttribute(this, "Missing start or end date in appointment");
			return;
		}

		const startDate = appointment?.startDate;

		// calc length of appointment
		const start = new Date(startDate);
		const end = new Date(appointment?.endDate);

		const appointmentRows = getAppointmentRowsUsingCalendar(
			start,
			end,
			calendarDays[0].date,
		);

		let date = appointment.startDate;

		const s =
			start.getFullYear() +
			"-" +
			("00" + (start.getMonth() + 1)).slice(-2) +
			"-" +
			("00" + start.getDate()).slice(-2);

		const e =
			end.getFullYear() +
			"-" +
			("00" + (end.getMonth() + 1)).slice(-2) +
			"-" +
			("00" + end.getDate()).slice(-2);

		let label;
		if (appointment.label) {
			label = appointment.label.replace(/\n/g, "<br>");
		} else {
			label =
				s !== e
					? `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`
					: start.toLocaleDateString();
		}

		for (let i = 0; i < appointmentRows.length; i++) {
			const cols = appointmentRows[i];

			const calendarStartDate = new Date(calendarDays[0].date); // First day of the calendar grid
			const appointmentDate = new Date(date);
			const startIndex = Math.floor(
				(appointmentDate - calendarStartDate) / (24 * 60 * 60 * 1000),
			);

			segments.push({
				columns: cols,
				label: label,
				start: date,
				startIndex: startIndex,
				appointment: appointment,
			});

			maxLineHeight = Math.max(maxLineHeight, getTextHeight.call(this, label));

			const k = new Date(start.setDate(start.getDate() + cols));

			// calc from k to next day
			date =
				k.getFullYear() +
				"-" +
				("00" + (k.getMonth() + 1)).slice(-2) +
				"-" +
				("00" + k.getDate()).slice(-2);
		}
	});

	let container = null;

	const sortedSegments = assignLinesToSegments(segments);

	const h = calcHeaderAndFooterHeight.call(this);
	const marginAppointmentContainer = h.maxFooterHeight + h.maxHeaderHeight;

	const popper = self.shadowRoot.querySelectorAll(
		"monster-appointment-segment",
	);
	if (popper) {
		for (const p of popper) {
			p.remove();
		}
	}

	const daysWithMoreAppointments = new Set();
	const moreAppointments = new Map();

	for (let i = 0; i < sortedSegments.length; i++) {
		const segment = sortedSegments[i];

		container = self.shadowRoot.querySelector(
			`[data-monster-day="${segment.start}"]`,
		);

		if (!container) {
			continue;
		}

		let containerHeight = container?.getBoundingClientRect()?.height || 0;
		if (containerHeight === 0) {
			addErrorAttribute(this, "Unable to retrieve container height");
			continue;
		}

		let availableHeight =
			containerHeight - maxLineHeight - marginAppointmentContainer;
		let linesThatCanBeShown = Math.floor(availableHeight / maxLineHeight);

		if (segment.line > linesThatCanBeShown) {
			daysWithMoreAppointments.add(segment.start);

			const calendarDays = this.getOption("calendarDays");
			const currentDay = calendarDays.find((day) => day.day === segment.start);

			if (currentDay) {
				if (!moreAppointments.has(currentDay.index)) {
					moreAppointments.set(currentDay.index, []);
				}

				moreAppointments.get(currentDay.index).push(segment);
			}

			continue;
		}

		if (!container) {
			addErrorAttribute(
				this,
				"Invalid, missing or out of range date in appointment" + segment.start,
			);
			continue;
		}

		const appointmentSegment = document.createElement(
			"monster-appointment-segment",
		);
		appointmentSegment.className = "appointment-segment";
		appointmentSegment.style.backgroundColor = segment.appointment.color;

		// search a color that is readable on the background color
		const rgb = appointmentSegment.style.backgroundColor.match(/\d+/g);
		const brightness = Math.round(
			(parseInt(rgb[0]) * 299 +
				parseInt(rgb[1]) * 587 +
				parseInt(rgb[2]) * 114) /
				1000,
		);

		if (brightness > 125) {
			appointmentSegment.style.color = "#000000";
		} else {
			appointmentSegment.style.color = "#ffffff";
		}

		appointmentSegment.style.width = `${currentWithOfGridCell * segment.columns}px`;
		appointmentSegment.style.height = maxLineHeight + "px";
		appointmentSegment.style.top = `${segment.line * maxLineHeight + h.maxHeaderHeight}px`;

		appointmentSegment.setOption("labels.text", segment.label);

		container.appendChild(appointmentSegment);
	}

	// mark days with more appointments, show a hint / link
	for (const day of daysWithMoreAppointments) {
		for (const cell of this.getOption("calendarDays")) {
			if (cell.day === day) {
				this.setOption(
					"calendarDays." + cell.index + ".classes.more-appointments",
					"visible",
				);
			}
		}
	}

	// get popper
	for (const [index, appointments] of moreAppointments) {
		const moreAppointmentsContainer = this.shadowRoot.querySelector(
			`[data-monster-role=day-cell][data-monster-index="${index}"] > [data-monster-role="appointment-popper"] `,
		);

		if (!moreAppointmentsContainer) {
			continue;
		}

		moreAppointmentsContainer.innerHTML = "";

		for (const appointment of appointments) {
			const appointmentSegment = document.createElement("monster-appointment");
			appointmentSegment.style.backgroundColor = appointment.appointment.color;
			appointmentSegment.style.position = "relative";
			appointmentSegment.style.top = "unset";

			// search a color that is readable on the background color
			const rgb = appointmentSegment.style.backgroundColor.match(/\d+/g);
			const brightness = Math.round(
				(parseInt(rgb[0]) * 299 +
					parseInt(rgb[1]) * 587 +
					parseInt(rgb[2]) * 114) /
					1000,
			);

			if (brightness > 125) {
				appointmentSegment.style.color = "#000000";
			} else {
				appointmentSegment.style.color = "#ffffff";
			}

			appointmentSegment.style.width = "100%";
			appointmentSegment.style.height = maxLineHeight + "px";

			appointmentSegment.setOption("labels.text", appointment.label);

			moreAppointmentsContainer.appendChild(appointmentSegment);
		}
	}
}

/**
 * @private
 * @returns {{maxHeaderHeight: number, maxFooterHeight: number}}
 */
function calcHeaderAndFooterHeight() {
	let maxHeaderHeight = 0;
	let maxFooterHeight = 0;

	const days = this.getOption("calendarDays");
	for (const day of days) {
		const current = day.date;

		const dayKey =
			current.getFullYear() +
			"-" +
			("00" + (current.getMonth() + 1)).slice(-2) +
			"-" +
			("00" + current.getDate()).slice(-2);

		const cell = this.shadowRoot.querySelector(
			`[data-monster-day="${dayKey}"]`,
		);

		if (!(cell instanceof HTMLDivElement)) {
			continue;
		}

		const header = cell.querySelector("[data-monster-role='day-header']");

		if (header instanceof HTMLDivElement) {
			maxHeaderHeight = Math.max(
				maxHeaderHeight,
				header.getBoundingClientRect().height +
					parseFloat(getComputedStyle(header).paddingTop) +
					parseFloat(getComputedStyle(header).paddingBottom) +
					parseFloat(getComputedStyle(header).marginTop) +
					parseFloat(getComputedStyle(header).marginBottom) +
					parseFloat(getComputedStyle(header.parentElement).paddingTop) +
					parseFloat(getComputedStyle(header.parentElement).paddingBottom) +
					parseFloat(getComputedStyle(header.parentElement).marginTop),
			);
		}

		const footer = cell.querySelector("[data-monster-role='day-footer']");

		if (footer instanceof HTMLDivElement) {
			maxFooterHeight = Math.max(
				maxFooterHeight,
				footer.getBoundingClientRect().height +
					parseFloat(getComputedStyle(footer).paddingTop) +
					parseFloat(getComputedStyle(footer).paddingBottom) +
					parseFloat(getComputedStyle(footer).marginTop) +
					parseFloat(getComputedStyle(footer).marginBottom) +
					parseFloat(getComputedStyle(footer.parentElement).paddingTop) +
					parseFloat(getComputedStyle(footer.parentElement).paddingBottom) +
					parseFloat(getComputedStyle(footer.parentElement).marginTop),
			);
		}
	}

	return { maxHeaderHeight, maxFooterHeight };
}

/**
 * Generates two arrays: one for the calendar grid (42 days) and one for the weekday headers (7 days).
 * The grid always starts on the Monday of the week that contains the first of the given month.
 *
 * @returns {Object} An object containing:
 *                   - calendarDays: Array of 42 objects, each representing a day.
 *                   - calendarWeekdays: Array of seven objects, each representing a weekday header.
 */
function generateCalendarData() {
	let selectedDate = this.getOption("startDate");
	if (!(selectedDate instanceof Date)) {
		if (typeof selectedDate === "string") {
			try {
				selectedDate = new Date(selectedDate);
			} catch (e) {
				addErrorAttribute(this, "Invalid calendar date");
				return { calendarDays, calendarWeekdays };
			}
		} else {
			addErrorAttribute(this, "Invalid calendar date");
			return { calendarDays, calendarWeekdays };
		}
	}

	const calendarDays = [];
	let calendarWeekdays = [];

	if (!(selectedDate instanceof Date)) {
		addErrorAttribute(this, "Invalid calendar date");
		return { calendarDays, calendarWeekdays };
	}

	// Get the year and month from the provided date
	const year = selectedDate.getFullYear();
	const month = selectedDate.getMonth(); // 0-based index (0 = January)

	// Create a Date object for the 1st of the given month
	const firstDayOfMonth = new Date(year, month, 1);

	// Determine the weekday index of the 1st day, ensuring Monday = 0
	const weekdayIndex = (firstDayOfMonth.getDay() + 6) % 7;

	// Calculate the start date: move backward to the Monday of the starting week
	const startDate = new Date(firstDayOfMonth);
	startDate.setDate(firstDayOfMonth.getDate() - weekdayIndex);

	// Generate 42 days (6 weeks × 7 days)
	for (let i = 0; i < 42; i++) {
		const current = new Date(startDate);
		current.setDate(startDate.getDate() + i);

		const label = current.getDate().toString();

		const dayKey =
			current.getFullYear() +
			"-" +
			("00" + (current.getMonth() + 1)).slice(-2) +
			"-" +
			("00" + current.getDate()).slice(-2);

		calendarDays.push({
			date: current,
			//day: current.getDate(),
			month: current.getMonth() + 1, // 1-based month (1-12)
			year: current.getFullYear(),
			isCurrentMonth: current.getMonth() === month,
			label: label,
			index: i,
			day: dayKey,
			classes: {
				"more-appointments": "hidden",
				cell:
					"day-cell " +
					(current.getMonth() === month ? "current-month" : "other-month") +
					(current.getDay() === 0 || current.getDay() === 6 ? " weekend" : "") +
					(current.toDateString() === new Date().toDateString()
						? " today"
						: ""),
			},
			appointments: [],
		});
	}

	// Generate weekday header array (Monday through Sunday)
	let format = this.getOption("locale.weekdayFormat");
	if (!["long", "short", "narrow"].includes(format)) {
		addErrorAttribute(this, "Invalid weekday format option " + format);
		format = "short";
	}
	const weekdayNames = getWeekdays(format);
	calendarWeekdays = weekdayNames.map((name, index) => {
		return {
			label: name,
			index: index,
		};
	});

	return { calendarDays, calendarWeekdays };
}

/**
 * Generates a map that contains an array of appointments for each day within the calendar range.
 * Multi-day appointments will appear on each day they span.
 *
 * @param {Array} appointments - Array of appointment objects. Expected properties: "startDate" and "endDate".
 * @param {Date|string} start - Calendar start date.
 * @param {Date|string} end - Calendar end date.
 * @returns {Object} A map in the format { "YYYY-MM-DD": [appointment1, appointment2, ...] }
 */
function getAppointmentsPerDay(appointments, start, end) {
	const appointmentsMap = {};

	// Convert start and end to Date objects if needed
	const startDate = start instanceof Date ? start : new Date(start);
	const endDate = end instanceof Date ? end : new Date(end);

	// Create an empty entry for each day in the calendar range
	let current = new Date(startDate);
	while (current <= endDate) {
		const key = current.toISOString().slice(0, 10);
		appointmentsMap[key] = [];
		current.setDate(current.getDate() + 1);
	}

	// Assign each appointment to the corresponding days
	appointments.forEach((appointment) => {
		if (!appointment.startDate || !appointment.endDate) {
			// Skip appointments with missing data
			return;
		}

		const appStart = new Date(appointment.startDate);
		const appEnd = new Date(appointment.endDate);

		// Determine the effective start and end dates to ensure appointments outside the calendar are ignored
		const effectiveStart = appStart < startDate ? startDate : appStart;
		const effectiveEnd = appEnd > endDate ? endDate : appEnd;

		let currentAppDate = new Date(effectiveStart);
		while (currentAppDate <= effectiveEnd) {
			const key = currentAppDate.toISOString().slice(0, 10);
			if (appointmentsMap[key]) {
				appointmentsMap[key].push(appointment);
			}
			currentAppDate.setDate(currentAppDate.getDate() + 1);
		}
	});

	return appointmentsMap;
}

/**
 * @private
 * @return {initEventHandler}
 */
function initEventHandler() {
	const self = this;

	setTimeout(() => {
		this.attachObserver(
			new Observer(() => {
				placeAppointments.call(this);
			}),
		);

		this[calendarElementSymbol]
			.querySelectorAll("[data-monster-role='day-cell']")
			.forEach((element) => {
				element.addEventListener("click", (event) => {
					const hoveredElement = this.shadowRoot.elementFromPoint(
						event.clientX,
						event.clientY,
					);

					if (hoveredElement instanceof AppointmentSegment) {
						return;
					}

					const element = findTargetElementFromEvent(
						event,
						"data-monster-role",
						"more-appointments",
					);

					if (!element) {
						return;
					}

					// parent is footer, and parent of footer is cell
					const popper = element.parentElement.parentElement.querySelector(
						'[data-monster-role="appointment-popper"]',
					);

					if (!popper) {
						return;
					}

					//const appointments = getAppointmentsPerDay() || [];
					const container = element.parentElement.parentElement;

					positionPopper(container, popper, {
						placement: "bottom",
					});

					popper.style.width = container.getBoundingClientRect().width + "px";
					popper.style.zIndex = 1000;

					popper.style.display = "block";
				});

				element.addEventListener("mouseleave", (event) => {
					const element = findTargetElementFromEvent(
						event,
						"data-monster-role",
						"day-cell",
					);
					if (!element) {
						return;
					}

					element.classList.remove("hover");
					const popper = element.querySelector(
						'[data-monster-role="appointment-popper"]',
					);
					if (!popper) {
						return;
					}

					setTimeout(() => {
						popper.style.display = "none";
					}, 0);
				});
			});
	}, 0);

	return this;
}

function getTextHeight(text) {
	// Ein unsichtbares div erstellen
	const div = document.createElement("div");
	div.style.position = "absolute";
	div.style.whiteSpace = "nowrap";
	div.style.visibility = "hidden";
	div.textContent = text;

	this.shadowRoot.appendChild(div);
	const height = div.clientHeight;
	this.shadowRoot.removeChild(div);

	return height;
}

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

	this[calendarBodyElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="calendar-body"]`,
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <template id="cell">
            <div data-monster-role="day-cell"
                 data-monster-attributes="class path:cell.classes.cell,
                 data-monster-day path:cell.day,
                                  data-monster-index path:cell.index">
                <div class="header" data-monster-role="day-header">
                    <div data-monster-replace="path:cell.label"></div>
                </div>
               
                <div class="footer" data-monster-role="day-footer">
                    <div data-monster-replace="path:labels.more"
                         data-monster-attributes="class path:cell.classes.more-appointments"
                         data-monster-role="more-appointments"></div>
                </div>
                <div data-monster-role="appointment-popper"
                     class="popper"></div>
            </div>
        </template>

        <template id="calendar-weekday-header">
            <div data-monster-attributes="class path:calendar-weekday-header.classes,
                                              data-monster-index path:calendar-weekday-header.index"
                 data-monster-replace="path:calendar-weekday-header.label"></div>
        </template>


        <div data-monster-role="control" part="control">
            <div class="weekday-header">
                <div data-monster-role="weekdays"
                     data-monster-insert="calendar-weekday-header path:calendarWeekdays"></div>
                <div class="calendar-body" data-monster-role="calendar-body">
                    <div data-monster-role="cells" data-monster-insert="cell path:calendarDays"></div>
                </div>
            </div>
    `;
}

registerCustomElement(MonthCalendar);
