import { styled, usePatchNodes } from '@toeverything/components/ui';
import {
    Protocol,
    services,
    type ReturnUnobserve,
} from '@toeverything/datasource/db-service';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { EditorProvider } from './Contexts';
import type { BlockEditor } from './editor';
import { useIsOnDrag } from './hooks';
import { addNewGroup, appendNewGroup } from './recast-block';
import { BlockRenderProvider, RenderBlock } from './render-block';
import { SelectionRect, SelectionRef } from './Selection';

interface RenderRootProps {
    editor: BlockEditor;
    editorElement: () => JSX.Element;
    /**
     * Scroll to the bottom of the article visually leave blank
     */
    scrollBlank?: boolean;
}

const MAX_PAGE_WIDTH = 5000;
export const MIN_PAGE_WIDTH = 1480;

export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
    const selectionRef = useRef<SelectionRef>(null);
    const triggeredBySelect = useRef(false);
    const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
    const isOnDrag = useIsOnDrag(editor);

    const { patch, has, patchedNodes } = usePatchNodes();

    editor.setReactRenderRoot({ patch, has });
    const rootId = editor.getRootBlockId();
    const fetchPageBlockWidth = useCallback(async () => {
        const dbPageBlock = await services.api.editorBlock.getBlock(
            editor.workspace,
            rootId
        );
        if (!dbPageBlock) return;
        if (dbPageBlock.getDecoration('fullWidthChecked')) {
            setPageWidth(MAX_PAGE_WIDTH);
        } else {
            setPageWidth(MIN_PAGE_WIDTH);
        }
    }, [editor.workspace, rootId]);

    useEffect(() => {
        fetchPageBlockWidth();

        let unobserve: ReturnUnobserve | undefined = undefined;
        const observe = async () => {
            unobserve = await services.api.editorBlock.observe(
                {
                    workspace: editor.workspace,
                    id: rootId,
                },
                fetchPageBlockWidth
            );
        };
        observe();
        return () => {
            unobserve?.();
        };
    }, [rootId, editor, fetchPageBlockWidth]);

    const onMouseMove = async (
        event: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        selectionRef.current?.onMouseMove(event);
        editor.getHooks().onRootNodeMouseMove(event);
    };

    const onMouseDown = (
        event: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        triggeredBySelect.current = true;
        selectionRef.current?.onMouseDown(event);
        editor.getHooks().onRootNodeMouseDown(event);
    };

    const onMouseUp = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
        selectionRef.current?.onMouseUp(event);
        editor.getHooks().onRootNodeMouseUp(event);
    };

    const onMouseOut = (
        event: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        editor.getHooks().onRootNodeMouseOut(event);
    };

    const onMouseLeave = (
        event: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        editor.getHooks().onRootNodeMouseLeave(event);
    };

    const onContextmenu = (
        event: React.MouseEvent<HTMLDivElement, MouseEvent>
    ) => {
        selectionRef.current?.onContextmenu(event);
    };

    const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
        // IMP move into keyboard managers?
        editor.getHooks().onRootNodeKeyDown(event);
    };

    const onKeyUp: React.KeyboardEventHandler<HTMLDivElement> = event => {
        // IMP move into keyboard managers?
        editor.getHooks().onRootNodeKeyUp(event);
    };

    const onKeyDownCapture: React.KeyboardEventHandler<
        HTMLDivElement
    > = event => {
        editor.getHooks().onRootNodeKeyDownCapture(event);
    };

    const onDragOver = (event: React.DragEvent<Element>) => {
        event.dataTransfer.dropEffect = 'move';
        event.preventDefault();
        editor.dragDropManager.handlerEditorDragOver(event);
        if (editor.dragDropManager.isEnabled()) {
            editor.getHooks().onRootNodeDragOver(event);
        }
    };

    const onDragLeave = (event: React.DragEvent<Element>) => {
        if (editor.dragDropManager.isEnabled()) {
            editor.getHooks().onRootNodeDragLeave(event);
        }
    };

    const onDragOverCapture = (event: React.DragEvent<Element>) => {
        event.preventDefault();
        if (editor.dragDropManager.isEnabled()) {
            editor.getHooks().onRootNodeDragOver(event);
        }
    };

    const onDragEnd = (event: React.DragEvent<Element>) => {
        editor.dragDropManager.handlerEditorDragEnd(event);
        editor.getHooks().onRootNodeDragEnd(event);
    };

    const onDrop = (event: React.DragEvent<Element>) => {
        event.preventDefault();
        editor.dragDropManager.handlerEditorDrop(event);
        editor.getHooks().onRootNodeDrop(event);
    };

    return (
        <EditorProvider editor={editor} editorElement={editorElement}>
            <BlockRenderProvider blockRender={RenderBlock}>
                <Container
                    isEdgeless={editor.isEdgeless}
                    ref={ref => {
                        if (ref != null && ref !== editor.container) {
                            editor.container = ref;
                            editor.getHooks().render();
                        }
                    }}
                    onMouseMove={onMouseMove}
                    onMouseDown={onMouseDown}
                    onMouseUp={onMouseUp}
                    onMouseLeave={onMouseLeave}
                    onMouseOut={onMouseOut}
                    onContextMenu={onContextmenu}
                    onKeyDown={onKeyDown}
                    onKeyDownCapture={onKeyDownCapture}
                    onKeyUp={onKeyUp}
                    onDragOver={onDragOver}
                    onDragLeave={onDragLeave}
                    onDragOverCapture={onDragOverCapture}
                    onDragEnd={onDragEnd}
                    onDrop={onDrop}
                    isOnDrag={isOnDrag}
                >
                    <Content style={{ maxWidth: pageWidth + 'px' }}>
                        <RenderBlock blockId={editor.getRootBlockId()} />
                    </Content>
                    {/** TODO: remove selectionManager insert */}
                    {editor && (
                        <SelectionRect ref={selectionRef} editor={editor} />
                    )}
                    {editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
                    {patchedNodes}
                </Container>
            </BlockRenderProvider>
        </EditorProvider>
    );
};

