import { RigidBody, RapierRigidBody } from '@react-three/rapier';
import React, { useEffect, useRef, useState } from 'react';
import { useLoadingProgress } from '../../../store/builder/store';
import { MaterialModel } from '../../simulation/materials/instances/material/materialModel';
import { useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { CameraControls, } from '@react-three/drei';
import { generateUniqueId } from '../../../functions/generateUniqueId';

type MaterialSpawnerProps = {
    position: [number, number, number];
    spawnInterval: number;
    spawnCount: number;
};

function MaterialSpawner({ position: initialPos, spawnInterval, spawnCount }: MaterialSpawnerProps) {
    const { loadingProgress } = useLoadingProgress();
    const [spawned, setSpawned] = useState<{
        id: string;
        position: [number, number, number];
        ref: React.RefObject<RapierRigidBody>;
        materialType: string;
    }[]>([]);
    const [spawningPaused, setSpawningPaused] = useState(true);
    const spawnedCount = useRef(0);
    const { gl, camera, pointer, controls } = useThree();
    const [draggedId, setDraggedId] = useState<string | null>(null);
    const dragOffset = useRef<THREE.Vector3>(new THREE.Vector3());
    const initialDepth = useRef<number>(0);
    const materialTypes = ['Default material', 'Material 1', 'Material 2', 'Material 3'];
    const spawnerRef = useRef<THREE.Mesh>(null!);
    const [isDraggingSpawner, setIsDraggingSpawner] = useState(false);
    const spawnerDragOffset = useRef<THREE.Vector3>(new THREE.Vector3());
    const spawnerInitialDepth = useRef<number>(0);
    const [boxPosition, setBoxPosition] = useState<[number, number, number]>(initialPos);

    useEffect(() => {
        if (loadingProgress !== 0) return;

        let interval: NodeJS.Timeout | null = null;

        const startSpawning = () => {
            if (!interval) {
                interval = setInterval(() => {
                    if (spawningPaused) return;
                    setSpawned(prev => {
                        if (spawnCount !== undefined && spawnedCount.current >= spawnCount) {
                            clearInterval(interval!);
                            interval = null;
                            return prev;
                        }
                        spawnedCount.current++;

                        const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];

                        
                        return [
                            ...prev,
                            {
                                id: generateUniqueId(),
                                position: [...boxPosition] as [number, number, number], // use latest position state
                                ref: React.createRef<RapierRigidBody>(),
                                materialType: randomMaterialType,
                            }
                        ];
                    });

                }, spawnInterval);
            }
        };

        const stopSpawning = () => {
            if (interval) {
                clearInterval(interval);
                interval = null;
            }
        };

        const handleVisibility = () => {
            if (document.visibilityState === 'visible') {
                startSpawning();
            } else {
                stopSpawning();
            }
        };

        document.addEventListener('visibilitychange', handleVisibility);
        handleVisibility();

        return () => {
            stopSpawning();
            document.removeEventListener('visibilitychange', handleVisibility);
        };

    }, [loadingProgress, spawnInterval, spawnCount, spawningPaused, boxPosition]);

    const handleSleep = (id: string) => {
        setSpawned(prev => prev.filter(obj => obj.id !== id));
    };

    const handlePointerDown = (id: string) => {
        if (controls) (controls as CameraControls).enabled = false;
        setDraggedId(id);

        const obj = spawned.find(o => o.id === id);
        if (!obj || !obj.ref.current) return;

        const currentPosition = obj.ref.current.translation();

        const screenPosition = new THREE.Vector3(currentPosition.x, currentPosition.y, currentPosition.z).project(camera);

        dragOffset.current = new THREE.Vector3(
            screenPosition.x - pointer.x,
            0,
            screenPosition.y - pointer.y
        );

        initialDepth.current = new THREE.Vector3(currentPosition.x, currentPosition.y, currentPosition.z)
            .sub(camera.position)
            .length();

        obj.ref.current.setGravityScale(0, true);
        obj.ref.current.setLinearDamping(10);
        obj.ref.current.setAngularDamping(10);
    };

    const handlePointerMove = () => {
        if (!draggedId) return;

        const obj = spawned.find(o => o.id === draggedId);
        if (!obj || !obj.ref.current) return;

        const targetScreenPos = new THREE.Vector3(
            pointer.x + dragOffset.current.x,
            0,
            pointer.y + dragOffset.current.z
        );

        const targetWorldPos = new THREE.Vector3(
            targetScreenPos.x,
            targetScreenPos.z,
            0.5
        ).unproject(camera);

        const direction = targetWorldPos.sub(camera.position).normalize();
        const finalPosition = camera.position.clone().add(direction.multiplyScalar(initialDepth.current));

        const currentPosition = obj.ref.current.translation();
        const moveDirection = new THREE.Vector3().subVectors(finalPosition, currentPosition);

        obj.ref.current.setLinvel({
            x: moveDirection.x * 20,
            y: moveDirection.y * 20,
            z: moveDirection.z * 20
        }, true);
    };

    const handlePointerUp = () => {
        if (controls) (controls as CameraControls).enabled = true;
        if (!draggedId) return;

        const obj = spawned.find(o => o.id === draggedId);
        if (obj?.ref.current) {
            obj.ref.current.setGravityScale(1, true);
            obj.ref.current.setLinearDamping(0.5);
            obj.ref.current.setAngularDamping(0.5);
        }

        setDraggedId(null);
    };

    useEffect(() => {
        const canvasElement = gl.domElement;
        canvasElement.addEventListener('pointermove', handlePointerMove);
        canvasElement.addEventListener('pointerup', handlePointerUp);

        return () => {
            canvasElement.removeEventListener('pointermove', handlePointerMove);
            canvasElement.removeEventListener('pointerup', handlePointerUp);
        };
    }, [draggedId, spawned, controls, camera, gl]);


    const handleSpawnerPointerDown = (e: any) => {
        if (e.button !== 2) return; // right click only
        e.stopPropagation();

        if (controls) (controls as CameraControls).enabled = false;
        setIsDraggingSpawner(true);

        const worldPos = spawnerRef.current.getWorldPosition(new THREE.Vector3());
        const screenPos = worldPos.clone().project(camera);

        spawnerDragOffset.current.set(
            screenPos.x - pointer.x,
            0,
            screenPos.y - pointer.y
        );

        spawnerInitialDepth.current = worldPos.clone().sub(camera.position).length();
    };

    const handleSpawnerPointerMove = () => {
        if (!isDraggingSpawner) return;

        const targetScreenPos = new THREE.Vector3(
            pointer.x + spawnerDragOffset.current.x,
            0,
            pointer.y + spawnerDragOffset.current.z
        );

        const targetWorldPos = new THREE.Vector3(
            targetScreenPos.x,
            targetScreenPos.z,
            0.5
        ).unproject(camera);

        const direction = targetWorldPos.sub(camera.position).normalize();
        const finalPosition = camera.position.clone().add(direction.multiplyScalar(spawnerInitialDepth.current));

        spawnerRef.current.position.copy(finalPosition);
    };

    const handleSpawnerPointerUp = () => {
        if (controls) (controls as CameraControls).enabled = true;
        setIsDraggingSpawner(false);

        if (spawnerRef.current) {
            const worldPos = spawnerRef.current.getWorldPosition(new THREE.Vector3());
            setBoxPosition([worldPos.x, worldPos.y, worldPos.z] as [number, number, number]);
        }
    };

    useEffect(() => {
        const canvas = gl.domElement;
        canvas.addEventListener("pointermove", handleSpawnerPointerMove);
        canvas.addEventListener("pointerup", handleSpawnerPointerUp);
        return () => {
            canvas.removeEventListener("pointermove", handleSpawnerPointerMove);
            canvas.removeEventListener("pointerup", handleSpawnerPointerUp);
        };
    }, [isDraggingSpawner, camera, gl, controls]);

    const handleBoxClick = () => {
        setSpawningPaused(prev => !prev);
    };

    return (
        <>
            <mesh
                name='Spawner Box'
                ref={spawnerRef}
                position={boxPosition}
                onClick={handleBoxClick}
                onPointerDown={handleSpawnerPointerDown}
            >
                <boxGeometry args={[1, 1, 1]} />
                <meshStandardMaterial color={spawningPaused ? "red" : "white"} transparent opacity={0.2} />
            </mesh>

            {spawned.map(({ id, position, materialType, ref }) => (
                <RigidBody
                    key={id}
                    ref={ref}
                    position={position}
                    colliders="hull"
                    angularDamping={0.5}
                    linearDamping={0.5}
                    restitution={0.1}
                    userData={{ materialType, materialUuid: id }}
                    onSleep={() => handleSleep(id)}
                >
                    <MaterialModel
                        materialId={id}
                        materialType={materialType}
                        onPointerDown={(e) => {
                            e.stopPropagation();
                            handlePointerDown(id);
                        }}
                    />
                </RigidBody>
            ))}
        </>
    );
}

export default MaterialSpawner;