<template>
	<div
		ref="wrapperRef"
		class="modrinth-date-picker relative inline-flex items-center"
		:class="[
			wrapperClass,
			calendarOnly ? 'calendar-only' : '',
			disabled ? 'cursor-not-allowed opacity-50' : '',
			showToday ? 'show-today' : '',
			props.mode === 'range' && selectedDates.length < 2 ? 'can-select-range' : '',
			props.mode === 'range' && selectedDates.length === 1 ? 'is-selecting-range' : '',
			props.mode === 'range' && selectedDates.length === 2 ? 'can-drag-range' : '',
			rangeDragState ? 'is-dragging-range' : '',
			rangeEndpointMoveState ? 'is-moving-range-end' : '',
		]"
	>
		<CalendarIcon
			v-if="showIcon && !calendarOnly"
			class="pointer-events-none absolute left-3 z-[1] h-5 w-5 text-secondary opacity-60 transition-colors"
			aria-hidden="true"
		/>
		<input
			:id="id"
			ref="inputRef"
			:name="name"
			:placeholder="placeholder"
			:disabled="disabled"
			:readonly="readonly"
			:autocomplete="autocomplete"
			:class="inputClasses"
			:tabindex="calendarOnly ? -1 : undefined"
			:aria-hidden="calendarOnly ? 'true' : undefined"
			type="text"
		/>
		<button
			v-if="hasClearButton"
			type="button"
			class="absolute right-0.5 top-px z-[1] touch-manipulation cursor-pointer select-none border-none bg-transparent p-2 text-secondary transition-colors hover:text-contrast"
			aria-label="Clear date"
			@click.stop="clearValue"
		>
			<XIcon class="h-5 w-5" aria-hidden="true" />
		</button>
	</div>
</template>

<script setup lang="ts">
import 'flatpickr/dist/flatpickr.css'

import { CalendarIcon, XIcon } from '@modrinth/assets'
import chevronLeftIcon from '@modrinth/assets/icons/chevron-left.svg?raw'
import chevronRightIcon from '@modrinth/assets/icons/chevron-right.svg?raw'
import flatpickr from 'flatpickr'
import type { Instance } from 'flatpickr/dist/types/instance'
import type { Options } from 'flatpickr/dist/types/options'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

type DatePickerValue = string | Date | null | undefined
type RangeEdge = 'start' | 'end'
type ViewDateAlignment = 'left' | 'right'
type DatePickerPosition =
	| 'auto'
	| 'above'
	| 'below'
	| 'auto left'
	| 'auto center'
	| 'auto right'
	| 'above left'
	| 'above center'
	| 'above right'
	| 'below left'
	| 'below center'
	| 'below right'
type CalendarViewMonth = { month: number; year: number }
type RangeDayElement = HTMLElement & { dateObj?: Date }
type RangeDragState = {
	edge: RangeEdge
	anchorDate: Date
	draggedEndpointDate: Date
	pointerId: number
	lastDateTime: number
	hasMoved: boolean
}
type RangeEndpointMoveState = {
	edge: RangeEdge
	anchorDate: Date
	endpointDate: Date
	lastPreviewDateTime: number
}

const model = defineModel<DatePickerValue | DatePickerValue[]>()

const props = withDefaults(
	defineProps<{
		id?: string
		name?: string
		placeholder?: string
		autocomplete?: string
		disabled?: boolean
		readonly?: boolean
		enableTime?: boolean
		mode?: 'single' | 'multiple' | 'range'
		showMonths?: number
		minDate?: string | Date
		maxDate?: string | Date
		/**
		 * The date the calendar opens to when no value is selected. Does not set
		 * the value — only controls which month/year is shown when the picker
		 * first opens (and on subsequent opens while the value is empty).
		 */
		defaultViewDate?: string | Date
		/**
		 * Controls which visible calendar contains the opening view date when
		 * multiple months are shown. In range mode, a complete range opens to
		 * the end date.
		 */
		viewDateAlignment?: ViewDateAlignment
		/**
		 * The stored value format emitted by v-model. See https://flatpickr.js.org/formatting/
		 *
		 * Examples:
		 * - `Y-m-d` renders as `2026-04-27`
		 * - `Y-m-d H:i` renders as `2026-04-27 14:30`
		 */
		dateFormat?: string
		/**
		 * The human-friendly format shown in the visible input. See https://flatpickr.js.org/formatting/
		 *
		 * Examples:
		 * - `F j, Y` renders as `April 27, 2026`
		 * - `F j, Y at h:i K` renders as `April 27, 2026 at 02:30 PM`
		 */
		altFormat?: string
		time24hr?: boolean
		clearable?: boolean
		showIcon?: boolean
		showToday?: boolean
		calendarOnly?: boolean
		closeOnSelect?: boolean
		/**
		 * Controls where the calendar opens relative to the input. Use `above`
		 * to force the calendar to open above the input.
		 */
		position?: DatePickerPosition
		/**
		 * When true (single mode only), navigating between months/years preserves the
		 * originally selected day number. If the target month has fewer days, the day
		 * is clamped to the last valid day, and a `clamp` event is emitted with the
		 * intended and resolved days. Navigating back to a month that supports the
		 * original day snaps the selection back.
		 */
		preserveDay?: boolean
		wrapperClass?: string
		inputClass?: string
		calendarClass?: string
	}>(),
	{
		disabled: false,
		readonly: true,
		enableTime: false,
		mode: 'single',
		showMonths: 1,
		time24hr: false,
		clearable: false,
		placeholder: 'Enter date',
		showIcon: true,
		showToday: false,
		calendarOnly: false,
		closeOnSelect: false,
		position: 'auto',
		preserveDay: false,
		viewDateAlignment: 'left',
	},
)

const emit = defineEmits<{
	change: [value: DatePickerValue | DatePickerValue[]]
	clear: []
	clamp: [intendedDay: number, resolvedDay: number]
}>()

const inputRef = ref<HTMLInputElement>()
const wrapperRef = ref<HTMLElement>()
const picker = ref<Instance>()
const isSyncingFromModel = ref(false)
const intendedViewMonth = ref<CalendarViewMonth | null>(null)
const preserveViewOnNextModelSync = ref(false)
const intendedDay = ref<number | null>(null)
const isPreservingDay = ref(false)
const timeInputDigits = ref(new WeakMap<HTMLInputElement, string>())
const rangeDragState = ref<RangeDragState | null>(null)
const rangeEndpointMoveState = ref<RangeEndpointMoveState | null>(null)
const suppressNextRangeClick = ref(false)
let rangeClickSuppressionTimeout: number | null = null
let monthSelectSyncFrame: number | null = null
let calendarPortal: HTMLElement | null = null
let inputFocusScrollSuppressionTarget: HTMLInputElement | null = null
let originalInputFocus: HTMLInputElement['focus'] | null = null
let suppressNextInputFocusScroll = false
const calendarBaseClass = 'modrinth-date-picker-calendar'
const twoCalendarClass = 'has-two-calendars'
const calendarPositionGap = 2
const calendarStateClasses = [
	'calendar-only',
	'show-today',
	'can-select-range',
	'is-selecting-range',
	'can-drag-range',
	'is-dragging-range',
	'is-moving-range-end',
]

function getCalendarStateClasses() {
	return [
		props.calendarOnly ? 'calendar-only' : '',
		props.showToday ? 'show-today' : '',
		props.mode === 'range' && selectedDates.value.length < 2 ? 'can-select-range' : '',
		props.mode === 'range' && selectedDates.value.length === 1 ? 'is-selecting-range' : '',
		props.mode === 'range' && selectedDates.value.length === 2 ? 'can-drag-range' : '',
		rangeDragState.value ? 'is-dragging-range' : '',
		rangeEndpointMoveState.value ? 'is-moving-range-end' : '',
	].filter(Boolean)
}

function syncCalendarStateClasses(instance?: Instance) {
	const container = (instance ?? picker.value)?.calendarContainer
	const targets = [container, calendarPortal].filter((target): target is HTMLElement =>
		Boolean(target),
	)
	if (targets.length === 0) return

	for (const target of targets) {
		for (const cls of calendarStateClasses) {
			target.classList.remove(cls)
		}

		for (const cls of getCalendarStateClasses()) {
			target.classList.add(cls)
		}
	}
}

