// react
import React, { useCallback, useEffect, useRef, useState } from "react";
// third-party
import classNames from "classnames";
import Slick, { Settings } from "react-slick";
// application
import { useDirection } from "~/services/i18n/hooks";

export type ISlickProps = Settings;

interface Props extends Omit<ISlickProps, "children"> {
    children: React.ReactNode;
    forwardRef: React.RefCallback<Slick | null> | React.MutableRefObject<Slick | null> | null;
}

function AppSlickInner(props: Props) {
    const { children, forwardRef, beforeChange, ...otherProps } = props;

    // Casteamos el componente para evitar errores de tipos con 'children' en React 18
    const SlickComponent = Slick as any;

    const direction = useDirection();
    const slickRef = useRef<Slick | null>(null);
    const originalSlickNextRef = useRef<() => void>(() => {});
    const originalSlickPrevRef = useRef<() => void>(() => {});
    const slickNextRef = useRef<() => void>(() => {});
    const slickPrevRef = useRef<() => void>(() => {});

    const getSlidesCount = () => React.Children.toArray(children).length;

    const getSlidesToShow = () => {
        const { responsive, slidesToShow } = props;

        let result = slidesToShow || 1;

        if (typeof window !== "undefined" && responsive) {
            responsive.forEach((options) => {
                if (options.settings === "unslick") {
                    return;
                }

                const { matches } = matchMedia(`(max-width: ${options.breakpoint}px)`);

                if (matches && options.settings.slidesToShow) {
                    result = options.settings.slidesToShow;
                }
            });
        }

        return result;
    };

    const getStartPosition = () => {
        let { infinite } = props;

        infinite = infinite === true || infinite === undefined;

        if (direction === "ltr") {
            return 0;
        }

        const slidesToShow = getSlidesToShow();
        const slidesCount = getSlidesCount();

        if (!infinite) {
            return Math.max(0, slidesCount - slidesToShow);
        }

        return (Math.ceil(slidesCount / slidesToShow) - 1) * slidesToShow;
    };

    const getActiveSlides = (currentIndex: number) => {
        const slidesToShow = getSlidesToShow();
        const activeSlides = [];
        const slidesCount = getSlidesCount();

        const firstSlide = Math.max(0, Math.min(slidesCount - slidesToShow, currentIndex));
        const lastSlide = Math.min(slidesCount, firstSlide + slidesToShow);

        for (let i = firstSlide; i < lastSlide; i += 1) {
            activeSlides.push(i);
        }

        return activeSlides;
    };

    const [preventClick, setPreventClick] = useState(false);
    const [currentSlidesToShow, setCurrentSlidesToShow] = useState(getSlidesToShow());
    const [activeSlides, setActiveSlides] = useState(getActiveSlides(getStartPosition()));

    const onMousedown = (event: React.MouseEvent<HTMLDivElement>) => {
        const downX = event.screenX;
        const downY = event.screenY;
        let clickPrevented = false;

        const onMousemove = (moveEvent: MouseEvent) => {
            if (clickPrevented) {
                return;
            }

            const distance = Math.sqrt(
                Math.abs(downX - moveEvent.screenX) ** 2 + Math.abs(downY - moveEvent.screenY) ** 2
            );

            if (moveEvent.cancelable && distance > 3) {
                moveEvent.preventDefault();
            }

            if (distance > 15) {
                clickPrevented = true;
                setPreventClick(clickPrevented);
            }
        };
        const onMouseup = () => {
            clickPrevented = false;
            setPreventClick(clickPrevented);

            document.removeEventListener("mousemove", onMousemove);
            document.removeEventListener("mouseup", onMouseup);
        };

        document.addEventListener("mousemove", onMousemove);
        document.addEventListener("mouseup", onMouseup);
    };

    const setSlickRef: React.RefCallback<Slick> = useCallback(
        (ref) => {
            if (forwardRef) {
                if ("current" in forwardRef) {
                    forwardRef.current = ref;
                } else {
                    forwardRef(ref);
                }
            }

            if (ref && ref !== slickRef.current) {
                originalSlickNextRef.current = ref.slickNext;
                originalSlickPrevRef.current = ref.slickPrev;

                ref.slickNext = () => slickNextRef.current && slickNextRef.current();
                ref.slickPrev = () => slickPrevRef.current && slickPrevRef.current();
            }

            slickRef.current = ref;
        },
        [forwardRef]
    );

    useEffect(() => {
        let unsubscribe = () => {};

        if (otherProps.responsive && otherProps.responsive.length > 0) {
            const subscriptions: Array<() => void> = [];

            const createMedia = (query: string, slidesToShow: number | undefined) => {
                const media = matchMedia(query);

                const onChange = () => {
                    const { matches } = media;

                    if (matches && slidesToShow) {
                        setCurrentSlidesToShow(slidesToShow);
                    }
                };

                if (media.addEventListener) {
                    media.addEventListener("change", onChange);
                } else {
                    media.addListener(onChange);
                }

                subscriptions.push(() => {
                    if (media.removeEventListener) {
                        media.removeEventListener("change", onChange);
                    } else {
                        media.removeListener(onChange);
                    }
                });
            };

            const responsive = otherProps.responsive;
            createMedia(`(min-width: ${responsive[0].breakpoint}.02px)`, otherProps.slidesToShow || 1);

            responsive.forEach((options, index) => {
                if (options.settings === "unslick") {
                    return;
                }

                const query = [`(max-width: ${options.breakpoint}px)`];

                if (responsive.length - 1 !== index) {
                    query.push(`(min-width: ${responsive[index + 1].breakpoint}.02px)`);
                }

                createMedia(query.join(" and "), options.settings.slidesToShow);
            });

            unsubscribe = () => {
                subscriptions.forEach((x) => x());
            };
        }

        return unsubscribe;
    }, [otherProps.responsive, otherProps.slidesToShow]);

    useEffect(() => {
        slickNextRef.current = () => {
            originalSlickNextRef.current();
        };

        slickPrevRef.current = () => {
            if (direction === "rtl") {
                originalSlickNextRef.current();
            } else {
                originalSlickPrevRef.current();
            }
        };
    }, [direction]);

    useEffect(() => {
        const timer = setTimeout(() => {
            setActiveSlides(getActiveSlides(getStartPosition()));
        }, 0);

        return () => {
            clearTimeout(timer);
        };
    }, [children]);

    useEffect(() => {
        if (slickRef.current) {
            slickRef.current.slickGoTo(getStartPosition(), true);
        }
    }, [direction]);

    const beforeChangeWrapper: Settings["beforeChange"] = useCallback(
        (oldIndex, newIndex) => {
            if (beforeChange) {
                beforeChange(oldIndex, newIndex);
            }

            setTimeout(() => {
                setActiveSlides(getActiveSlides(newIndex));
            }, 0);
        },
        [beforeChange]
    );

    const classes = classNames("slick-prevent-click", {
        "slick-prevent-click--active": preventClick,
    });

    let reversedChildren = React.Children.toArray(children);

    if (direction === "rtl") {
        reversedChildren = reversedChildren.slice(0);
        reversedChildren.reverse();
    }

    reversedChildren = reversedChildren.map((slide, index) => {
        const slideClasses = classNames({ "correct-slick-active": activeSlides.includes(index) });

        return (
            <div key={index} dir={direction} className={slideClasses}>
                {slide}
            </div>
        );
    });

    return (
        <div className={classes} onMouseDown={onMousedown}>
            <SlickComponent
                {...otherProps}
                rtl={direction === "rtl"}
                beforeChange={beforeChangeWrapper}
                infinite={otherProps.infinite && React.Children.count(children) > currentSlidesToShow}
                ref={setSlickRef}
            >
                {reversedChildren}
            </SlickComponent>
        </div>
    );
}

export default React.forwardRef<Slick, React.PropsWithChildren<ISlickProps>>((props, ref) => {
    const { children } = props;

    return (
        <AppSlickInner forwardRef={ref} {...props}>
            {children}
        </AppSlickInner>
    );
});
