<script setup lang="ts">
	import useCustomizableOptions, { type CustomizableOptions } from '@/composables/useCustomizableOptions'
	import { mdiClose } from '@mdi/js'
	import { nextTick, ref, toRef, useId, watch } from 'vue'
	import type { VBtn, VDialog } from 'vuetify/components'
	import { useDisplay } from 'vuetify/lib/framework.mjs'
	import SyIcon from '../Customs/SyIcon/SyIcon.vue'
	import { config } from './config'
	import { locales } from './locales'
	import { useDraggable } from './useDraggable'
	import SyHeading from '@/components/SyHeading/SyHeading.vue'

	const props = withDefaults(defineProps<{
		title?: string
		width?: string
		cancelBtnText?: string
		confirmBtnText?: string
		hideActions?: boolean
		persistent?: boolean
		autofocusValidateBtn?: boolean
		draggable?: boolean
		headingLevel?: 1 | 2 | 3 | 4 | 5 | 6
		scrollable?: boolean
	} & CustomizableOptions>(), {
		title: undefined,
		width: '800px',
		cancelBtnText: locales.cancelBtn,
		confirmBtnText: locales.confirmBtn,
		hideActions: false,
		persistent: false,
		autofocusValidateBtn: false,
		headingLevel: 2,
		scrollable: false,
	})

	defineEmits(['cancel', 'confirm', 'update:modelValue'])
	defineSlots<{
		default?: () => undefined
		title?: (props: { id: string }) => undefined
		actions?: () => undefined
	}>()

	const dialog = defineModel<boolean>({
		default: false,
	})

	const confirmBtn = ref<VBtn | null>(null)
	// Restore the focus to the last active element when the dialog is closed
	let activeElement: HTMLElement | null = null
	watch(dialog, (newValue) => {
		if (newValue) {
			activeElement = document.activeElement as HTMLElement
			if (props.autofocusValidateBtn) {
				nextTick(() => {
					confirmBtn.value?.$el.focus()
				})
			}
			else if (props.draggable) {
				nextTick(() => {
					dialogContent.value?.$el.querySelector('.sy-dialog-box-title')?.focus()
				})
			}
		}
		else if (activeElement) {
			activeElement.focus()
		}
	}, { immediate: true })

	const id = `dialog-${useId()}`
	const dialogContent = ref<VDialog | undefined>(undefined)

	const options = useCustomizableOptions(config, props)

	const closeIcon = mdiClose

	async function getSelectableElements(): Promise<HTMLElement[]> {
		const parentNode = dialogContent?.value?.$el // Is undefined when dialog is closed

		if (!parentNode) {
			return []
		}

		const elements = Array.from<HTMLElement>(parentNode.querySelectorAll(
			'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
		))

		return elements.filter(element => (
			!element.hasAttribute('disabled')
			&& !element.getAttribute('aria-hidden')
		))
	}

	async function handleFocus(e: KeyboardEvent): Promise<void> {
		const selectableElements = await getSelectableElements()

		const focused = selectableElements.findIndex(
			(el: HTMLElement) => el === e.target,
		)

		const isOutside = focused === -1
		const lastElement = selectableElements.length - 1

		if (!e.shiftKey && (isOutside || focused === lastElement)) {
			e.preventDefault()
			selectableElements[0]?.focus()
		}
		else if (e.shiftKey && (isOutside || focused === 0)) {
			e.preventDefault()
			selectableElements[lastElement]?.focus()
		}
	}

	const display = useDisplay()

	const {
		isGrabbing,
		startDragging,
		moveToLeft,
		moveToRight,
		moveToBottom,
		moveToTop,
	} = useDraggable(toRef(props, 'draggable'), dialogContent)
</script>