function ensureCalendarPortal() {
	if (props.calendarOnly || typeof document === 'undefined') return undefined

	if (!calendarPortal) {
		calendarPortal = document.createElement('div')
		calendarPortal.classList.add('modrinth-date-picker', 'modrinth-date-picker-portal')

		for (const attr of wrapperRef.value?.getAttributeNames() ?? []) {
			if (attr.startsWith('data-v-')) {
				calendarPortal.setAttribute(attr, '')
			}
		}

		document.body.appendChild(calendarPortal)
	}

	syncCalendarStateClasses()
	return calendarPortal
}

function destroyCalendarPortal() {
	calendarPortal?.remove()
	calendarPortal = null
}

function getDayFromDateStr(dateStr: string): number | null {
	const parts = dateStr.split('-')
	if (parts.length < 3) return null
	const day = Number.parseInt(parts[2], 10)
	return Number.isInteger(day) ? day : null
}

function daysInMonth(month: number, year: number): number {
	return new Date(year, month + 1, 0).getDate()
}

function applyPreserveDay(instance: Instance) {
	if (!props.preserveDay) return
	if (props.mode !== 'single') return
	if (intendedDay.value === null) return

	const selected = instance.selectedDates[0]
	if (!selected) return

	if (
		selected.getMonth() === instance.currentMonth &&
		selected.getFullYear() === instance.currentYear
	) {
		return
	}

	const maxDay = daysInMonth(instance.currentMonth, instance.currentYear)
	const resolvedDay = Math.min(intendedDay.value, maxDay)
	const newDate = new Date(instance.currentYear, instance.currentMonth, resolvedDay)

	emit('clamp', intendedDay.value, resolvedDay)

	isPreservingDay.value = true
	instance.setDate(newDate, true)
	nextTick(() => {
		isPreservingDay.value = false
	})
}

let appliedCalendarClasses: string[] = []

function parseClassString(value: string | undefined): string[] {
	if (!value) return []
	return value.split(/\s+/).filter(Boolean)
}

function syncCalendarClasses(instance?: Instance) {
	const container = (instance ?? picker.value)?.calendarContainer
	if (!container) return

	container.classList.add(calendarBaseClass)
	container.classList.toggle(twoCalendarClass, resolvedShowMonths.value === 2)

	for (const cls of appliedCalendarClasses) {
		container.classList.remove(cls)
	}

	const nextClasses = parseClassString(props.calendarClass)
	for (const cls of nextClasses) {
		container.classList.add(cls)
	}

	appliedCalendarClasses = nextClasses
	syncCalendarStateClasses(instance)
}

function syncHeaderControlState(instance: Instance) {
	const prevDisabled = instance.prevMonthNav.classList.contains('flatpickr-disabled')
	const nextDisabled = instance.nextMonthNav.classList.contains('flatpickr-disabled')

	instance.prevMonthNav.setAttribute('aria-disabled', String(prevDisabled))
	instance.nextMonthNav.setAttribute('aria-disabled', String(nextDisabled))

	if (instance.monthsDropdownContainer) {
		const monthSelectDisabled = instance.monthsDropdownContainer.options.length <= 1
		instance.monthsDropdownContainer.disabled = monthSelectDisabled
		instance.monthsDropdownContainer.setAttribute('aria-disabled', String(monthSelectDisabled))
	}
}

function getDisplayedMonthDate(instance: Instance, index: number) {
	return new Date(instance.currentYear, instance.currentMonth + index, 1)
}

function getMonthName(instance: Instance, month: number) {
	const monthNames = instance.config.shorthandCurrentMonth
		? instance.l10n.months.shorthand
		: instance.l10n.months.longhand
	return monthNames[month] ?? ''
}

function syncMultiMonthSelects(instance = picker.value) {
	if (!instance) return

	for (const select of instance.monthNav.querySelectorAll('.modrinth-monthDropdown-months')) {
		select.remove()
	}

	for (const monthElement of instance.monthNav.querySelectorAll('.modrinth-hidden-cur-month')) {
		monthElement.classList.remove('modrinth-hidden-cur-month')
	}

	const showMultiMonthSelects =
		resolvedShowMonths.value >= 1 && !instance.isMobile && !instance.config.noCalendar
	instance.calendarContainer.classList.toggle('has-multi-month-selects', showMultiMonthSelects)
	if (!showMultiMonthSelects) return

	instance.monthElements.forEach((monthElement, index) => {
		const currentMonth = monthElement.closest('.flatpickr-current-month')
		if (!currentMonth) return

		const displayedMonthDate = getDisplayedMonthDate(instance, index)
		const displayedYear = displayedMonthDate.getFullYear()
		const displayedMonth = displayedMonthDate.getMonth()
		const monthSelect = document.createElement('select')
		monthSelect.className = 'flatpickr-monthDropdown-months modrinth-monthDropdown-months'
		monthSelect.setAttribute('aria-label', instance.l10n.monthAriaLabel)
		monthSelect.tabIndex = -1

		for (let month = 0; month < 12; month++) {
			const option = document.createElement('option')
			option.className = 'flatpickr-monthDropdown-month'
			option.value = String(month)
			option.textContent = getMonthName(instance, month)
			option.selected = month === displayedMonth
			monthSelect.appendChild(option)
		}

		monthSelect.addEventListener('change', () => {
			const selectedMonth = Number.parseInt(monthSelect.value, 10)
			const selectedYear = Number.parseInt(
				instance.yearElements[index]?.value ?? String(displayedYear),
				10,
			)
			if (!Number.isFinite(selectedMonth) || !Number.isFinite(selectedYear)) return

			instance.jumpToDate(new Date(selectedYear, selectedMonth - index, 1), true)
			syncHeaderControlState(instance)
			syncMissingRangeEndState()
			syncRangeEndpointMoveState(instance)
			syncMultiMonthSelects(instance)
		})

		monthElement.classList.add('modrinth-hidden-cur-month')
		currentMonth.insertBefore(monthSelect, monthElement)
	})
}

function changeYearInput(instance: Instance, input: HTMLInputElement, delta: number) {
	const index = instance.yearElements.indexOf(input)
	if (index < 0) return

	const displayedMonthDate = getDisplayedMonthDate(instance, index)
	const displayedYear = displayedMonthDate.getFullYear()
	const selectedYear = Number.parseInt(input.value, 10)
	const nextYear = (Number.isFinite(selectedYear) ? selectedYear : displayedYear) + delta

	instance.jumpToDate(new Date(nextYear, displayedMonthDate.getMonth() - index, 1), true)
	syncHeaderControlState(instance)
	syncMissingRangeEndState()
	syncRangeEndpointMoveState(instance)
	syncMultiMonthSelects(instance)
}

function queueMultiMonthSelectSync(instance: Instance) {
	if (monthSelectSyncFrame !== null) window.cancelAnimationFrame(monthSelectSyncFrame)

	monthSelectSyncFrame = window.requestAnimationFrame(() => {
		monthSelectSyncFrame = null
		syncMultiMonthSelects(instance)
	})
}

function parseViewDate(value: DatePickerValue, instance: Instance): Date | null {
	if (!value) return null
	if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value

	const parsedDate =
		instance.parseDate(value, resolvedDateFormat.value) ?? instance.parseDate(value)
	if (parsedDate && !Number.isNaN(parsedDate.getTime())) return parsedDate

	const nativeDate = new Date(value)
	return Number.isNaN(nativeDate.getTime()) ? null : nativeDate
}

function getAlignedViewDate(date: Date) {
	if (props.viewDateAlignment !== 'right') return date

	const calendarOffset = resolvedShowMonths.value - 1
	if (calendarOffset <= 0) return date

	return new Date(date.getFullYear(), date.getMonth() - calendarOffset, 1)
}

function getOpeningViewDate() {
	if (props.mode === 'range' && selectedDates.value.length >= 2) {
		return selectedDates.value[selectedDates.value.length - 1] ?? null
	}

	return selectedDates.value[0] ?? props.defaultViewDate ?? null
}

function hasCompleteRange(instance: Instance) {
	return props.mode === 'range' && instance.selectedDates.length === 2
}

function hasCompleteModelRange() {
	return props.mode === 'range' && selectedDates.value.length === 2
}

function getCompleteRangeInputValue(instance: Instance, format: string) {
	if (!hasCompleteRange(instance)) return null

	const rangeValues = instance.selectedDates.map((date) => instance.formatDate(date, format))
	if (rangeValues[0] === rangeValues[1]) return rangeValues[0] ?? null

	return rangeValues.join(instance.l10n.rangeSeparator)
}

