/*
 * 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 { MutableRefObject } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import { useLingui } from '@lingui/react/macro';
import type {
    IReaderSettings,
    ReaderPageBackgroundColor,
    ReaderPagerProps,
    ReaderPageSpreadState,
    ReaderResumeMode,
    ReaderStatePages,
} from '@/features/reader/Reader.types.ts';
import { ReaderTransitionPageMode, ReadingDirection, ReadingMode } from '@/features/reader/Reader.types.ts';
import { getDoublePageModePages } from '@/features/reader/viewer/pager/ReaderPager.utils.tsx';
import type { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
import {
    getPagerForReadingMode,
    isContinuousReadingMode,
    isContinuousVerticalReadingMode,
    shouldApplyReaderWidth,
} from '@/features/reader/settings/ReaderSettings.utils.tsx';
import { createHandleReaderPageLoadError, createUpdateReaderPageLoadState } from '@/features/reader/Reader.utils.ts';
import { useReaderConvertPagesForReadingMode } from '@/features/reader/viewer/hooks/useReaderConvertPagesForReadingMode.ts';
import { ReaderTransitionPage } from '@/features/reader/viewer/components/ReaderTransitionPage.tsx';
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { useReaderSetPagesState } from '@/features/reader/viewer/hooks/useReaderSetPagesState.ts';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { ReaderInfiniteScrollUpdateChapter } from '@/features/reader/infinite-scroll/ReaderInfiniteScrollUpdateChapter.tsx';
import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx';
import type { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
import { READER_DEFAULT_PAGES_STATE } from '@/features/reader/stores/ReaderPagesStore.ts';

import { getReaderChaptersStore, getReaderPagesStore } from '@/features/reader/stores/ReaderStore.ts';

const BaseReaderChapterViewer = ({
    currentPageIndex,
    updateCurrentPageIndex,
    transitionPageMode,
    retryFailedPagesKeyPrefix,
    readingMode,
    readerWidth,
    safeAreaInset,
    pageScaleMode,
    shouldOffsetDoubleSpreads,
    readingDirection,
    imagePreLoadAmount,
    pageGap,
    chapterId,
    previousChapterId,
    nextChapterId,
    isPreviousChapterVisible,
    isNextChapterVisible,
    lastPageRead,
    isInitialChapter,
    isCurrentChapter,
    isPreviousChapter,
    isNextChapter,
    isLeadingChapter,
    isTrailingChapter,
    isPreloadMode,
    imageRefs: globalImageRefs,
    scrollIntoView,
    resumeMode,
    customFilter,
    shouldStretchPage,
    readerNavBarWidth,
    onSizeChange,
    minWidth,
    minHeight,
    scrollElement,
    currentChapterRemainingPages,
}: Pick<ReaderStatePages, 'currentPageIndex' | 'transitionPageMode' | 'retryFailedPagesKeyPrefix'> &
    Omit<ReaderPagerProps, 'pages' | 'totalPages' | 'pageLoadStates' | 'handleAsInitialRender' | 'resumeMode'> &
    Pick<
        IReaderSettings,
        'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode'
    > & {
        updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
        chapterId: ChapterIdInfo['id'];
        previousChapterId?: ChapterIdInfo['id'];
        nextChapterId?: ChapterIdInfo['id'];
        isPreviousChapterVisible: boolean;
        isNextChapterVisible: boolean;
        lastPageRead: number;
        isInitialChapter: boolean;
        isCurrentChapter: boolean;
        isPreviousChapter: boolean;
        isNextChapter: boolean;
        isLeadingChapter: boolean;
        isTrailingChapter: boolean;
        imageRefs: MutableRefObject<(HTMLElement | null)[]>;
        scrollIntoView: boolean;
        resumeMode: ReaderResumeMode;
        onSizeChange: (width: number, height: number, chapterId: ChapterIdInfo['id']) => void;
        minWidth: number;
        minHeight: number;
        scrollElement: HTMLElement | null;
    }) => {
    const { t } = useLingui();
    const { direction: themeDirection } = useTheme();

    const [fetchPages, pagesResponse] = requestManager.useGetChapterPagesFetch(chapterId ?? -1);

    const [arePagesFetched, setArePagesFetched] = useState(false);
    const [totalPages, setTotalPages] = useState<ReaderStatePages['totalPages']>(READER_DEFAULT_PAGES_STATE.totalPages);
    const [pageUrls, setPageUrls] = useState<ReaderStatePages['pageUrls']>(READER_DEFAULT_PAGES_STATE.pageUrls);
    const [pages, setPages] = useState<ReaderStatePages['pages']>(READER_DEFAULT_PAGES_STATE.pages);
    const [pageLoadStates, setPageLoadStates] = useState<ReaderStatePages['pageLoadStates']>(
        READER_DEFAULT_PAGES_STATE.pageLoadStates,
    );
    const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
        READER_DEFAULT_PAGES_STATE.pageSpreadStates,
    );
    const [pageBackgroundColors, setPageBackgroundColors] = useState<ReaderPageBackgroundColor[]>(
        READER_DEFAULT_PAGES_STATE.pageBackgroundColors,
    );

    const ref = useRef<HTMLDivElement>(null);
    const isCurrentChapterRef = useRef(isCurrentChapter);
    const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
    const pagerRef = useRef<HTMLDivElement>(null);

    const actualPages = useMemo(() => {
        const arePagesLoaded = !!totalPages;
        if (!arePagesLoaded) {
            return pages;
        }

        const isSpreadStateUpdated = totalPages === pagesToSpreadState.length;
        if (!isSpreadStateUpdated) {
            return pages;
        }

        if (readingMode === ReadingMode.DOUBLE_PAGE) {
            return getDoublePageModePages(pageUrls, pagesToSpreadState, shouldOffsetDoubleSpreads, readingDirection);
        }

        return pages;
    }, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]);

    const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]);
    const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
    const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
    const shouldHideChapter = (!isContinuousReadingModeActive && !isCurrentChapter) || isPreloadMode;

    const isCurrentChapterInSinglePager = !isContinuousReadingModeActive && isCurrentChapter;
    const showPreviousTransitionPage =
        !shouldHideChapter &&
        (isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter)));
    const showNextTransitionPage =
        !shouldHideChapter &&
        (isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter)));

    isCurrentChapterRef.current = isCurrentChapter;
    if (isCurrentChapter) {
        // oxlint-disable-next-line no-param-reassign
        globalImageRefs.current = imageRefs.current;
    }

    const doFetchPages = useCallback(() => {
        if (!chapterId) {
            return;
        }

        setArePagesFetched(false);

        fetchPages({ variables: { input: { chapterId } } }).catch(
            defaultPromiseErrorHandler(`ReaderChapterViewer(${chapterId})::fetchPages`),
        );
    }, [fetchPages, chapterId]);

    const onLoad = useMemo(
        () =>
            createUpdateReaderPageLoadState(
                actualPages,
                (value) => {
                    if (isCurrentChapterRef.current) {
                        getReaderPagesStore().setPageSpreadStates(value);
                    }

                    setPagesToSpreadState(value);
                },
                (value) => {
                    if (isCurrentChapterRef.current) {
                        getReaderPagesStore().setPageLoadStates(value);
                    }

                    setPageLoadStates(value);
                },
                (value) => {
                    if (isCurrentChapterRef.current) {
                        getReaderPagesStore().setPageBackgroundColor(value);
                    }

                    setPageBackgroundColors(value);
                },
                readingMode,
            ),
        [actualPages, readingMode],
    );

    const onError = useMemo(
        () =>
            createHandleReaderPageLoadError((value) => {
                if (isCurrentChapterRef.current) {
                    getReaderPagesStore().setPageLoadStates(value);
                }

                setPageLoadStates(value);
            }),
        [],
    );

    useEffect(() => {
        doFetchPages();
    }, [chapterId]);

    useResizeObserver(
        ref,
        useCallback(
            (entries) => {
                const { clientWidth, clientHeight } = entries[0].target;
                onSizeChange(clientWidth, clientHeight, chapterId);
            },
            [onSizeChange, ref.current, chapterId],
        ),
    );

    const updateState = <T,>(
        value: T,
        setLocalState: (value: T) => void,
        setGlobalState: (value: T) => void,
        forceLocal: boolean = false,
    ) => {
        if (forceLocal || !arePagesFetched) {
            setLocalState(value);
        }

        if (isCurrentChapter) {
            setGlobalState(value);
        }
    };
    useReaderSetPagesState(
        isCurrentChapter,
        pagesResponse,
        resumeMode,
        lastPageRead,
        actualPages,
        pageLoadStates,
        pagesToSpreadState,
        pageBackgroundColors,
        arePagesFetched,
        setArePagesFetched,
        (value) => updateState(value, noOp, getReaderChaptersStore().setReaderStateChapters),
        (value) => updateState(value, setTotalPages, getReaderPagesStore().setTotalPages.bind(getReaderPagesStore())),
        (value) => updateState(value, setPages, getReaderPagesStore().setPages.bind(getReaderPagesStore())),
        (value) => updateState(value, setPageUrls, getReaderPagesStore().setPageUrls.bind(getReaderPagesStore())),
        (value) =>
            updateState(value, setPageLoadStates, getReaderPagesStore().setPageLoadStates.bind(getReaderPagesStore())),
        (value) =>
            updateState(
                value,
                setPagesToSpreadState,
                getReaderPagesStore().setPageSpreadStates.bind(getReaderPagesStore()),
            ),
        (value) =>
            updateState(
                value,
                setPageBackgroundColors,
                getReaderPagesStore().setPageBackgroundColor.bind(getReaderPagesStore()),
            ),
        (value) => updateState(value, noOp, getReaderPagesStore().setCurrentPageIndex.bind(getReaderPagesStore())),
        (value) => {
            if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
                getReaderPagesStore().setPageToScrollToIndex(value);
                getReaderChaptersStore().setReaderStateChapters((prevState) => ({
                    ...prevState,
                    visibleChapters: { ...prevState.visibleChapters, scrollIntoView: false, resumeMode: undefined },
                }));
            }
        },
        (value) => updateState(value, noOp, getReaderPagesStore().setTransitionPageMode.bind(getReaderPagesStore())),
    );

    useReaderConvertPagesForReadingMode(
        currentPageIndex,
        actualPages,
        pageUrls,
        (value) => updateState(value, setPages, getReaderPagesStore().setPages.bind(getReaderPagesStore()), true),
        (value) => updateState(value, setPagesToSpreadState, noOp, true),
        (value) => updateState(value, noOp, updateCurrentPageIndex),
        readingMode,
    );

    // for non-continuous reading modes, only the current, previous and next chapter are relevant
    // every other chapter does not need to be rendered all the time since it's not affecting the
    // visible content anyway
    // the previous and next chapter are rendered so that going to the previous/next chapter feels smoother
    // since the relevant pages are already rendered
    const shouldRenderChapterViewer =
        isContinuousReadingModeActive || isCurrentChapter || isPreviousChapter || isNextChapter;
    if (!shouldRenderChapterViewer) {
        return null;
    }

    if (pagesResponse.error) {
        if (shouldHideChapter) {
            return null;
        }

        return (
            <Box
                sx={{
                    minHeight: '100%',
                    minWidth: '100%',
                    display: 'grid',
                    placeItems: 'center',
                    position: 'relative',
                }}
            >
                <EmptyViewAbsoluteCentered
                    message={t`Unable to load data`}
                    messageExtra={getErrorMessage(pagesResponse.error)}
                    retry={() => {
                        doFetchPages();
                    }}
                />
            </Box>
        );
    }

    if (pagesResponse.loading || !arePagesFetched) {
        if (shouldHideChapter) {
            return null;
        }

        return (
            <Box
                sx={{
                    minHeight: '100%',
                    minWidth: '100%',
                    display: 'grid',
                    placeItems: 'center',
                }}
            >
                <LoadingPlaceholder />
            </Box>
        );
    }

    if (chapterId != null && !totalPages) {
        if (shouldHideChapter) {
            return null;
        }

        return (
            <Box sx={{ minWidth: '100%', minHeight: '100%', position: 'relative' }}>
                <EmptyViewAbsoluteCentered message={t`No pages found`} retry={doFetchPages} />
            </Box>
        );
    }

    return (
        <Stack
            ref={ref}
            sx={{
                width: 'fit-content',
                height: 'fit-content',
                margin: 'auto',
                flexWrap: 'nowrap',
                ...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
                    minHeight,
                }),
                ...applyStyles(isContinuousVerticalReadingMode(readingMode), {
                    minWidth,
                }),
                ...applyStyles(shouldHideChapter, {
                    maxWidth: 0,
                    maxHeight: 0,
                    minWidth: 'unset',
                    minHeight: 'unset',
                    overflow: 'hidden',
                    margin: 'unset',
                }),
                ...applyStyles(
                    isContinuousVerticalReadingMode(readingMode) && shouldApplyReaderWidth(readerWidth, pageScaleMode),
                    { alignItems: 'center' },
                ),
                ...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
                    ...applyStyles(themeDirection === 'ltr', {
                        flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
                    }),
                    ...applyStyles(themeDirection === 'rtl', {
                        flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
                    }),
                }),
            }}
        >
            {!isPreloadMode && (
                <ReaderInfiniteScrollUpdateChapter
                    chapterId={chapterId}
                    previousChapterId={previousChapterId}
                    nextChapterId={nextChapterId}
                    isCurrentChapter={isCurrentChapter}
                    isPreviousChapterVisible={isPreviousChapterVisible}
                    isNextChapterVisible={isNextChapterVisible}
                    imageWrapper={pagerRef.current}
                    scrollElement={scrollElement}
                />
            )}
            {showPreviousTransitionPage && (
                <ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.PREVIOUS} />
            )}
            <Pager
                ref={pagerRef}
                totalPages={totalPages}
                currentPageIndex={currentPageIndex}
                pages={actualPages}
                transitionPageMode={transitionPageMode}
                pageLoadStates={pageLoadStates}
                retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
                imageRefs={imageRefs}
                onLoad={onLoad}
                onError={onError}
                isCurrentChapter={isCurrentChapter}
                isPreviousChapter={isPreviousChapter}
                isNextChapter={isNextChapter}
                readingMode={readingMode}
                imagePreLoadAmount={imagePreLoadAmount}
                readingDirection={readingDirection}
                pageScaleMode={pageScaleMode}
                pageGap={pageGap}
                customFilter={customFilter}
                shouldStretchPage={shouldStretchPage}
                readerWidth={readerWidth}
                safeAreaInset={safeAreaInset}
                readerNavBarWidth={readerNavBarWidth}
                isPreloadMode={isPreloadMode}
                resumeMode={resumeMode}
                handleAsInitialRender={scrollIntoView}
                currentChapterRemainingPages={currentChapterRemainingPages}
            />
            {showNextTransitionPage && (
                <ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.NEXT} />
            )}
        </Stack>
    );
};

export const ReaderChapterViewer = memo(BaseReaderChapterViewer);
