import { useFrame } from "@react-three/fiber";
import { useMemo, useRef } from "react";
import * as THREE from "three";
import type { PoolTelegraphKind, WorldPosition } from "../../game/types";

const TAU = Math.PI * 2;
const LANE_PARTICLE_COUNT = 7;
const AREA_PARTICLE_COUNT = 8;
const BREATH_PARTICLE_COUNT = 12;

function useReducedMotion() {
  return useMemo(() => (
    document.documentElement.classList.contains("force-reduced-motion")
      || window.matchMedia("(prefers-reduced-motion: reduce)").matches
  ), []);
}

export type LaneVfxVariant = "beam" | "charge" | "slash" | "web";

function LaneParticleGeometry({ variant }: { variant: LaneVfxVariant }) {
  if (variant === "beam" || variant === "web") return <sphereGeometry args={[0.11, 7, 5]} />;
  if (variant === "slash") return <octahedronGeometry args={[0.15, 0]} />;
  return <tetrahedronGeometry args={[0.13, 0]} />;
}

/** One instanced draw for motion/readability layered over an existing lane telegraph. */
export function LaneEnergyVfx({
  start,
  end,
  width,
  color,
  active,
  variant,
  height = 0.13,
}: {
  start: WorldPosition;
  end: WorldPosition;
  width: number;
  color: string;
  active: boolean;
  variant: LaneVfxVariant;
  height?: number;
}) {
  const particles = useRef<THREE.InstancedMesh>(null);
  const material = useRef<THREE.MeshBasicMaterial>(null);
  const transform = useMemo(() => new THREE.Object3D(), []);
  const reducedMotion = useReducedMotion();
  const dx = end[0] - start[0];
  const dz = end[1] - start[1];
  const length = Math.hypot(dx, dz);
  const angle = Math.atan2(dx, dz);
  const midpoint: [number, number, number] = [(start[0] + end[0]) * 0.5, height, (start[1] + end[1]) * 0.5];

  useFrame(({ clock }) => {
    if (!particles.current) return;
    const time = reducedMotion ? 0.35 : clock.elapsedTime;
    const speed = active ? 2.6 : variant === "web" ? 0.5 : 0.85;
    for (let index = 0; index < LANE_PARTICLE_COUNT; index += 1) {
      const offset = index / LANE_PARTICLE_COUNT;
      const cycle = (time * speed + offset) % 1;
      const cross = Math.sin(time * 2.4 + index * 1.7) * width * (variant === "web" ? 0.08 : 0.28);
      const rise = variant === "slash"
        ? Math.sin(cycle * Math.PI) * (active ? 0.65 : 0.28)
        : variant === "web" ? Math.sin(time * 3 + index) * 0.07 : Math.sin(cycle * Math.PI) * 0.18;
      transform.position.set(cross, rise, -length * 0.5 + cycle * length);
      transform.rotation.set(time * 0.8 + index, index * 0.63, active ? time * 1.5 : 0);
      const scale = (active ? 1.25 : 0.78) * (0.75 + Math.sin(cycle * Math.PI) * 0.4);
      transform.scale.set(
        variant === "slash" ? scale * 0.7 : scale,
        variant === "charge" ? scale * 0.55 : scale,
        variant === "charge" ? scale * 2.1 : scale,
      );
      transform.updateMatrix();
      particles.current.setMatrixAt(index, transform.matrix);
    }
    particles.current.instanceMatrix.needsUpdate = true;
    if (material.current) {
      const pulse = reducedMotion ? 0.65 : 0.58 + Math.sin(time * 6.2) * 0.14;
      material.current.opacity = active ? pulse + 0.22 : pulse * 0.62;
    }
  });

  return (
    <group position={midpoint} rotation={[0, angle, 0]}>
      <instancedMesh ref={particles} args={[undefined, undefined, LANE_PARTICLE_COUNT]} frustumCulled={false}>
        <LaneParticleGeometry variant={variant} />
        <meshBasicMaterial
          ref={material}
          color={color}
          transparent
          opacity={active ? 0.82 : 0.38}
          depthWrite={false}
          blending={THREE.AdditiveBlending}
          toneMapped={false}
        />
      </instancedMesh>
    </group>
  );
}

