import { jsx as _jsx } from "react/jsx-runtime";
import throttle from "lodash/throttle";
import { createContext, Fragment, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
import UncontrolledTransition from "../UncontrolledTransition";
import TransitionStyles from "../Transition/Transition.module.scss";
import useHero from "../../hooks/useHero";
import AnimatedEntrance from "../AnimatedEntrance";
import { TransitionAnimationTypes } from "../Transition";
import Styles from "./OrderableList.module.scss";
import useEvents from "../../hooks/utility/useEvents";
import ownEvent from "../../utils/ownEvent";
const OrderableListContext = createContext(null);
export var OrderableListReorderMode;
(function (OrderableListReorderMode) {
    OrderableListReorderMode["VERTICAL"] = "v";
    OrderableListReorderMode["TWO_DIMENSIONS"] = "hv";
})(OrderableListReorderMode || (OrderableListReorderMode = {}));
function cleanKeys(keys) {
    return keys.map((a) => a.split(";")[0]);
}
/**
 * This component displays a list of items that can be reordered by dragging them with your mouse or touch.
 * It supports simple vertical dragging and more advanced 2D reordering, with smooth animations
 * when items move around.
 */
export default function OrderableList({ children, className = "", mode = OrderableListReorderMode.VERTICAL, shrinkToOnOrder, reorderingClassName, cloneClassName, ...props }) {
    const availableKeys = children.map((a) => a.key);
    const availableKeysDep = availableKeys.join(",");
    const initializeOrder = useCallback(() => {
        const missingOrderKeys = "keyOrder" in props && props.keyOrder
            ? availableKeys.filter((a) => !props.keyOrder.includes(a))
            : [];
        return (("keyOrder" in props && props.keyOrder
            ? [...props.keyOrder, ...missingOrderKeys]
            : undefined) || availableKeys);
    }, [availableKeysDep, props.keyOrder]);
    const { current: anchorsList } = useRef([]);
    const eventEmitter = useEvents();
    const currentClone = useRef(null);
    const currentWorkingKey = useRef(undefined);
    const rootRef = useRef(null);
    const [_order, setOrder] = useState(initializeOrder);
    useEffect(() => setOrder(initializeOrder()), [initializeOrder]);
    const order = useMemo(() => {
        return ("currentOrder" in props ? props.currentOrder || _order : _order).filter((o) => o.includes(";") || availableKeys.includes(o));
    }, [props.currentOrder, _order, availableKeysDep]);
    const cleanOrder = useMemo(() => order.map((a) => a.split(";")[0]), [order]);
    const orderId = useMemo(() => "key-" + cleanOrder.join(""), [cleanOrder]);
    function rootEl() {
        if (mode === OrderableListReorderMode.TWO_DIMENSIONS)
            return rootRef.current.lastElementChild
                .lastElementChild;
        return rootRef.current;
    }
    const findParentElement = (target) => {
        let parent = target;
        while (parent.parentElement !== rootEl()) {
            if (parent.parentElement === null)
                break;
            parent = parent.parentElement;
        }
        if (process.env.NODE_ENV === "development" && parent === null)
            throw new Error("It seems like we could not find a relation between this element and the root list. Are you using portals maybe?");
        return parent;
    };
    useLayoutEffect(() => {
        if (shrinkToOnOrder)
            rootEl().style.setProperty("--shrink-to", `${shrinkToOnOrder}px`);
    }, [shrinkToOnOrder]);
    const calculateReordering = useMemo(() => {
        return throttle((e, els, currentOrder) => {
            if (els.length > currentOrder.length || !currentWorkingKey.current)
                return;
            const parent = findParentElement(e.target);
            const indexOfKeyInCleanOrder = els.indexOf(parent);
            const keyOfTheOverElement = currentOrder[indexOfKeyInCleanOrder];
            const isDraggingOwnElement = currentWorkingKey.current === keyOfTheOverElement;
            if (indexOfKeyInCleanOrder === -1 || keyOfTheOverElement === undefined)
                return;
            if (!isDraggingOwnElement) {
                const { isAfter, isBefore } = (() => {
                    if (mode === OrderableListReorderMode.TWO_DIMENSIONS) {
                        const distanceFromLeft = e.offsetX;
                        const widthOfEl = parent.clientWidth;
                        const offset = widthOfEl * 0.25;
                        const isAfter = distanceFromLeft > widthOfEl - offset;
                        const isBefore = distanceFromLeft < offset;
                        return { isAfter, isBefore };
                    }
                    else {
                        const distanceFromTop = e.offsetY;
                        const heightOfEl = parent.clientHeight;
                        const offset = heightOfEl * 0.25;
                        const isAfter = distanceFromTop > heightOfEl - offset;
                        const isBefore = distanceFromTop < offset;
                        return { isAfter, isBefore };
                    }
                })();
                const checkIfCanMove = (keyOfOverElement) => {
                    return (keyOfOverElement === undefined ||
                        keyOfOverElement !== currentWorkingKey.current);
                };
                const indexOfTheNewElement = indexOfKeyInCleanOrder + (isAfter ? 1 : -1);
                const shouldTriggerReordering = (isAfter || isBefore) &&
                    checkIfCanMove(currentOrder[indexOfTheNewElement]);
                if (shouldTriggerReordering) {
                    setOrder((p) => {
                        const previousIndex = currentOrder.indexOf(currentWorkingKey.current);
                        const indexOfKeyInProvidedOrder = cleanKeys(p).indexOf(keyOfTheOverElement);
                        let indexOfTheNewElement = indexOfKeyInProvidedOrder + (isBefore ? -1 : 0);
                        if (previousIndex > indexOfTheNewElement)
                            indexOfTheNewElement++;
                        const orderWithoutCurrent = p.filter((a) => {
                            if (a.startsWith(`${currentWorkingKey.current};`))
                                return false;
                            return a !== currentWorkingKey.current;
                        });
                        orderWithoutCurrent.splice(indexOfTheNewElement, 0, `${currentWorkingKey.current};${Date.now()}`);
                        return orderWithoutCurrent;
                    });
                }
            }
        }, 1000 / 60);
    }, []);
    useEffect(() => {
        function registerListeners() {
            const els = Array.from(rootEl().children);
            const calculateReorderingCall = (e) => {
                const els = Array.from(rootEl().children);
                calculateReordering(e, els, cleanOrder);
            };
            for (let el of els)
                el.addEventListener("mousemove", calculateReorderingCall);
            return () => {
                for (let el of els)
                    el.removeEventListener("mousemove", calculateReorderingCall);
            };
        }
        if (mode === OrderableListReorderMode.VERTICAL) {
            return registerListeners();
        }
        else {
            let unsubscribe;
            let timeout = setTimeout(() => {
                unsubscribe = registerListeners();
            }, 250);
            return () => {
                clearTimeout(timeout);
                if (unsubscribe)
                    unsubscribe();
            };
        }
    }, [cleanOrder]);
    useEffect(() => {
        if ("onChangeKeyOrder" in props)
            props.onChangeKeyOrder(cleanKeys(_order));
    }, [_order]);
    const cleanOrderRef = useRef(cleanOrder);
    useEffect(() => {
        cleanOrderRef.current = cleanOrder;
    }, [cleanOrder]);
    const onAnchorClick = useCallback(({ target: anchor }) => {
        let offset;
        const orderableListItemForAnchor = findParentElement(anchor);
        const box = orderableListItemForAnchor.getBoundingClientRect();
        const els = Array.from(rootEl().children);
        const elIndex = els.indexOf(orderableListItemForAnchor);
        const clone = orderableListItemForAnchor.cloneNode(true);
        currentClone.current = clone;
        if (process.env.NODE_ENV === "development")
            clone.setAttribute("data-testid", "orderable-list-clone");
        clone.setAttribute("data-orderableid", "orderable-list-clone");
        clone.style.width = `${box.width}px`;
        clone.style.height = `${box.height}px`;
        clone.style.top = `${box.top}px`;
        clone.style.left = `${box.left}px`;
        clone.classList.add(Styles.clone);
        if (shrinkToOnOrder)
            clone.style.maxHeight = `${box.height}px`;
        setTimeout(() => {
            if (cloneClassName)
                clone.classList.add(cloneClassName);
            if (shrinkToOnOrder)
                clone.style.maxHeight = `${shrinkToOnOrder}px`;
        }, 0);
        orderableListItemForAnchor.classList.add(Styles.currentOrdering);
        orderableListItemForAnchor.classList.remove(Styles.visible);
        const movementControl = (e) => {
            const { x: x1, y: y1 } = "touches" in e ? { x: e.touches[0].pageX, y: e.touches[0].pageY } : e;
            if (!offset)
                offset = [x1 - box.left, y1 - box.top];
            const [offsetX, offsetY] = offset;
            clone.style.top = `${y1 - offsetY}px`;
            clone.style.left = `${x1 - offsetX}px`;
        };
        document.body.appendChild(clone);
        const deleteClone = () => {
            window.removeEventListener("mousemove", movementControl);
            window.removeEventListener("touchmove", movementControl);
            window.removeEventListener("touchend", deleteClone);
            window.removeEventListener("mouseup", deleteClone);
            orderableListItemForAnchor.style.visibility = "initial";
            currentWorkingKey.current = undefined;
            window.document.body.classList.remove(Styles.unselectable);
            {
                const els = Array.from(rootEl().children);
                const elInvisible = els.find((a) => !a.classList.contains(Styles.visible) &&
                    a.style.maxHeight !== "0px");
                if (shrinkToOnOrder) {
                    const child = elInvisible.lastElementChild;
                    child.style.maxHeight = `${child.scrollHeight}px`;
                    child.classList.add(Styles.transitionHeight);
                    const onEnd = ownEvent(() => {
                        child.removeEventListener("transitionend", onEnd);
                        child.style.maxHeight = `initial`;
                        child.classList.remove(Styles.transitionHeight);
                    });
                    child.addEventListener("transitionend", onEnd);
                }
                const box = elInvisible.getBoundingClientRect();
                function cleanUp() {
                    if (shrinkToOnOrder) {
                        rootEl().style.removeProperty("padding-top");
                        rootEl().style.removeProperty("min-height");
                    }
                    eventEmitter.dispatcher("order-stop");
                    rootEl().classList.remove(Styles.ordering);
                    if (reorderingClassName)
                        rootEl().classList.remove(reorderingClassName);
                    for (let el of els)
                        el.classList.remove(Styles.visible);
                    clone.remove();
                    currentClone.current = null;
                    for (let el of Array.from(elInvisible.children))
                        el.style.height = "";
                }
                if (clone.style.top.startsWith(Math.floor(box.top).toString()) &&
                    clone.style.left.startsWith(Math.floor(box.left).toString())) {
                    cleanUp();
                }
                else {
                    clone.style.top = `${box.top}px`;
                    clone.style.left = `${box.left}px`;
                    clone.style.transition = `top 500ms linear, left 500ms linear`;
                    clone.addEventListener("transitionend", () => {
                        cleanUp();
                    });
                }
            }
        };
        for (let el of els) {
            el.classList.add(Styles.visible);
        }
        if (shrinkToOnOrder) {
            for (let el of els)
                el.style.maxHeight = `${el.scrollHeight}px`;
            const targetHeight = elIndex * shrinkToOnOrder;
            const currentHeight = orderableListItemForAnchor.offsetTop - els[0].offsetTop;
            const paddingTop = currentHeight - targetHeight;
            rootEl().style.paddingTop = `${paddingTop}px`;
            rootEl().style.minHeight = `${rootEl().clientHeight}px`;
        }
        orderableListItemForAnchor.style.visibility = "hidden";
        orderableListItemForAnchor.classList.remove(Styles.visible);
        rootEl().classList.add(Styles.ordering);
        if (reorderingClassName)
            rootEl().classList.add(reorderingClassName);
        currentWorkingKey.current = cleanOrderRef.current[elIndex];
        eventEmitter.dispatcher("order-start");
        window.document.body.classList.add(Styles.unselectable);
        window.addEventListener("mouseup", deleteClone);
        window.addEventListener("mousemove", movementControl);
        window.addEventListener("touchend", deleteClone);
        window.addEventListener("touchmove", movementControl);
    }, []);
    useLayoutEffect(() => {
        if (currentClone.current) {
            const els = Array.from(rootEl().children);
            const elInvisible = els.find((a) => !a.classList.contains(Styles.visible) && a.style.maxHeight !== "0px");
            elInvisible.children[0].style.height = `${currentClone.current.clientHeight}px`;
            setTimeout(() => {
                elInvisible.children[1].style.height = `${currentClone.current.clientHeight}px`;
            }, 150);
        }
    }, [cleanOrder]);
    const toSpread = { ...props };
    delete toSpread.keyOrder;
    delete toSpread.onChangeKeyOrder;
    const childrenToRender = [...children]
        .filter((a) => cleanOrder.includes(a.key))
        .sort((a, b) => cleanOrder.indexOf(a.key) -
        cleanOrder.indexOf(b.key))
        .map((a, i) => ({
        ...a,
        key: order[i],
    }));
    useLayoutEffect(() => {
        rootEl().addEventListener("touchstart", (e) => {
            const relatedAnchor = anchorsList.find((anchor) => anchor.contains(e.target));
            if (relatedAnchor) {
                e.preventDefault();
                onAnchorClick({
                    target: relatedAnchor,
                });
                const moveCb = throttle((e) => {
                    const touch = e.touches[0];
                    const [x, y] = [touch.clientX, touch.clientY];
                    const els = Array.from(rootEl().children);
                    const currentElementIdx = els.findIndex((c, i) => {
                        const rect = c?.getBoundingClientRect();
                        if (!rect)
                            return false;
                        return rect.top < y && rect.top + rect.height > y;
                    });
                    if (currentElementIdx !== -1) {
                        const el = els[currentElementIdx];
                        const rect = el.getBoundingClientRect();
                        calculateReordering({
                            target: el,
                            offsetY: y - rect.top,
                        }, els, cleanOrderRef.current);
                    }
                }, 500);
                const t = e.target;
                const touchMoveCb = (e) => {
                    e.stopPropagation();
                    window.dispatchEvent(new TouchEvent(e.type, e));
                    moveCb(e);
                };
                const removeCb = (e) => {
                    t.removeEventListener("touchmove", touchMoveCb);
                    t.removeEventListener("touchend", removeCb);
                    window.dispatchEvent(new TouchEvent(e.type, e));
                };
                t.addEventListener("touchmove", touchMoveCb);
                t.addEventListener("touchend", removeCb);
            }
        });
    }, []);
    const e = useMemo(() => ({
        bindAnchor: (el) => {
            el.addEventListener("mousedown", onAnchorClick);
            anchorsList.push(el);
        },
        unbindAnchor: (el) => {
            el.removeEventListener("mousedown", onAnchorClick);
            anchorsList.splice(anchorsList.indexOf(el), 1);
        },
        on: eventEmitter.subscriber,
    }), []);
    return (_jsx(OrderableListContext.Provider, { value: e, children: _jsx("div", { ref: rootRef, className: `${Styles.root} ${className} ${shrinkToOnOrder ? Styles.shrinkable : ""}`, ...toSpread, children: mode === OrderableListReorderMode.VERTICAL ? (_jsx(AnimatedEntrance, { children: childrenToRender })) : (_jsx(UncontrolledTransition, { className: Styles.transition, transitionType: TransitionAnimationTypes.CUSTOM, config: {
                    backward: {
                        elementEntering: "",
                        elementExiting: "",
                    },
                    forward: {
                        elementEntering: Styles.stubEntering,
                        elementExiting: TransitionStyles.fadeOut,
                    },
                }, contentClassName: `${className}`, children: _jsx(Fragment, { children: [...children]
                        .filter((a) => cleanOrder.includes(a.key))
                        .sort((a, b) => cleanOrder.indexOf(a.key) -
                        cleanOrder.indexOf(b.key))
                        .map((a, i) => (_jsx(HeroWrapper, { id: a.key, children: a }, i))) }, orderId) })) }) }));
}
function HeroWrapper({ children, id }) {
    const { heroRef } = useHero(id, undefined, {
        onHeroStart: (_c, _o, t) => {
            t.querySelectorAll("img").forEach((img) => (img.style.visibility = "hidden"));
        },
        onHeroCloned(clone) {
            // if (!clone.classList.contains(Styles.visible)) clone.style.opacity = "0";
        },
        onHeroEnd: () => {
            if (heroRef.current)
                heroRef.current
                    .querySelectorAll("img")
                    .forEach((img) => (img.style.visibility = ""));
        },
    });
    return _jsx("div", { ref: heroRef, children: children });
}
export function useOrderableListAnchor() {
    const anchorRef = useRef(null);
    const ctx = useContext(OrderableListContext);
    useLayoutEffect(() => {
        if (!ctx)
            return;
        const { bindAnchor, unbindAnchor } = ctx;
        bindAnchor(anchorRef.current);
        return () => unbindAnchor(anchorRef.current);
    }, []);
    return {
        anchorRef: ctx ? anchorRef : undefined,
    };
}
export function useOrderableEvents(events) {
    const { on } = useContext(OrderableListContext);
    useEffect(() => {
        const unsubscribers = [];
        for (let eventName in events) {
            unsubscribers.push(on(eventName, events[eventName]));
        }
        return () => {
            unsubscribers.forEach((u) => u());
        };
    }, [events]);
}
//# sourceMappingURL=OrderableList.js.map