/* eslint-disable max-lines */
import { Renderer } from '@tldraw/core';
import { shapeUtils } from '@toeverything/components/board-shapes';
import {
    TLDR,
    TldrawApp,
    TldrawAppCtorProps,
} from '@toeverything/components/board-state';
import {
    GRID_SIZE,
    TDDocument,
    TDMeta,
    TDStatus,
} from '@toeverything/components/board-types';
import { styled } from '@toeverything/components/ui';
// import { FocusButton } from '~components/FocusButton';
import { usePageClientWidth } from '@toeverything/datasource/state';
import {
    memo,
    useEffect,
    useLayoutEffect,
    useMemo,
    useRef,
    useState,
    type RefObject,
} from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { CommandPanel } from './components/command-panel';
// import { TopPanel } from '~components/TopPanel';
import { ContextMenu } from './components/context-menu';
import { ErrorFallback } from './components/error-fallback';
import { Loading } from './components/loading';
import { ToolsPanel } from './components/tools-panel';
import { ZoomBar } from './components/zoom-bar';
import {
    TldrawContext,
    useKeyboardShortcuts,
    useStylesheet,
    useTldrawApp,
} from './hooks';

export interface TldrawProps extends TldrawAppCtorProps {
    /**
     * (optional) If provided, the component will load / persist state under this key.
     */
    id?: string;

    /**
     * (optional) The document to load or update from.
     */
    document?: TDDocument;

    /**
     * (optional) The current page id.
     */
    currentPageId?: string;

    /**
     * (optional) Whether the editor should immediately receive focus. Defaults to true.
     */
    autofocus?: boolean;

    /**
     * (optional) Whether to show the menu UI.
     */
    showMenu?: boolean;

    /**
     * (optional) Whether to show the multiplayer menu.
     */
    showMultiplayerMenu?: boolean;
    /**
     * (optional) Whether to show the pages UI.
     */
    showPages?: boolean;

    /**
     * (optional) Whether to show the styles UI.
     */
    showStyles?: boolean;

    /**
     * (optional) Whether to show the zoom UI.
     */
    showZoom?: boolean;

    /**
     * (optional) Whether to show the tools UI.
     */
    showTools?: boolean;

    /**
     * (optional) Whether to show a sponsor link for Tldraw.
     */
    showSponsorLink?: boolean;

    /**
     * (optional) Whether to show the UI.
     */
    showUI?: boolean;

    /**
     * (optional) Whether to the document should be read only.
     */
    readOnly?: boolean;

    /**
     * (optional) Whether to to show the app's dark mode UI.
     */
    darkMode?: boolean;

    /**
     * (optional) If provided, image/video componnets will be disabled.
     *
     * Warning: Keeping this enabled for multiplayer applications without provifing a storage
     * bucket based solution will cause massive base64 string to be written to the liveblocks room.
     */
    disableAssets?: boolean;
}