export function BreathParticleVfx({
  position,
  angle,
  range,
  halfAngle,
  active,
}: {
  position: WorldPosition;
  angle: number;
  range: number;
  halfAngle: number;
  active: boolean;
}) {
  const particles = useRef<THREE.InstancedMesh>(null);
  const material = useRef<THREE.MeshBasicMaterial>(null);
  const transform = useMemo(() => new THREE.Object3D(), []);
  const reducedMotion = useReducedMotion();

  useFrame(({ clock }) => {
    if (!particles.current) return;
    const time = reducedMotion ? 0.4 : clock.elapsedTime;
    for (let index = 0; index < BREATH_PARTICLE_COUNT; index += 1) {
      const row = index % 4;
      const lane = Math.floor(index / 4);
      const offsetAngle = -halfAngle + (lane / 2) * halfAngle * 2;
      const cycle = (time * (active ? 1.7 : 0.52) + row * 0.24 + lane * 0.08) % 1;
      const distance = range * (0.08 + cycle * 0.9);
      const scale = (active ? 1 : 0.55) * (1.15 - cycle * 0.55);
      transform.position.set(
        Math.sin(offsetAngle) * distance,
        0.18 + Math.sin(cycle * Math.PI) * (active ? 0.72 : 0.32),
        Math.cos(offsetAngle) * distance,
      );
      transform.rotation.set(time * 1.2 + index, -offsetAngle, cycle * TAU);
      transform.scale.set(scale * 0.72, scale * 1.45, scale * 0.72);
      transform.updateMatrix();
      particles.current.setMatrixAt(index, transform.matrix);
    }
    particles.current.instanceMatrix.needsUpdate = true;
    if (material.current) material.current.opacity = active ? 0.74 : 0.26;
  });

  return (
    <group position={[position[0], 0.1, position[1]]} rotation={[0, angle, 0]}>
      <instancedMesh ref={particles} args={[undefined, undefined, BREATH_PARTICLE_COUNT]} frustumCulled={false}>
        <tetrahedronGeometry args={[0.2, 0]} />
        <meshBasicMaterial
          ref={material}
          color={active ? "#7ceaff" : "#b8f4ff"}
          transparent
          opacity={active ? 0.74 : 0.26}
          depthWrite={false}
          blending={THREE.AdditiveBlending}
          toneMapped={false}
        />
      </instancedMesh>
    </group>
  );
}

export function CircleMechanicVfx({
  radius,
  innerRadius = 0,
  kind,
  color,
  active,
}: {
  radius: number;
  innerRadius?: number;
  kind: Extract<PoolTelegraphKind, "donut" | "soak" | "spread"> | "pounce";
  color: string;
  active: boolean;
}) {
  const particles = useRef<THREE.InstancedMesh>(null);
  const material = useRef<THREE.MeshBasicMaterial>(null);
  const transform = useMemo(() => new THREE.Object3D(), []);
  const reducedMotion = useReducedMotion();

  useFrame(({ clock }) => {
    if (!particles.current) return;
    const time = reducedMotion ? 0.3 : clock.elapsedTime;
    for (let index = 0; index < AREA_PARTICLE_COUNT; index += 1) {
      const offset = index / AREA_PARTICLE_COUNT;
      const cycle = (time * (active ? 1.2 : 0.55) + offset) % 1;
      const orbit = offset * TAU + time * (kind === "soak" ? -0.55 : 0.7);
      let radial = radius * 0.82;
      if (kind === "spread") radial = radius * (0.18 + cycle * 0.75);
      if (kind === "soak" || kind === "pounce") radial = radius * (0.92 - cycle * 0.68);
      if (kind === "donut") radial = index % 2 === 0 ? Math.max(0.2, innerRadius + 0.18) : radius - 0.18;
      const scale = (active ? 1 : 0.64) * (0.72 + Math.sin(cycle * Math.PI) * 0.48);
      transform.position.set(Math.sin(orbit) * radial, 0.12 + Math.sin(cycle * Math.PI) * 0.52, Math.cos(orbit) * radial);
      transform.rotation.set(time + index, orbit, cycle * TAU);
      transform.scale.setScalar(scale);
      transform.updateMatrix();
      particles.current.setMatrixAt(index, transform.matrix);
    }
    particles.current.instanceMatrix.needsUpdate = true;
    if (material.current) material.current.opacity = active ? 0.84 : 0.42;
  });

  return (
    <instancedMesh ref={particles} args={[undefined, undefined, AREA_PARTICLE_COUNT]} frustumCulled={false}>
      <octahedronGeometry args={[0.16, 0]} />
      <meshBasicMaterial
        ref={material}
        color={color}
        transparent
        opacity={active ? 0.84 : 0.42}
        depthWrite={false}
        blending={THREE.AdditiveBlending}
        toneMapped={false}
      />
    </instancedMesh>
  );
}
