import { CreateLinkEvent, GetRenderer, GetRoomEngine, OctaneTexture, TextureUtils } from '@octane/renderer';
import { FC, useEffect, useRef } from 'react';
import { blitRoomCanvasToViewfinder, CameraPicture, CaptureCameraRenderMessage, GetCameraRenderMode, GetRoomSession, getViewfinderRoomFrame, LocalizeText, NotificationAlertType, PlaySound, SoundNames } from '../../../api';
import { Column, DraggableWindow } from '../../../common';
import { useCamera, useNotification, useRoom } from '../../../hooks';
import { getNextEmptyCameraSlot, willFillLastCameraSlot } from '../CameraAirUtilities';
import { CameraCenteredText, CameraNativeText } from './CameraNativeText';
import { BaseButton } from '../../../common/BaseButton';
import { Hint } from '../../../common/Tooltip';
import { NativeTextTree } from '../../../common/native-text/NativeTextTree';

export interface CameraWidgetCaptureViewProps {
    onClose: () => void;
    onEdit: () => void;
    onDelete: () => void;
}

const CAMERA_ROLL_LIMIT: number = 5;
const AIR_FALLBACK_FRAME_INTERVAL: number = 100;
const ROOM_STREAM_FRAME_RATE: number = 60;
let hasShownFullRollAlert: boolean = false;