export function Tldraw({
    id,
    document,
    currentPageId,
    autofocus = true,
    showMenu = true,
    showMultiplayerMenu = true,
    showPages = true,
    showTools = true,
    showZoom = true,
    showStyles = true,
    // eslint-disable-next-line @typescript-eslint/naming-convention
    showUI = true,
    readOnly = false,
    disableAssets = false,
    darkMode = false,
    showSponsorLink,
    callbacks,
    commands,
    getSession,
    tools,
}: TldrawProps) {
    const [sId, setSid] = useState(id);
    const { pageClientWidth } = usePageClientWidth();
    // page padding left and right total 300px
    const editorShapeInitSize = pageClientWidth - 300;

    // Create a new app when the component mounts.
    const [app, setApp] = useState(() => {
        const app = new TldrawApp({
            id,
            callbacks,
            commands,
            getSession,
            editorShapeInitSize,
            tools,
        });
        return app;
    });

    // Create a new app if the `id` prop changes.
    useLayoutEffect(() => {
        if (id === sId) return;
        const newApp = new TldrawApp({
            id,
            callbacks,
            commands,
            getSession,
            tools,
        });

        setSid(id);

        setApp(newApp);
    }, [sId, id]);

    // Update the document if the `document` prop changes but the ids,
    // are the same, or else load a new document if the ids are different.
    useEffect(() => {
        if (!document) return;

        if (document.id === app.document.id) {
            app.updateDocument(document);
        } else {
            app.loadDocument(document);
        }
    }, [document, app]);

    // Disable assets when the `disableAssets` prop changes.
    useEffect(() => {
        app.setDisableAssets(disableAssets);
    }, [app, disableAssets]);

    // Change the page when the `currentPageId` prop changes.
    useEffect(() => {
        if (!currentPageId) return;
        app.changePage(currentPageId);
    }, [currentPageId, app]);

    // Toggle the app's readOnly mode when the `readOnly` prop changes.
    useEffect(() => {
        app.readOnly = readOnly;
    }, [app, readOnly]);

    // Toggle the app's darkMode when the `darkMode` prop changes.
    useEffect(() => {
        if (darkMode !== app.settings.isDarkMode) {
            app.toggleDarkMode();
        }
    }, [app, darkMode]);

    // Update the app's callbacks when any callback changes.
    useEffect(() => {
        app.callbacks = callbacks || {};
    }, [app, callbacks]);

    useLayoutEffect(() => {
        if (typeof window === 'undefined') return;
        if (!window.document?.fonts) return;

        function refreshBoundingBoxes() {
            app.refreshBoundingBoxes();
        }
        window.document.fonts.addEventListener(
            'loadingdone',
            refreshBoundingBoxes
        );
        return () => {
            window.document.fonts.removeEventListener(
                'loadingdone',
                refreshBoundingBoxes
            );
        };
    }, [app]);

    // Use the `key` to ensure that new selector hooks are made when the id changes
    return (
        <TldrawContext.Provider value={app}>
            <InnerTldraw
                key={sId || 'Tldraw'}
                id={sId}
                autofocus={autofocus}
                showPages={showPages}
                showMenu={showMenu}
                showMultiplayerMenu={showMultiplayerMenu}
                showStyles={showStyles}
                showZoom={showZoom}
                showTools={showTools}
                showUI={showUI}
                showSponsorLink={showSponsorLink}
                readOnly={readOnly}
            />
        </TldrawContext.Provider>
    );
}

interface InnerTldrawProps {
    id?: string;
    autofocus: boolean;
    readOnly: boolean;
    showPages: boolean;
    showMenu: boolean;
    showMultiplayerMenu: boolean;
    showZoom: boolean;
    showStyles: boolean;
    showUI: boolean;
    showTools: boolean;
    showSponsorLink?: boolean;
}

