import { useMemo, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import type { FlowSegment, PlacedPart, SimResult } from '../types';
import { useBuilder } from '../store/builderStore';
import { useSimulation } from '../simulation/flowSimulator';
import {
  ensureWaterVisual,
  freezeWaterClock,
  getWaterTime,
  getWaterVisual,
  tickWaterClock,
  waterVisuals,
} from '../simulation/waterState';

/**
 * FlowArrows — mount root for all time-based water visual systems:
 *
 *  - WaterDriver (always mounted): advances a global fill wavefront each
 *    frame and writes per-part fill state into the shared water map
 *    (simulation/waterState), which the meshes in PartBody read in their
 *    own useFrame callbacks. No React state is touched per frame.
 *  - Direction arrows (gated by the showFlow toggle): small cones marching
 *    along flowing segments, fading in/out at segment ends and appearing
 *    only once the pipe has actually filled with water.
 */
export function FlowArrows() {
  const showFlow = useBuilder((s) => s.showFlow);
  const parts = useBuilder((s) => s.parts);
  const sim = useSimulation();
  return (
    <group>
      <WaterDriver sim={sim} parts={parts} />
      {showFlow &&
        sim.segments.map((seg, i) => <ArrowStream key={`${seg.partId}-${i}`} seg={seg} />)}
    </group>
  );
}

// ---------------------------------------------------------------------------
// Water fill driver
// ---------------------------------------------------------------------------

// Module-level so remounts / panel toggles don't reset the animation.
let waveFront = 0;

const clamp01 = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x);
const clamp = (x: number, lo: number, hi: number) => (x < lo ? lo : x > hi ? hi : x);

const GAL_PER_FT3 = 7.48;

/** Approximate internal water volume (gal) — sets how fast a part fills. */
function partVolumeGal(part: PlacedPart, pathLen: number): number {
  const p = part.params;
  const d = Math.max(0.25, p.diameter ?? 1);
  switch (part.type) {
    case 'pipe':
    case 'elbow':
    case 'tee':
    case 'valve':
      // Bore volume: π·r² × path length (d inches → ft).
      return Math.PI * Math.pow(d / 24, 2) * pathLen * GAL_PER_FT3;
    case 'pump':
      return 0.06;
    case 'tower':
      return 0.1 * (p.height ?? 4);
    case 'tray':
      return 0.08 * (p.length ?? 3) * (p.width ?? 1.2);
    case 'wallPanel':
      return 0.25;
    case 'emitter':
      return 0.02;
    case 'drain':
      return 0.05;
    case 'reservoir':
      return 2;
    default:
      return 0.1;
  }
}

interface WaveSpan {
  dist: number;
  len: number;
  /** Wavefront speed through this part (ft/s) ≈ len · GPH / volume. */
  speed: number;
}

