import React, { useMemo, useState } from "react";
import { connect } from "react-redux";
import {
	closestCenter,
	DndContext,
	DragEndEvent,
	DragMoveEvent,
	DragOverEvent,
	DragStartEvent,
	KeyboardSensor,
	MeasuringStrategy,
	PointerSensor,
	UniqueIdentifier,
	useSensor,
	useSensors,
} from "@dnd-kit/core";
import {
	arrayMove,
	SortableContext,
	sortableKeyboardCoordinates,
	verticalListSortingStrategy,
} from "@dnd-kit/sortable";

import { menuActions } from "@ax/containers/Navigation";
import { appActions } from "@ax/containers/App";
import { Loading, ErrorToast } from "@ax/components";
import { RouteLeavingGuard } from "@ax/guards";
import { IMenuItem, IRootState } from "@ax/types";
import { buildTree, FlattenedItem, flattenTree, getProjection, removeChildrenOf, setProperty } from "./helpers";

import Header from "./Header";
import Item from "./Item";
import * as S from "./style";

const ItemList = (props: IItemList): JSX.Element => {
	const { menu, reorderMenu } = props;

	const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
	const [overId, setOverId] = useState<UniqueIdentifier | null>(null);
	const [offsetLeft, setOffsetLeft] = useState(0);
	const indentationWidth = 35;

	const flattenedItems = useMemo(() => {
		const flattenedTree = flattenTree(menu);
		const collapsedItems = flattenedTree.reduce<number[]>(
			(acc, { children, collapsed, id }) => (collapsed && children.length ? [...acc, id] : acc),
			[],
		);

		return removeChildrenOf(flattenedTree, activeId != null ? [activeId, ...collapsedItems] : collapsedItems);
	}, [activeId, menu]);

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

	const sensors = useSensors(
		useSensor(PointerSensor, {
			activationConstraint: {
				distance: 5,
			},
		}),
		useSensor(KeyboardSensor, {
			coordinateGetter: sortableKeyboardCoordinates,
		}),
	);

	const projected =
		activeId && overId ? getProjection(flattenedItems, activeId, overId, offsetLeft, indentationWidth) : null;

	const itemIds = useMemo(() => flattenedItems.map(({ id }) => id), [flattenedItems]);

	const handleDragStart = ({ active: { id: activeId } }: DragStartEvent) => {
		setActiveId(activeId);
		setOverId(activeId);
	};

	const handleDragMove = ({ delta }: DragMoveEvent) => {
		setOffsetLeft(delta.x);
	};

	const handleDragOver = ({ over }: DragOverEvent) => {
		setOverId(over?.id ?? null);
	};

	const handleDragEnd = async ({ active, over }: DragEndEvent) => {
		resetState();

		if (projected && over) {
			const { depth, parentId } = projected;
			const clonedItems: FlattenedItem[] = JSON.parse(JSON.stringify(flattenTree(menu)));
			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);

			reorderMenu(newItems);
		}
	};

	const handleDragCancel = () => resetState();

	const resetState = () => {
		setOverId(null);
		setActiveId(null);
		setOffsetLeft(0);
	};

	const handleCollapse = (id: UniqueIdentifier) => {
		const newMenu = setProperty(menu, id, "collapsed", (value) => {
			return !value;
		});
		reorderMenu(newMenu);
	};

	return (
		<S.Droppable>
			<DndContext
				sensors={sensors}
				collisionDetection={closestCenter}
				measuring={measuring}
				onDragStart={handleDragStart}
				onDragMove={handleDragMove}
				onDragOver={handleDragOver}
				onDragEnd={handleDragEnd}
				onDragCancel={handleDragCancel}
			>
				<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
					{flattenedItems.map((item) => {
						const { id, collapsed, depth, children } = item;
						return (
							<Item
								key={id}
								item={item}
								depth={id === activeId && projected ? projected.depth : depth}
								indentationWidth={indentationWidth}
								collapsed={Boolean(collapsed && children.length)}
								onCollapse={children && children.length ? () => handleCollapse(id) : undefined}
							/>
						);
					})}
				</SortableContext>
			</DndContext>
		</S.Droppable>
	);
};

const Table = (props: ITable): JSX.Element => {
	const { editorMenu, isLoading, setHistoryPush, isDirty, reorderMenu } = props;

	const action = (path: string) => setHistoryPush(path);
	const text = (
		<>
			Some changes <strong>are not saved</strong>.
		</>
	);
	const allowedRoutes = ["/sites/navigations/menus"];

	if (isLoading) {
		return (
			<S.LoadingWrapper>
				<Loading />
			</S.LoadingWrapper>
		);
	}

	return (
		<S.TableWrapper>
			<RouteLeavingGuard when={isDirty} action={action} text={text} allowedRoutes={allowedRoutes} />
			<ErrorToast />
			<S.Table>
				<Header />
				{editorMenu && <ItemList menu={editorMenu} reorderMenu={reorderMenu} />}
			</S.Table>
		</S.TableWrapper>
	);
};

const mapDispatchToProps = {
	setHistoryPush: appActions.setHistoryPush,
	reorderMenu: menuActions.reorderMenu,
};

const mapStateToProps = (state: IRootState) => ({
	isLoading: state.app.isLoading,
	editorMenu: state.menu.editorMenu?.elements || [],
});

interface ITableStateProps {
	isLoading: boolean;
	editorMenu: IMenuItem[] | undefined;
	isDirty: boolean;
}

interface ITableDispatchProps {
	setHistoryPush(path: string): any;
	reorderMenu(elements: IMenuItem[]): void;
}

interface IItemList {
	menu: IMenuItem[];
	reorderMenu(elements: IMenuItem[]): void;
}

type ITable = ITableStateProps & ITableDispatchProps;

export default connect(mapStateToProps, mapDispatchToProps)(Table);
