import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { useRef, useMemo, useEffect, useCallback } from 'react';
import { useThree, useFrame } from '@react-three/fiber';
import * as THREE from 'three';

// ── Helpers ──────────────────────────────────────────────────────────────────
/** Convert a hex colour string to a normalised THREE.Color. */
function hexToColor(hex) {
    return new THREE.Color(hex);
}
/**
 * Seed particle data: positions scattered across an area that maps to the
 * viewport, plus per-particle velocity and depth factor.
 */
function createParticleData(count, width, height) {
    const positions = new Float32Array(count * 3);
    const velocities = new Float32Array(count * 3);
    const depths = new Float32Array(count); // 0 = far, 1 = near
    const originals = new Float32Array(count * 3);
    for (let i = 0; i < count; i++) {
        const i3 = i * 3;
        const x = (Math.random() - 0.5) * width;
        const y = (Math.random() - 0.5) * height;
        const z = 0;
        positions[i3] = x;
        positions[i3 + 1] = y;
        positions[i3 + 2] = z;
        originals[i3] = x;
        originals[i3 + 1] = y;
        originals[i3 + 2] = z;
        // Random gentle velocity: slight upward float + horizontal drift
        velocities[i3] = (Math.random() - 0.5) * 0.3; // vx
        velocities[i3 + 1] = Math.random() * 0.2 + 0.1; // vy (upward)
        velocities[i3 + 2] = 0; // vz
        depths[i] = Math.random();
    }
    return { positions, velocities, depths, originals };
}
// ── Scene ────────────────────────────────────────────────────────────────────
const ParticlesScene = ({ count, color, size, speed, connectionDistance, connectionColor, mouseRadius, mouseForce, opacity, reducedMotion, }) => {
    const { viewport } = useThree();
    // Refs
    const pointsRef = useRef(null);
    const linesRef = useRef(null);
    const mouseNDC = useRef(new THREE.Vector2(9999, 9999));
    // Keep latest props in a ref so useFrame reads current values
    const propsRef = useRef({
        speed,
        mouseRadius,
        mouseForce,
        connectionDistance,
        opacity,
        reducedMotion,
    });
    propsRef.current = {
        speed,
        mouseRadius,
        mouseForce,
        connectionDistance,
        opacity,
        reducedMotion,
    };
    // Dimensions in world units
    const worldWidth = viewport.width;
    const worldHeight = viewport.height;
    // ── Particle data (re-created when count or viewport changes) ────────────
    const data = useMemo(() => createParticleData(count, worldWidth, worldHeight), [count, worldWidth, worldHeight]);
    // ── Points geometry + material ───────────────────────────────────────────
    const pointsGeometry = useMemo(() => {
        const geo = new THREE.BufferGeometry();
        geo.setAttribute("position", new THREE.BufferAttribute(data.positions, 3));
        geo.setAttribute("depth", new THREE.BufferAttribute(data.depths, 1));
        return geo;
    }, [data]);
    const pointsMaterial = useMemo(() => {
        return new THREE.ShaderMaterial({
            transparent: true,
            depthWrite: false,
            uniforms: {
                u_color: { value: hexToColor(color) },
                u_size: { value: size },
                u_opacity: { value: opacity },
                u_pixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
            },
            vertexShader: /* glsl */ `
        attribute float depth;
        uniform float u_size;
        uniform float u_pixelRatio;
        varying float vDepth;

        void main() {
          vDepth = depth;
          vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
          // Larger particles appear closer (depth 1 = near, 0 = far)
          gl_PointSize = u_size * u_pixelRatio * (0.5 + depth * 0.8);
          gl_Position = projectionMatrix * mvPos;
        }
      `,
            fragmentShader: /* glsl */ `
        uniform vec3 u_color;
        uniform float u_opacity;
        varying float vDepth;

        void main() {
          // Circular point shape
          float dist = length(gl_PointCoord - vec2(0.5));
          if (dist > 0.5) discard;

          // Soft edge falloff
          float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
          // Depth affects brightness: closer = brighter
          float brightness = 0.4 + vDepth * 0.6;
          gl_FragColor = vec4(u_color * brightness, alpha * u_opacity * (0.3 + vDepth * 0.7));
        }
      `,
        });
    }, [color, size, opacity]);
    // Update material uniforms when props change
    useEffect(() => {
        if (pointsMaterial) {
            pointsMaterial.uniforms.u_color.value = hexToColor(color);
            pointsMaterial.uniforms.u_size.value = size;
            pointsMaterial.uniforms.u_opacity.value = opacity;
        }
    }, [color, size, opacity, pointsMaterial]);
    // ── Connection lines geometry + material ─────────────────────────────────
    const maxLines = count * 6; // upper bound on line segments
    const linesGeometry = useMemo(() => {
        const geo = new THREE.BufferGeometry();
        const linePositions = new Float32Array(maxLines * 6); // 2 verts * 3 coords per segment
        const lineAlphas = new Float32Array(maxLines * 2); // alpha per vertex
        geo.setAttribute("position", new THREE.BufferAttribute(linePositions, 3));
        geo.setAttribute("alpha", new THREE.BufferAttribute(lineAlphas, 1));
        geo.setDrawRange(0, 0);
        return geo;
    }, [maxLines]);
    const linesMaterial = useMemo(() => {
        return new THREE.ShaderMaterial({
            transparent: true,
            depthWrite: false,
            uniforms: {
                u_color: { value: hexToColor(connectionColor || color) },
                u_opacity: { value: opacity },
            },
            vertexShader: /* glsl */ `
        attribute float alpha;
        varying float vAlpha;

        void main() {
          vAlpha = alpha;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
            fragmentShader: /* glsl */ `
        uniform vec3 u_color;
        uniform float u_opacity;
        varying float vAlpha;

        void main() {
          gl_FragColor = vec4(u_color, vAlpha * u_opacity * 0.35);
        }
      `,
        });
    }, [color, connectionColor, opacity]);
    // Update line material when props change
    useEffect(() => {
        if (linesMaterial) {
            linesMaterial.uniforms.u_color.value = hexToColor(connectionColor || color);
            linesMaterial.uniforms.u_opacity.value = opacity;
        }
    }, [color, connectionColor, opacity, linesMaterial]);
    // ── Mouse tracking ───────────────────────────────────────────────────────
    const handlePointerMove = useCallback((event) => {
        mouseNDC.current.set(event.point.x, event.point.y);
    }, []);
    const handlePointerLeave = useCallback(() => {
        // Move mouse far away so it has no effect
        mouseNDC.current.set(9999, 9999);
    }, []);
    // ── Cleanup on unmount ───────────────────────────────────────────────────
    useEffect(() => {
        return () => {
            pointsGeometry.dispose();
            pointsMaterial.dispose();
            linesGeometry.dispose();
            linesMaterial.dispose();
        };
    }, [pointsGeometry, pointsMaterial, linesGeometry, linesMaterial]);
    // ── Animation loop ───────────────────────────────────────────────────────
    useFrame((_, delta) => {
        const { speed: spd, mouseRadius: mRadius, mouseForce: mForce, connectionDistance: connDist, opacity: opac, reducedMotion: reduced, } = propsRef.current;
        if (reduced)
            return; // Respect reduced motion preference
        const clampedDelta = Math.min(delta, 0.05); // cap to prevent large jumps
        const dt = clampedDelta * spd;
        const posAttr = pointsGeometry.getAttribute("position");
        const posArr = posAttr.array;
        const halfW = worldWidth / 2;
        const halfH = worldHeight / 2;
        const mx = mouseNDC.current.x;
        const my = mouseNDC.current.y;
        // Convert connectionDistance from screen-pixels to world units.
        // viewport.width corresponds to the canvas pixel width, so we scale.
        const connDistWorld = (connDist / (viewport.width * 50)) * worldWidth;
        const mouseRadiusWorld = (mRadius / (viewport.width * 50)) * worldWidth;
        // ── Update particle positions ────────────────────────────────────────
        for (let i = 0; i < count; i++) {
            const i3 = i * 3;
            // Apply velocity (floating upward + drift)
            posArr[i3] += data.velocities[i3] * dt;
            posArr[i3 + 1] += data.velocities[i3 + 1] * dt;
            // Mouse repulsion
            if (mRadius > 0) {
                const dx = posArr[i3] - mx;
                const dy = posArr[i3 + 1] - my;
                const distSq = dx * dx + dy * dy;
                const mRadSq = mouseRadiusWorld * mouseRadiusWorld;
                if (distSq < mRadSq && distSq > 0.0001) {
                    const dist = Math.sqrt(distSq);
                    const force = ((mouseRadiusWorld - dist) / mouseRadiusWorld) * mForce * dt;
                    posArr[i3] += (dx / dist) * force;
                    posArr[i3 + 1] += (dy / dist) * force;
                }
            }
            // Wrap around edges
            if (posArr[i3] > halfW)
                posArr[i3] = -halfW;
            if (posArr[i3] < -halfW)
                posArr[i3] = halfW;
            if (posArr[i3 + 1] > halfH) {
                posArr[i3 + 1] = -halfH;
                // Re-randomize x when wrapping vertically for natural look
                posArr[i3] = (Math.random() - 0.5) * worldWidth;
            }
            if (posArr[i3 + 1] < -halfH)
                posArr[i3 + 1] = halfH;
        }
        posAttr.needsUpdate = true;
        // ── Update connection lines ──────────────────────────────────────────
        if (connDist > 0 && linesGeometry) {
            const linePosAttr = linesGeometry.getAttribute("position");
            const lineAlphaAttr = linesGeometry.getAttribute("alpha");
            const linePosArr = linePosAttr.array;
            const lineAlphaArr = lineAlphaAttr.array;
            let lineIdx = 0;
            const connDistSq = connDistWorld * connDistWorld;
            for (let i = 0; i < count && lineIdx < maxLines; i++) {
                const i3 = i * 3;
                for (let j = i + 1; j < count && lineIdx < maxLines; j++) {
                    const j3 = j * 3;
                    const dx = posArr[i3] - posArr[j3];
                    const dy = posArr[i3 + 1] - posArr[j3 + 1];
                    const distSq = dx * dx + dy * dy;
                    if (distSq < connDistSq) {
                        const lineBase = lineIdx * 6;
                        const alphaBase = lineIdx * 2;
                        linePosArr[lineBase] = posArr[i3];
                        linePosArr[lineBase + 1] = posArr[i3 + 1];
                        linePosArr[lineBase + 2] = 0;
                        linePosArr[lineBase + 3] = posArr[j3];
                        linePosArr[lineBase + 4] = posArr[j3 + 1];
                        linePosArr[lineBase + 5] = 0;
                        const alpha = 1.0 - Math.sqrt(distSq) / connDistWorld;
                        lineAlphaArr[alphaBase] = alpha;
                        lineAlphaArr[alphaBase + 1] = alpha;
                        lineIdx++;
                    }
                }
            }
            linesGeometry.setDrawRange(0, lineIdx * 2);
            linePosAttr.needsUpdate = true;
            lineAlphaAttr.needsUpdate = true;
            if (linesMaterial) {
                linesMaterial.uniforms.u_opacity.value = opac;
            }
        }
        else if (linesGeometry) {
            linesGeometry.setDrawRange(0, 0);
        }
    });
    return (jsxs(Fragment, { children: [jsxs("mesh", { onPointerMove: handlePointerMove, onPointerLeave: handlePointerLeave, children: [jsx("planeGeometry", { args: [worldWidth * 2, worldHeight * 2] }), jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] }), jsx("points", { ref: pointsRef, geometry: pointsGeometry, material: pointsMaterial }), connectionDistance > 0 && (jsx("lineSegments", { ref: linesRef, geometry: linesGeometry, material: linesMaterial }))] }));
};

export { ParticlesScene };