export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = (props) => {
    const { onClose = null, onEdit = null, onDelete = null } = props;
    const {
        cameraRoll = Array(CAMERA_ROLL_LIMIT).fill(null),
        setCameraRoll = null,
        selectedPictureIndex = -1,
        setSelectedPictureIndex = null,
        activePictureSlotIndex = 0,
        setActivePictureSlotIndex = null
    } = useCamera();
    const { simpleAlert = null } = useNotification();
    const { roomBackgroundColor = 0 } = useRoom();
    const elementRef = useRef<HTMLCanvasElement>(null);
    const videoRef = useRef<HTMLVideoElement>(null);
    const flashRef = useRef<HTMLDivElement>(null);
    const isTakingPictureRef = useRef(false);
    const isMountedRef = useRef(true);
    const pendingCapturedSlotRef = useRef(-1);
    const pendingShouldShowFullAlertRef = useRef(false);

    const selectedPicture = selectedPictureIndex > -1 ? cameraRoll[selectedPictureIndex] : null;

    useEffect(() => {
        isMountedRef.current = true;

        return () => {
            isMountedRef.current = false;
        };
    }, []);

    useEffect(() => {
        if (selectedPicture) return;

        const target = elementRef.current;
        const video = videoRef.current;
        const source = GetRenderer()?.canvas;

        if (!target || !video || !source) return;

        let frame = 0;
        let stream: MediaStream = null;
        let streamIsReady = false;
        let previousPosition = '';

        const markStreamReady = () => {
            streamIsReady = true;
            video.classList.add('octane-camera-viewfinder__stream--ready');
        };

        const markStreamUnavailable = () => {
            streamIsReady = false;
            video.classList.remove('octane-camera-viewfinder__stream--ready');
        };

        try {
            if (typeof source.captureStream === 'function') {
                stream = source.captureStream(ROOM_STREAM_FRAME_RATE);
                video.srcObject = stream;
                video.addEventListener('playing', markStreamReady);
                video.addEventListener('error', markStreamUnavailable);
                void video.play().catch(markStreamUnavailable);
            }
        } catch {
            stream = null;
        }

        const tick = (now: number) => {
            const sourceBounds = source.getBoundingClientRect();
            const targetBounds = target.getBoundingClientRect();

            if (sourceBounds.width > 0 && sourceBounds.height > 0 && targetBounds.width > 0 && targetBounds.height > 0) {
                const position = `${sourceBounds.left - targetBounds.left},${sourceBounds.top - targetBounds.top},${sourceBounds.width},${sourceBounds.height}`;

                if (position !== previousPosition) {
                    previousPosition = position;
                    video.style.width = `${sourceBounds.width}px`;
                    video.style.height = `${sourceBounds.height}px`;
                    video.style.transform = `translate3d(${sourceBounds.left - targetBounds.left}px, ${sourceBounds.top - targetBounds.top}px, 0)`;
                }
            }

            // AIR registers CameraViewFinder as a 100 ms update receiver. Keep
            // that cadence only for browsers where canvas.captureStream is not
            // available; the normal path stays entirely in the compositor.
            if (!streamIsReady) {
                blitRoomCanvasToViewfinder(target, 320, 320, AIR_FALLBACK_FRAME_INTERVAL, now);
            }

            frame = window.requestAnimationFrame(tick);
        };

        frame = window.requestAnimationFrame(tick);

        return () => {
            window.cancelAnimationFrame(frame);
            video.removeEventListener('playing', markStreamReady);
            video.removeEventListener('error', markStreamUnavailable);
            video.pause();
            video.srcObject = null;
            video.classList.remove('octane-camera-viewfinder__stream--ready');
            stream?.getTracks().forEach((track) => track.stop());
        };
    }, [selectedPicture]);

    useEffect(() => {
        const capturedSlot = pendingCapturedSlotRef.current;

        if (capturedSlot < 0) return;

        pendingCapturedSlotRef.current = -1;

        const nextEmptySlot = getNextEmptyCameraSlot(cameraRoll);

        if (nextEmptySlot >= 0) {
            setActivePictureSlotIndex(nextEmptySlot);
            pendingShouldShowFullAlertRef.current = false;
            return;
        }

        setActivePictureSlotIndex(capturedSlot);

        if (pendingShouldShowFullAlertRef.current && !hasShownFullRollAlert) {
            hasShownFullRollAlert = true;
            simpleAlert(LocalizeText('camera.full.body'), NotificationAlertType.WINDOW, null, null, LocalizeText('camera.full.header'));
        }

        pendingShouldShowFullAlertRef.current = false;
    }, [cameraRoll, setActivePictureSlotIndex, simpleAlert]);

    const takePicture = async () => {
        if (selectedPictureIndex > -1) {
            setSelectedPictureIndex(-1);
            return;
        }

        if (isTakingPictureRef.current) return;

        const frame = getViewfinderRoomFrame(elementRef.current, 320, 320);

        if (!frame) {
            simpleAlert(LocalizeText('camera.error.creation'), NotificationAlertType.WINDOW, null, null, LocalizeText('generic.alert.title'));
            return;
        }

        isTakingPictureRef.current = true;

        const targetSlot = activePictureSlotIndex >= 0 && activePictureSlotIndex < CAMERA_ROLL_LIMIT ? activePictureSlotIndex : 0;
        let texture: OctaneTexture = null;

        try {
            texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, frame);

            if (!texture) return;

            // camera.render.mode "server": AIR captures the scene for the server at the shutter, from the same
            // viewfinder frame; no scene means camera.alert.too_much_stuff and no photo.
            let renderRoomMessage = null;

            if (GetCameraRenderMode() === 'server') {
                renderRoomMessage = CaptureCameraRenderMessage(frame, roomBackgroundColor);

                if (!renderRoomMessage) {
                    texture.destroy?.(true);
                    texture = null;
                    simpleAlert(LocalizeText('camera.alert.too_much_stuff'), NotificationAlertType.WINDOW, null, null, LocalizeText('generic.alert.title'));
                    return;
                }
            }

            PlaySound(SoundNames.CAMERA_SHUTTER);
            flashRef.current?.classList.remove('octane-camera-capture__flash--active');
            // Restart the CSS flash even when two photographs are taken quickly.
            void flashRef.current?.offsetWidth;
            flashRef.current?.classList.add('octane-camera-capture__flash--active');

            const imageUrl = await TextureUtils.generateImageUrl(texture);

            if (!imageUrl || !isMountedRef.current) {
                texture?.destroy?.(true);
                texture = null;

                if (isMountedRef.current) {
                    simpleAlert(LocalizeText('camera.alert.too_much_stuff'), NotificationAlertType.WINDOW, null, null, LocalizeText('generic.alert.title'));
                }

                return;
            }

            cameraRoll[targetSlot]?.texture?.destroy?.(true);

            const nextRoll = Array.from({ length: CAMERA_ROLL_LIMIT }, (_, index) => cameraRoll[index] ?? null);

            nextRoll[targetSlot] = new CameraPicture(texture, imageUrl, renderRoomMessage);
            pendingCapturedSlotRef.current = targetSlot;
            pendingShouldShowFullAlertRef.current = !hasShownFullRollAlert && willFillLastCameraSlot(cameraRoll, targetSlot);
            texture = null;
            setCameraRoll(nextRoll);
        } catch {
            texture?.destroy?.(true);

            if (isMountedRef.current) {
                simpleAlert(LocalizeText('camera.alert.too_much_stuff'), NotificationAlertType.WINDOW, null, null, LocalizeText('generic.alert.title'));
            }
        } finally {
            isTakingPictureRef.current = false;
        }
    };

    const activeSlotIndex = selectedPictureIndex > -1 ? selectedPictureIndex : activePictureSlotIndex;
    const hasPictures = cameraRoll.some((picture) => !!picture);

    return (
        <NativeTextTree>
            <DraggableWindow>
                <Column center className="octane-camera-capture" gap={0}>
                    <div className="octane-camera-capture__body drag-handler">
                        <div className="octane-camera-capture__title">
                            <CameraCenteredText
                                background={0x000000}
                                color={0xffffff}
                                text={LocalizeText('camera.interface.title')}
                                textStyle="u_frame_title"
                                width={340}
                            />
                        </div>
                        <BaseButton type="button" className="octane-camera-capture__help" aria-label={LocalizeText('generic.help')} onClick={() => CreateLinkEvent('habbopages/camera')} />
                        <BaseButton type="button" className="octane-camera-capture__close" aria-label={LocalizeText('generic.close')} onClick={onClose} />
                        <div className="octane-camera-viewfinder">
                            {!selectedPicture && (
                                <>
                                    <canvas ref={elementRef} className="octane-camera-viewfinder__fallback" width={320} height={320} />
                                    <video ref={videoRef} className="octane-camera-viewfinder__stream" aria-hidden="true" muted playsInline />
                                </>
                            )}
                            {selectedPicture && <img alt="" className="octane-camera-viewfinder__photo" src={selectedPicture.imageUrl} />}
                        </div>
                        {!selectedPicture && <div className="octane-camera-capture__crosshair" aria-hidden="true" />}
                        <div ref={flashRef} className="octane-camera-capture__flash" aria-hidden="true" />
                        {selectedPicture && (
                            <div className="octane-camera-capture__preview-actions">
                                {/* A real <BaseButton>: the drag handle lets only native controls take the mouse. */}
                                <Hint content={LocalizeText('camera.editor.button.tooltip')}>
                                    <BaseButton
                                        className="habbo-btn-primary octane-camera-capture__editor-button"
                                        aria-label={LocalizeText('camera.editor.button.tooltip')}
                                        type="button"
                                        onClick={onEdit}
                                    >
                                        <CameraNativeText
                                            background={0x000000}
                                            color={0xffffff}
                                            style={{ mixBlendMode: 'screen' }}
                                            text={LocalizeText('camera.editor.button.text')}
                                            textStyle="button_shiny_bold"
                                        />
                                    </BaseButton>
                                </Hint>
                            </div>
                        )}
                        <Hint content={LocalizeText('camera.take.photo.button.tooltip')}>
                            <BaseButton
                                type="button"
                                className="octane-camera-capture__shutter"
                                aria-label={LocalizeText('camera.take.photo.button.tooltip')}
                                onClick={takePicture}
                            />
                        </Hint>
                    </div>
                    <div className={`octane-camera-roll${hasPictures ? '' : ' octane-camera-roll--hidden'}`} aria-hidden={!hasPictures}>
                        {Array.from({ length: CAMERA_ROLL_LIMIT }, (_, index) => {
                            const picture = cameraRoll[index];
                            const isActive = index === activeSlotIndex;

                            return (
                                <div key={index} className={`octane-camera-roll__slot${isActive ? ' octane-camera-roll__slot--active' : ''}`}>
                                    <BaseButton
                                        type="button"
                                        className="octane-camera-roll__slot-button"
                                        aria-label={picture ? LocalizeText('camera.editor.button.tooltip') : LocalizeText('camera.take.photo.button.tooltip')}
                                        onClick={() => {
                                            if (isTakingPictureRef.current) return;

                                            setActivePictureSlotIndex(index);
                                            setSelectedPictureIndex(picture ? index : -1);
                                        }}
                                    >
                                        {picture && <img alt="" src={picture.imageUrl} />}
                                    </BaseButton>
                                    {picture && selectedPictureIndex === index && (
                                        <Hint content={LocalizeText('camera.delete.button.text')}>
                                            <BaseButton
                                                type="button"
                                                className="octane-camera-roll__delete"
                                                aria-label={LocalizeText('camera.delete.button.text')}
                                                onClick={() => {
                                                    if (!isTakingPictureRef.current) onDelete();
                                                }}
                                            />
                                        </Hint>
                                    )}
                                </div>
                            );
                        })}
                    </div>
                </Column>
            </DraggableWindow>
        </NativeTextTree>
    );
};
