import React, { useEffect, useMemo, useRef, useState } from 'react';

import { createPortal } from 'react-dom';

import {
	Announcements,
	DndContext,
	DragEndEvent,
	DragMoveEvent,
	DragOverEvent,
	DragOverlay,
	DragStartEvent,
	DropAnimation,
	KeyboardSensor,
	MeasuringStrategy,
	Modifier,
	PointerSensor,
	closestCenter,
	defaultDropAnimation,
	useSensor,
	useSensors,
} from '@dnd-kit/core';
import {
	SortableContext,
	arrayMove,
	verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

import { BuilderModule } from '../../../@types/settings';

import { SortableTreeItem } from './components/index.js';
import { sortableTreeKeyboardCoordinates } from './keyboardCoordinates.js';
import type { FlattenedItem, SensorContext, TreeItems } from './types.js';
import {
	buildTree,
	flattenTree,
	getChildCount,
	getProjection,
	removeChildrenOf,
	removeItem,
	setProperty,
} from './utilities.js';

const measuring = {
	droppable: {
		strategy: MeasuringStrategy.Always,
	},
};

const dropAnimationConfig: DropAnimation = {
	keyframes({ transform }) {
		return [
			{ opacity: 1, transform: CSS.Transform.toString(transform.initial) },
			{
				opacity: 0,
				transform: CSS.Transform.toString({
					...transform.final,
					x: transform.final.x + 5,
					y: transform.final.y + 5,
				}),
			},
		];
	},
	easing: 'ease-out',
	sideEffects({ active }) {
		active.node.animate([{ opacity: 0 }, { opacity: 1 }], {
			duration: defaultDropAnimation.duration,
			easing: defaultDropAnimation.easing,
		});
	},
};

interface Props {
	collapsible?: boolean;
	treeItems?: TreeItems;
	currItemId?: string;
	indentationWidth?: number;
	indicator?: boolean;
	removable?: boolean;
	onSelect?: (
		id: string,
		e?: React.MouseEvent<HTMLSpanElement, MouseEvent>
	) => void;
	onDelete?: (
		id: string,
		e?: React.MouseEvent<HTMLDivElement, MouseEvent>
	) => void;
	reorderModules?: (items: FlattenedItem[]) => void;
}

function mapTreeItemsToItems(item: BuilderModule) {
	return {
		id: item.id,
		module: {
			name: item.module.name,
			options: item.module.options,
		},
		props: {
			...item.props,
			children: item.props.children
				? item.props.children.map(mapTreeItemsToItems)
				: [],
		},
	} as BuilderModule;
}

export function SortableTree({
	collapsible,
	treeItems = [],
	currItemId = '',
	indicator = false,
	indentationWidth = 24,
	removable,
	onSelect,
	onDelete,
	reorderModules,
}: Props) {
	const [items, setItems] = useState(treeItems);
	const [collapsedItemIds, setCollapsedItemIds] = useState<Set<string>>(
		new Set()
	);
	const [activeId, setActiveId] = useState<string | null>(null);
	const [overId, setOverId] = useState<string | null>(null);
	const [offsetLeft, setOffsetLeft] = useState(0);
	const [currentPosition, setCurrentPosition] = useState<{
		parentId: string | null;
		overId: string;
	} | null>(null);

	useEffect(() => {
		const newItems: BuilderModule[] = treeItems.map((item) =>
			mapTreeItemsToItems(item)
		);

		setItems(newItems);
	}, [treeItems]);

	const flattenedItems = useMemo(() => {
		const flattenedTree = flattenTree(items);
		const collapsedItems = flattenedTree.reduce<string[]>(
			(acc, { props: { children }, id }) =>
				collapsedItemIds.has(id) && children && children.length
					? [...acc, id]
					: acc,
			[]
		);

		return removeChildrenOf(
			flattenedTree,
			activeId ? [activeId, ...collapsedItems] : collapsedItems
		);
	}, [activeId, items, collapsedItemIds]);
	const projected =
		activeId && overId
			? getProjection(
					flattenedItems,
					activeId,
					overId,
					offsetLeft,
					indentationWidth
			  )
			: null;
	const sensorContext: SensorContext = useRef({
		items: flattenedItems,
		offset: offsetLeft,
	});
	const [coordinateGetter] = useState(() =>
		sortableTreeKeyboardCoordinates(sensorContext, indicator, indentationWidth)
	);
	const sensors = useSensors(
		useSensor(PointerSensor),
		useSensor(KeyboardSensor, {
			coordinateGetter,
		})
	);

	const sortedIds = useMemo(
		() => flattenedItems.map(({ id }) => id),
		[flattenedItems]
	);
	const activeItem = activeId
		? flattenedItems.find(({ id }) => id === activeId)
		: null;

	useEffect(() => {
		sensorContext.current = {
			items: flattenedItems,
			offset: offsetLeft,
		};
	}, [flattenedItems, offsetLeft]);

	const announcements: Announcements = {
		onDragStart({ active }) {
			return `Picked up ${active.id}.`;
		},
		onDragMove({ active, over }) {
			return getMovementAnnouncement(
				'onDragMove',
				active.id as string,
				over?.id as string
			);
		},
		onDragOver({ active, over }) {
			return getMovementAnnouncement(
				'onDragOver',
				active.id as string,
				over?.id as string
			);
		},
		onDragEnd({ active, over }) {
			return getMovementAnnouncement(
				'onDragEnd',
				active.id as string,
				over?.id as string
			);
		},
		onDragCancel({ active }) {
			return `Moving was cancelled. ${active.id} was dropped in its original position.`;
		},
	};

	return (
		<DndContext
			accessibility={{ announcements }}
			sensors={sensors}
			collisionDetection={closestCenter}
			measuring={measuring}
			onDragStart={handleDragStart}
			onDragMove={handleDragMove}
			onDragOver={handleDragOver}
			onDragEnd={handleDragEnd}
			onDragCancel={handleDragCancel}
		>
			<SortableContext
				items={sortedIds}
				strategy={verticalListSortingStrategy}
			>
				{flattenedItems.map(
					({ id, props: { children }, depth, module: { name } }, index) => (
						<SortableTreeItem
							key={id}
							id={id}
							index={index}
							value={name}
							depth={id === activeId && projected ? projected.depth : depth}
							indentationWidth={indentationWidth}
							indicator={indicator}
							highlighted={id === currItemId}
							collapsed={Boolean(
								collapsedItemIds.has(id) && children && children.length
							)}
							onClickName={onSelect}
							onCollapse={
								collapsible && children && children.length
									? () => handleCollapse(id)
									: undefined
							}
							onRemove={removable ? () => handleRemove(id) : undefined}
						/>
					)
				)}
				{createPortal(
					<DragOverlay
						dropAnimation={dropAnimationConfig}
						zIndex={9999}
						modifiers={indicator ? [adjustTranslate] : undefined}
					>
						{activeId && activeItem ? (
							<SortableTreeItem
								id={activeId}
								depth={activeItem.depth}
								clone
								childCount={getChildCount(items, activeId) + 1}
								value={
									flattenedItems.find(({ id }) => id === activeId)?.module
										.name ?? ''
								}
								indentationWidth={indentationWidth}
							/>
						) : null}
					</DragOverlay>,
					document.body
				)}
			</SortableContext>
		</DndContext>
	);

	function handleDragStart({ active: { id: activeId } }: DragStartEvent) {
		setActiveId(activeId as string);
		setOverId(activeId as string);

		const activeItem = flattenedItems.find(({ id }) => id === activeId);

		if (activeItem) {
			setCurrentPosition({
				parentId: activeItem.parentId,
				overId: activeId as string,
			});
		}

		document.body.style.setProperty('cursor', 'grabbing');
	}

	function handleDragMove({ delta }: DragMoveEvent) {
		setOffsetLeft(delta.x);
	}

	function handleDragOver({ over }: DragOverEvent) {
		setOverId((over?.id as string) ?? null);
	}

	function handleDragEnd({ active, over }: DragEndEvent) {
		resetState();

		if (projected && over) {
			const { depth, parentId } = projected;
			const clonedItems: FlattenedItem[] = JSON.parse(
				JSON.stringify(flattenTree(items))
			);
			const overIndex = clonedItems.findIndex(({ id }) => id === over.id);
			const activeIndex = clonedItems.findIndex(({ id }) => id === active.id);
			const activeTreeItem = clonedItems[activeIndex];

			clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId };

			const sortedItems = arrayMove(clonedItems, activeIndex, overIndex);
			const newItems = buildTree(sortedItems);

			// console.log({ sortedItems, newItems });

			if (reorderModules) {
				reorderModules(sortedItems);
				return;
			}

			setItems(newItems);
		}
	}

	function handleDragCancel() {
		resetState();
	}

	function resetState() {
		setOverId(null);
		setActiveId(null);
		setOffsetLeft(0);
		setCurrentPosition(null);

		document.body.style.setProperty('cursor', '');
	}

	function handleRemove(id: string) {
		if (onDelete) {
			onDelete(id);
			return;
		}

		setItems((items) => removeItem(items, id));
	}

	function handleCollapse(id: string) {
		const newSet = new Set(collapsedItemIds);
		if (collapsedItemIds.has(id)) {
			console.log('here', id);
			newSet.delete(id);
		} else {
			console.log('there', id);
			newSet.add(id);
		}
		setCollapsedItemIds(newSet);
		// setItems((items) =>
		// 	setProperty(items, id, 'collapsed', (value) => {
		// 		return !value;
		// 	})
		// );
	}

	function getMovementAnnouncement(
		eventName: string,
		activeId: string,
		overId?: string
	) {
		if (overId && projected) {
			if (eventName !== 'onDragEnd') {
				if (
					currentPosition &&
					projected.parentId === currentPosition.parentId &&
					overId === currentPosition.overId
				) {
					return;
				} else {
					// console.log('setCurrentPosition', projected.parentId, overId);
					setCurrentPosition({
						parentId: projected.parentId,
						overId,
					});
				}
			}

			const clonedItems: FlattenedItem[] = JSON.parse(
				JSON.stringify(flattenTree(items))
			);
			const overIndex = clonedItems.findIndex(({ id }) => id === overId);
			const activeIndex = clonedItems.findIndex(({ id }) => id === activeId);
			const sortedItems = arrayMove(clonedItems, activeIndex, overIndex);

			const previousItem = sortedItems[overIndex - 1];

			let announcement;
			const movedVerb = eventName === 'onDragEnd' ? 'dropped' : 'moved';
			const nestedVerb = eventName === 'onDragEnd' ? 'dropped' : 'nested';

			if (!previousItem) {
				const nextItem = sortedItems[overIndex + 1];
				announcement = `${activeId} was ${movedVerb} before ${nextItem.id}.`;
			} else {
				if (projected.depth > previousItem.depth) {
					announcement = `${activeId} was ${nestedVerb} under ${previousItem.id}.`;
				} else {
					let previousSibling: FlattenedItem | undefined = previousItem;
					while (previousSibling && projected.depth < previousSibling.depth) {
						const parentId: string | null = previousSibling.parentId;
						previousSibling = sortedItems.find(({ id }) => id === parentId);
					}

					if (previousSibling) {
						announcement = `${activeId} was ${movedVerb} after ${previousSibling.id}.`;
					}
				}
			}

			// console.log(announcement);
			return announcement;
		}

		return;
	}
}

const adjustTranslate: Modifier = ({ transform }) => {
	return {
		...transform,
		y: transform.y - 25,
	};
};