function syncInputDisplayValue(instance = picker.value) {
	if (!instance) return

	const inputValue = getCompleteRangeInputValue(instance, resolvedDateFormat.value)
	if (inputValue) {
		instance.input.value = inputValue
	}

	const altInputValue = getCompleteRangeInputValue(instance, resolvedAltFormat.value)
	if (instance.altInput && altInputValue) {
		instance.altInput.value = altInputValue
	}
}

function syncCalendarView(instance: Instance) {
	const viewDate = parseViewDate(getOpeningViewDate(), instance)
	if (!viewDate) return

	instance.jumpToDate(getAlignedViewDate(viewDate))
}

function getRangeDayElement(target: EventTarget | null): RangeDayElement | null {
	if (!(target instanceof Element)) return null
	return target.closest<RangeDayElement>('.flatpickr-day')
}

function isSelectableDay(dayElem: RangeDayElement) {
	return (
		Boolean(dayElem.dateObj) &&
		!dayElem.classList.contains('flatpickr-disabled') &&
		!dayElem.classList.contains('hidden')
	)
}

function getRangeEdge(dayElem: RangeDayElement): RangeEdge | null {
	if (props.mode !== 'range') return null
	if (picker.value?.selectedDates.length !== 2) return null
	if (!isSelectableDay(dayElem)) return null
	if (dayElem.classList.contains('startRange') && !dayElem.classList.contains('endRange')) {
		return 'start'
	}
	if (dayElem.classList.contains('endRange') && !dayElem.classList.contains('startRange')) {
		return 'end'
	}
	return null
}

function dateTime(date: Date) {
	return date.getTime()
}

function dateOnlyTime(date: Date) {
	return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
}

function jumpToViewMonth(instance: Instance, viewMonth: CalendarViewMonth) {
	if (instance.currentMonth === viewMonth.month && instance.currentYear === viewMonth.year) return

	instance.jumpToDate(new Date(viewMonth.year, viewMonth.month, 1))
}

function withEndpointTime(date: Date, endpoint: Date) {
	return new Date(
		date.getFullYear(),
		date.getMonth(),
		date.getDate(),
		endpoint.getHours(),
		endpoint.getMinutes(),
		endpoint.getSeconds(),
		endpoint.getMilliseconds(),
	)
}

function resolveRangeEndpointMove(targetDate: Date, endpointDate: Date, anchorDate: Date) {
	const movedDate = withEndpointTime(targetDate, endpointDate)
	const [nextStartDate, nextEndDate] =
		dateOnlyTime(movedDate) <= dateOnlyTime(anchorDate)
			? [movedDate, anchorDate]
			: [anchorDate, movedDate]

	return {
		nextStartDate,
		nextEndDate,
		nextEndpointDate: movedDate,
	}
}

function withPreservedCalendarView(instance: Instance, callback: () => void) {
	const currentViewMonth = {
		month: instance.currentMonth,
		year: instance.currentYear,
	}

	callback()
	jumpToViewMonth(instance, currentViewMonth)
}

function syncRangeEndpointMoveState(instance = picker.value) {
	if (!instance) return

	for (const dayElem of instance.calendarContainer.querySelectorAll('.is-moving-range-end')) {
		dayElem.classList.remove('is-moving-range-end')
	}

	const state = rangeEndpointMoveState.value
	if (!state || props.mode !== 'range' || instance.selectedDates.length !== 2) return

	const selector =
		state.edge === 'start'
			? '.flatpickr-day.startRange:not(.endRange)'
			: '.flatpickr-day.endRange:not(.startRange)'
	instance.calendarContainer.querySelector(selector)?.classList.add('is-moving-range-end')
}

function setRangeEndpointMoveState(state: RangeEndpointMoveState | null) {
	rangeEndpointMoveState.value = state
	syncCalendarStateClasses()
	syncRangeEndpointMoveState()
}

function selectRangeEndpointForMove(edge: RangeEdge, startDate: Date, endDate: Date) {
	setRangeEndpointMoveState({
		edge,
		anchorDate: new Date(edge === 'start' ? endDate : startDate),
		endpointDate: new Date(edge === 'start' ? startDate : endDate),
		lastPreviewDateTime: dateTime(edge === 'start' ? startDate : endDate),
	})
}

function cancelRangeEndpointMovePreview() {
	if (!rangeEndpointMoveState.value) return

	setRangeEndpointMoveState(null)
	syncPickerFromModel()
}

function previewSelectedRangeEndpoint(event: MouseEvent) {
	const state = rangeEndpointMoveState.value
	const instance = picker.value
	if (!state || !instance) return

	const dayElem = getRangeDayElement(event.target)
	if (!dayElem || !instance.calendarContainer.contains(dayElem) || !isSelectableDay(dayElem)) return

	const { nextStartDate, nextEndDate, nextEndpointDate } = resolveRangeEndpointMove(
		dayElem.dateObj!,
		state.endpointDate,
		state.anchorDate,
	)
	const nextPreviewDateTime = dateTime(nextEndpointDate)
	if (nextPreviewDateTime === state.lastPreviewDateTime) return

	state.lastPreviewDateTime = nextPreviewDateTime
	withPreservedCalendarView(instance, () => {
		instance.setDate([nextStartDate, nextEndDate], false)
	})
	syncHeaderControlState(instance)
	syncMissingRangeEndState()
	syncRangeEndpointMoveState(instance)
}

function moveSelectedRangeEndpoint(dayElem: RangeDayElement) {
	const state = rangeEndpointMoveState.value
	const instance = picker.value
	if (!state || !instance || !dayElem.dateObj) return false

	const { nextStartDate, nextEndDate, nextEndpointDate } = resolveRangeEndpointMove(
		dayElem.dateObj,
		state.endpointDate,
		state.anchorDate,
	)

	setRangeEndpointMoveState(null)

	if (dateTime(nextEndpointDate) === dateTime(state.endpointDate)) return true

	intendedViewMonth.value = {
		month: instance.currentMonth,
		year: instance.currentYear,
	}

	instance.setDate([nextStartDate, nextEndDate], true)
	syncHeaderControlState(instance)

	return true
}

function clearRangeClickSuppressionTimeout() {
	if (rangeClickSuppressionTimeout === null) return

	window.clearTimeout(rangeClickSuppressionTimeout)
	rangeClickSuppressionTimeout = null
}

function clearRangeClickSuppressionSoon() {
	clearRangeClickSuppressionTimeout()
	rangeClickSuppressionTimeout = window.setTimeout(() => {
		suppressNextRangeClick.value = false
		rangeClickSuppressionTimeout = null
	}, 50)
}

function startRangeDrag(event: PointerEvent) {
	if (event.button !== 0) return
	if (rangeEndpointMoveState.value) return

	const instance = picker.value
	const dayElem = getRangeDayElement(event.target)
	if (!instance || !dayElem) return

	const edge = getRangeEdge(dayElem)
	if (!edge) return

	const [startDate, endDate] = instance.selectedDates
	if (!startDate || !endDate) return

	const draggedEndpointDate = new Date(edge === 'start' ? startDate : endDate)
	rangeDragState.value = {
		edge,
		anchorDate: new Date(edge === 'start' ? endDate : startDate),
		draggedEndpointDate,
		pointerId: event.pointerId,
		lastDateTime: dateTime(draggedEndpointDate),
		hasMoved: false,
	}
	syncCalendarStateClasses(instance)
	clearRangeClickSuppressionTimeout()
	suppressNextRangeClick.value = true

	document.addEventListener('pointermove', updateRangeDrag, true)
	document.addEventListener('pointerup', stopRangeDrag, true)
	document.addEventListener('pointercancel', stopRangeDrag, true)

	event.preventDefault()
	event.stopImmediatePropagation()
}

function updateRangeDrag(event: PointerEvent) {
	const state = rangeDragState.value
	const instance = picker.value
	if (!state || !instance || event.pointerId !== state.pointerId) return

	const target = document.elementFromPoint(event.clientX, event.clientY)
	const dayElem = getRangeDayElement(target)
	if (!dayElem || !instance.calendarContainer.contains(dayElem) || !isSelectableDay(dayElem)) return

	const { nextStartDate, nextEndDate, nextEndpointDate } = resolveRangeEndpointMove(
		dayElem.dateObj!,
		state.draggedEndpointDate,
		state.anchorDate,
	)

	const nextDraggedTime = dateTime(nextEndpointDate)
	if (nextDraggedTime === state.lastDateTime) return

	intendedViewMonth.value = {
		month: instance.currentMonth,
		year: instance.currentYear,
	}

	state.lastDateTime = nextDraggedTime
	state.hasMoved = true
	instance.setDate([nextStartDate, nextEndDate], true)
	syncHeaderControlState(instance)

	event.preventDefault()
	event.stopImmediatePropagation()
}

