import produce from "immer";
import Sortable from "sortablejs";

import { addClass, detectIE, getTreeItemById, removeClass } from "./index.js";
import { toId } from "../components/TreeNode.js";

import type { TreeNodeId, TreeNodeProps } from "../components/TreeNode.js";
import type { Tree, TreeProps } from "../index.js";

const IS_IE = detectIE();

const CLASS_GETS_EMPTY = "aiot-sortable-gets-empty";

export interface onSortParameters {
    evt: Sortable.SortableEvent;
    from: HTMLElement;
    to: HTMLElement;
    oldIndex: number;
    newIndex: number;
    id: TreeNodeId;
    nextObj: Element;
    prevObj: Element;
    nextId: TreeNodeId;
    prevId: TreeNodeId;
    parentFromId: TreeNodeId;
    parentToId: TreeNodeId;
    /**
     * You can call this function to get the new tree property for this Tree, you can use it for your <code>setState({ tree: buildTree })</code>.
     */
    buildTree: () => TreeNodeProps[];
}

export function handleSortableTreeDidUpdate(this: Tree, prevProps: TreeProps) {
    const { sortableDelay, isSortableDisabled, thresholdPx } = this.props;
    const sortables = (this._sortables = this._sortables || {});
    let update = sortableDelay !== prevProps.sortableDelay;
    if (!update && isSortableDisabled !== prevProps.isSortableDisabled) {
        update = true;
    }
    if (!update && thresholdPx !== prevProps.thresholdPx) {
        update = true;
    }

    // Update sortablejs
    update &&
        Object.values(sortables).forEach((sortable) => {
            sortable.option("delay", sortableDelay);
            sortable.option("disabled", isSortableDisabled);
            sortable.option("touchStartThreshold", thresholdPx);
            sortable.option("fallbackTolerance", thresholdPx);
        });
}

async function handleSortableTreeEnd(this: Tree, evt: Sortable.SortableEvent) {
    const { from, to, oldIndex, newIndex, item } = evt;
    const parentFromId = toId((from.attributes as any)["data-childs-for"].value);
    const parentToId = toId((to.attributes as any)["data-childs-for"].value);
    removeClass(from, CLASS_GETS_EMPTY);

    // Skip no updates
    if (parentFromId === parentToId && oldIndex === newIndex) {
        removeClass(document.body, "aiot-currently-sorting");
        return;
    }

    // Collect data
    const id = toId((item.attributes as any)["data-li-id"].value);
    const nextSibling = item.nextElementSibling;
    const nextObj = nextSibling && (nextSibling.attributes as any)["data-li-id"] ? nextSibling : undefined;
    const prevObj = item.previousElementSibling;
    const nextId = nextObj && toId((nextObj.attributes as any)["data-li-id"].value);
    const prevId = prevObj && toId((prevObj.attributes as any)["data-li-id"].value);
    const { onSort, onSortEnd } = this.props;
    onSortEnd && onSortEnd(evt);

    //this.setState({ sortingBusy: true }); Should be set by onSort method
    if (!onSort) {
        throw new Error("You have to define a onSort function to enable sorting functionality.");
    }

    // Function for getting the new tree prop with modified order
    const buildTree = () =>
        produce(this.props.tree, (draft) => {
            // Find parent trees with children
            const { rootId } = this.props;
            let parentFromTree: any;
            let parentToTree: any;
            if (parentFromId === rootId) {
                parentFromTree = draft;
            } else {
                parentFromTree = getTreeItemById(parentFromId, draft as any);
                parentFromTree = parentFromTree.childNodes = parentFromTree.childNodes || [];
            }

            if (parentToId === rootId) {
                parentToTree = draft;
            } else {
                parentToTree = getTreeItemById(parentToId, draft as any);
                parentToTree = parentToTree.childNodes = parentToTree.childNodes || [];
            }

            // Modify tree
            const treeItem = parentFromTree[oldIndex];
            parentFromTree.splice(oldIndex, 1);
            parentToTree.splice(newIndex, 0, treeItem);
        });

    // React does not allow to modify DOM nodes, so revert it back...
    if (from !== to) {
        from.appendChild(item);
        item.style.display = "none";
        Object.values(this._sortables).forEach(
            (sortable: any) => sortable._currentOrder && sortable.sort(sortable._currentOrder),
        );
    }

    try {
        await onSort({
            evt,
            from,
            to,
            oldIndex,
            newIndex, // Event + calculated attributes
            id,
            nextObj,
            prevObj,
            nextId,
            prevId,
            parentFromId,
            parentToId,
            buildTree,
        });
    } catch (e) {
        // Silence is golden. You have to implement error handling in onSort
    } finally {
        removeClass(document.body, "aiot-currently-sorting");
        //this.setState({ sortingBusy: false });
    }
}

export function handleSortableTree(this: Tree, ul: HTMLUListElement | "destroy", id: TreeNodeId = 0) {
    if (!this.props.isSortable) {
        return;
    }

    const sortables = (this._sortables = this._sortables || {});

    if ("destroy" === ul) {
        // Destroy all sortable instances
        Object.values(sortables).forEach((sortable) => sortable.destroy());
    } else if (ul) {
        //console.log("create", ul, id, IS_IE);
        const { sortableDelay, isSortableDisabled, onSortStart, onSortMove, forceSortableFallback, thresholdPx } =
            this.props;

        // Create sortable
        sortables[id] = Sortable.create(ul, {
            group: this.id("handleSortableTree"),
            sort: ul.className.indexOf("aiot-sortable-one") === -1,
            delay: sortableDelay,
            filter: ".aiot-expander",
            touchStartThreshold: thresholdPx,
            fallbackTolerance: thresholdPx,
            /**
             * SortableJs does currently not support the usage of delay option
             * in IE11 and Edge browser. So for those browser we have to use the
             * implemented fallback instead of native drag and drop.
             *
             * @internal
             */
            forceFallback: typeof forceSortableFallback === "boolean" ? forceSortableFallback : !!IS_IE,
            disabled: isSortableDisabled,
            draggable: ".aiot-sortable",
            ghostClass: "aiot-sortable-ghost", // Class name for the drop placeholder
            chosenClass: "aiot-sortable-chosen", // Class name for the chosen item
            dragClass: "aiot-sortable-drag", // Class name for the dragging item
            dataIdAttr: "data-li-id",
            setData: (dataTransfer) => {
                // Hide native draggable ghost
                if (dataTransfer.setDragImage) {
                    const elem = document.createElement("div");
                    elem.style.display = "none";
                    document.body.appendChild(elem);
                    dataTransfer.setDragImage(elem, 0, 0);
                }
            },
            onMove: onSortMove,
            onStart: (evt) => {
                // Save order before sort
                Object.values(this._sortables).forEach(
                    (sortable: any) => (sortable._currentOrder = sortable.toArray()),
                );
                addClass(document.body, "aiot-currently-sorting");
                addClass(evt.from, CLASS_GETS_EMPTY);

                // Add class if the parent gets empty through this sort
                /*if ([].slice.call(evt.from.children).filter(({ className, tagName, children }) => {
                        return className.indexOf('aiot-sortable-ghost') === -1 && tagName.toLowerCase() === 'li';
                    }).length === 1) {
                    addClass(evt.from, CLASS_GETS_EMPTY);
                }*/
                onSortStart && onSortStart(evt);
            },
            onEnd: handleSortableTreeEnd.bind(this),
            animation: 100,
        });
    } else if (sortables[id]) {
        //console.log("destroy", id);

        // Destroy sortable instance
        sortables[id].destroy();
        delete sortables[id];
    }
}
