// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React, {
    ReactElement, SyntheticEvent, useEffect, useReducer, useRef,
} from 'react';
import Layout from 'antd/lib/layout/layout';
import {
    ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
} from '@ant-design/icons';
import { ResizableBox } from 'react-resizable';
import {
    ColorBy, ContextMenuType, ObjectType, Workspace,
} from 'reducers';
import {
    CameraAction, Canvas3d, ViewType, ViewsDOM,
} from 'cvat-canvas3d-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
import ContextImage from 'components/annotation-page/standard-workspace/context-image/context-image';
import CVATTooltip from 'components/common/cvat-tooltip';
import { LogType } from 'cvat-logger';
import { getCore } from 'cvat-core-wrapper';

const cvat = getCore();

interface Props {
    opacity: number;
    selectedOpacity: number;
    outlined: boolean;
    outlineColor: string;
    colorBy: ColorBy;
    frameFetching: boolean;
    canvasInstance: Canvas3d | Canvas;
    jobInstance: any;
    frameData: any;
    annotations: any[];
    contextMenuVisibility: boolean;
    activeLabelID: number;
    activeObjectType: ObjectType;
    activatedStateID: number | null;
    onSetupCanvas: () => void;
    onGroupObjects: (enabled: boolean) => void;
    onResetCanvas(): void;
    onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
    onActivateObject(activatedStateID: number | null): void;
    onUpdateAnnotations(states: any[]): void;
    onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
    onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
    onEditShape: (enabled: boolean) => void;
    onDragCanvas: (enabled: boolean) => void;
    onShapeDrawn: () => void;
    workspace: Workspace;
    frame: number;
    resetZoom: boolean;
}

interface ViewSize {
    fullHeight: number;
    fullWidth: number;
    vertical: number;
    top: number;
    side: number;
    front: number;
}

function viewSizeReducer(
    state: ViewSize,
    action: { type: ViewType | 'set' | 'resize'; e?: SyntheticEvent; data?: ViewSize },
): ViewSize {
    const event = (action.e as unknown) as MouseEvent;
    const canvas3dContainer = document.getElementById('canvas3d-container');
    if (canvas3dContainer) {
        switch (action.type) {
            case ViewType.TOP: {
                const width = event.clientX - canvas3dContainer.getBoundingClientRect().left;
                const topWidth = state.top;
                if (topWidth < width) {
                    const top = state.top + (width - topWidth);
                    const side = state.side - (width - topWidth);
                    return {
                        ...state,
                        top,
                        side,
                    };
                }
                const top = state.top - (topWidth - width);
                const side = state.side + (topWidth - width);
                return {
                    ...state,
                    top,
                    side,
                };
            }
            case ViewType.SIDE: {
                const width = event.clientX - canvas3dContainer.getBoundingClientRect().left;
                const topSideWidth = state.top + state.side;
                if (topSideWidth < width) {
                    const side = state.side + (width - topSideWidth);
                    const front = state.front - (width - topSideWidth);
                    return {
                        ...state,
                        side,
                        front,
                    };
                }
                const side = state.side - (topSideWidth - width);
                const front = state.front + (topSideWidth - width);
                return {
                    ...state,
                    side,
                    front,
                };
            }
            case ViewType.PERSPECTIVE:
                return {
                    ...state,
                    vertical: event.clientY - canvas3dContainer.getBoundingClientRect().top,
                };
            case 'set':
                return action.data as ViewSize;
            case 'resize': {
                const canvasPerspectiveContainer = document.getElementById('cvat-canvas3d-perspective');
                let midState = { ...state };
                if (canvasPerspectiveContainer) {
                    if (state.fullHeight !== canvas3dContainer.clientHeight) {
                        const diff = canvas3dContainer.clientHeight - state.fullHeight;
                        midState = {
                            ...midState,
                            fullHeight: canvas3dContainer.clientHeight,
                            vertical: state.vertical + diff,
                        };
                    }
                    if (state.fullWidth !== canvasPerspectiveContainer.clientWidth) {
                        const oldWidth = state.fullWidth;
                        const width = canvasPerspectiveContainer.clientWidth;
                        midState = {
                            ...midState,
                            fullWidth: width,
                            top: (state.top / oldWidth) * width,
                            side: (state.side / oldWidth) * width,
                            front: (state.front / oldWidth) * width,
                        };
                    }
                    return midState;
                }
                return state;
            }
            default:
                throw new Error();
        }
    }
    return state;
}