<template>
	<VDialog
		v-model="dialog"
		:scrim="props.draggable ? 'transparent' : true"
		v-bind="$attrs"
		:width="props.width"
		:persistent="props.persistent"
		:retain-focus="false"
		aria-modal="true"
		class="sy-dialog-box"
		:aria-labelledby="id"
		@keydown.tab="handleFocus"
	>
		<VCard
			v-bind="options.card"
			ref="dialogContent"
			:class="{
				'sy-dialog-box-draggable': props.draggable,
				'sy-dialog-box-draggable--active': props.draggable && isGrabbing,
				'sy-dialog-box--scrollable': props.scrollable,
			}"
			:aria-label="props.draggable ? locales.draggable : undefined"
		>
			<VCardTitle
				class="sy-dialog-box-title"
				v-bind="options.cardTitle"
				:title="props.draggable ? locales.dragInstruction : undefined"
				:aria-label="props.draggable ? locales.dragInstructionLabel : undefined"
				:tabindex="props.draggable ? 0 : undefined"
				@mousedown="startDragging"
				@keydown.left.self="moveToLeft"
				@keydown.right.self="moveToRight"
				@keydown.up.self="moveToTop"
				@keydown.down.self="moveToBottom"
			>
				<slot
					:id="id"
					name="title"
				>
					<SyHeading
						v-if="title"
						:id="id"
						:class="headingLevel === 2 ? 'text-h4 mr-6 font-weight-bold' : 'mr-6 font-weight-bold'"
						:level="headingLevel"
					>
						{{ props.title }}
					</SyHeading>
				</slot>

				<VSpacer v-bind="options.spacer" />

				<VBtn
					v-if="!props.persistent"
					class="sy-dialog-box-close-btn"
					v-bind="options.closeBtn"
					:aria-label="locales.closeBtn"
					@click.stop="dialog = false"
				>
					<SyIcon
						:icon="closeIcon"
						:decorative="true"
						v-bind="options.icon"
					/>
				</VBtn>
			</VCardTitle>

			<div
				class="px-6"
				:class="{ 'sy-dialog-box-content--scrollable': props.scrollable }"
				:tabindex="props.scrollable ? 0 : undefined"
				:role="props.scrollable ? 'region' : undefined"
				:aria-label="props.scrollable ? (props.title || locales.scrollableContent) : undefined"
				v-bind="options.cardText"
			>
				<slot />
			</div>

			<div
				v-if="!props.hideActions"
				v-bind="options.cardActions"
				:class="options.actionsSlot?.class"
				class="sy-dialog-box-actions-ctn"
			>
				<VSpacer v-bind="options.actionsSpacer" />

				<slot name="actions">
					<template v-if="display.xs.value">
						<VBtn
							v-bind="options.confirmBtn"
							data-test-id="confirm-btn"
							@click="$emit('confirm')"
						>
							{{ props.confirmBtnText }}
						</VBtn>

						<VBtn
							v-bind="options.cancelBtn"
							@click="$emit('cancel')"
						>
							{{ props.cancelBtnText }}
						</VBtn>
					</template>
					<template v-else>
						<VBtn
							class="sy-dialog-box-cancel-btn"
							v-bind="options.cancelBtn"
							@click="$emit('cancel')"
						>
							{{ props.cancelBtnText }}
						</VBtn>

						<VBtn
							v-bind="options.confirmBtn"
							ref="confirmBtn"
							class="sy-dialog-box-confirm-btn"
							:class="props.autofocusValidateBtn ? 'sy-dialog-box-confirm-btn--autofocus' : ''"
							data-test-id="confirm-btn"
							@click="$emit('confirm')"
						>
							{{ props.confirmBtnText }}
						</VBtn>
					</template>
				</slot>
			</div>
		</VCard>
	</VDialog>
</template>

<style lang="scss" scoped>
.sy-dialog-box-actions :deep(.v-btn) {
	box-shadow: none;
}

.sy-dialog-box-title {
	line-height: normal;
	flex-shrink: 0;
}

.v-btn--icon {
	color: rgb(var(--v-theme-primary));
	position: absolute;
	right: 24px;
}

h2 {
	word-break: break-word;
	text-wrap: balance;
}

.sy-dialog-box-close-btn:focus-visible,
.sy-dialog-box-cancel-btn:focus-visible,
.sy-dialog-box-confirm-btn:focus-visible,
.sy-dialog-box-confirm-btn--autofocus:focus {
	:deep(.v-btn__overlay) {
		display: none;
	}

	&::after {
		opacity: 1;
		border: transparent;
		outline: 2px solid rgb(var(--v-theme-primary));
		outline-offset: 2px;
	}
}

.sy-dialog-box-actions-ctn {
	display: flex;
	flex-direction: column;
	justify-content: stretch;
	gap: 16px;
	flex-shrink: 0;
}

.sy-dialog-box-draggable .sy-dialog-box-title {
	cursor: grab;

	&:focus-visible {
		outline: 2px solid rgb(var(--v-theme-primary));
		outline-offset: -2px;
		border-radius: 3px;
	}
}

.sy-dialog-box-draggable--active .sy-dialog-box-title {
	cursor: grabbing;
	user-select: none;
}

.sy-dialog-box--scrollable {
	display: flex;
	flex-direction: column;
	max-height: calc(100vh - 48px);
}

.sy-dialog-box-content--scrollable {
	overflow-y: auto;
	min-height: 0;
}

@media screen and (width >= 600px) {
	.sy-dialog-box-actions-ctn {
		flex-direction: row;
	}
}
</style>
