import { useFrame } from "@react-three/fiber";
import { useMemo, useRef } from "react";
import * as THREE from "three";
import { bossHazardVfxProfile, bossHazardVfxSeed, type BossHazardVfxShape } from "../../game/bossHazardVisuals";
import { useGameStore } from "../../game/store";
import type { CircleHazard } from "../../game/types";

const TAU = Math.PI * 2;

function ParticleGeometry({ shape }: { shape: BossHazardVfxShape }) {
  if (shape === "orb") return <sphereGeometry args={[0.16, 8, 6]} />;
  if (shape === "spike") return <coneGeometry args={[0.16, 0.9, 5]} />;
  return <octahedronGeometry args={[0.19, 0]} />;
}

/**
 * One instanced particle draw plus one transient accent and an optional column.
 * Effects mutate preallocated transforms and never touch combat state.
 */
export function CircleHazardVfx({ hazard, active }: { hazard: CircleHazard; active: boolean }) {
  const profile = bossHazardVfxProfile(hazard.kind);
  const particles = useRef<THREE.InstancedMesh>(null);
  const particleMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const warningRing = useRef<THREE.Mesh>(null);
  const warningMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const impactRing = useRef<THREE.Mesh>(null);
  const impactMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const column = useRef<THREE.Mesh>(null);
  const columnMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const transform = useMemo(() => new THREE.Object3D(), []);
  const seed = useMemo(() => bossHazardVfxSeed(hazard.id), [hazard.id]);
  const reducedMotion = useMemo(() => (
    document.documentElement.classList.contains("force-reduced-motion")
      || window.matchMedia("(prefers-reduced-motion: reduce)").matches
  ), []);

  useFrame(({ clock }) => {
    const time = useGameStore.getState().time;
    const activeAge = Math.max(0, time - hazard.activatesAt);
    const warningRemaining = Math.max(0, hazard.activatesAt - time);
    const effectTime = reducedMotion ? seed * 2 : clock.elapsedTime + seed * 7.3;
    const activeDuration = Math.max(0.22, hazard.expiresAt - hazard.activatesAt);
    const impactDuration = Math.min(0.48, activeDuration);
    const impactProgress = THREE.MathUtils.clamp(activeAge / impactDuration, 0, 1);

    if (particles.current) {
      for (let index = 0; index < profile.particleCount; index += 1) {
        const offset = (index + 0.5) / profile.particleCount;
        const baseAngle = offset * TAU + seed * TAU;
        const angle = baseAngle + effectTime * profile.spinSpeed;
        const stagger = (offset + seed * 0.37) % 1;
        let radial = hazard.radius * (0.28 + (index % 3) * 0.2);
        let y = 0.12;
        let width = 0.72;
        let height = 0.72;

        if (profile.motion === "bubble") {
          const cycle = (effectTime * 0.48 + stagger) % 1;
          radial = hazard.radius * (0.22 + (index % 3) * 0.22);
          y = 0.08 + cycle * profile.height;
          width = 0.55 + Math.sin(cycle * Math.PI) * 0.65;
          height = width;
        } else if (profile.motion === "fall") {
          const cycle = 1 - ((effectTime * 0.72 + stagger) % 1);
          radial = hazard.radius * (0.18 + (index % 3) * 0.2);
          y = 0.22 + cycle * profile.height;
          width = active ? 0.82 : 0.55;
          height = active ? 2.9 : 2.15;
        } else if (profile.motion === "flame") {
          const cycle = (effectTime * 0.95 + stagger) % 1;
          radial = hazard.radius * (0.2 + (index % 3) * 0.21);
          y = 0.12 + cycle * profile.height * 0.62;
          width = 0.5 + Math.sin(cycle * Math.PI) * 0.85;
          height = 0.75 + Math.sin(cycle * Math.PI) * 1.6;
        } else if (profile.motion === "orbit") {
          radial = hazard.radius * (0.3 + (index % 2) * 0.24);
          y = 0.28 + ((index % 4) / 3) * profile.height + Math.sin(effectTime * 2.2 + index) * 0.16;
          width = 0.65 + (index % 2) * 0.35;
          height = profile.shape === "crystal" ? 1.55 : width;
        } else if (profile.motion === "shockwave") {
          const wave = active ? impactProgress : 0.22 + (1 - Math.min(1, warningRemaining / 1.6)) * 0.18;
          radial = hazard.radius * wave;
          y = 0.13 + Math.sin(offset * Math.PI) * profile.height * 0.12;
          width = active ? 0.72 + impactProgress * 0.75 : 0.58;
          height = active ? 1.25 : 0.75;
        } else if (profile.motion === "spike") {
          const rise = active ? THREE.MathUtils.clamp(activeAge / 0.16, 0, 1) : 0.12;
          radial = hazard.radius * (0.2 + (index % 3) * 0.23);
          width = 0.75 + (index % 2) * 0.35;
          height = Math.max(0.18, rise * (profile.height + (index % 3) * 0.34));
          y = height * 0.45;
        } else {
          const cycle = (effectTime * 0.72 + stagger) % 1;
          radial = hazard.radius * (0.18 + cycle * 0.68);
          y = 0.12 + Math.sin(cycle * Math.PI) * profile.height;
          width = 0.6 + Math.sin(cycle * Math.PI) * 0.72;
          height = width * 1.4;
        }

        const warningScale = active ? 1 : 0.56;
        transform.position.set(Math.sin(angle) * radial, y, Math.cos(angle) * radial);
        transform.rotation.set(index * 0.73, -angle, profile.motion === "fall" ? 0 : effectTime * 0.45 + index);
        transform.scale.set(width * warningScale, height * warningScale, width * warningScale);
        transform.updateMatrix();
        particles.current.setMatrixAt(index, transform.matrix);
      }
      particles.current.instanceMatrix.needsUpdate = true;
    }

    if (particleMaterial.current) {
      const pulse = reducedMotion ? 0.72 : 0.64 + Math.sin(effectTime * 4.6) * 0.12;
      particleMaterial.current.opacity = active ? pulse + 0.16 : pulse * 0.48;
    }

    if (warningRing.current && warningMaterial.current) {
      const countdown = THREE.MathUtils.clamp(warningRemaining / 1.8, 0, 1);
      const scale = hazard.radius * (0.18 + countdown * 0.82);
      warningRing.current.visible = !active;
      warningRing.current.scale.setScalar(scale);
      warningRing.current.rotation.z = reducedMotion ? seed * TAU : effectTime * profile.spinSpeed * 0.42;
      warningMaterial.current.opacity = 0.48 + (reducedMotion ? 0 : Math.sin(effectTime * 7) * 0.16);
    }

    if (impactRing.current && impactMaterial.current) {
      impactRing.current.visible = active && impactProgress < 0.999;
      impactRing.current.scale.setScalar(hazard.radius * (0.16 + impactProgress * 1.02));
      impactMaterial.current.opacity = (1 - impactProgress) * 0.92;
    }

    if (column.current && columnMaterial.current) {
      const columnPulse = reducedMotion ? 0.78 : 0.7 + Math.sin(effectTime * 3.8) * 0.18;
      column.current.rotation.y = effectTime * profile.spinSpeed * 0.3;
      column.current.scale.set(
        hazard.radius * (active ? 0.9 : 0.62),
        profile.height * (active ? 1 : 0.72),
        hazard.radius * (active ? 0.9 : 0.62),
      );
      columnMaterial.current.opacity = profile.columnOpacity * columnPulse * (active ? 1 : 0.52);
    }
  });

  const color = active ? profile.activeColor : profile.warningColor;
  return (
    <group>
      <instancedMesh ref={particles} args={[undefined, undefined, profile.particleCount]} frustumCulled={false}>
        <ParticleGeometry shape={profile.shape} />
        <meshBasicMaterial
          ref={particleMaterial}
          color={color}
          transparent
          opacity={active ? 0.8 : 0.35}
          depthWrite={false}
          blending={THREE.AdditiveBlending}
          toneMapped={false}
        />
      </instancedMesh>
      <mesh ref={warningRing} position={[0, 0.085, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <torusGeometry args={[1, 0.026, 5, 36]} />
        <meshBasicMaterial
          ref={warningMaterial}
          color={profile.highlightColor}
          transparent
          opacity={0.58}
          depthWrite={false}
          blending={THREE.AdditiveBlending}
          toneMapped={false}
        />
      </mesh>
      <mesh ref={impactRing} position={[0, 0.12, 0]} rotation={[-Math.PI / 2, 0, 0]} visible={false}>
        <ringGeometry args={[0.84, 1, 40]} />
        <meshBasicMaterial
          ref={impactMaterial}
          color={profile.highlightColor}
          transparent
          opacity={0}
          depthWrite={false}
          blending={THREE.AdditiveBlending}
          toneMapped={false}
        />
      </mesh>
      {profile.columnOpacity > 0 && (
        <mesh ref={column} position={[0, profile.height * 0.5, 0]}>
          <cylinderGeometry args={[0.48, 0.16, 1, 18, 1, true]} />
          <meshBasicMaterial
            ref={columnMaterial}
            color={color}
            transparent
            opacity={profile.columnOpacity}
            depthWrite={false}
            side={THREE.DoubleSide}
            blending={THREE.AdditiveBlending}
            toneMapped={false}
          />
        </mesh>
      )}
    </group>
  );
}