const CanvasWrapperComponent = (props: Props): ReactElement => {
    const animateId = useRef(0);
    const [viewSize, setViewSize] = useReducer(viewSizeReducer, {
        fullHeight: 0,
        fullWidth: 0,
        vertical: 0,
        top: 0,
        side: 0,
        front: 0,
    });
    const perspectiveView = useRef<HTMLDivElement | null>(null);
    const topView = useRef<HTMLDivElement | null>(null);
    const sideView = useRef<HTMLDivElement | null>(null);
    const frontView = useRef<HTMLDivElement | null>(null);

    const {
        opacity,
        outlined,
        outlineColor,
        selectedOpacity,
        colorBy,
        contextMenuVisibility,
        frameData,
        onResetCanvas,
        onSetupCanvas,
        annotations,
        frame,
        jobInstance,
        activeLabelID,
        activatedStateID,
        resetZoom,
        activeObjectType,
        onShapeDrawn,
        onCreateAnnotations,
        frameFetching,
    } = props;
    const { canvasInstance } = props as { canvasInstance: Canvas3d };

    const onCanvasSetup = (): void => {
        onSetupCanvas();
    };

    const onCanvasDragStart = (): void => {
        const { onDragCanvas } = props;
        onDragCanvas(true);
    };

    const onCanvasDragDone = (): void => {
        const { onDragCanvas } = props;
        onDragCanvas(false);
    };

    const animateCanvas = (): void => {
        canvasInstance.render();
        animateId.current = requestAnimationFrame(animateCanvas);
    };

    const updateCanvas = (): void => {
        if (frameData !== null) {
            canvasInstance.setup(
                frameData,
                annotations.filter((e) => e.objectType !== ObjectType.TAG),
            );
        }
    };

    const onCanvasCancel = (): void => {
        onResetCanvas();
    };

    const onCanvasShapeDrawn = (event: any): void => {
        if (!event.detail.continue) {
            onShapeDrawn();
        }

        const { state, duration } = event.detail;
        const isDrawnFromScratch = !state.label;
        if (isDrawnFromScratch) {
            jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
        } else {
            jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
        }

        state.objectType = state.objectType || activeObjectType;
        state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
        state.occluded = state.occluded || false;
        state.frame = frame;
        state.zOrder = 0;
        const objectState = new cvat.classes.ObjectState(state);
        onCreateAnnotations(jobInstance, frame, [objectState]);
    };

    const onCanvasClick = (e: MouseEvent): void => {
        const { onUpdateContextMenu } = props;
        if (contextMenuVisibility) {
            onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
        }
    };

    const initialSetup = (): void => {
        const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
        canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup);
        canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel);
        canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart);
        canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone);
        canvasInstance.configure({ resetZoom });
    };

    const keyControlsKeyDown = (key: KeyboardEvent): void => {
        canvasInstance.keyControls(key);
    };

    const keyControlsKeyUp = (key: KeyboardEvent): void => {
        if (key.code === 'ControlLeft') {
            canvasInstance.keyControls(key);
        }
    };

    const onCanvasShapeSelected = (event: any): void => {
        const { onActivateObject } = props;
        const { clientID } = event.detail;
        onActivateObject(clientID);
        canvasInstance.activate(clientID);
    };

    const onCanvasEditDone = (event: any): void => {
        const { onEditShape, onUpdateAnnotations } = props;
        onEditShape(false);
        const { state, points } = event.detail;
        state.points = points;
        onUpdateAnnotations([state]);
    };

    useEffect(() => {
        const canvasInstanceDOM = canvasInstance.html();
        if (
            perspectiveView &&
            perspectiveView.current &&
            topView &&
            topView.current &&
            sideView &&
            sideView.current &&
            frontView &&
            frontView.current
        ) {
            perspectiveView.current.appendChild(canvasInstanceDOM.perspective);
            topView.current.appendChild(canvasInstanceDOM.top);
            sideView.current.appendChild(canvasInstanceDOM.side);
            frontView.current.appendChild(canvasInstanceDOM.front);
            const canvas3dContainer = document.getElementById('canvas3d-container');
            if (canvas3dContainer) {
                const width = canvas3dContainer.clientWidth / 3;
                setViewSize({
                    type: 'set',
                    data: {
                        fullHeight: canvas3dContainer.clientHeight,
                        fullWidth: canvas3dContainer.clientWidth,
                        vertical: canvas3dContainer.clientHeight / 2,
                        top: width,
                        side: width,
                        front: width,
                    },
                });
            }
        }

        document.addEventListener('keydown', keyControlsKeyDown);
        document.addEventListener('keyup', keyControlsKeyUp);

        initialSetup();
        updateCanvas();
        animateCanvas();

        return () => {
            canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup);
            canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel);
            canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart);
            canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone);
            document.removeEventListener('keydown', keyControlsKeyDown);
            document.removeEventListener('keyup', keyControlsKeyUp);
            cancelAnimationFrame(animateId.current);
        };
    }, []);

    useEffect(() => {
        canvasInstance.activate(activatedStateID);
    }, [activatedStateID]);

    useEffect(() => {
        canvasInstance.configure({ resetZoom });
    }, [resetZoom]);

    const updateShapesView = (): void => {
        (canvasInstance as Canvas3d).configureShapes({
            opacity,
            outlined,
            outlineColor,
            selectedOpacity,
            colorBy,
        });
    };

    const onContextMenu = (event: any): void => {
        const { onUpdateContextMenu, onActivateObject } = props;
        onActivateObject(event.detail.clientID);
        onUpdateContextMenu(
            event.detail.clientID !== null,
            event.detail.clientX,
            event.detail.clientY,
            ContextMenuType.CANVAS_SHAPE,
        );
    };

    const onResize = (): void => {
        setViewSize({
            type: 'resize',
        });
    };

    const onCanvasObjectsGroupped = (event: any): void => {
        const { onGroupAnnotations, onGroupObjects } = props;

        onGroupObjects(false);

        const { states } = event.detail;
        onGroupAnnotations(jobInstance, frame, states);
    };

    useEffect(() => {
        updateShapesView();
    }, [opacity, outlined, outlineColor, selectedOpacity, colorBy]);

    useEffect(() => {
        const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
        updateCanvas();
        canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn);
        canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected);
        canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone);
        canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu);
        canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick);
        canvasInstanceDOM.perspective.addEventListener('canvas.fit', onResize);
        canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped);
        window.addEventListener('resize', onResize);

        return () => {
            canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn);
            canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected);
            canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone);
            canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu);
            canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick);
            canvasInstanceDOM.perspective.removeEventListener('canvas.fit', onResize);
            canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped);
            window.removeEventListener('resize', onResize);
        };
    }, [frameData, annotations, activeLabelID, contextMenuVisibility]);

    const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
        canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
    };

    const ArrowGroup = (): ReactElement => (
        <span className='cvat-canvas3d-perspective-arrow-directions'>
            <CVATTooltip title='Shift+Arrow Up' placement='topRight'>
                <button
                    data-cy='arrow-up'
                    onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
                    type='button'
                    className='cvat-canvas3d-perspective-arrow-directions-icons-up'
                >
                    <ArrowUpOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
                </button>
            </CVATTooltip>
            <br />
            <CVATTooltip title='Shift+Arrow Left' placement='topRight'>
                <button
                    onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
                    type='button'
                    className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
                >
                    <ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
                </button>
            </CVATTooltip>
            <CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
                <button
                    onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
                    type='button'
                    className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
                >
                    <ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
                </button>
            </CVATTooltip>
            <CVATTooltip title='Shift+Arrow Right' placement='topRight'>
                <button
                    onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
                    type='button'
                    className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
                >
                    <ArrowRightOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
                </button>
            </CVATTooltip>
        </span>
    );

    const ControlGroup = (): ReactElement => (
        <span className='cvat-canvas3d-perspective-directions'>
            <CVATTooltip title='Alt+U' placement='topLeft'>
                <button
                    onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
                    type='button'
                    className='cvat-canvas3d-perspective-directions-icon'
                >
                    U
                </button>
            </CVATTooltip>
            <CVATTooltip title='Alt+I' placement='topLeft'>
                <button
                    onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
                    type='button'
                    className='cvat-canvas3d-perspective-directions-icon'
                >
                    I
                </button>
            </CVATTooltip>
            <CVATTooltip title='Alt+O' placement='topLeft'>
                <button
                    onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
                    type='button'
                    className='cvat-canvas3d-perspective-directions-icon'
                >
                    O
                </button>
            </CVATTooltip>
            <br />
            <CVATTooltip title='Alt+J' placement='topLeft'>
                <button
                    onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
                    type='button'
                    className='cvat-canvas3d-perspective-directions-icon'
                >
                    J
                </button>
            </CVATTooltip>
            <CVATTooltip title='Alt+K' placement='topLeft'>
                <button
                    onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
                    type='button'
                    className='cvat-canvas3d-perspective-directions-icon'
                >
                    K
                </button>
            </CVATTooltip>
            <CVATTooltip title='Alt+L' placement='topLeft'>
                <button
                    onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
                    type='button'
                    className='cvat-canvas3d-perspective-directions-icon'
                >
                    L
                </button>
            </CVATTooltip>
        </span>
    );

    return (
        <Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'>
            <ContextImage />
            <ResizableBox
                className='cvat-resizable'
                width={Infinity}
                height={viewSize.vertical}
                axis='y'
                handle={<span className='cvat-resizable-handle-horizontal' />}
                onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })}
            >
                <>
                    {frameFetching ? (
                        <svg id='cvat_canvas_loading_animation'>
                            <circle id='cvat_canvas_loading_circle' r='30' cx='50%' cy='50%' />
                        </svg>
                    ) : null}
                    <div className='cvat-canvas3d-perspective' id='cvat-canvas3d-perspective'>
                        <div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} />
                        <ArrowGroup />
                        <ControlGroup />
                    </div>
                </>
            </ResizableBox>
            <div
                className='cvat-canvas3d-orthographic-views'
                style={{ height: viewSize.fullHeight - viewSize.vertical }}
            >
                <ResizableBox
                    className='cvat-resizable'
                    width={viewSize.top}
                    height={viewSize.fullHeight - viewSize.vertical}
                    axis='x'
                    handle={<span className='cvat-resizable-handle-vertical-top' />}
                    onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.TOP, e })}
                >
                    <div className='cvat-canvas3d-orthographic-view cvat-canvas3d-topview'>
                        <div className='cvat-canvas3d-header'>TOP</div>
                        <div className='cvat-canvas3d-fullsize' ref={topView} />
                    </div>
                </ResizableBox>
                <ResizableBox
                    className='cvat-resizable'
                    width={viewSize.side}
                    height={viewSize.fullHeight - viewSize.vertical}
                    axis='x'
                    handle={<span className='cvat-resizable-handle-vertical-side' />}
                    onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.SIDE, e })}
                >
                    <div className='cvat-canvas3d-orthographic-view cvat-canvas3d-sideview'>
                        <div className='cvat-canvas3d-header'>SIDE</div>
                        <div className='cvat-canvas3d-fullsize' ref={sideView} />
                    </div>
                </ResizableBox>
                <div
                    className='cvat-canvas3d-orthographic-view cvat-canvas3d-frontview'
                    style={{ width: viewSize.front, height: viewSize.fullHeight - viewSize.vertical }}
                >
                    <div className='cvat-canvas3d-header'>FRONT</div>
                    <div className='cvat-canvas3d-fullsize' ref={frontView} />
                </div>
            </div>
        </Layout.Content>
    );
};

export default React.memo(CanvasWrapperComponent);
