<template>
	<TreeItem ref="el" v-bind="forward" v-slot="{ isExpanded, isSelected, isIndeterminate, handleToggle, handleSelect }">
		<slot
				:is-expanded="isExpanded"
				:is-selected="isSelected"
				:is-indeterminate="isIndeterminate"
				:handle-select="handleSelect"
				:handle-toggle="handleToggle"
				:isDragging="isDragging"
				:isDraggedOver="isDraggedOver"
		/>
		<div v-if="instruction">
			<slot name="hint" :instruction="instruction" />
		</div>
	</TreeItem>
</template>

<script setup lang="ts" generic="T extends Record<string, any>">
import { useForwardPropsEmits, type FlattenedItem, type TreeItemEmits, type TreeItemProps } from 'radix-vue';
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'

const props = defineProps<TreeItemProps<T> & FlattenedItem<T>>();
const emits = defineEmits<TreeItemEmits<T>>();

defineSlots<{
	default: (props: {
		isExpanded: boolean
		isSelected: boolean
		isIndeterminate: boolean | undefined
		isDragging: boolean
		isDraggedOver: boolean
		handleToggle: () => void
		handleSelect: () => void
	}) => any,
	hint: (props: {
		instruction: Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null
	}) => any,
}>()

const forward = useForwardPropsEmits(props, emits);

const element = templateRef('el');
const isDragging = ref(false);
const isDraggedOver = ref(false);
const isInitialExpanded = ref(false);
const instruction = ref<Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null>(null);

const mode = computed(() => {
	if (props.hasChildren)
		return 'expanded'
	if (props.index + 1 === props.parentItem?.children?.length)
		return 'last-in-group'
	return 'standard'
});

watchEffect((onCleanup) => {
	const currentElement = unrefElement(element) as HTMLElement;

	if (!currentElement)
		return

	const item = { ...props.value, level: props.level, id: props._id }

	const expandItem = () => {
		if (!element.value?.isExpanded) {
				element.value?.handleToggle()
		}
	}

	const closeItem = () => {
		if (element.value?.isExpanded) {
				element.value?.handleToggle()
		}
	}

	const dndFunction = combine(
		draggable({
			element: currentElement,
			getInitialData: () => item,
			onDragStart: () => {
				isDragging.value = true
				isInitialExpanded.value = element.value?.isExpanded ?? false
				closeItem()
			},
			onDrop: () => {
				isDragging.value = false
				if (isInitialExpanded.value)
					expandItem()
			},
		}),

		dropTargetForElements({
			element: currentElement,
			getData: ({ input, element }) => {
				const data = { id: item.id }

				return attachInstruction(data, {
					input,
					element,
					indentPerLevel: 16,
					currentLevel: props.level,
					mode: mode.value,
					block: [],
				})
			},
			canDrop: ({ source }) => {
				return source.data.id !== item.id
			},
			onDrag: ({ self }) => {
				instruction.value = extractInstruction(self.data) as typeof instruction.value
			},
			onDragEnter: ({ source }) => {
				if (source.data.id !== item.id) {
					isDraggedOver.value = true
				}
			},
			onDragLeave: () => {
				isDraggedOver.value = false
				instruction.value = null
			},
			onDrop: ({ location }) => {
				isDraggedOver.value = false
				instruction.value = null
			},
			getIsSticky: () => true,
		}),

		monitorForElements({
			canMonitor: ({ source }) => {
				return source.data.id !== item.id
			},
		}),
	)

	// Cleanup dnd function
	onCleanup(() => dndFunction())
})
</script>