function stopRangeDrag(event: PointerEvent) {
	const state = rangeDragState.value
	if (!state || event.pointerId !== state.pointerId) return

	const instance = picker.value
	rangeDragState.value = null
	syncCalendarStateClasses(instance)
	document.removeEventListener('pointermove', updateRangeDrag, true)
	document.removeEventListener('pointerup', stopRangeDrag, true)
	document.removeEventListener('pointercancel', stopRangeDrag, true)
	clearRangeClickSuppressionSoon()
	if (state.hasMoved) {
		setRangeEndpointMoveState(null)
	} else {
		selectRangeEndpointForMove(
			state.edge,
			state.edge === 'start' ? state.draggedEndpointDate : state.anchorDate,
			state.edge === 'end' ? state.draggedEndpointDate : state.anchorDate,
		)
	}

	if (
		document.activeElement instanceof HTMLElement &&
		instance?.calendarContainer.contains(document.activeElement)
	) {
		document.activeElement.blur()
	}

	event.preventDefault()
	event.stopImmediatePropagation()
}

function stopRangeEndpointMouseEvent(event: MouseEvent) {
	if (
		rangeDragState.value ||
		suppressNextRangeClick.value ||
		(rangeEndpointMoveState.value && getRangeDayElement(event.target))
	) {
		event.preventDefault()
		event.stopImmediatePropagation()
	}
}

function suppressRangeDragClick(event: MouseEvent) {
	if (!suppressNextRangeClick.value) return

	clearRangeClickSuppressionTimeout()
	suppressNextRangeClick.value = false
	event.preventDefault()
	event.stopImmediatePropagation()
}

function handleRangeEndpointMoveClick(event: MouseEvent) {
	if (suppressNextRangeClick.value) {
		suppressRangeDragClick(event)
		return
	}

	const instance = picker.value
	const dayElem = getRangeDayElement(event.target)
	if (!instance || !dayElem || props.mode !== 'range') return

	const moveState = rangeEndpointMoveState.value
	if (moveState) {
		if (!isSelectableDay(dayElem)) return

		moveSelectedRangeEndpoint(dayElem)
		event.preventDefault()
		event.stopImmediatePropagation()
		return
	}

	const edge = getRangeEdge(dayElem)
	if (!edge) return

	const [startDate, endDate] = instance.selectedDates
	if (!startDate || !endDate) return

	selectRangeEndpointForMove(edge, startDate, endDate)
	event.preventDefault()
	event.stopImmediatePropagation()
}

function hasRangeEnd(instance: Instance) {
	return (
		instance.selectedDates.length === 2 ||
		Boolean(instance.calendarContainer.querySelector('.flatpickr-day.endRange:not(.startRange)'))
	)
}

function shouldSuppressMissingRangeEndBackground(instance?: Instance) {
	if (props.mode !== 'range' || !instance) return false
	if (instance.selectedDates.length !== 1) return false

	const hasRangeStart = Boolean(
		instance.calendarContainer.querySelector('.flatpickr-day.startRange:not(.endRange)'),
	)
	const hasRangePreview = Boolean(
		instance.calendarContainer.querySelector('.flatpickr-day.inRange'),
	)

	return hasRangeStart && !hasRangeEnd(instance) && !hasRangePreview
}

function syncMissingRangeEndState() {
	const instance = picker.value

	instance?.calendarContainer.classList.toggle(
		'is-missing-range-end',
		shouldSuppressMissingRangeEndBackground(instance),
	)
}

function isTimeInput(target: EventTarget | null): target is HTMLInputElement {
	return (
		target instanceof HTMLInputElement &&
		target.matches('input.flatpickr-hour, input.flatpickr-minute, input.flatpickr-second')
	)
}

function getTimeInputFromArrow(target: EventTarget | null): HTMLInputElement | null {
	if (!(target instanceof Element)) return null
	if (!target.classList.contains('arrowUp') && !target.classList.contains('arrowDown')) return null

	return target.closest('.numInputWrapper')?.querySelector<HTMLInputElement>('input') ?? null
}

function getTimeInputDigits(value: string) {
	return value.replace(/\D/g, '').slice(0, 2)
}

function sanitizeTimeInputValue(input: HTMLInputElement) {
	const nextValue = getTimeInputDigits(input.value)
	if (input.value === nextValue) return nextValue

	const cursorPosition = input.selectionStart ?? nextValue.length
	const removedBeforeCursor =
		input.value.slice(0, cursorPosition).length -
		getTimeInputDigits(input.value.slice(0, cursorPosition)).length
	const nextCursorPosition = Math.max(0, cursorPosition - removedBeforeCursor)

	input.value = nextValue
	input.setSelectionRange(nextCursorPosition, nextCursorPosition)
	return nextValue
}

function normalizeTimeInputValue(input: HTMLInputElement) {
	sanitizeTimeInputValue(input)
	if (input.value.length === 2) return

	const minValue = Number.parseInt(input.min, 10)
	const maxValue = Number.parseInt(input.max, 10)
	const parsedValue = Number.parseInt(input.value, 10)
	const fallbackValue = Number.isFinite(minValue) ? minValue : 0
	let nextValue = Number.isFinite(parsedValue) ? parsedValue : fallbackValue

	if (Number.isFinite(minValue)) nextValue = Math.max(nextValue, minValue)
	if (Number.isFinite(maxValue)) nextValue = Math.min(nextValue, maxValue)

	input.value = String(nextValue).padStart(2, '0')
}

function normalizeTimeInputForArrowIncrement(event: MouseEvent) {
	const input = getTimeInputFromArrow(event.target)
	if (input && isTimeInput(input)) normalizeTimeInputValue(input)
}

function normalizeTimeInputForKeyboardIncrement(event: KeyboardEvent) {
	if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
	if (isTimeInput(event.target)) normalizeTimeInputValue(event.target)
}

function preventNonNumericTimeInput(event: InputEvent) {
	if (!isTimeInput(event.target)) return
	if (event.data && /\D/.test(event.data)) event.preventDefault()
}

function preventNonNumericTimeKeydown(event: KeyboardEvent) {
	if (!isTimeInput(event.target)) return
	if (event.metaKey || event.ctrlKey || event.altKey) return
	if (event.key.length === 1 && /\D/.test(event.key)) event.preventDefault()
}

function getTimeInputs(instance: Instance) {
	return [instance.hourElement, instance.minuteElement, instance.secondElement].filter(
		(input): input is HTMLInputElement => Boolean(input),
	)
}

function getTimeInputDraft(input: HTMLInputElement) {
	return document.activeElement === input
		? (timeInputDigits.value.get(input) ?? getTimeInputDigits(input.value))
		: getTimeInputDigits(input.value)
}

function getNormalizedTimeInputDraft(input: HTMLInputElement) {
	const draft = getTimeInputDraft(input)
	if (!draft) return null
	if (draft.length === 2) return draft

	const minValue = Number.parseInt(input.min, 10)
	const maxValue = Number.parseInt(input.max, 10)
	let nextValue = Number.parseInt(draft, 10)

	if (Number.isFinite(minValue)) nextValue = Math.max(nextValue, minValue)
	if (Number.isFinite(maxValue)) nextValue = Math.min(nextValue, maxValue)

	return String(nextValue).padStart(2, '0')
}

function prepareTimeInputValuesForCommit(instance: Instance) {
	for (const input of getTimeInputs(instance)) {
		const nextValue = getNormalizedTimeInputDraft(input)
		if (nextValue === null) return false

		input.value = nextValue
	}

	return true
}

function syncTimeInputDrafts(instance: Instance) {
	for (const input of getTimeInputs(instance)) {
		timeInputDigits.value.set(input, getTimeInputDigits(input.value))
	}
}

