/*
 * Copyright (C) Contributors to the Suwayomi project
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import { RefObject, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { useWindowEvent } from '@mantine/hooks';
import {
    ReaderPageScaleMode,
    ReaderStateChapters,
    ReadingDirection,
    ReadingMode,
} from '@/features/reader/Reader.types.ts';
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
import {
    isContinuousReadingMode,
    isContinuousVerticalReadingMode,
    isHeightPageScaleMode,
    isReaderWidthEditable,
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
import { getPreviousNextChapterVisibility } from '@/features/reader/Reader.utils.ts';
import { ChapterIdInfo, TChapterReader } from '@/features/chapter/Chapter.types.ts';

const shouldPreserveOnResizeChange = (
    readingMode: ReadingMode,
    pageScaleMode: ReaderPageScaleMode,
    previousWidth: number,
    previousHeight: number,
): boolean => {
    const isEditableReaderWidth = isReaderWidthEditable(pageScaleMode);
    const isHeightPageScaleModeActive = isHeightPageScaleMode(pageScaleMode);

    const didWidthChange = previousWidth !== window.innerWidth;
    const didHeightChange = previousHeight !== window.innerHeight;

    const handleWidthChange = isEditableReaderWidth && didWidthChange;
    const handleHeightChange = isHeightPageScaleModeActive && didHeightChange;

    if (!isContinuousVerticalReadingMode(readingMode) && didWidthChange) {
        return true;
    }

    return handleWidthChange || handleHeightChange;
};

const usePreserveOnValueChange = (
    value: unknown,
    currentPageIndex: number,
    setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
) => {
    useLayoutEffect(() => {
        setPageToScrollToIndex(currentPageIndex);
    }, [value]);
};

const usePreserveOnWindowResize = (
    readingMode: ReadingMode,
    pageScaleMode: ReaderPageScaleMode,
    setPageToScrollToIndex: React.Dispatch<React.SetStateAction<number | null>>,
    pageIndex: number,
) => {
    const previousDimensionsRef = useRef({ width: window.innerWidth, height: window.innerHeight });

    const handleResize = useCallback(() => {
        const { width, height } = previousDimensionsRef.current;
        previousDimensionsRef.current = { width: window.innerWidth, height: window.innerHeight };

        if (!shouldPreserveOnResizeChange(readingMode, pageScaleMode, width, height)) {
            return;
        }

        setPageToScrollToIndex(pageIndex);
    }, [readingMode, pageScaleMode, pageIndex]);

    useWindowEvent('resize', handleResize);
};

interface ScrollPreservationInfo {
    left: number;
    top: number;
    visibleElement: HTMLElement | undefined;
    visibleElementLeft: number;
    visibleElementTop: number;
}

const useScrollPreservationData = (
    scrollElementRef: RefObject<HTMLElement | null>,
): RefObject<ScrollPreservationInfo> => {
    const dataRef = useRef<ScrollPreservationInfo>({
        left: 0,
        top: 0,
        visibleElement: undefined,
        visibleElementLeft: 0,
        visibleElementTop: 0,
    });

    useEffect(() => {
        const scrollElement = scrollElementRef.current;

        if (!scrollElement) {
            return () => {};
        }

        const onScroll = () => {
            const { visibleElement } = dataRef.current;
            dataRef.current = {
                ...dataRef.current,
                left: scrollElement.scrollLeft,
                top: scrollElement.scrollTop,
                visibleElementLeft: visibleElement?.offsetLeft ?? 0,
                visibleElementTop: visibleElement?.offsetTop ?? 0,
            };
        };

        scrollElement.addEventListener('scroll', onScroll);

        return () => scrollElement.removeEventListener('scroll', onScroll);
    }, []);

    useEffect(() => {
        const scrollElement = scrollElementRef.current;
        if (!scrollElement) {
            return () => {};
        }

        const updateObservation = (nodes: NodeList, intersectionAction: (element: Element) => void) =>
            Array.from(nodes)
                .filter((node) => node instanceof HTMLElement)
                .flatMap((element) =>
                    element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
                )
                .forEach(intersectionAction);

        const intersectionObserver = new IntersectionObserver((entries) => {
            const firstVisibleElement = entries.find((entry) => entry.isIntersecting);

            if (!(firstVisibleElement?.target instanceof HTMLElement)) {
                return;
            }

            dataRef.current = {
                ...dataRef.current,
                visibleElement: firstVisibleElement.target,
                visibleElementLeft: firstVisibleElement.target.offsetLeft,
                visibleElementTop: firstVisibleElement.target.offsetTop,
            };
        });
        const mutationObserver = new MutationObserver((entries) => {
            for (const entry of entries) {
                updateObservation(entry.addedNodes, (element) => intersectionObserver.observe(element));
                updateObservation(entry.removedNodes, (element) => intersectionObserver.unobserve(element));
            }
        });
        mutationObserver.observe(scrollElement, {
            childList: true,
            subtree: true,
        });
        return () => {
            mutationObserver.disconnect();
            intersectionObserver.disconnect();
        };
    }, []);

    return dataRef;
};

const usePreserveOnLeadingPageRender = (scrollElementRef: RefObject<HTMLElement | null>, readingMode: ReadingMode) => {
    const preservationData = useScrollPreservationData(scrollElementRef);

    const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
    const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);

    useEffect(() => {
        const scrollElement = scrollElementRef.current;

        if (!scrollElement || !isContinuousReadingModeActive) {
            return () => {};
        }

        const preserveScrollPosition: ResizeObserverCallback = (entries) => {
            const { left, top, visibleElement, visibleElementLeft, visibleElementTop } = preservationData.current;

            if (!visibleElement) {
                return;
            }

            const entriesBeforeScrollPosition = entries.filter((entry) => {
                if (!(entry.target instanceof HTMLElement)) {
                    return false;
                }

                const isPreloadPage = !entry.target.clientWidth && !entry.target.clientHeight;
                if (isPreloadPage) {
                    return false;
                }

                if (isContinuousVerticalReadingModeActive) {
                    return entry.target.offsetTop < top;
                }

                return Math.abs(entry.target.offsetLeft) < Math.abs(left);
            });

            const includesElementsBeforeScrollPosition = !!entriesBeforeScrollPosition.length;
            if (!includesElementsBeforeScrollPosition) {
                return;
            }

            const newLeft = left - visibleElementLeft + visibleElement.offsetLeft;
            const newTop = top - visibleElementTop + visibleElement.offsetTop;

            scrollElement.scrollTo(newLeft, newTop);
        };

        const updateObservation = (nodes: NodeList, resizeAction: (element: Element) => void) =>
            Array.from(nodes)
                .filter((node) => node instanceof HTMLElement)
                .flatMap((element) =>
                    element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
                )
                .forEach(resizeAction);

        const resizeObserver = new ResizeObserver(preserveScrollPosition);
        const mutationObserver = new MutationObserver((entries) => {
            for (const entry of entries) {
                updateObservation(entry.addedNodes, (element) => resizeObserver.observe(element));
                updateObservation(entry.removedNodes, (element) => resizeObserver.unobserve(element));
            }
        });

        mutationObserver.observe(scrollElement, {
            childList: true,
            subtree: true,
        });

        return () => {
            mutationObserver.disconnect();
            resizeObserver.disconnect();
        };
    }, [isContinuousReadingModeActive, isContinuousVerticalReadingModeActive]);
};

const usePreserveOnInfiniteScrollPreviousChapterInitialRender = (
    scrollElementRef: RefObject<HTMLElement | null>,
    currentChapterId: ChapterIdInfo['id'] | undefined,
    currentChapterIndex: number,
    currentPageIndex: number,
    chaptersToRender: TChapterReader[],
    visibleChapters: ReaderStateChapters['visibleChapters'],
    isContinuousReadingModeActive: boolean,
) => {
    const preservationDataRef = useScrollPreservationData(scrollElementRef);

    const preserveScrollPosition = (): boolean => {
        const scrollElement = scrollElementRef.current;
        const { left, top, visibleElementLeft, visibleElementTop, visibleElement } = preservationDataRef.current;

        if (!scrollElement || !isContinuousReadingModeActive || !visibleElement) {
            return false;
        }

        const previousNextChapterVisibility = getPreviousNextChapterVisibility(
            currentChapterIndex,
            chaptersToRender,
            visibleChapters,
        );

        const isRenderOfPreviousChapter = currentPageIndex === 0;

        // only relevant when prepending content to the dom due to the resulting layout shift
        const isFirstRenderOfPreviousChapter = !previousNextChapterVisibility.previous && isRenderOfPreviousChapter;
        if (!isFirstRenderOfPreviousChapter) {
            return false;
        }

        const newLeft = left - visibleElementLeft + visibleElement.offsetLeft;
        const newTop = top - visibleElementTop + visibleElement.offsetTop;

        scrollElement.scrollTo(newLeft, newTop);
        return true;
    };

    useEffect(() => {
        const scrollElement = scrollElementRef.current;
        if (!scrollElement || !isContinuousReadingModeActive) {
            return () => {};
        }

        let preservedScrollPosition = false;

        const updateObservation = (nodes: NodeList, resizeAction: (element: Element) => void) => {
            Array.from(nodes)
                .filter((node) => node instanceof HTMLElement)
                .flatMap((element) =>
                    element instanceof HTMLImageElement ? [element] : Array.from(element.querySelectorAll('img')),
                )
                .forEach(resizeAction);
        };

        const resizeObserver = new ResizeObserver(() => {
            if (preservedScrollPosition) {
                return;
            }

            preservedScrollPosition = preserveScrollPosition();
            if (preservedScrollPosition) {
                resizeObserver.disconnect();
            }
        });
        const mutationObserver = new MutationObserver((entries) => {
            for (const entry of entries) {
                updateObservation(entry.addedNodes, (element) => resizeObserver.observe(element));
                updateObservation(entry.removedNodes, (element) => resizeObserver.unobserve(element));
            }
        });

        mutationObserver.observe(scrollElement, {
            childList: true,
            subtree: true,
        });

        return () => {
            mutationObserver.disconnect();
            resizeObserver.disconnect();
        };
    }, [currentChapterId]);

    useLayoutEffect(() => {
        preserveScrollPosition();
    }, [currentChapterId]);
};

export const useReaderPreserveScrollPosition = (
    scrollElementRef: RefObject<HTMLElement | null>,
    currentChapterId: ChapterIdInfo['id'] | undefined,
    currentChapterIndex: number,
    currentPageIndex: number,
    chaptersToRender: TChapterReader[],
    visibleChapters: ReaderStateChapters['visibleChapters'],
    readingMode: ReadingMode,
    readingDirection: ReadingDirection,
    setPageToScrollToIndex: ReaderStatePages['setPageToScrollToIndex'],
    pageScaleMode: ReaderPageScaleMode,
) => {
    usePreserveOnInfiniteScrollPreviousChapterInitialRender(
        scrollElementRef,
        currentChapterId,
        currentChapterIndex,
        currentPageIndex,
        chaptersToRender,
        visibleChapters,
        isContinuousReadingMode(readingMode),
    );
    usePreserveOnLeadingPageRender(scrollElementRef, readingMode);
    usePreserveOnWindowResize(readingMode, pageScaleMode, setPageToScrollToIndex, currentPageIndex);
    usePreserveOnValueChange(readingDirection, currentPageIndex, setPageToScrollToIndex);
    usePreserveOnValueChange(readingMode, currentPageIndex, setPageToScrollToIndex);
};
