import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, } from "react";
import ownEvent from "../utils/ownEvent";
import Styles from "./useHero.module.scss";
const ID = (id) => `${id}-hero`;
const viewport = window.visualViewport;
const DATA_TAG_HERO_COMPONENT = "data-hero-component";
function triggerDynamicComponents(clone, componentsFromOriginThatWillAppear, origin) {
    const idsThatWillAppear = componentsFromOriginThatWillAppear.map((a) => a.getAttribute(DATA_TAG_HERO_COMPONENT));
    const preexistingComponents = Array.from(clone.querySelectorAll(`[${DATA_TAG_HERO_COMPONENT}]`));
    const removedElements = preexistingComponents.filter((el) => !idsThatWillAppear.includes(el.getAttribute(DATA_TAG_HERO_COMPONENT)));
    for (let elementThatIsBeingRemoved of removedElements) {
        for (let heightKey of ["height", "minHeight", "maxHeight"])
            elementThatIsBeingRemoved.style[heightKey] = `${elementThatIsBeingRemoved.clientHeight}px`;
    }
    setTimeout(() => {
        for (let elementThatIsBeingRemoved of removedElements) {
            for (let heightKey of ["height", "minHeight", "maxHeight"])
                elementThatIsBeingRemoved.style[heightKey] = `0px`;
            elementThatIsBeingRemoved.style.opacity = `0`;
        }
    }, 0);
    for (let componentThatWillAppear of componentsFromOriginThatWillAppear) {
        const alreadyInjectedElement = !!clone.querySelector(`[${DATA_TAG_HERO_COMPONENT}="${componentThatWillAppear.getAttribute(DATA_TAG_HERO_COMPONENT)}"]`);
        if (!alreadyInjectedElement) {
            function recursiveBuildPath(fromElement, pathSuffix = "") {
                if (fromElement === origin)
                    return pathSuffix;
                const parent = fromElement.parentElement;
                const parentChilds = Array.from(parent.children);
                const elChildSelector = `:nth-child(${parentChilds.indexOf(fromElement) + 1})`;
                if (fromElement !== origin)
                    return recursiveBuildPath(parent, `${elChildSelector}${pathSuffix ? `>${pathSuffix}` : ""}`);
                else
                    return pathSuffix;
            }
            const whatIsTheElementPathFromCloneElement = recursiveBuildPath(componentThatWillAppear.parentElement);
            const insertAtIndex = Array.from(componentThatWillAppear.parentElement.children).indexOf(componentThatWillAppear);
            const targetElementClone = componentThatWillAppear.cloneNode(true);
            for (let heightKey of ["height", "minHeight", "maxHeight"])
                targetElementClone.style[heightKey] = "0px";
            const shouldBeInsertedAtElement = whatIsTheElementPathFromCloneElement
                ? clone.querySelector(whatIsTheElementPathFromCloneElement)
                : clone;
            const remainingIndexes = Array.from(shouldBeInsertedAtElement.children)
                .map((el, i) => removedElements.includes(el) ? undefined : i)
                .filter((e) => e !== undefined);
            if (insertAtIndex === remainingIndexes.length)
                shouldBeInsertedAtElement.append(targetElementClone);
            else
                shouldBeInsertedAtElement.insertBefore(targetElementClone, shouldBeInsertedAtElement.childNodes.item(remainingIndexes[insertAtIndex]));
            setTimeout(() => {
                for (let heightKey of ["height", "minHeight", "maxHeight"])
                    targetElementClone.style[heightKey] = `${componentThatWillAppear.clientHeight}px`;
            }, 0);
        }
    }
}
const ALWAYS_TRANSITION = ["top", "left"];
/**
 * This hook implements the logic for a hero animation between 2 elements sharing the same id
 */