function commitTimeInputForInput(event: Event) {
	if (!isTimeInput(event.target)) return

	const instance = picker.value
	if (!instance) return

	const previousValue = instance.input.value
	const nextDigits = sanitizeTimeInputValue(event.target)
	timeInputDigits.value.set(event.target, nextDigits)
	if (!prepareTimeInputValuesForCommit(instance)) return

	event.target.dispatchEvent(new Event('increment', { bubbles: true }))
	if (nextDigits.length === 1 && document.activeElement === event.target) {
		event.target.value = nextDigits
		event.target.setSelectionRange(nextDigits.length, nextDigits.length)
	}
	syncTimeInputDrafts(instance)
	if (instance.input.value === previousValue) return

	const nextValue =
		props.mode === 'single'
			? instance.input.value || null
			: instance.selectedDates.map((date) => instance.formatDate(date, resolvedDateFormat.value))
	model.value = nextValue
	emit('change', nextValue)
}

function syncTimeInputTypes(instance: Instance) {
	const timeInputs = getTimeInputs(instance)
	const activeTimeInput = timeInputs.find((input) => document.activeElement === input)
	const activeDraft = activeTimeInput ? timeInputDigits.value.get(activeTimeInput) : undefined

	for (const input of timeInputs) {
		input.type = 'text'
		input.inputMode = 'numeric'
		input.pattern = '[0-9]*'
		normalizeTimeInputValue(input)
	}

	if (activeTimeInput && activeDraft !== undefined && activeDraft.length < 2) {
		activeTimeInput.value = activeDraft
		activeTimeInput.setSelectionRange(activeDraft.length, activeDraft.length)
	}

	syncTimeInputDrafts(instance)
}

function openPickerWithoutInputFocus(event: PointerEvent) {
	if (!props.readonly || props.disabled || props.calendarOnly) return
	if (event.button !== 0) return
	if (event.pointerType === 'mouse') return

	event.preventDefault()
	suppressNextInputFocusScroll = true
	picker.value?.open()
}

function suppressInputFocusScrollForCalendarPointer(event: PointerEvent) {
	if (!props.readonly || props.disabled || props.calendarOnly || !props.closeOnSelect) return
	if (event.button !== 0) return
	if (event.pointerType === 'mouse') return

	const dayElem = getRangeDayElement(event.target)
	if (!dayElem || !isSelectableDay(dayElem)) return

	suppressNextInputFocusScroll = true
}

function patchInputFocusScrollSuppression(target: HTMLInputElement) {
	originalInputFocus = target.focus
	target.focus = ((options?: FocusOptions) => {
		if (suppressNextInputFocusScroll) {
			originalInputFocus?.call(target, { preventScroll: true })
			return
		}

		originalInputFocus?.call(target, options)
	}) as HTMLInputElement['focus']
}

function teardownInputFocusScrollSuppression() {
	inputFocusScrollSuppressionTarget?.removeEventListener('pointerdown', openPickerWithoutInputFocus)
	if (inputFocusScrollSuppressionTarget && originalInputFocus) {
		inputFocusScrollSuppressionTarget.focus = originalInputFocus
	}
	inputFocusScrollSuppressionTarget = null
	originalInputFocus = null
	suppressNextInputFocusScroll = false
}

function syncInputFocusScrollSuppression() {
	const target = picker.value?.altInput ?? inputRef.value ?? null
	if (!target || !props.readonly || props.disabled || props.calendarOnly) {
		teardownInputFocusScrollSuppression()
		return
	}

	if (inputFocusScrollSuppressionTarget === target) return

	teardownInputFocusScrollSuppression()
	target.addEventListener('pointerdown', openPickerWithoutInputFocus)
	patchInputFocusScrollSuppression(target)
	inputFocusScrollSuppressionTarget = target
}

function setCalendarPositionClass(container: HTMLElement, className: string, isEnabled: boolean) {
	container.classList.toggle(className, isEnabled)
}

function getCalendarPositionParts() {
	const parts = props.position.split(' ')
	return {
		vertical: parts[0] ?? 'auto',
		horizontal: parts[1] ?? null,
	}
}

function getCalendarHeight(container: HTMLElement) {
	const height = container.getBoundingClientRect().height
	if (height > 0) return height

	return Array.from(container.children).reduce(
		(total, child) => total + (child instanceof HTMLElement ? child.offsetHeight : 0),
		0,
	)
}

function positionCalendar(instance: Instance, customPositionElement?: HTMLElement) {
	const container = instance.calendarContainer
	const positionElement = customPositionElement ?? instance._positionElement
	if (!container || !positionElement) return

	const calendarHeight = getCalendarHeight(container)
	const calendarWidth = container.offsetWidth
	const { vertical, horizontal } = getCalendarPositionParts()
	const inputBounds = positionElement.getBoundingClientRect()
	const distanceFromBottom = window.innerHeight - inputBounds.bottom
	const showOnTop =
		vertical === 'above' ||
		(vertical !== 'below' &&
			distanceFromBottom < calendarHeight &&
			inputBounds.top > calendarHeight)

	const top =
		window.pageYOffset +
		inputBounds.top +
		(showOnTop
			? -calendarHeight - calendarPositionGap
			: positionElement.offsetHeight + calendarPositionGap)
	let left = window.pageXOffset + inputBounds.left
	let isCenter = false
	let isRight = false

	if (horizontal === 'center') {
		left -= (calendarWidth - inputBounds.width) / 2
		isCenter = true
	} else if (horizontal === 'right') {
		left -= calendarWidth - inputBounds.width
		isRight = true
	}

	const viewportLeft = window.pageXOffset
	const viewportRight = viewportLeft + document.documentElement.clientWidth
	const isOverflowingRight = left + calendarWidth > viewportRight
	const clampedLeft = Math.min(
		Math.max(viewportLeft, left),
		Math.max(viewportLeft, viewportRight - calendarWidth),
	)

	setCalendarPositionClass(container, 'arrowTop', !showOnTop)
	setCalendarPositionClass(container, 'arrowBottom', showOnTop)
	setCalendarPositionClass(container, 'arrowLeft', !isCenter && !isRight)
	setCalendarPositionClass(container, 'arrowCenter', isCenter)
	setCalendarPositionClass(container, 'arrowRight', isRight)
	setCalendarPositionClass(container, 'rightMost', isOverflowingRight)
	setCalendarPositionClass(container, 'centerMost', false)

	container.style.top = `${top}px`
	container.style.left = `${clampedLeft}px`
	container.style.right = 'auto'
}

const resolvedDateFormat = computed(
	() => props.dateFormat ?? (props.enableTime ? 'Y-m-d H:i' : 'Y-m-d'),
)
const resolvedAltFormat = computed(
	() => props.altFormat ?? (props.enableTime ? 'F j, Y at h:i K' : 'F j, Y'),
)
const resolvedShowMonths = computed(() =>
	Number.isFinite(props.showMonths) ? Math.max(1, Math.floor(props.showMonths)) : 1,
)

const selectedDates = computed(() => {
	const value = model.value
	if (Array.isArray(value)) {
		return value.filter(Boolean) as Array<string | Date>
	}

	return value ? [value] : []
})

const hasClearButton = computed(
	() => !props.calendarOnly && props.clearable && !props.disabled && selectedDates.value.length > 0,
)

const inputClasses = computed(() => [
	props.calendarOnly
		? 'sr-only pointer-events-none absolute h-0 w-0 opacity-0'
		: 'w-full touch-manipulation text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none focus:ring-4 focus:ring-brand-shadow !outline-0',
	!props.calendarOnly && props.showIcon ? 'pl-10' : '',
	!props.calendarOnly && !props.showIcon ? 'pl-3' : '',
	!props.calendarOnly
		? `${hasClearButton.value ? 'pr-10' : 'pr-3'} h-9 py-2 text-base outline-none bg-surface-4 border-none rounded-xl`
		: '',
	props.disabled && !props.calendarOnly ? 'cursor-not-allowed' : '',
	props.inputClass,
])

watch(
	() => [
		model.value,
		props.disabled,
		props.readonly,
		props.minDate,
		props.maxDate,
		props.enableTime,
		props.mode,
		props.showMonths,
		props.defaultViewDate,
		props.viewDateAlignment,
		props.dateFormat,
		props.altFormat,
		props.time24hr,
		props.calendarOnly,
		props.closeOnSelect,
		props.position,
		props.clearable,
	],
	() => {
		if (!picker.value) return

		picker.value.set({
			...flatpickrOptions(),
			onChange: picker.value.config.onChange,
			onClose: picker.value.config.onClose,
			onReady: picker.value.config.onReady,
			onMonthChange: picker.value.config.onMonthChange,
			onYearChange: picker.value.config.onYearChange,
			onOpen: picker.value.config.onOpen,
		})

		if (!isPreservingDay.value && props.mode === 'single') {
			const value = model.value
			if (typeof value === 'string' && value) {
				const day = getDayFromDateStr(value)
				if (day !== null) intendedDay.value = day
			} else {
				intendedDay.value = null
			}
		}

		syncAltInputState()
		syncTimeInputTypes(picker.value)
		if (props.mode !== 'range' || selectedDates.value.length !== 2) cancelRangeEndpointMovePreview()
		syncPickerFromModel()
		syncCalendarClasses()
	},
	{ deep: true },
)