// eslint-disable-next-line @typescript-eslint/naming-convention
function ScrollBlank({ editor }: { editor: BlockEditor }) {
    const mouseMoved = useRef(false);

    const onMouseDown = useCallback(() => (mouseMoved.current = false), []);
    const onMouseMove = useCallback(() => (mouseMoved.current = true), []);
    const onClick = useCallback(
        async (e: React.MouseEvent) => {
            if (mouseMoved.current) {
                mouseMoved.current = false;
                return;
            }
            const rootBlock = await editor.getBlockById(
                editor.getRootBlockId()
            );
            if (!rootBlock) {
                throw new Error('root block is not found');
            }

            const lastRootChildren = await rootBlock.lastChild();
            // If last block is not a group
            // create a group with a empty text
            if (lastRootChildren == null) {
                appendNewGroup(editor, rootBlock, true);
                return;
            }

            if (
                lastRootChildren.type !== Protocol.Block.Type.group ||
                lastRootChildren.childrenIds.length > 1
            ) {
                addNewGroup(editor, lastRootChildren, true);
                return;
            }

            // If the **only** block in the group is text and is empty
            // active the text block
            const theGroupChildBlock = await lastRootChildren.firstChild();

            if (
                theGroupChildBlock &&
                theGroupChildBlock.type === Protocol.Block.Type.text &&
                theGroupChildBlock.blockProvider?.isEmpty()
            ) {
                await editor.selectionManager.activeNodeByNodeId(
                    theGroupChildBlock.id
                );
                return;
            }
            // else create a new group
            addNewGroup(editor, lastRootChildren, true);
        },
        [editor]
    );

    return (
        <ScrollBlankContainer
            onMouseDown={onMouseDown}
            onMouseMove={onMouseMove}
            onClick={onClick}
        />
    );
}

const PADDING_X = 150;

const Container = styled('div')(
    ({ isEdgeless, isOnDrag }: { isEdgeless: boolean; isOnDrag: boolean }) => ({
        width: '100%',
        padding: isEdgeless ? 0 : `72px ${PADDING_X}px 0 ${PADDING_X}px`,
        minWidth: isEdgeless ? 'unset' : '940px',
        position: 'relative',
        ...(isOnDrag && {
            cursor: 'grabbing',
            // expected css selector
            // eslint-disable-next-line @typescript-eslint/naming-convention
            '& *': { cursor: 'grabbing' },
        }),
    })
);

const Content = styled('div')({
    width: '100%',
    margin: '0 auto',
    transitionDuration: '.2s',
    transitionTimingFunction: 'ease-in',
});

const ScrollBlankContainer = styled('div')({
    paddingBottom: '30vh',
    margin: `0 -${PADDING_X}px`,
});