export default function useHero(id, options = {
    propsToTransition: [],
}, events = {}) {
    const { propsToTransition: _p = [], "data-preffix": dataPreffix, repeatable, } = options;
    const propsToTransition = useMemo(() => _p.map((p) => p.replace(/([A-Z])/g, (_, val) => `-${val.toLowerCase()}`)), [_p]);
    const _dataPreffix = dataPreffix ? `-${dataPreffix}` : "";
    const dataProperty = `data${_dataPreffix}-hero`;
    const heroRef = useRef(null);
    useLayoutEffect(() => {
        heroRef.current.setAttribute(dataProperty, id);
    }, [id]);
    const getHerosOnScreen = useCallback(() => {
        const otherElements = Array.from(document.querySelectorAll(`[${dataProperty}="${id}"]`)).filter((el) => el !== heroRef.current);
        return otherElements;
    }, [id]);
    function triggerHeroAnimation() {
        const allPropsToTransition = options.overridePropsToTransition
            ? [...ALWAYS_TRANSITION, ...propsToTransition]
            : ["width", "height", ...ALWAYS_TRANSITION, ...propsToTransition];
        const shouldHeroFn = events.onHeroDetect || ((...a) => true);
        const otherElements = getHerosOnScreen().filter((el) => shouldHeroFn(el, heroRef.current));
        const currentElement = heroRef.current;
        const currentElCoordinates = currentElement.getBoundingClientRect();
        if (process.env.NODE_ENV === "development" && otherElements.length > 2)
            console.warn("There are too many elements to transition to, I will transition to the first I find", otherElements);
        const otherElement = otherElements.find((el) => el !== heroRef.current);
        if (otherElement) {
            const [originContainer, targetContainer] = events.onBeforeTransition
                ? events.onBeforeTransition(otherElement, heroRef.current)
                : [viewport, viewport];
            const shouldSkip = isElementOutsideContainer(targetContainer, currentElCoordinates) ||
                isElementOutsideContainer(originContainer, otherElement.getBoundingClientRect())
                ? (events.onHeroSkip || (() => true))(otherElement, heroRef.current)
                : false;
            if (shouldSkip) {
                events.onHeroSkipped?.();
                return;
            }
            const oldClone = document.querySelector(`[${dataProperty}-clone="${ID(id)}"]`);
            const clone = (oldClone ||
                otherElement.cloneNode(true));
            if (events.onHeroCloned && !oldClone)
                events.onHeroCloned(clone);
            // Clean up thos properties that can cause confusion since it's a clone
            clone.style.visibility = "";
            clone.removeAttribute(dataProperty);
            clone.setAttribute(`${dataProperty}-clone`, ID(id));
            /**
             * If it's repeatable, we should keep the flag that indicates the hero existance
             */
            if (!repeatable)
                // Since a transition is now triggering from the old element, he cannot be considered for other transitions
                otherElement.removeAttribute(dataProperty);
            function willTheHeroMove(origin, target) {
                const willItMove = !(Math.trunc(origin.top) === Math.trunc(target.top) &&
                    Math.trunc(origin.left) === Math.trunc(target.left) &&
                    Math.trunc(origin.width) === Math.trunc(target.width) &&
                    Math.trunc(origin.height) === Math.trunc(target.height));
                return willItMove;
            }
            /**
             *
             * @param el
             * @returns Returns if it will move
             */
            function setCloneToCoordinatesOf(el) {
                const coordinates = el.getBoundingClientRect();
                const currentCoordinates = clone.getBoundingClientRect();
                const willMove = willTheHeroMove(coordinates, currentCoordinates);
                clone.style.position = "fixed";
                clone.style.top = `${coordinates.top}px`;
                clone.style.left = `${coordinates.left}px`;
                if (allPropsToTransition.includes("width"))
                    clone.style.width = `${coordinates.width}px`;
                if (allPropsToTransition.includes("height"))
                    clone.style.height = `${coordinates.height}px`;
                return willMove;
            }
            const coordinates = otherElement.getBoundingClientRect();
            const currentCoordinates = heroRef.current.getBoundingClientRect();
            const itWontMove = !willTheHeroMove(coordinates, currentCoordinates);
            if (itWontMove) {
                events.onHeroEnd?.();
                return;
            }
            if (!oldClone) {
                /** Set the clone over the starting element */
                setCloneToCoordinatesOf(otherElement);
                document.body.appendChild(clone);
            }
            heroRef.current.style.visibility = "hidden";
            otherElement.style.visibility = "hidden";
            for (let propToTransition of propsToTransition) {
                clone.style[propToTransition] =
                    window.getComputedStyle(otherElement)[propToTransition];
            }
            clone.style.transition = `${allPropsToTransition
                .map((prop) => `${prop.replace(/[A-Z]/g, (sub) => `-${sub.toLowerCase()}`)} var(--animation--speed-fast, 250ms) ease-out`)
                .join(", ")}`;
            setTimeout(() => {
                const elItWillTransitionTo = heroRef.current;
                const cleanup = () => {
                    if (events.onHeroEnd)
                        events.onHeroEnd();
                    if (elItWillTransitionTo) {
                        if (repeatable)
                            elItWillTransitionTo.setAttribute(dataProperty, id);
                        elItWillTransitionTo.style.visibility = "";
                    }
                    setTimeout(() => {
                        clone.remove();
                    }, 0);
                };
                triggerDynamicComponents(clone, Array.from(heroRef.current.querySelectorAll(`[${DATA_TAG_HERO_COMPONENT}]`)), currentElement);
                if (events.onHeroStart) {
                    events.onHeroStart(clone, otherElement, heroRef.current);
                }
                if (!elItWillTransitionTo) {
                    cleanup();
                    return;
                }
                /** Set the clone over the new position */
                const willMove = setCloneToCoordinatesOf(elItWillTransitionTo);
                if (!willMove)
                    cleanup();
                else {
                    for (let propToTransition of propsToTransition)
                        clone.style[propToTransition] =
                            window.getComputedStyle(elItWillTransitionTo)[propToTransition];
                    let initialOffset;
                    const s = ({ target }) => {
                        const d = target;
                        if (d.contains(elItWillTransitionTo)) {
                            if (initialOffset === undefined)
                                initialOffset = d.scrollTop;
                            else
                                clone.style.marginTop = `${-(d.scrollTop - initialOffset)}px`;
                        }
                    };
                    const transitionEndCb = ownEvent(({ target, currentTarget }) => {
                        if (target === currentTarget)
                            cleanup();
                        document.removeEventListener("scroll", s, true);
                    });
                    document.addEventListener("scroll", s, true);
                    clone.addEventListener("transitionend", transitionEndCb);
                    clone.addEventListener("transitionstart", () => {
                        const onCancelCb = ownEvent((e) => {
                            clone.removeEventListener("transitionend", transitionEndCb);
                            clone.removeEventListener("transitioncancel", onCancelCb);
                        });
                        clone.addEventListener("transitioncancel", onCancelCb);
                    });
                }
            }, 0);
        }
    }
    useEffect(() => {
        triggerHeroAnimation();
    }, []);
    return {
        heroRef,
        getHerosOnScreen,
        trigger: triggerHeroAnimation,
        heroComponentRef: (componentId) => (elRef) => {
            if (elRef) {
                elRef.setAttribute(DATA_TAG_HERO_COMPONENT, componentId);
                elRef.classList.add(Styles.component);
            }
        },
    };
}
function isElementOutsideViewport(coordinates) {
    const elementOverflowsViewport = coordinates.left >= viewport.width || coordinates.top >= viewport.height;
    const elementUnderflowsViewport = coordinates.left <= -coordinates.width ||
        coordinates.top <= -coordinates.height;
    return elementOverflowsViewport || elementUnderflowsViewport;
}
function isElementOutsideContainer(container, coordinates) {
    if (container instanceof VisualViewport) {
        return isElementOutsideViewport(coordinates);
    }
    else {
        const containerBounds = container.getBoundingClientRect();
        const isContainerOutsideViewport = isElementOutsideViewport(containerBounds);
        if (isContainerOutsideViewport)
            return true;
        const elementOverflowsViewport = coordinates.left >= containerBounds.left + containerBounds.width ||
            coordinates.top >= containerBounds.top + containerBounds.height;
        const elementUnderflowsViewport = containerBounds.left - coordinates.left >= coordinates.width ||
            containerBounds.top - coordinates.top >= coordinates.height;
        return elementUnderflowsViewport || elementOverflowsViewport;
    }
}
function centerPoint(rect) {
    return [rect.left + rect.width / 2, rect.top + rect.height / 2];
}
const angle = (pointA, pointB) => Number((Math.atan2(pointA[1] - pointB[1], pointA[0] - pointB[0]) *
    (179.08 / Math.PI)).toFixed(5));