watch(
	() => props.calendarClass,
	() => syncCalendarClasses(),
)

watch(
	() => [
		props.showToday,
		props.calendarOnly,
		props.mode,
		selectedDates.value.length,
		Boolean(rangeDragState.value),
		Boolean(rangeEndpointMoveState.value),
	],
	() => syncCalendarStateClasses(),
)

onMounted(async () => {
	await nextTick()
	if (!inputRef.value) return

	picker.value = flatpickr(inputRef.value, {
		...flatpickrOptions(),
		onReady: (_selectedDates, _dateStr, instance) => {
			syncCalendarView(instance)

			instance.calendarContainer.addEventListener(
				'pointerdown',
				suppressInputFocusScrollForCalendarPointer,
				true,
			)
			instance.calendarContainer.addEventListener('pointerdown', startRangeDrag, true)
			instance.calendarContainer.addEventListener('mousedown', stopRangeEndpointMouseEvent, true)
			instance.calendarContainer.addEventListener('mouseup', stopRangeEndpointMouseEvent, true)
			instance.calendarContainer.addEventListener('click', handleRangeEndpointMoveClick, true)
			instance.calendarContainer.addEventListener('mouseover', previewSelectedRangeEndpoint, true)
			instance.calendarContainer.addEventListener('mouseover', syncMissingRangeEndState)
			instance.calendarContainer.addEventListener('mouseleave', syncMissingRangeEndState)
			instance.calendarContainer.addEventListener(
				'click',
				normalizeTimeInputForArrowIncrement,
				true,
			)
			instance.calendarContainer.addEventListener(
				'keydown',
				normalizeTimeInputForKeyboardIncrement,
				true,
			)
			instance.calendarContainer.addEventListener('beforeinput', preventNonNumericTimeInput, true)
			instance.calendarContainer.addEventListener('keydown', preventNonNumericTimeKeydown, true)
			instance.calendarContainer.addEventListener('input', commitTimeInputForInput, true)

			instance.calendarContainer.addEventListener('mousedown', (event) => {
				if (props.mode !== 'range') return
				const target = event.target as HTMLElement | null
				const dayElem = target?.closest<RangeDayElement>('.flatpickr-day')
				if (!dayElem) return

				if (resolvedShowMonths.value > 1) {
					if (!isSelectableDay(dayElem)) return
				} else if (
					!dayElem.classList.contains('prevMonthDay') &&
					!dayElem.classList.contains('nextMonthDay')
				) {
					return
				}

				intendedViewMonth.value = {
					month: instance.currentMonth,
					year: instance.currentYear,
				}
			})

			instance.calendarContainer.addEventListener(
				'keydown',
				(event) => {
					const target = event.target as HTMLElement | null
					if (!target?.matches('input.cur-year')) return
					if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
						event.preventDefault()
						event.stopImmediatePropagation()
						changeYearInput(instance, target as HTMLInputElement, event.key === 'ArrowUp' ? 1 : -1)
						return
					}
					if (
						event.key === 'ArrowLeft' ||
						event.key === 'ArrowRight' ||
						event.key === 'Home' ||
						event.key === 'End'
					) {
						event.stopPropagation()
					}
				},
				true,
			)
			instance.calendarContainer.addEventListener('keyup', (event) => {
				const target = event.target as HTMLElement | null
				if (!target?.matches('input.cur-year') || event.key !== 'Enter') return

				window.requestAnimationFrame(() => target.blur())
			})

			syncTimeInputTypes(instance)
			syncHeaderControlState(instance)
			syncCalendarClasses(instance)
			syncCalendarStateClasses(instance)
			syncRangeEndpointMoveState(instance)
			syncMultiMonthSelects(instance)
			syncInputFocusScrollSuppression()
		},
		onChange: (_selectedDates, dateStr, instance) => {
			if (isSyncingFromModel.value) return

			const nextValue =
				props.mode === 'single'
					? dateStr || null
					: _selectedDates.map((date) => instance.formatDate(date, resolvedDateFormat.value))
			if (intendedViewMonth.value !== null) {
				preserveViewOnNextModelSync.value = true
			}
			model.value = nextValue
			emit('change', nextValue)

			if (intendedViewMonth.value !== null) {
				jumpToViewMonth(instance, intendedViewMonth.value)
			}

			syncHeaderControlState(instance)
			syncMissingRangeEndState()
			syncCalendarStateClasses(instance)
			syncRangeEndpointMoveState(instance)
			syncInputDisplayValue(instance)
		},
		onClose: (_selectedDates, dateStr, instance) => {
			cancelRangeEndpointMovePreview()
			if (suppressNextInputFocusScroll) {
				const focusTarget = instance.altInput ?? inputRef.value
				focusTarget?.blur()
				suppressNextInputFocusScroll = false
			}

			if (hasCompleteModelRange()) {
				syncPickerFromModel()
				return
			}

			syncInputDisplayValue(instance)
			if (!props.clearable || dateStr || hasCompleteRange(instance)) return

			const nextValue = props.mode === 'single' ? null : []
			model.value = nextValue
			instance.clear(false)
			emit('clear')
			emit('change', nextValue)
		},
		onMonthChange: (_selectedDates, _dateStr, instance) => {
			applyPreserveDay(instance)
			syncHeaderControlState(instance)
			syncMissingRangeEndState()
			syncCalendarStateClasses(instance)
			syncRangeEndpointMoveState(instance)
			queueMultiMonthSelectSync(instance)
		},
		onYearChange: (_selectedDates, _dateStr, instance) => {
			applyPreserveDay(instance)
			syncHeaderControlState(instance)
			syncMissingRangeEndState()
			syncCalendarStateClasses(instance)
			syncRangeEndpointMoveState(instance)
			queueMultiMonthSelectSync(instance)
		},
		onOpen: (_selectedDates, _dateStr, instance) => {
			syncCalendarView(instance)
			syncTimeInputTypes(instance)
			syncHeaderControlState(instance)
			syncMissingRangeEndState()
			syncCalendarStateClasses(instance)
			syncRangeEndpointMoveState(instance)
			syncMultiMonthSelects(instance)
		},
	})

	if (props.mode === 'single' && typeof model.value === 'string' && model.value) {
		const day = getDayFromDateStr(model.value)
		if (day !== null) intendedDay.value = day
	}

	syncAltInputState()
	syncInputFocusScrollSuppression()
	syncPickerFromModel()
	syncHeaderControlState(picker.value)
	syncRangeEndpointMoveState(picker.value)
	syncMultiMonthSelects(picker.value)
})

onBeforeUnmount(() => {
	clearRangeClickSuppressionTimeout()
	if (monthSelectSyncFrame !== null) window.cancelAnimationFrame(monthSelectSyncFrame)
	document.removeEventListener('pointermove', updateRangeDrag, true)
	document.removeEventListener('pointerup', stopRangeDrag, true)
	document.removeEventListener('pointercancel', stopRangeDrag, true)
	teardownInputFocusScrollSuppression()
	picker.value?.destroy()
	destroyCalendarPortal()
})

function flatpickrOptions(): Options {
	return {
		allowInput: !props.calendarOnly,
		altInput: !props.calendarOnly,
		altInputClass: props.calendarOnly ? undefined : inputClasses.value.filter(Boolean).join(' '),
		altFormat: resolvedAltFormat.value,
		appendTo: ensureCalendarPortal(),
		closeOnSelect: props.closeOnSelect,
		dateFormat: resolvedDateFormat.value,
		disableMobile: true,
		enableTime: props.enableTime,
		inline: props.calendarOnly,
		maxDate: props.maxDate,
		minDate: props.minDate,
		mode: props.mode,
		noCalendar: false,
		nextArrow: chevronRightIcon,
		position: positionCalendar,
		prevArrow: chevronLeftIcon,
		showMonths: resolvedShowMonths.value,
		static: false,
		time_24hr: props.time24hr,
	}
}