const InnerTldraw = memo(function InnerTldraw({
    id,
    autofocus,
    showPages,
    showMenu,
    showMultiplayerMenu,
    showZoom,
    showStyles,
    showTools,
    showSponsorLink,
    readOnly,
    // eslint-disable-next-line @typescript-eslint/naming-convention
    showUI,
}: InnerTldrawProps) {
    const app = useTldrawApp();

    const rWrapper = useRef<HTMLDivElement>(null);

    const state = app.useStore();

    const { document, settings, appState, room } = state;
    const isSelecting = state.appState.activeTool === 'select';

    const page = document.pages[appState.currentPageId];
    const pageState = document.pageStates[page.id];
    const assets = document.assets;
    const { selectedIds } = pageState;
    const isHideBoundsShape =
        selectedIds.length === 1 &&
        page.shapes[selectedIds[0]] &&
        TLDR.get_shape_util(page.shapes[selectedIds[0]].type).hideBounds;

    const isHideResizeHandlesShape =
        selectedIds.length === 1 &&
        page.shapes[selectedIds[0]] &&
        TLDR.get_shape_util(page.shapes[selectedIds[0]].type).hideResizeHandles;

    // Custom rendering meta, with dark mode for shapes
    const meta: TDMeta = useMemo(() => {
        return { isDarkMode: settings.isDarkMode, app };
    }, [settings.isDarkMode, app]);

    const showDashedBrush = settings.isCadSelectMode
        ? !appState.selectByContain
        : appState.selectByContain;

    // Custom theme, based on darkmode
    const theme = useMemo(() => {
        const { selectByContain } = appState;
        const { isDarkMode, isCadSelectMode } = settings;

        if (isDarkMode) {
            const brushBase = isCadSelectMode
                ? selectByContain
                    ? '69, 155, 255'
                    : '105, 209, 73'
                : '180, 180, 180';
            return {
                brushFill: `rgba(${brushBase}, ${
                    isCadSelectMode ? 0.08 : 0.05
                })`,
                brushStroke: `rgba(${brushBase}, ${
                    isCadSelectMode ? 0.5 : 0.25
                })`,
                brushDashStroke: `rgba(${brushBase}, .6)`,
                selected: 'rgba(38, 150, 255, 1.000)',
                selectFill: 'rgba(38, 150, 255, 0.05)',
                background: '#212529',
                foreground: '#49555f',
            };
        }

        const brushBase = isCadSelectMode
            ? selectByContain
                ? '0, 89, 242'
                : '51, 163, 23'
            : '0,0,0';

        return {
            brushFill: `rgba(${brushBase}, ${isCadSelectMode ? 0.08 : 0.05})`,
            brushStroke: `rgba(${brushBase}, ${isCadSelectMode ? 0.4 : 0.25})`,
            brushDashStroke: `rgba(${brushBase}, .6)`,
            background: '#fff',
        };
    }, [
        settings.isDarkMode,
        settings.isCadSelectMode,
        appState.selectByContain,
    ]);

    const isInSession = app.session !== undefined;

    // Hide bounds when not using the select tool, or when the only selected shape has handles
    const hideBounds =
        (isInSession && app.session?.constructor.name === 'BrushSession') ||
        !isSelecting ||
        isHideBoundsShape ||
        !!pageState.editingId;

    // Hide bounds when not using the select tool, or when in session
    const hideHandles = isInSession || !isSelecting;

    // Hide indicators when not using the select tool, or when in session
    const hideIndicators =
        (isInSession && state.appState.status !== TDStatus.Brushing) ||
        !isSelecting;

    const hideCloneHandles =
        isInSession ||
        !isSelecting ||
        !settings.showCloneHandles ||
        pageState.camera.zoom < 0.2;

    return (
        <StyledLayout
            ref={rWrapper}
            panning={settings.forcePanning}
            tabIndex={-0}
            penColor={app?.appState?.currentStyle?.stroke}
        >
            <Loading />
            <OneOff focusableRef={rWrapper} autofocus={autofocus} />
            <ContextMenu>
                <ErrorBoundary FallbackComponent={ErrorFallback}>
                    <Renderer
                        id={id}
                        containerRef={rWrapper}
                        shapeUtils={shapeUtils}
                        page={page}
                        pageState={pageState}
                        assets={assets}
                        snapLines={appState.snapLines}
                        eraseLine={appState.laserLine}
                        grid={GRID_SIZE}
                        users={room?.users}
                        userId={room?.userId}
                        theme={theme}
                        meta={meta as unknown as Record<string, unknown>}
                        hideBounds={hideBounds}
                        hideHandles={hideHandles}
                        hideResizeHandles={isHideResizeHandlesShape}
                        hideIndicators={hideIndicators}
                        hideBindingHandles={!settings.showBindingHandles}
                        hideCloneHandles={hideCloneHandles}
                        hideRotateHandles={!settings.showRotateHandles}
                        hideGrid={!settings.showGrid}
                        showDashedBrush={showDashedBrush}
                        performanceMode={app.session?.performanceMode}
                        onPinchStart={app.onPinchStart}
                        onPinchEnd={app.onPinchEnd}
                        onPinch={app.onPinch}
                        onPan={app.onPan}
                        onZoom={app.onZoom}
                        onPointerDown={app.onPointerDown}
                        onPointerMove={app.onPointerMove}
                        onPointerUp={app.onPointerUp}
                        onPointCanvas={app.onPointCanvas}
                        onDoubleClickCanvas={app.onDoubleClickCanvas}
                        onRightPointCanvas={app.onRightPointCanvas}
                        onDragCanvas={app.onDragCanvas}
                        onReleaseCanvas={app.onReleaseCanvas}
                        onPointShape={app.onPointShape}
                        onDoubleClickShape={app.onDoubleClickShape}
                        onRightPointShape={app.onRightPointShape}
                        onDragShape={app.onDragShape}
                        onHoverShape={app.onHoverShape}
                        onUnhoverShape={app.onUnhoverShape}
                        onReleaseShape={app.onReleaseShape}
                        onPointBounds={app.onPointBounds}
                        onDoubleClickBounds={app.onDoubleClickBounds}
                        onRightPointBounds={app.onRightPointBounds}
                        onDragBounds={app.onDragBounds}
                        onHoverBounds={app.onHoverBounds}
                        onUnhoverBounds={app.onUnhoverBounds}
                        onReleaseBounds={app.onReleaseBounds}
                        onPointBoundsHandle={app.onPointBoundsHandle}
                        onDoubleClickBoundsHandle={
                            app.onDoubleClickBoundsHandle
                        }
                        onRightPointBoundsHandle={app.onRightPointBoundsHandle}
                        onDragBoundsHandle={app.onDragBoundsHandle}
                        onHoverBoundsHandle={app.onHoverBoundsHandle}
                        onUnhoverBoundsHandle={app.onUnhoverBoundsHandle}
                        onReleaseBoundsHandle={app.onReleaseBoundsHandle}
                        onPointHandle={app.onPointHandle}
                        onDoubleClickHandle={app.onDoubleClickHandle}
                        onRightPointHandle={app.onRightPointHandle}
                        onDragHandle={app.onDragHandle}
                        onHoverHandle={app.onHoverHandle}
                        onUnhoverHandle={app.onUnhoverHandle}
                        onReleaseHandle={app.onReleaseHandle}
                        onError={app.onError}
                        onRenderCountChange={app.onRenderCountChange}
                        onShapeChange={app.onShapeChange}
                        onShapeBlur={app.onShapeBlur}
                        onShapeClone={app.onShapeClone}
                        onBoundsChange={app.updateBounds}
                        onKeyDown={app.onKeyDown}
                        onKeyUp={app.onKeyUp}
                        onDragOver={app.onDragOver}
                        onDrop={app.onDrop}
                    />
                </ErrorBoundary>
            </ContextMenu>
            {showUI && (
                <StyledUI>
                    <>
                        <StyledSpacer />
                        {showTools && !readOnly && <ToolsPanel app={app} />}
                        <CommandPanel app={app} />
                    </>
                </StyledUI>
            )}
            <ZoomBar />
        </StyledLayout>
    );
});

