import { IMenuItem } from "@ax/types";
import type { UniqueIdentifier } from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable";

const flatten = (items: TreeItem[], parentId: UniqueIdentifier | null = null, depth = 0): FlattenedItem[] => {
	return items.reduce<FlattenedItem[]>((acc, item, index) => {
		return [...acc, { ...item, parentId, depth, index }, ...flatten(item.children || [], item.id, depth + 1)];
	}, []);
};

const flattenTree = (items: TreeItem[]): FlattenedItem[] => {
	return flatten(items);
};

const buildTree = (flattenedItems: FlattenedItem[]): TreeItem[] => {
	const root = { id: 0, children: [] };
	const nodes: Record<string, Partial<TreeItem>> = { [root.id]: root };
	const items = flattenedItems.map((item) => ({ ...item, children: [] })) as FlattenedItem[];

	for (const item of items) {
		const { id, children } = item;
		const parentId = item.parentId ?? root.id;
		const parent = nodes[parentId] ?? findItem(items, parentId);

		nodes[id] = { id, children };
		parent.children && parent.children.push(item);
	}

	return root.children;
};

const findItem = (items: FlattenedItem[], itemId: UniqueIdentifier) => {
	return items.find(({ id }) => id === itemId);
};

function getDragDepth(offset: number, indentationWidth: number) {
	return Math.round(offset / indentationWidth);
}

export function getProjection(
	items: FlattenedItem[],
	activeId: UniqueIdentifier,
	overId: UniqueIdentifier,
	dragOffset: number,
	indentationWidth: number,
) {
	const overItemIndex = items.findIndex(({ id }) => id === overId);
	const activeItemIndex = items.findIndex(({ id }) => id === activeId);
	const activeItem = items[activeItemIndex];
	const newItems = arrayMove(items, activeItemIndex, overItemIndex);
	const previousItem = newItems[overItemIndex - 1];
	const nextItem = newItems[overItemIndex + 1];
	const dragDepth = getDragDepth(dragOffset, indentationWidth);
	const projectedDepth = activeItem.depth + dragDepth;
	const maxDepth = getMaxDepth({
		previousItem,
	});
	const minDepth = getMinDepth({ nextItem });
	let depth = projectedDepth;

	if (projectedDepth >= maxDepth) {
		depth = maxDepth;
	} else if (projectedDepth < minDepth) {
		depth = minDepth;
	}

	return { depth, maxDepth, minDepth, parentId: getParentId() };

	function getParentId() {
		if (depth === 0 || !previousItem) {
			return null;
		}

		if (depth === previousItem.depth) {
			return previousItem.parentId;
		}

		if (depth > previousItem.depth) {
			return previousItem.id;
		}

		const newParent = newItems
			.slice(0, overItemIndex)
			.reverse()
			.find((item) => item.depth === depth)?.parentId;

		return newParent ?? null;
	}
}

function getMinDepth({ nextItem }: { nextItem: FlattenedItem }) {
	if (nextItem) {
		return nextItem.depth;
	}

	return 0;
}

function getMaxDepth({ previousItem }: { previousItem: FlattenedItem }) {
	if (previousItem) {
		return previousItem.depth + 1;
	}

	return 0;
}

const removeChildrenOf = (items: FlattenedItem[], ids: UniqueIdentifier[]) => {
	const excludeParentIds = [...ids];

	return items.filter((item) => {
		if (item.parentId && excludeParentIds.includes(item.parentId)) {
			if (item.children && item.children.length) {
				excludeParentIds.push(item.id);
			}
			return false;
		}

		return true;
	});
};

export function setProperty<T extends keyof TreeItem>(
	items: TreeItem[],
	id: UniqueIdentifier,
	property: T,
	setter: (value: TreeItem[T]) => TreeItem[T],
) {
	for (const item of items) {
		if (item.id === id) {
			item[property] = setter(item[property]);
			continue;
		}

		if (item.children && item.children.length) {
			item.children = setProperty(item.children || [], id, property, setter);
		}
	}

	return [...items];
}

export interface FlattenedItem extends IMenuItem {
	parentId: UniqueIdentifier | null;
	depth: number;
	index: number;
}

export type TreeItem = IMenuItem;

export { flattenTree, buildTree, removeChildrenOf };