function syncPickerFromModel() {
	if (!picker.value) return

	const preservedViewMonth =
		intendedViewMonth.value ??
		(preserveViewOnNextModelSync.value
			? {
					month: picker.value.currentMonth,
					year: picker.value.currentYear,
				}
			: null)

	isSyncingFromModel.value = true
	picker.value.setDate(selectedDates.value, false, resolvedDateFormat.value)
	syncTimeInputTypes(picker.value)

	if (preservedViewMonth !== null) {
		jumpToViewMonth(picker.value, preservedViewMonth)
		intendedViewMonth.value = null
		preserveViewOnNextModelSync.value = false
	} else {
		syncCalendarView(picker.value)
	}

	isSyncingFromModel.value = false
	syncHeaderControlState(picker.value)
	syncInputDisplayValue()
	syncMissingRangeEndState()
	syncCalendarStateClasses()
	syncRangeEndpointMoveState()
	syncMultiMonthSelects()
}

function syncAltInputState() {
	if (!picker.value?.altInput) {
		syncInputFocusScrollSuppression()
		return
	}

	picker.value.altInput.disabled = props.disabled
	picker.value.altInput.readOnly = props.readonly
	syncInputFocusScrollSuppression()
}

function clearValue() {
	const nextValue = props.mode === 'single' ? null : []
	model.value = nextValue
	picker.value?.clear(false)
	setRangeEndpointMoveState(null)
	syncMissingRangeEndState()
	emit('clear')
	emit('change', nextValue)
}

defineExpose({
	focus: () => picker.value?.altInput?.focus() ?? inputRef.value?.focus(),
	open: () => picker.value?.open(),
	close: () => picker.value?.close(),
	clear: clearValue,
})
</script>

<style scoped>
.modrinth-date-picker :deep(.flatpickr-wrapper) {
	@apply w-full;
}

.modrinth-date-picker :deep(.flatpickr-calendar) {
	@apply mt-2 touch-manipulation rounded-2xl border border-solid border-surface-5 bg-surface-3 shadow-none p-3 text-primary select-none;
	box-sizing: content-box;
}

.modrinth-date-picker :deep(.flatpickr-calendar.arrowBottom) {
	margin-top: -0.5rem;
}

.modrinth-date-picker.calendar-only {
	@apply block;
}

.modrinth-date-picker.calendar-only :deep(.flatpickr-wrapper) {
	@apply block w-full;
}

.modrinth-date-picker.calendar-only :deep(.flatpickr-calendar) {
	@apply m-0;
	box-sizing: content-box;
}

.modrinth-date-picker :deep(.dayContainer) {
	@apply max-w-[307.875px] min-w-[307.875px] w-[307.875px];
}

.modrinth-date-picker :deep(.flatpickr-calendar.multiMonth .flatpickr-rContainer),
.modrinth-date-picker :deep(.flatpickr-calendar.multiMonth .flatpickr-days) {
	@apply max-w-none;
}

.modrinth-date-picker :deep(.flatpickr-calendar.multiMonth .flatpickr-days) {
	overflow: visible;
}

.modrinth-date-picker :deep(.flatpickr-calendar.multiMonth .dayContainer + .dayContainer) {
	box-shadow: none;
}

.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars) {
	width: calc(615.75px + 0.75rem) !important;
}

.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-months),
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-weekdays),
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-days) {
	@apply gap-3;
}

.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-rContainer),
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-weekdays),
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-days) {
	width: calc(615.75px + 0.75rem) !important;
}

.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-month),
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-weekdaycontainer) {
	@apply max-w-[307.875px] min-w-[307.875px] flex-none;
}

.modrinth-date-picker :deep(.flatpickr-calendar::before),
.modrinth-date-picker :deep(.flatpickr-calendar::after) {
	display: none;
}

.modrinth-date-picker :deep(.flatpickr-months),
.modrinth-date-picker :deep(.flatpickr-month) {
	@apply items-center h-10 flex mb-1 overflow-visible;
}

.modrinth-date-picker :deep(.flatpickr-month),
.modrinth-date-picker :deep(.flatpickr-current-month),
.modrinth-date-picker :deep(.flatpickr-weekdays),
.modrinth-date-picker :deep(.flatpickr-weekdaycontainer) {
	@apply text-contrast shadow-none;
}

.modrinth-date-picker :deep(.flatpickr-current-month) {
	@apply flex h-full items-center justify-center gap-2 py-0 text-base font-semibold;
}

.modrinth-date-picker :deep(.flatpickr-current-month input.cur-year),
.modrinth-date-picker :deep(.flatpickr-current-month .flatpickr-monthDropdown-months) {
	@apply touch-manipulation rounded-xl bg-surface-4 py-1 font-semibold text-contrast hover:bg-surface-5 min-h-10;
}

.modrinth-date-picker :deep(.flatpickr-current-month .flatpickr-monthDropdown-months) {
	@apply min-w-28 pl-3 pr-8;
	appearance: none;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
	background-position: calc(100% - 8px) calc(50%);
	background-repeat: no-repeat;
	background-size: 16px 16px;
}
.modrinth-date-picker :deep(.flatpickr-current-month .modrinth-hidden-cur-month) {
	display: none;
}
.modrinth-date-picker :deep(.flatpickr-current-month .flatpickr-monthDropdown-months:disabled) {
	@apply cursor-not-allowed opacity-40 hover:bg-surface-4;
}
.modrinth-date-picker :deep(.flatpickr-current-month .numInputWrapper:has(input.cur-year)) {
	@apply w-[76px];
}
.modrinth-date-picker :deep(.flatpickr-current-month input.cur-year) {
	@apply min-w-[76px] px-2 text-center;
}
.modrinth-date-picker :deep(input[type='number']) {
	-moz-appearance: textfield;
	appearance: textfield;
}
.modrinth-date-picker :deep(input[type='number']::-webkit-inner-spin-button),
.modrinth-date-picker :deep(input[type='number']::-webkit-outer-spin-button) {
	margin: 0;
	-webkit-appearance: none;
}
.modrinth-date-picker
	:deep(.flatpickr-current-month .numInputWrapper:has(input.cur-year:disabled)) {
	@apply cursor-not-allowed;
	background: none;
}
.modrinth-date-picker :deep(.flatpickr-current-month input.cur-year:disabled) {
	@apply opacity-40 bg-surface-4 hover:bg-surface-4;
}
.modrinth-date-picker
	:deep(.flatpickr-current-month .numInputWrapper:has(input.cur-year:disabled) span) {
	@apply pointer-events-none opacity-0;
}

.modrinth-date-picker :deep(.flatpickr-current-month input.cur-year:focus),
.modrinth-date-picker :deep(.flatpickr-current-month .flatpickr-monthDropdown-months:focus) {
	@apply outline-none ring-4 ring-brand-shadow;
}

.modrinth-date-picker :deep(.numInputWrapper span.arrowUp) {
	@apply border-0 rounded-tr-xl;
}
.modrinth-date-picker :deep(.numInputWrapper span.arrowDown) {
	@apply border-0 rounded-br-xl;
}

.modrinth-date-picker :deep(.numInputWrapper span.arrowUp::after) {
	border-bottom-color: var(--color-text-secondary);
}

.modrinth-date-picker :deep(.numInputWrapper span.arrowDown::after) {
	border-top-color: var(--color-text-secondary);
}

.modrinth-date-picker :deep(.flatpickr-prev-month),
.modrinth-date-picker :deep(.flatpickr-next-month) {
	@apply top-2.5 mx-3.5 flex h-10 w-10 touch-manipulation items-center justify-center rounded-full p-0 text-secondary hover:bg-surface-4 hover:text-contrast;
}

.modrinth-date-picker :deep(.flatpickr-prev-month.flatpickr-disabled),
.modrinth-date-picker :deep(.flatpickr-next-month.flatpickr-disabled) {
	@apply cursor-not-allowed opacity-40 hover:bg-transparent hover:text-secondary;
}

.modrinth-date-picker :deep(.flatpickr-prev-month svg),
.modrinth-date-picker :deep(.flatpickr-next-month svg) {
	@apply h-5 w-5 stroke-current text-secondary;
	fill: none;
	stroke-width: 3;
}

.modrinth-date-picker :deep(.flatpickr-prev-month:hover svg),
.modrinth-date-picker :deep(.flatpickr-next-month:hover svg) {
	fill: none;
}