export const TRANSITION_FACTORY = {
    ACCELERATION: (events) => ({
        ...events,
        onHeroStart(clone, origin, target) {
            if (events?.onHeroStart)
                events.onHeroStart(clone, origin, target);
            clone.classList.add(Styles.acceleration);
            const centerPointOrigin = centerPoint(origin.getBoundingClientRect());
            const centerPointDestination = centerPoint(target.getBoundingClientRect());
            const angleBetweenElements = angle(centerPointOrigin, centerPointDestination);
            const vectorX = Math.sin(angleBetweenElements);
            const vectorY = Math.cos(angleBetweenElements);
            const rotateY = (10 * vectorY).toFixed(0);
            const originX = (vectorY + 1) * 50;
            const rotateX = -(10 * vectorX).toFixed(0);
            const originY = (vectorX + 1) * 50;
            clone.style.setProperty("--stage-1", `rotateY(${rotateY}deg) rotateX(${-rotateX}deg)`);
            clone.style.setProperty("--stage-2", `rotateY(${-rotateY * 0.3}deg) rotateX(${rotateX * 0.3}deg)`);
            clone.style.setProperty("--origin-1", `${originX}% ${originY}%`);
            clone.style.setProperty("--origin-2", `${100 - originX}% ${100 - originY}%`);
            document.body.style.perspective = "100vw";
        },
        onHeroEnd() {
            if (events?.onHeroEnd)
                events.onHeroEnd();
            document.body.style.perspective = "";
        },
    }),
};
//# sourceMappingURL=useHero.js.map