/*
 * 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 { MessageDescriptor } from '@lingui/core';
import type { MutableRefObject, RefObject } from 'react';
import { useCallback, useEffect } from 'react';
import type { Direction } from '@mui/material/styles';
import { d } from 'koration';
import { msg, t } from '@lingui/core/macro';
import {
    getIndexOfPage,
    getNextPageIndex,
    getPage,
    getPageForMousePos,
    getProgressBarPositionInfo,
} from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
import type {
    PageInViewportType,
    ProgressBarPosition,
    ReaderStatePages,
    ReadingDirection,
} from '@/features/reader/Reader.types.ts';
import { ReaderScrollAmount, ReaderTransitionPageMode, ReadingMode } from '@/features/reader/Reader.types.ts';
import { DirectionOffset, ScrollDirection, ScrollOffset } from '@/base/Base.types';
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/features/reader/settings/ReaderSettings.constants.tsx';
import {
    isATransitionPageVisible,
    isEndOfPageInViewport,
    isPageInViewport,
} from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
import { TapZoneRegionType } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
import { ReaderTapZoneService } from '@/features/reader/tap-zones/ReaderTapZoneService.ts';
import { isContinuousReadingMode } from '@/features/reader/settings/ReaderSettings.utils.tsx';
import {
    getReaderChapterFromCache,
    getReaderOpenChapterResumeMode,
    updateReaderStateVisibleChapters,
} from '@/features/reader/Reader.utils.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import type { ChapterIdInfo, TChapterReader } from '@/features/chapter/Chapter.types.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import type { TReaderProgressCurrentPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
import {
    getReaderChaptersStore,
    getReaderOverlayStore,
    getReaderPagesStore,
    getReaderSettingsStore,
    getReaderStore,
    getReaderTapZoneStore,
} from '@/features/reader/stores/ReaderStore.ts';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
import { i18n } from '@/i18n';

const getScrollDirectionInvert = (
    scrollDirection: ScrollDirection,
    scrollOffset: ScrollOffset,
    themeDirection: Direction,
): 1 | -1 => {
    if (scrollDirection === ScrollDirection.X) {
        if (scrollOffset === ScrollOffset.BACKWARD) {
            return getOptionForDirection(-1, 1, themeDirection);
        }

        if (scrollOffset === ScrollOffset.FORWARD) {
            return getOptionForDirection(1, -1, themeDirection);
        }
    }

    // handle ScrollDirection.XY the same as ScrollDirection.Y
    if (scrollOffset === ScrollOffset.BACKWARD) {
        return -1;
    }

    return 1;
};

class ReaderControlsClass {
    private updateCurrentPageTimeout: NodeJS.Timeout | undefined;

    private windowFocus: boolean = true;

    private windowFocusUpdateImminent = false;

    constructor() {
        let focusTimeout: NodeJS.Timeout | undefined;

        window.addEventListener('blur', () => {
            clearTimeout(focusTimeout);
            this.windowFocus = false;
            this.windowFocusUpdateImminent = false;
        });
        window.addEventListener('focus', () => {
            this.windowFocusUpdateImminent = true;

            clearTimeout(focusTimeout);
            focusTimeout = setTimeout(() => {
                this.windowFocus = true;
                this.windowFocusUpdateImminent = false;
            }, d(250).milliseconds.inWholeMilliseconds);
        });
    }

    private checkAndUpdateWindowsFocus(): boolean {
        const hasFocus = this.windowFocus;

        if (this.windowFocusUpdateImminent) {
            this.windowFocus = true;
            this.windowFocusUpdateImminent = false;
        }

        return hasFocus;
    }

    scroll(
        offset: ScrollOffset,
        direction: ScrollDirection,
        readingMode: ReadingMode,
        readingDirection: ReadingDirection,
        element: HTMLElement,
        scrollAmountPercentage: number = ReaderScrollAmount.LARGE,
    ): void {
        if (!element) {
            return;
        }

        const themeDirectionOfReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
        const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);

        const isAtStartY = Math.abs(element.scrollTop) <= 1;
        const isAtEndY =
            Math.floor(element.scrollTop) === element.scrollHeight - element.clientHeight ||
            Math.ceil(element.scrollTop) === element.scrollHeight - element.clientHeight;
        const isAtStartX = Math.floor(Math.abs(element.scrollLeft)) === 0;
        const isAtEndX =
            Math.floor(Math.abs(element.scrollLeft)) === element.scrollWidth - element.clientWidth ||
            Math.ceil(Math.abs(element.scrollLeft)) === element.scrollWidth - element.clientWidth;

        const scrollAmount = scrollAmountPercentage / 100;
        const scrollDirection = getScrollDirectionInvert(direction, offset, themeDirectionOfReadingDirection);

        const getNewScrollPosition = (currentPos: number, elementSize: number) =>
            currentPos + elementSize * scrollAmount * scrollDirection;

        getReaderOverlayStore().setIsVisible(false);
        getReaderTapZoneStore().setShowPreview(false);

        const doScroll = (
            isAtStartForDirection: boolean,
            isAtEndForDirection: boolean,
            scrollToOptions: ScrollToOptions,
        ) => {
            if (isAtStartForDirection && offset === ScrollOffset.BACKWARD && isContinuousReadingModeActive) {
                this.openChapter('previous');
                return;
            }

            if (isAtEndForDirection && offset === ScrollOffset.FORWARD && isContinuousReadingModeActive) {
                this.openChapter('next');
                return;
            }

            element.scroll({
                ...scrollToOptions,
                behavior: 'smooth',
            });
        };

        switch (direction) {
            case ScrollDirection.X:
                doScroll(isAtStartX, isAtEndX, {
                    left: getNewScrollPosition(element.scrollLeft, element.clientWidth),
                });
                break;
            case ScrollDirection.Y:
                doScroll(isAtStartY, isAtEndY, { top: getNewScrollPosition(element.scrollTop, element.clientHeight) });
                break;
            default:
                throw new Error(`Unexpected "ScrollDirection" (${direction})`);
        }
    }

    openChapter(
        offset: 'previous' | 'next' | ChapterIdInfo['id'],
        doTransitionCheck: boolean = true,
        scrollIntoView: boolean = true,
    ): void {
        const {
            currentChapter,
            previousChapter,
            nextChapter,
            chapters,
            visibleChapters: { lastLeadingChapterSourceOrder, lastTrailingChapterSourceOrder },
            setReaderStateChapters,
        } = getReaderChaptersStore();
        const { shouldInformAboutMissingChapter, shouldInformAboutScanlatorChange, shouldUseInfiniteScroll } =
            getReaderSettingsStore();

        if (!currentChapter) {
            return;
        }

        const isSpecificChapterMode = typeof offset === 'number';
        const isPreviousOffset = offset === 'previous';

        const doesPreviousChapterExist = isPreviousOffset && !!previousChapter;
        const doesNextChapterExist = !isPreviousOffset && !!nextChapter;

        const canOpenNextChapter = isSpecificChapterMode || doesPreviousChapterExist || doesNextChapterExist;
        if (!canOpenNextChapter) {
            return;
        }

        const doOpenChapter = async () => {
            const chapterToOpen = (() => {
                if (isSpecificChapterMode) {
                    return chapters.find((chapter) => chapter.id === offset);
                }

                if (isPreviousOffset) {
                    return previousChapter;
                }

                return nextChapter;
            })();

            if (!chapterToOpen) {
                return;
            }

            const isPreviousChapter = chapterToOpen.sourceOrder < currentChapter.sourceOrder;

            try {
                if (doTransitionCheck) {
                    await this.checkNextChapterConsistency(
                        isPreviousChapter ? 'previous' : 'next',
                        currentChapter,
                        chapterToOpen,
                        shouldInformAboutMissingChapter,
                        shouldInformAboutScanlatorChange,
                    );
                }

                const isAlreadyLoaded =
                    lastLeadingChapterSourceOrder <= chapterToOpen.sourceOrder &&
                    lastTrailingChapterSourceOrder >= chapterToOpen.sourceOrder;
                const keepRenderedChapters = shouldUseInfiniteScroll && (!scrollIntoView || isAlreadyLoaded);

                if (keepRenderedChapters) {
                    setReaderStateChapters((prevState) =>
                        updateReaderStateVisibleChapters(
                            isPreviousChapter,
                            prevState,
                            chapterToOpen.sourceOrder,
                            scrollIntoView,
                            isPreviousChapter ? false : undefined,
                            !isPreviousChapter ? false : undefined,
                        ),
                    );
                }

                ReaderService.navigateToChapter(chapterToOpen, {
                    resumeMode: getReaderOpenChapterResumeMode(
                        isSpecificChapterMode || !keepRenderedChapters,
                        isPreviousChapter,
                    ),
                    updateInitialChapter: !keepRenderedChapters,
                });
            } catch (error) {
                defaultPromiseErrorHandler('ReaderControls#useOpenChapter#doOpenChapter:')(error);
            }
        };

        doOpenChapter().catch(defaultPromiseErrorHandler('ReaderControls#useOpenChapter'));
    }

    private async checkNextChapterConsistency(
        offset: 'previous' | 'next',
        currentChapter: TChapterReader | null | undefined,
        chapterToOpen: TChapterReader | null | undefined,
        shouldInformAboutMissingChapter: boolean,
        shouldInformAboutScanlatorChange: boolean,
    ): Promise<void> {
        if (!currentChapter || !chapterToOpen) {
            return;
        }

        const missingChapters = Chapters.getGap(currentChapter, chapterToOpen);
        const isSameScanlator =
            !shouldInformAboutScanlatorChange || currentChapter.scanlator === chapterToOpen.scanlator;
        const isContinuousChapter = !shouldInformAboutMissingChapter || !missingChapters;

        const showWarning = !isSameScanlator || !isContinuousChapter;
        if (!showWarning) {
            return;
        }

        const offsetToTranslations: Record<typeof offset, Record<'scanlator' | 'chapter_number', MessageDescriptor>> = {
            previous: {
                scanlator: msg`The previous chapter has a different scanlator then the current one.\nPrevious: {nextScanlator}\nCurrent: {currentScanlator}`,
                chapter_number: msg`{count, plural, one {There is # missing chapter.\nPrevious: {nextChapter}\nCurrent: {currentChapter}} other {There are # missing chapters.\nPrevious: {nextChapter}\nCurrent: {currentChapter}}}`,
            },
            next: {
                scanlator: msg`The next chapter has a different scanlator then the current one.\nCurrent: {currentScanlator}\nNext: {nextScanlator}`,
                chapter_number: msg`{count, plural, one {There is # missing chapter.\nCurrent: {currentChapter}\nNext: {nextChapter}} other {There are # missing chapters.\nCurrent: {currentChapter}\nNext: {nextChapter}}}`,
            },
        };

        const sameScanlator = isSameScanlator
            ? ''
            : /* lingui-extract-ignore */
              i18n.t({
                  ...offsetToTranslations[offset].scanlator,
                  values: {
                      nextScanlator: chapterToOpen.scanlator,
                      currentScanlator: currentChapter.scanlator,
                  },
              });
        const continuousChapter = isContinuousChapter
            ? ''
            : /* lingui-extract-ignore */
              i18n.t({
                  ...offsetToTranslations[offset].chapter_number,
                  values: {
                      count: missingChapters,
                      nextChapter: `#${chapterToOpen.chapterNumber} ${chapterToOpen.name}`,
                      currentChapter: `#${currentChapter.chapterNumber} ${currentChapter.name}`,
                  },
              });
        const warningLineBreak = !isSameScanlator && !isContinuousChapter ? '\n\n' : '';
        const warning = `${sameScanlator}${warningLineBreak}${continuousChapter}`;

        await Confirmation.show({
            title: t`Chapter transition warning`,
            message: warning,
            actions: {
                confirm: {
                    title: t`Open`,
                },
            },
        });
    }

    openPage(page: number | 'previous' | 'next', forceDirection?: Direction, hideOverlay: boolean = true): void {
        const { currentPageIndex, setPageToScrollToIndex, pages, transitionPageMode, setTransitionPageMode } =
            getReaderPagesStore();
        const { readingDirection, readingMode, shouldShowTransitionPage } = getReaderSettingsStore();

        const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection.value];

        const convertedPage = getOptionForDirection(
            page,
            page === 'previous' ? 'next' : 'previous',
            forceDirection ?? direction,
        );

        const currentPage = getPage(currentPageIndex, pages);
        const previousPageIndex = getNextPageIndex('previous', currentPage.pagesIndex, pages);
        const nextPageIndex = getNextPageIndex('next', currentPage.pagesIndex, pages);
        const indexOfFirstPage = getIndexOfPage(pages[0]);
        const indexOfLastPage = getIndexOfPage(pages[pages.length - 1]);

        const isFirstPage = currentPage.primary.index === 0;
        const isLastPage = currentPageIndex === indexOfLastPage;
        const isATransitionPageVisibleFlag = isATransitionPageVisible(transitionPageMode, readingMode.value);
        const isContinuousReadingModeActive = isContinuousReadingMode(readingMode.value);

        if (hideOverlay) {
            getReaderOverlayStore().setIsVisible(false);
            getReaderTapZoneStore().setShowPreview(false);
        }

        const hideTransitionPage = () => setTransitionPageMode(ReaderTransitionPageMode.NONE);

        if (typeof page === 'number') {
            setPageToScrollToIndex(page);
            hideTransitionPage();
            return;
        }

        const areContinuousPagerTransitionPagesVisible = isContinuousReadingModeActive && isATransitionPageVisibleFlag;
        const isPreviousTransitionPageVisible =
            (!isContinuousReadingModeActive && transitionPageMode === ReaderTransitionPageMode.PREVIOUS) ||
            areContinuousPagerTransitionPagesVisible;
        const isNextTransitionPageVisible =
            (!isContinuousReadingModeActive && transitionPageMode === ReaderTransitionPageMode.NEXT) ||
            areContinuousPagerTransitionPagesVisible;

        const shouldOpenPreviousChapter =
            isFirstPage &&
            (!shouldShowTransitionPage || isPreviousTransitionPageVisible) &&
            convertedPage === 'previous' &&
            !!getReaderChaptersStore().previousChapter;
        if (shouldOpenPreviousChapter) {
            this.openChapter('previous');
            return;
        }

        const shouldOpenNextChapter =
            isLastPage &&
            (!shouldShowTransitionPage || isNextTransitionPageVisible) &&
            convertedPage === 'next' &&
            !!getReaderChaptersStore().nextChapter;
        if (shouldOpenNextChapter) {
            this.openChapter('next');
            return;
        }

        const isPreviousMode = convertedPage === 'previous';
        const isNextMode = convertedPage === 'next';

        const closePreviousTransitionPage = isPreviousTransitionPageVisible && isNextMode;
        const closeNextTransitionPage = isNextTransitionPageVisible && isPreviousMode;

        const needToHideTransitionPage =
            isATransitionPageVisibleFlag &&
            !isContinuousReadingModeActive &&
            (closePreviousTransitionPage || closeNextTransitionPage);
        if (needToHideTransitionPage) {
            hideTransitionPage();
            setPageToScrollToIndex(isPreviousTransitionPageVisible ? indexOfFirstPage : indexOfLastPage);

            return;
        }

        const needToOpenTransitionPage =
            ((isFirstPage && isPreviousMode) || (isLastPage && isNextMode)) && !isContinuousReadingModeActive;
        if (needToOpenTransitionPage) {
            setTransitionPageMode(isPreviousMode ? ReaderTransitionPageMode.PREVIOUS : ReaderTransitionPageMode.NEXT);
            return;
        }

        setPageToScrollToIndex(isPreviousMode ? previousPageIndex : nextPageIndex);
    }

    useUpdateCurrentPageIndex(): (pageIndex: number, debounceChapterUpdate?: boolean, endReached?: boolean) => void {
        const updateChapter = ReaderService.useUpdateChapter();
        const {
            settings: { downloadAheadLimit },
        } = useMetadataServerSettings();

        return useCallback(
            (pageIndex, debounceChapterUpdate = true, endReached = false) => {
                const { currentPageIndex, setCurrentPageIndex } = getReaderPagesStore();
                const { currentChapter, chapters, previousChapter, nextChapter, visibleChapters } =
                    getReaderChaptersStore();

                if (pageIndex === currentPageIndex && !endReached) {
                    return;
                }

                setCurrentPageIndex(pageIndex);

                if (!currentChapter) {
                    return;
                }

                const nextChapters = Chapters.getNextChapters(currentChapter, chapters, {
                    offset: DirectionOffset.NEXT,
                });

                const direction = currentPageIndex > pageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT;

                ReaderService.downloadAhead(currentChapter, nextChapter, nextChapters, pageIndex, downloadAheadLimit);
                ReaderService.preloadChapter(
                    pageIndex,
                    currentChapter.pageCount,
                    direction === DirectionOffset.NEXT ? nextChapter : previousChapter,
                    visibleChapters.lastLeadingChapterSourceOrder,
                    visibleChapters.lastTrailingChapterSourceOrder,
                    direction,
                );

                const handleCurrentPageIndexChange = () => {
                    const currentChapterUpToDate = getReaderChapterFromCache(currentChapter.id);
                    if (!currentChapterUpToDate) {
                        return;
                    }

                    const actualPageIndex = endReached ? currentChapterUpToDate.pageCount - 1 : pageIndex;
                    const isLastPage = actualPageIndex === currentChapterUpToDate.pageCount - 1;

                    updateChapter({
                        lastPageRead: actualPageIndex,
                        isRead: isLastPage ? true : undefined,
                    });
                };

                clearTimeout(this.updateCurrentPageTimeout);
                if (debounceChapterUpdate) {
                    this.updateCurrentPageTimeout = setTimeout(
                        handleCurrentPageIndexChange,
                        d(1).seconds.inWholeMilliseconds,
                    );
                    return;
                }

                handleCurrentPageIndexChange();
            },
            [downloadAheadLimit],
        );
    }

    updateCurrentPageOnScroll(
        imageRefs: MutableRefObject<(HTMLElement | null)[]>,
        lastPageIndex: number,
        updateCurrentPageIndex: ReturnType<typeof this.useUpdateCurrentPageIndex>,
        type: PageInViewportType,
        readingDirection: ReadingDirection,
    ) {
        const themeDirectionOfReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
        // In case the reader navigation bar is static, the readers "viewport" x position gets moved, thus, this needs to be considered as a threshold.
        const scrollContainerX = getReaderStore().autoScroll.scrollRef?.current?.getBoundingClientRect().x ?? 0;
        const pageHorizontalEndInViewportThreshold = getOptionForDirection(
            scrollContainerX + 1,
            window.innerWidth + 1,
            themeDirectionOfReadingDirection,
        );

        const firstVisibleImageIndex = imageRefs.current.findIndex(
            (image) =>
                image &&
                isPageInViewport(image, type, {
                    bounds: {
                        bottom: {
                            min: 1,
                        },
                        left: {
                            min: getOptionForDirection(
                                0,
                                pageHorizontalEndInViewportThreshold,
                                themeDirectionOfReadingDirection,
                            ),
                        },
                        right: {
                            min: getOptionForDirection(
                                pageHorizontalEndInViewportThreshold,
                                0,
                                themeDirectionOfReadingDirection,
                            ),
                        },
                    },
                }),
        );
        const lastPage = imageRefs.current?.[imageRefs.current.length - 1];
        const isEndReached = lastPage && isEndOfPageInViewport(lastPage, type, readingDirection);

        if (firstVisibleImageIndex === -1) {
            return;
        }

        // handle cases where the last page is too small to ever be the "firstVisibleImageIndex"
        if (isEndReached) {
            updateCurrentPageIndex(firstVisibleImageIndex, false, true);
            return;
        }

        updateCurrentPageIndex(firstVisibleImageIndex, firstVisibleImageIndex !== lastPageIndex);
    }

    handleClick(scrollElement: HTMLElement | null, e: React.MouseEvent<HTMLDivElement, MouseEvent>): void {
        if (!scrollElement || !this.checkAndUpdateWindowsFocus()) {
            return;
        }

        const { readingMode, readingDirection, isStaticNav, scrollAmount } = getReaderSettingsStore();

        const rect = e.currentTarget.getBoundingClientRect();
        const rectRelativeX = e.clientX - rect.left;
        const rectRelativeY = e.clientY - rect.top;
        const action = ReaderTapZoneService.getAction(rectRelativeX, rectRelativeY);

        getReaderTapZoneStore().setShowPreview(false);

        const isContinuousReadingModeActive = isContinuousReadingMode(readingMode.value);
        const scrollDirection =
            readingMode.value === ReadingMode.CONTINUOUS_HORIZONTAL ? ScrollDirection.X : ScrollDirection.Y;

        switch (action) {
            case TapZoneRegionType.MENU:
                getReaderOverlayStore().setIsVisible(isStaticNav || !getReaderOverlayStore().isVisible);
                break;
            case TapZoneRegionType.PREVIOUS:
            case TapZoneRegionType.NEXT:
                if (isContinuousReadingModeActive) {
                    this.scroll(
                        action === TapZoneRegionType.PREVIOUS ? ScrollOffset.BACKWARD : ScrollOffset.FORWARD,
                        scrollDirection,
                        readingMode.value,
                        readingDirection.value,
                        scrollElement,
                        scrollAmount,
                    );
                } else {
                    this.openPage(action === TapZoneRegionType.PREVIOUS ? 'previous' : 'next', 'ltr');
                }
                break;
            default:
                throw new Error(`Unexpected "TapZoneRegionType" (${action})`);
        }
    }

    useHandleProgressDragging(
        progressBarRef: RefObject<HTMLDivElement | null>,
        isDragging: boolean,
        currentPage: TReaderProgressCurrentPage,
        pages: ReaderStatePages['pages'],
        progressBarPosition: ProgressBarPosition,
        getOptionForDirectionFn: typeof getOptionForDirection,
        fullSegmentClicks: boolean,
    ): void {
        useEffect(() => {
            if (!isDragging) {
                return () => undefined;
            }

            const { isHorizontal } = getProgressBarPositionInfo(progressBarPosition);

            const handleMove = (coordinates: { clientX: number; clientY: number }) => {
                if (!progressBarRef.current) {
                    return;
                }

                const newPageIndex = getIndexOfPage(
                    getPageForMousePos(
                        coordinates,
                        progressBarRef.current,
                        pages,
                        isHorizontal,
                        fullSegmentClicks,
                        getOptionForDirectionFn,
                    ),
                );

                const hasCurrentPageIndexChanged = getIndexOfPage(currentPage) !== newPageIndex;
                if (!hasCurrentPageIndexChanged) {
                    return;
                }

                this.openPage(newPageIndex, undefined, false);
            };

            const handleMouseMove = (e: MouseEvent) => {
                handleMove(e);
            };

            const handleTouchMove = (e: TouchEvent) => {
                if (e.touches.length > 0) {
                    handleMove(e.touches[0]);
                }
            };

            document.addEventListener('mousemove', handleMouseMove, { passive: true });
            document.addEventListener('touchmove', handleTouchMove, { passive: true });

            return () => {
                document.removeEventListener('mousemove', handleMouseMove);
                document.removeEventListener('touchmove', handleTouchMove);
            };
        }, [isDragging, currentPage, pages, progressBarPosition]);
    }
}

export const ReaderControls = new ReaderControlsClass();