.modrinth-date-picker :deep(.flatpickr-weekday) {
	@apply text-xs font-semibold text-secondary;
}

.modrinth-date-picker :deep(.flatpickr-day) {
	@apply relative z-0 m-0 max-w-none touch-manipulation rounded-full border border-solid border-transparent text-primary hover:bg-surface-4 hover:text-contrast font-semibold aspect-square h-auto;
}
.modrinth-date-picker
	:deep(
		.flatpickr-day:focus:not(:focus-visible):not(.selected):not(.startRange):not(.endRange):not(
				.inRange
			)
	) {
	@apply border-transparent bg-transparent text-primary outline-none;
}
.modrinth-date-picker
	:deep(.flatpickr-day:focus-visible:not(.selected):not(.startRange):not(.endRange):not(.inRange)) {
	@apply border-transparent bg-surface-4 text-contrast outline-none;
}
.modrinth-date-picker :deep(.flatpickr-day.flatpickr-disabled) {
	@apply hover:bg-transparent;
}

.modrinth-date-picker :deep(.flatpickr-day::before) {
	content: '';
	position: absolute;
	inset: 0 0;
	z-index: -1;
	background: transparent;
}

.modrinth-date-picker :deep(.flatpickr-day.today) {
	@apply border-transparent;
}

.modrinth-date-picker.show-today :deep(.flatpickr-day.today::after) {
	content: '';
	position: absolute;
	bottom: 6px;
	left: 50%;
	width: 4px;
	height: 4px;
	border-radius: 9999px;
	transform: translateX(-50%);
	@apply bg-brand;
}

.modrinth-date-picker.show-today :deep(.flatpickr-day.today.selected::after),
.modrinth-date-picker.show-today :deep(.flatpickr-day.today.startRange::after),
.modrinth-date-picker.show-today :deep(.flatpickr-day.today.endRange::after) {
	@apply bg-brand-inverted;
}

.modrinth-date-picker :deep(.flatpickr-day.selected),
.modrinth-date-picker :deep(.flatpickr-day.startRange),
.modrinth-date-picker :deep(.flatpickr-day.endRange) {
	@apply z-[1] border-brand bg-brand text-brand-inverted !shadow-none hover:border-brand hover:bg-brand hover:text-brand-inverted hover:shadow-none;
}

.modrinth-date-picker :deep(.flatpickr-day.inRange) {
	@apply rounded-none border-transparent bg-transparent text-brand shadow-none hover:rounded-none hover:border-transparent hover:bg-transparent;
}

.modrinth-date-picker :deep(.flatpickr-calendar.multiMonth .flatpickr-day.inRange) {
	box-shadow: none !important;
}

.modrinth-date-picker :deep(.flatpickr-day.inRange::before),
.modrinth-date-picker :deep(.flatpickr-day.startRange:not(.endRange)::before),
.modrinth-date-picker :deep(.flatpickr-day.endRange:not(.startRange)::before) {
	background: var(--color-brand-highlight);
}

.modrinth-date-picker :deep(.flatpickr-day.inRange::before) {
	left: -1px;
	right: -1px;
}

.modrinth-date-picker :deep(.flatpickr-day.inRange:nth-child(7n + 1)::before) {
	@apply rounded-l-xl;
	left: 0;
}

.modrinth-date-picker :deep(.flatpickr-day.inRange:nth-child(7n)::before) {
	@apply rounded-r-xl;
	right: 0;
}

.modrinth-date-picker :deep(.flatpickr-day.startRange:not(.endRange)) {
	@apply rounded-full border-brand;
}

.modrinth-date-picker :deep(.flatpickr-day.startRange:not(.endRange)::before) {
	@apply rounded-l-xl;
	left: 50%;
	right: -1px;
}

.modrinth-date-picker
	:deep(.flatpickr-calendar.is-missing-range-end .flatpickr-day.startRange:not(.endRange)::before),
.modrinth-date-picker :deep(.flatpickr-calendar.is-missing-range-end .flatpickr-day:hover::before) {
	background: transparent;
}

.modrinth-date-picker :deep(.flatpickr-day.startRange:nth-child(7n):not(.endRange)::before) {
	@apply rounded-r-xl;
	right: 0;
}

.modrinth-date-picker :deep(.flatpickr-day.endRange:not(.startRange)) {
	@apply rounded-full border-brand;
}

.modrinth-date-picker :deep(.flatpickr-day.endRange:not(.startRange)::before) {
	@apply rounded-r-xl;
	left: -1px;
	right: 50%;
}

.modrinth-date-picker :deep(.flatpickr-day.endRange:nth-child(7n + 1):not(.startRange)::before) {
	@apply rounded-l-xl;
	left: 0;
}

.modrinth-date-picker :deep(.flatpickr-calendar.multiMonth .flatpickr-day.hidden::before) {
	display: none;
}

.modrinth-date-picker
	:deep(.flatpickr-calendar.multiMonth .flatpickr-day.hidden + .flatpickr-day.inRange::before),
.modrinth-date-picker
	:deep(
		.flatpickr-calendar.multiMonth
			.flatpickr-day.hidden
			+ .flatpickr-day.endRange:not(.startRange)::before
	) {
	@apply rounded-l-xl;
	left: 0;
}

.modrinth-date-picker
	:deep(.flatpickr-calendar.multiMonth .flatpickr-day.inRange:has(+ .hidden)::before),
.modrinth-date-picker
	:deep(
		.flatpickr-calendar.multiMonth .flatpickr-day.startRange:not(.endRange):has(+ .hidden)::before
	) {
	@apply rounded-r-xl;
	right: 0;
}

.modrinth-date-picker.can-select-range :deep(.flatpickr-day:not(.flatpickr-disabled):not(.hidden)) {
	cursor: pointer;
}

.modrinth-date-picker.is-selecting-range
	:deep(.flatpickr-day:not(.flatpickr-disabled):not(.hidden)) {
	cursor: grabbing;
}

.modrinth-date-picker.can-drag-range
	:deep(.flatpickr-day.startRange:not(.endRange):not(.flatpickr-disabled)),
.modrinth-date-picker.can-drag-range
	:deep(.flatpickr-day.endRange:not(.startRange):not(.flatpickr-disabled)) {
	cursor: grab;
}

.modrinth-date-picker.is-moving-range-end
	:deep(.flatpickr-day:not(.flatpickr-disabled):not(.hidden)) {
	cursor: grabbing !important;
}

.modrinth-date-picker.is-dragging-range :deep(.flatpickr-day) {
	cursor: grabbing !important;
}

.modrinth-date-picker.is-dragging-range
	:deep(.flatpickr-day:not(.selected):not(.startRange):not(.endRange):not(.inRange):hover),
.modrinth-date-picker.is-dragging-range
	:deep(.flatpickr-day:not(.selected):not(.startRange):not(.endRange):not(.inRange):focus) {
	@apply border-transparent bg-transparent text-primary;
}

.modrinth-date-picker
	:deep(.flatpickr-day.prevMonthDay:not(.inRange):not(.startRange):not(.endRange)),
.modrinth-date-picker
	:deep(.flatpickr-day.nextMonthDay:not(.inRange):not(.startRange):not(.endRange)),
.modrinth-date-picker :deep(.flatpickr-day.flatpickr-disabled) {
	@apply text-secondary opacity-40;
}

.modrinth-date-picker :deep(.flatpickr-time) {
	@apply mt-2 flex h-11 max-h-none items-center  gap-2 border-0 border-t border-solid border-surface-5 px-1 pt-2 overflow-visible leading-none;
}

.modrinth-date-picker :deep(.flatpickr-time .numInputWrapper) {
	@apply h-full flex-1 rounded-xl bg-surface-4;
}

.modrinth-date-picker :deep(.flatpickr-time input),
.modrinth-date-picker :deep(.flatpickr-time .flatpickr-am-pm) {
	@apply h-full touch-manipulation rounded-xl bg-transparent px-2 text-center font-semibold text-primary hover:bg-surface-5 focus:bg-surface-5;
}

.modrinth-date-picker :deep(.flatpickr-time .flatpickr-time-separator) {
	@apply flex h-full items-center justify-center text-secondary text-center;
}

.modrinth-date-picker :deep(.flatpickr-time .flatpickr-am-pm) {
	@apply flex w-14 items-center justify-center bg-surface-4 px-3;
}
</style>