function WaterDriver({ sim, parts }: { sim: SimResult; parts: Record<string, PlacedPart> }) {
  const simRef = useRef(sim);
  simRef.current = sim;

  // Per-part fill data, recomputed only when the sim solution changes.
  const wave = useMemo(() => {
    const partIds = Object.keys(sim.fillLength);
    const spans: WaveSpan[] = [];
    for (const pid of partIds) {
      const dist = sim.fillDistance[pid];
      const flow = sim.flows[pid] ?? 0;
      const part = parts[pid];
      if (dist === undefined || flow <= 0 || !part) continue;
      const len = Math.max(0.2, sim.fillLength[pid]);
      const vol = Math.max(0.004, partVolumeGal(part, len));
      spans.push({ dist, len, speed: clamp((len * (flow / 3600)) / vol, 0.5, 5) });
    }
    return { partIds, spans };
  }, [sim, parts]);
  const waveRef = useRef(wave);
  waveRef.current = wave;

  useFrame((_, rawDt) => {
    // Master play/pause: freeze ALL water animation in place.
    if (!useBuilder.getState().simRunning) {
      freezeWaterClock();
      return;
    }
    const s = simRef.current;
    const dt = Math.min(rawDt, 0.1);
    tickWaterClock(dt);

    // Advance the wavefront while pumping; retreat (drain back) when idle.
    // The front moves at the speed of the slowest part it is currently
    // filling (∝ GPH / part volume), so fat tanks fill slower than thin pipes.
    const flowing = s.totalGph > 0.5;
    if (flowing) {
      let speed = Infinity;
      for (const span of waveRef.current.spans) {
        if (waveFront >= span.dist && waveFront < span.dist + span.len) {
          speed = Math.min(speed, span.speed);
        }
      }
      if (!Number.isFinite(speed)) speed = 2; // bridging a junction gap
      waveFront = Math.min(waveFront + speed * dt, s.totalPathLength + 1);
    } else {
      waveFront = Math.max(waveFront - 5 * dt, 0);
    }

    for (const pid of waveRef.current.partIds) ensureWaterVisual(pid);

    for (const [pid, v] of waterVisuals) {
      if (!(pid in s.fillLength)) {
        // Part was deleted — let its water vanish, then drop the entry.
        v.fill = Math.max(0, v.fill - dt * 1.5);
        if (v.fill <= 0) waterVisuals.delete(pid);
        continue;
      }
      const flow = s.flows[pid] ?? 0;
      const backed = s.backedUp.has(pid);
      const dist = s.fillDistance[pid];
      const len = Math.max(0.2, s.fillLength[pid]);

      if (flow > 0 && dist !== undefined) {
        // A part D ft from the pump fills once the wavefront passes D.
        v.fill = clamp01((waveFront - dist) / len);
      } else if (backed) {
        v.fill = Math.min(1, v.fill + dt * 0.6); // stagnant water backing up
      } else {
        v.fill = Math.max(0, v.fill - dt * 0.9); // no supply — drain out
      }

      v.flow = flow;
      v.backedUp = backed;
      v.overflow = false;
      v.spill = false;
      v.entry = s.entryConnector[pid];
      v.state = backed
        ? 'backedUp'
        : v.fill <= 0.002
          ? 'empty'
          : v.fill >= 0.998
            ? 'full'
            : 'filling';
    }

    for (const d of s.drains) {
      if (!d.overflowing) continue;
      const v = waterVisuals.get(d.partId);
      if (v) v.overflow = true;
    }
    for (const pot of s.netPots) {
      if (!pot.overflowing) continue;
      const v = ensureWaterVisual(pot.partId);
      v.fill = 1;
      v.flow = pot.inflowGph;
      v.backedUp = true;
      v.overflow = false;
      v.spill = true;
      v.state = 'backedUp';
    }
  });

  return null;
}

// ---------------------------------------------------------------------------
// Direction arrows
// ---------------------------------------------------------------------------

const UP = new THREE.Vector3(0, 1, 0);

function ArrowStream({ seg }: { seg: FlowSegment }) {
  const groupRef = useRef<THREE.Group>(null);

  const { from, dir, len, quat, count, speed } = useMemo(() => {
    const from = new THREE.Vector3(...seg.from);
    const to = new THREE.Vector3(...seg.to);
    const dir = to.clone().sub(from);
    const len = dir.length();
    dir.normalize();
    // Arrow speed tracks water VELOCITY (∝ GPH / d²), not raw GPH — the same
    // flow squeezed through a narrow pipe visibly rushes.
    const dia = Math.max(0.5, seg.diameter ?? 1);
    return {
      from,
      dir,
      len,
      quat: new THREE.Quaternion().setFromUnitVectors(UP, dir),
      count: Math.max(1, Math.round(len / 0.55)),
      speed: 0.35 + Math.min(2.5, seg.gph / (dia * dia) / 220),
    };
  }, [seg]);

  useFrame(() => {
    const g = groupRef.current;
    if (!g) return;
    const t = getWaterTime() * speed;
    // Arrows only appear once the water has actually reached this part.
    const fill = getWaterVisual(seg.partId)?.fill ?? 1;
    g.children.forEach((child, i) => {
      const f = (((i + t) % count) + count) % count; // 0..count
      const u = f / count;
      child.position.copy(from).addScaledVector(dir, u * len);
      const mat = (child as THREE.Mesh).material as THREE.MeshBasicMaterial;
      // Fade in/out near the segment ends.
      mat.opacity = 0.85 * fill * Math.min(1, Math.min(u, 1 - u) * 4 + 0.15);
    });
  });

  if (len < 0.2) return null;

  return (
    <group ref={groupRef}>
      {Array.from({ length: count }, (_, i) => (
        <mesh key={i} quaternion={quat}>
          <coneGeometry args={[0.05, 0.14, 8]} />
          <meshBasicMaterial color="#38bdf8" toneMapped={false} transparent opacity={0.85} />
        </mesh>
      ))}
    </group>
  );
}
