/*
 * 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 type { RefObject } from 'react';
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import type {
    IReaderSettingsManga,
    ReaderPageScaleMode,
    ReaderStateChapters,
    ReadingMode,
} from '@/features/reader/Reader.types.ts';
import { ReadingDirection } from '@/features/reader/Reader.types.ts';
import {
    isContinuousReadingMode,
    isContinuousVerticalReadingMode,
    isHeightPageScaleMode,
    isWidthPageScaleMode,
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
import { getPreviousNextChapterVisibility } from '@/features/reader/Reader.utils.ts';
import type { ChapterIdInfo, TChapterReader } from '@/features/chapter/Chapter.types.ts';
import { getReaderPagesStore, getReaderSettingsStore } from '@/features/reader/stores/ReaderStore.ts';

const shouldPreserveOnResizeChange = (
    readingMode: ReadingMode,
    pageScaleMode: ReaderPageScaleMode,
    previousWidth: number,
    previousHeight: number,
): boolean => {
    if (!isContinuousReadingMode(readingMode)) {
        return false;
    }

    const isWidthPageScaleModeActive = isWidthPageScaleMode(pageScaleMode);
    const isHeightPageScaleModeActive = isHeightPageScaleMode(pageScaleMode);

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

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

    return handleWidthChange || handleHeightChange;
};

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

const usePreserveOnWindowResize = (readingMode: ReadingMode, pageScaleMode: ReaderPageScaleMode, pageIndex: number) => {
    const previousDimensionsRef = useRef({ width: window.innerWidth, height: window.innerHeight });
    const isResizeInProgressRef = useRef(false);
    const activeResizeTimeoutRef = useRef<NodeJS.Timeout>(undefined);
    const pageIndexOnResizeStartRef = useRef<number | null>(null);

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

        if (!isResizeInProgressRef.current) {
            isResizeInProgressRef.current = true;
            pageIndexOnResizeStartRef.current = pageIndex;
        }

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

        getReaderPagesStore().setPageToScrollToIndex(pageIndex);

        clearTimeout(activeResizeTimeoutRef.current);
        activeResizeTimeoutRef.current = setTimeout(() => {
            isResizeInProgressRef.current = false;
            pageIndexOnResizeStartRef.current = null;
        }, 50);
    }, [readingMode, pageScaleMode, pageIndex]);

    // TODO - revert back to mantines useWindowEvent hook once the issue of it using a stale callback can be fixed
    useEffect(() => {
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
    }, [handleResize]);
};

interface ScrollPreservationInfo {
    left: number;
    top: number;
    visibleElement: HTMLElement | undefined;
    visibleElementOffsetLeft: number;
    visibleElementOffsetTop: number;
}

const useScrollPreservationData = (
    scrollElementRef: RefObject<HTMLElement | null>,
): RefObject<ScrollPreservationInfo> => {
    const dataRef = useRef<ScrollPreservationInfo>({
        left: 0,
        top: 0,
        visibleElement: undefined,
        visibleElementOffsetLeft: 0,
        visibleElementOffsetTop: 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,
                visibleElementOffsetLeft: visibleElement?.offsetLeft ?? 0,
                visibleElementOffsetTop: visibleElement?.offsetTop ?? 0,
            };
        };

        scrollElement.addEventListener('scroll', onScroll, { passive: true });

        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,
                visibleElementOffsetLeft: firstVisibleElement.target.offsetLeft,
                visibleElementOffsetTop: 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,
        });

        updateObservation(scrollElement.childNodes, (element) => intersectionObserver.observe(element));

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

    return dataRef;
};

const usePreserveOnLeadingPageRender = (
    scrollElementRef: RefObject<HTMLElement | null>,
    readingMode: ReadingMode,
    readingDirection: ReadingDirection,
) => {
    const preservationDataRef = 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, visibleElementOffsetLeft, visibleElementOffsetTop } =
                preservationDataRef.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 readingDirection === ReadingDirection.LTR
                    ? entry.target.offsetLeft < left
                    : entry.target.offsetLeft > left;
            });

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

            const newLeft = left - visibleElementOffsetLeft + visibleElement.offsetLeft;
            const newTop = top - visibleElementOffsetTop + 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,
        });

        updateObservation(scrollElement.childNodes, (element) => resizeObserver.observe(element));

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

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, visibleElementOffsetLeft, visibleElementOffsetTop, visibleElement } =
            preservationDataRef.current;

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

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

        const doesPreviousChapterExist = currentChapterIndex > 0;
        const isRenderOfPreviousChapter = currentPageIndex === 0;

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

        const newLeft = left - visibleElementOffsetLeft + visibleElement.offsetLeft;
        const newTop = top - visibleElementOffsetTop + 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,
        });

        updateObservation(scrollElement.childNodes, (element) => resizeObserver.observe(element));

        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,
    pageScaleMode: ReaderPageScaleMode,
    shouldStretchPage: boolean,
    readerWidth: IReaderSettingsManga['readerWidth'],
) => {
    usePreserveOnInfiniteScrollPreviousChapterInitialRender(
        scrollElementRef,
        currentChapterId,
        currentChapterIndex,
        currentPageIndex,
        chaptersToRender,
        visibleChapters,
        isContinuousReadingMode(readingMode),
    );
    usePreserveOnLeadingPageRender(scrollElementRef, readingMode, readingDirection);
    usePreserveOnWindowResize(readingMode, pageScaleMode, currentPageIndex);
    usePreserveOnValueChange(readingDirection, currentPageIndex);
    usePreserveOnValueChange(readingMode, currentPageIndex);
    usePreserveOnValueChange(pageScaleMode, currentPageIndex);
    usePreserveOnValueChange(shouldStretchPage, currentPageIndex);
    usePreserveOnValueChange(`${readerWidth.value}_${readerWidth.enabled}`, currentPageIndex);
};