const OneOff = memo(function OneOff({
    focusableRef,
    autofocus,
}: {
    autofocus?: boolean;
    focusableRef: RefObject<HTMLDivElement>;
}) {
    useKeyboardShortcuts(focusableRef);
    useStylesheet();

    useEffect(() => {
        if (autofocus) {
            focusableRef.current?.focus();
        }
    }, [autofocus]);

    return null;
});

const StyledLayout = styled('div')<{ penColor: string; panning: boolean }>(
    ({ theme, panning, penColor }) => {
        return {
            position: 'relative',
            height: '100%',
            width: '100vw',
            minHeight: 0,
            minWidth: 0,
            maxHeight: '100%',
            maxWidth: '100%',
            overflow: 'hidden',
            boxSizing: 'border-box',
            outline: 'none',
            cursor: panning ? 'grab' : 'unset',

            '& .tl-container': {
                position: 'absolute',
                top: 0,
                left: 0,
                height: '100%',
                width: '100%',
                zIndex: 1,
                '.tl-erase-line': {
                    fill: penColor,
                },
            },

            '& input, textarea, button, select, label, button': {
                webkitTouchCallout: 'none',
                webkitUserSelect: 'none',
                WebkitTapHighlightColor: 'transparent',
                tapHighlightColor: 'transparent',
            },
        };
    }
);

// eslint-disable-next-line @typescript-eslint/naming-convention
const StyledUI = styled('div')({
    position: 'absolute',
    top: 0,
    left: 0,
    height: '100%',
    width: '100%',
    padding: '8px 8px 0 8px',
    display: 'flex',
    alignItems: 'flex-start',
    justifyContent: 'flex-start',
    pointerEvents: 'none',
    zIndex: 2,
    '& > *': {
        pointerEvents: 'all',
    },
});

const StyledSpacer = styled('div')({
    flexGrow: 2,
});
