/**
 * WordPress dependencies
 */
import { useId, useRef, useState } from '@safe-wordpress/element';

/**
 * External dependencies
 */
import clsx from 'clsx';
import { isEqual } from 'lodash';
import { useDrag, useDrop, type XYCoord } from 'react-dnd';

/**
 * Internal dependencies
 */
import './style.scss';

export type SortableListProps< T extends { readonly id: string | number } > = {
	readonly className?: string;
	readonly disabled: boolean;
	readonly items: ReadonlyArray< T >;
	readonly render: ( props: {
		readonly item: NoInfer< T >;
		readonly index: number;
	} ) => JSX.Element;
	readonly onSort: ( items: ReadonlyArray< T[ 'id' ] > ) => void;
};

export function SortableList< T extends { readonly id: string | number } >( {
	className,
	disabled,
	items,
	render: Item,
	onSort,
}: SortableListProps< T > ): JSX.Element {
	const dndType = useId();
	const [ draggingItems, setDraggingItems ] = useState< typeof items >( [] );

	const onDragStart = () => {
		setDraggingItems( items );
	};

	const onDragEnd = () => {
		if (
			! isEqual( items, draggingItems ) &&
			items.length === draggingItems.length
		) {
			onSort( draggingItems.map( ( di ) => di.id ) );
		}
		setDraggingItems( [] );
	};

	const onMove = ( dragIndex: number, hoverIndex: number ) => {
		if ( ! draggingItems[ dragIndex ] ) {
			return;
		}
		const aux = [ ...draggingItems ];
		aux.splice( dragIndex, 1 );
		aux.splice( hoverIndex, 0, draggingItems[ dragIndex ] );
		setDraggingItems( aux );
	};

	const currentItems = draggingItems.length ? draggingItems : items;
	return (
		<div className={ className }>
			{ currentItems.map( ( item, index ) =>
				disabled ? (
					<Item key={ item.id } item={ item } index={ index } />
				) : (
					<ItemWrapper
						key={ item.id }
						dndType={ dndType }
						item={ item }
						index={ index }
						onDragStart={ onDragStart }
						onDragEnd={ onDragEnd }
						onMove={ onMove }
					>
						<Item item={ item } index={ index } />
					</ItemWrapper>
				)
			) }
		</div>
	);
}

function ItemWrapper< T extends { readonly id: string | number } >( {
	dndType,
	children,
	item,
	index,
	onDragStart,
	onDragEnd,
	onMove,
}: {
	readonly dndType: string;
	readonly children: JSX.Element;
	readonly item: T;
	readonly index: number;
	readonly onDragStart: () => void;
	readonly onDragEnd: () => void;
	readonly onMove: ( dragIndex: number, hoverIndex: number ) => void;
} ): JSX.Element | null {
	const ref = useRef< HTMLDivElement >( null );

	const [ _, dropReference ] = useDrop< {
		id: string | number;
		index: number;
	} >( {
		accept: dndType,
		hover: ( dragItem, monitor ) => {
			if ( ! ref.current ) {
				return;
			}
			const dragIndex = dragItem.index;
			const hoverIndex = index;

			// Don't replace items with themselves
			if ( dragIndex === hoverIndex ) {
				return;
			}

			// Determine rectangle on screen
			const hoverBoundingRect = ref.current?.getBoundingClientRect();

			// Get vertical middle
			const hoverMiddleY =
				( hoverBoundingRect.bottom - hoverBoundingRect.top ) / 2;

			// Determine mouse position
			const clientOffset = monitor.getClientOffset();

			// Get pixels to the top
			const hoverClientY =
				( clientOffset as XYCoord ).y - hoverBoundingRect.top;

			// Only perform the move when the mouse has crossed half of the items height
			// When dragging downwards, only move when the cursor is below 50%
			// When dragging upwards, only move when the cursor is above 50%

			// Dragging downwards
			if ( dragIndex < hoverIndex && hoverClientY < hoverMiddleY ) {
				return;
			}

			// Dragging upwards
			if ( dragIndex > hoverIndex && hoverClientY > hoverMiddleY ) {
				return;
			}

			// Time to actually perform the action
			onMove( dragIndex, hoverIndex );

			// Note: we're mutating the monitor item here!
			// Generally it's better to avoid mutations,
			// but it's good here for the sake of performance
			// to avoid expensive index searches.
			dragItem.index = hoverIndex;
		},
	} );

	const [ { isDragging }, dragReference ] = useDrag( {
		type: dndType,
		item: () => {
			onDragStart();
			return { id: item.id, index };
		},
		end: onDragEnd,
		collect: ( monitor ) => ( {
			isDragging: monitor.isDragging(),
		} ),
	} );

	dropReference( dragReference( ref ) );

	return (
		<div
			className={ clsx( 'nelio-ab-testing-sortable-list__item', {
				'nelio-ab-testing-sortable-list__item--is-dragging': isDragging,
			} ) }
			ref={ ref }
		>
			{ children }
		</div>
	);
}
