import { HolderOutlined } from "@ant-design/icons";
import { DndContext } from "@dnd-kit/core";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Button } from "antd";
import { createContext, useContext, useMemo, useRef } from "react";

import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities";
import type { CSSProperties, ComponentProps, Context, FC, HTMLAttributes, PropsWithChildren } from "react";

interface RowsContextProps {
    elementType?: "tr" | "li" | "div";
}

interface RowContextProps {
    setActivatorNodeRef?: (element: HTMLElement | null) => void;
    listeners?: SyntheticListenerMap;
}

interface RowProps extends HTMLAttributes<HTMLTableRowElement | HTMLLIElement | HTMLDivElement>, PropsWithChildren {
    "data-row-key": string;
}

let RowContext: Context<RowContextProps>;
let RowsContext: Context<RowsContextProps>;

const DragHandle: FC = () => {
    const { setActivatorNodeRef, listeners } = useContext(RowContext);
    return (
        <Button
            type="text"
            size="small"
            icon={<HolderOutlined />}
            style={{ cursor: "move" }}
            ref={setActivatorNodeRef}
            {...listeners}
        />
    );
};

const Row: FC<RowProps> = (props) => {
    const { elementType = "tr" } = useContext(RowsContext);
    const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({
        id: props["data-row-key"],
    });

    const style: CSSProperties = {
        ...props.style,
        transform: CSS.Translate.toString(transform),
        transition,
        ...(isDragging ? { position: "relative", zIndex: 9999 } : {}),
    };

    const contextValue = useMemo<RowContextProps>(
        () => ({ setActivatorNodeRef, listeners }),
        [setActivatorNodeRef, listeners],
    );

    const renderedProps = {
        ...props,
        ref: setNodeRef,
        style,
        ...attributes,
    };

    return (
        <RowContext.Provider value={contextValue}>
            {elementType === "li" ? (
                <li {...renderedProps} />
            ) : elementType === "div" ? (
                <div {...renderedProps} />
            ) : (
                <tr {...renderedProps} />
            )}
        </RowContext.Provider>
    );
};

function useDndSortable() {
    if (!RowContext) {
        RowContext = createContext<RowContextProps>({});
        RowsContext = createContext<RowsContextProps>({});
    }

    // We can safely use `useRef` as we are using contexts, to keep `useEffect` from re-rendering the component intact
    return useRef({
        SortableContext: function ({
            children,
            onDragEnd,
            items,
            ...rowsContextProps
        }: PropsWithChildren<
            Pick<ComponentProps<typeof DndContext>, "onDragEnd"> &
                Pick<ComponentProps<typeof SortableContext>, "items"> &
                RowsContextProps
        >) {
            return (
                <RowsContext.Provider value={rowsContextProps}>
                    <DndContext
                        modifiers={[restrictToVerticalAxis]}
                        onDragEnd={onDragEnd}
                        // Avoid validateDOMNesting warning when using the SortableContext inside a table.
                        // See also https://github.com/clauderic/dnd-kit/issues/899#issuecomment-1264700799
                        accessibility={{ container: document.body }}
                    >
                        <SortableContext items={items} strategy={verticalListSortingStrategy}>
                            {children}
                        </SortableContext>
                    </DndContext>
                </RowsContext.Provider>
            );
        },
        SortableRow: Row,
        DragHandle,
    }).current;
}

export { useDndSortable };
