/* === Contact "Black Hole" Particle Guide + Hyperspace Jump ===
 *
 * Two modes share one canvas:
 *
 * 1) Guide mode (active=true, jumpPhase="idle"):
 *    Particles flow from one form target to the next based on validation:
 *      Name (>=2) → E-Mail (regex) → Nachricht (any) → Submit-Button
 *    On invalid email: red repulsion. On hover: glowing halo.
 *
 * 2) Hyperspace jump (jumpPhase: "charge" | "jump" | "tunnel" | "snap" | "confirm"):
 *    - charge:  particles tremble, button glows, energy gathers (~0.4s)
 *    - jump:    radial explosion, particles stretch into light streaks (~0.6s)
 *    - tunnel:  hundreds of streaks rocket outward, parallax depth (~0.8s)
 *    - snap:    streaks collapse to dots, white flash (~0.3s)
 *    - confirm: ambient afterglow, particles drift gently (~0.5s+)
 *
 * Performance: single rAF, additive blend, ~120 base particles
 * (jump phase temporarily upgrades to ~400 streaks for depth).
 * Reduced motion: skips jump entirely — caller renders confirmation directly.
 *
 * Props:
 *   - active     : boolean
 *   - target     : { x, y, w, h } | null
 *   - mode       : "attract" | "repel" | "suck"
 *   - tone       : "default" | "error" | "success"
 *   - jumpPhase  : "idle" | "charge" | "jump" | "tunnel" | "snap" | "confirm"
 *   - jumpCenter : { x, y } | null  (center of explosion, usually the button)
 */

const HYPER_PALETTE = [
  { h: 0,   s: 0,   l: 100 }, // pure white
  { h: 210, s: 100, l: 70  }, // bright blue
  { h: 220, s: 100, l: 80  }, // sky
  { h: 270, s: 80,  l: 65  }, // violet
];

const easeInQuart = (t) => t * t * t * t;
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);

const BlackHoleParticles = ({
  active,
  target,
  mode = "attract",
  tone = "default",
  jumpPhase = "idle",
  jumpCenter = null,
}) => {
  const canvasRef = React.useRef(null);
  const stateRef = React.useRef({
    particles: [],
    streaks: [],         // hyperspace-only line particles
    raf: 0,
    lastT: 0,
    target: null,
    mode: "attract",
    tone: "default",
    active: false,
    jumpPhase: "idle",
    jumpCenter: null,
    phaseT0: 0,          // timestamp when current phase started
    flashAlpha: 0,
    dpr: 1,
    w: 0,
    h: 0,
    reduced: false,
  });

  // Sync incoming props into the mutable state ref
  React.useEffect(() => {
    const s = stateRef.current;
    s.target = target;
    s.mode = mode;
    s.tone = tone;
    s.active = active;
  }, [target, mode, tone, active]);

  // Phase transitions need a timestamp anchor + streak spawn for tunnel
  React.useEffect(() => {
    const s = stateRef.current;
    s.jumpPhase = jumpPhase;
    s.jumpCenter = jumpCenter;
    s.phaseT0 = performance.now();
    if (jumpPhase === "jump" || jumpPhase === "tunnel") {
      // Spawn the dense streak field for hyperspace tunnel
      spawnStreaks(s, jumpPhase === "tunnel" ? 280 : 160);
    }
    if (jumpPhase === "snap") {
      s.flashAlpha = 1;
    }
    if (jumpPhase === "idle") {
      s.streaks = [];
      s.flashAlpha = 0;
    }
  }, [jumpPhase, jumpCenter]);

  const spawnStreaks = (s, n) => {
    const cx = s.jumpCenter ? s.jumpCenter.x : s.w / 2;
    const cy = s.jumpCenter ? s.jumpCenter.y : s.h / 2;
    const arr = [];
    for (let i = 0; i < n; i++) {
      const angle = Math.random() * Math.PI * 2;
      const startR = Math.random() * 8;             // start near center
      const speed = 600 + Math.random() * 1800;     // depth via varied speed
      const palette = HYPER_PALETTE[Math.floor(Math.random() * HYPER_PALETTE.length)];
      arr.push({
        x: cx + Math.cos(angle) * startR,
        y: cy + Math.sin(angle) * startR,
        angle,
        speed,
        dist: startR,
        life: 0,
        thickness: 0.6 + Math.random() * 1.6,
        hue: palette.h,
        sat: palette.s,
        light: palette.l,
        alpha: 0,
      });
    }
    s.streaks = arr;
  };

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d", { alpha: true });
    const s = stateRef.current;

    const reduceMQ = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)");
    s.reduced = !!(reduceMQ && reduceMQ.matches);

    const baseCount = () => {
      if (s.reduced) return 30;
      if (window.innerWidth < 720) return 70;
      return 120;
    };

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const rect = canvas.getBoundingClientRect();
      s.dpr = dpr;
      s.w = rect.width;
      s.h = rect.height;
      canvas.width = Math.max(1, Math.floor(rect.width * dpr));
      canvas.height = Math.max(1, Math.floor(rect.height * dpr));
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(canvas);

    const spawn = (p) => {
      const w = s.w, h = s.h;
      const side = Math.floor(Math.random() * 4);
      let x, y;
      if (side === 0) { x = Math.random() * w; y = -10; }
      else if (side === 1) { x = w + 10; y = Math.random() * h; }
      else if (side === 2) { x = Math.random() * w; y = h + 10; }
      else { x = -10; y = Math.random() * h; }
      const speed = 0.2 + Math.random() * 0.4;
      const angle = Math.random() * Math.PI * 2;
      p.x = x;
      p.y = y;
      p.vx = Math.cos(angle) * speed;
      p.vy = Math.sin(angle) * speed;
      p.r = 0.6 + Math.random() * 1.6;
      p.life = 0;
      p.maxLife = 4 + Math.random() * 4;
      p.hue = 200 + Math.random() * 80;
      p.alpha = 0;
    };

    const initPool = () => {
      const n = baseCount();
      s.particles = new Array(n).fill(0).map(() => {
        const p = {};
        spawn(p);
        return p;
      });
    };
    initPool();

    const tick = (t) => {
      s.raf = requestAnimationFrame(tick);
      const dt = Math.min(0.05, (t - (s.lastT || t)) / 1000);
      s.lastT = t;

      ctx.clearRect(0, 0, s.w, s.h);

      const phase = s.jumpPhase;
      const phaseElapsed = (t - s.phaseT0) / 1000;

      if (phase === "idle") {
        runGuideMode(ctx, s, dt, spawn);
      } else {
        runJumpMode(ctx, s, dt, phase, phaseElapsed, spawn);
      }
    };

    s.raf = requestAnimationFrame(tick);
    return () => {
      cancelAnimationFrame(s.raf);
      ro.disconnect();
    };
  }, []);

  // Soft edge mask: particles fade smoothly into the surrounding page
  // instead of clipping at the canvas rectangle. The mask is symmetrical
  // on all four sides; it blends into whatever background the section sits on.
  const edgeMask =
    "radial-gradient(ellipse 110% 95% at 50% 50%, #000 55%, transparent 100%)";
  return (
    <canvas
      ref={canvasRef}
      aria-hidden="true"
      style={{
        position: "absolute",
        inset: "-60px -80px",
        width: "calc(100% + 160px)",
        height: "calc(100% + 120px)",
        pointerEvents: "none",
        zIndex: 1,
        WebkitMaskImage: edgeMask,
        maskImage: edgeMask,
      }} />
  );
};

// === Guide mode (idle) ===
function runGuideMode(ctx, s, dt, spawn) {
  if (!s.active) {
    for (const p of s.particles) {
      p.alpha *= 0.92;
      p.x += p.vx;
      p.y += p.vy;
    }
    drawParticles(ctx, s);
    return;
  }

  const tgt = s.target;
  const cx = tgt ? tgt.x + tgt.w / 2 : s.w / 2;
  const cy = tgt ? tgt.y + tgt.h / 2 : s.h / 2;

  const strength =
    s.mode === "suck" ? 1400 :
    s.mode === "repel" ? -700 :
    420;
  const swirl = s.mode === "repel" ? 0 : (s.reduced ? 0.5 : 1.6);
  const toneHue =
    s.tone === "error" ? 0 :
    s.tone === "success" ? 130 :
    null;

  for (const p of s.particles) {
    p.life += dt;
    const dx = cx - p.x;
    const dy = cy - p.y;
    const dist = Math.max(8, Math.sqrt(dx * dx + dy * dy));
    const nx = dx / dist;
    const ny = dy / dist;
    const g = strength / (dist * 0.6);
    p.vx += nx * g * dt;
    p.vy += ny * g * dt;
    const swirlForce = (swirl * 60) / Math.max(20, dist * 0.4);
    p.vx += -ny * swirlForce * dt;
    p.vy += nx * swirlForce * dt;
    const drag = s.mode === "suck" ? 0.97 : 0.985;
    p.vx *= drag;
    p.vy *= drag;
    p.x += p.vx;
    p.y += p.vy;
    p.alpha = Math.min(1, p.alpha + dt * 3.2);

    const consumed = dist < (tgt ? Math.max(8, Math.min(tgt.w, tgt.h) * 0.18) : 12);
    const stray = p.x < -60 || p.x > s.w + 60 || p.y < -60 || p.y > s.h + 60;
    if ((s.mode !== "repel" && consumed) || p.life > p.maxLife || stray) {
      spawn(p);
      if (s.mode === "suck") p.maxLife = 0.6 + Math.random() * 0.6;
    }
    if (toneHue !== null) p.hue = toneHue + (Math.random() - 0.5) * 30;
  }
  drawParticles(ctx, s);
}

// === Jump mode (charge/jump/tunnel/snap/confirm) ===
function runJumpMode(ctx, s, dt, phase, phaseElapsed, spawn) {
  const cx = s.jumpCenter ? s.jumpCenter.x : s.w / 2;
  const cy = s.jumpCenter ? s.jumpCenter.y : s.h / 2;

  if (phase === "charge") {
    // Particles tremble + drift toward center, button glow intensifies
    const tProg = Math.min(1, phaseElapsed / 0.4);
    for (const p of s.particles) {
      const dx = cx - p.x;
      const dy = cy - p.y;
      const dist = Math.max(8, Math.sqrt(dx * dx + dy * dy));
      const nx = dx / dist, ny = dy / dist;
      // gentle pull
      p.vx += nx * 200 / dist * dt;
      p.vy += ny * 200 / dist * dt;
      // jitter (the "tension")
      const jitter = 80 * tProg;
      p.x += (Math.random() - 0.5) * jitter * dt;
      p.y += (Math.random() - 0.5) * jitter * dt;
      p.vx *= 0.96; p.vy *= 0.96;
      p.x += p.vx; p.y += p.vy;
      p.alpha = Math.min(1, p.alpha + dt * 4);
      // shift to blue/violet
      p.hue = 220 + Math.random() * 50;
    }
    // Pulsing core glow
    drawCoreGlow(ctx, cx, cy, 30 + tProg * 60, 0.4 + tProg * 0.5);
    drawParticles(ctx, s);
    return;
  }

  if (phase === "jump") {
    // Existing particles convert to outward streaks + dedicated streak field
    const tProg = Math.min(1, phaseElapsed / 0.6);
    const accel = easeInQuart(tProg);

    // Convert dot particles into streak velocities (radial)
    for (const p of s.particles) {
      const dx = p.x - cx;
      const dy = p.y - cy;
      const dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
      const nx = dx / dist, ny = dy / dist;
      const v = (200 + accel * 1800);
      p.vx = nx * v;
      p.vy = ny * v;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.alpha = Math.min(1, p.alpha + dt * 3);
    }

    updateAndDrawStreaks(ctx, s, dt, cx, cy, accel, 0.9);
    drawCoreGlow(ctx, cx, cy, 90 + accel * 80, 0.7);
    // Light dots underneath as motion-blurred trails
    drawParticleStreaks(ctx, s, cx, cy, 0.4 + accel * 0.5);
    return;
  }

  if (phase === "tunnel") {
    // Full hyperspace tunnel: streaks dominate, dots gone
    const tProg = Math.min(1, phaseElapsed / 0.8);
    updateAndDrawStreaks(ctx, s, dt, cx, cy, 1, 1);
    // Subtle radial dim vignette to enhance depth
    drawCoreGlow(ctx, cx, cy, 200, 0.5 - tProg * 0.3);
    return;
  }

  if (phase === "snap") {
    // Streaks decelerate (easeOutCubic) and collapse, white flash dies down
    const tProg = Math.min(1, phaseElapsed / 0.3);
    const decel = 1 - easeOutCubic(tProg);
    for (const st of s.streaks) {
      st.dist += st.speed * dt * decel * 0.4;
      st.alpha = Math.max(0, st.alpha - dt * 4);
      st.x = cx + Math.cos(st.angle) * st.dist;
      st.y = cy + Math.sin(st.angle) * st.dist;
    }
    drawStreaks(ctx, s);
    // White flash
    s.flashAlpha = Math.max(0, 1 - tProg * 1.4);
    if (s.flashAlpha > 0) {
      ctx.fillStyle = `rgba(255,255,255,${s.flashAlpha})`;
      ctx.fillRect(0, 0, s.w, s.h);
    }
    return;
  }

  if (phase === "confirm") {
    // Soft afterglow — gentle drifting particles, faint cyan halo
    for (const p of s.particles) {
      p.alpha *= 0.97;
      p.x += p.vx * 0.2 * dt;
      p.y += p.vy * 0.2 * dt;
      p.vx *= 0.96; p.vy *= 0.96;
      // re-center occasionally for ambient sparkle
      if (p.alpha < 0.05) {
        p.x = cx + (Math.random() - 0.5) * s.w * 0.6;
        p.y = cy + (Math.random() - 0.5) * s.h * 0.6;
        p.vx = (Math.random() - 0.5) * 20;
        p.vy = (Math.random() - 0.5) * 20;
        p.alpha = 0.3 + Math.random() * 0.4;
        p.hue = 180 + Math.random() * 40;
      }
    }
    drawCoreGlow(ctx, cx, cy, 140, 0.18);
    drawParticles(ctx, s);
    return;
  }
}

// === Drawing helpers ===
function drawParticles(ctx, s) {
  ctx.globalCompositeOperation = "lighter";
  const tgt = s.target;
  if (tgt && s.active && s.jumpPhase === "idle") {
    const cx = tgt.x + tgt.w / 2;
    const cy = tgt.y + tgt.h / 2;
    const r = Math.max(tgt.w, tgt.h) * 0.7;
    const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
    const haloHue =
      s.tone === "error" ? "rgba(255,80,80," :
      s.tone === "success" ? "rgba(120,220,140," :
      "rgba(150,170,255,";
    grad.addColorStop(0, haloHue + "0.18)");
    grad.addColorStop(0.6, haloHue + "0.05)");
    grad.addColorStop(1, haloHue + "0)");
    ctx.fillStyle = grad;
    ctx.fillRect(tgt.x - r, tgt.y - r, tgt.w + r * 2, tgt.h + r * 2);
  }
  for (const p of s.particles) {
    if (p.alpha <= 0.01) continue;
    ctx.fillStyle = `hsla(${p.hue}, 90%, 65%, ${p.alpha * 0.9})`;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalCompositeOperation = "source-over";
}

// During the jump phase, dot particles render as motion-blurred lines too
function drawParticleStreaks(ctx, s, cx, cy, alphaScale) {
  ctx.globalCompositeOperation = "lighter";
  ctx.lineCap = "round";
  for (const p of s.particles) {
    if (p.alpha <= 0.01) continue;
    const dx = p.x - cx, dy = p.y - cy;
    const len = Math.min(60, Math.sqrt(p.vx * p.vx + p.vy * p.vy) * 0.06);
    const dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
    const nx = dx / dist, ny = dy / dist;
    ctx.strokeStyle = `hsla(${p.hue}, 90%, 75%, ${p.alpha * alphaScale})`;
    ctx.lineWidth = p.r * 1.2;
    ctx.beginPath();
    ctx.moveTo(p.x - nx * len, p.y - ny * len);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
  }
  ctx.globalCompositeOperation = "source-over";
}

function updateAndDrawStreaks(ctx, s, dt, cx, cy, accel, alphaScale) {
  for (const st of s.streaks) {
    st.life += dt;
    st.dist += st.speed * (0.3 + accel) * dt;
    st.x = cx + Math.cos(st.angle) * st.dist;
    st.y = cy + Math.sin(st.angle) * st.dist;
    st.alpha = Math.min(1, st.alpha + dt * 5);
    // Recycle if off-canvas
    if (st.x < -100 || st.x > s.w + 100 || st.y < -100 || st.y > s.h + 100) {
      st.dist = Math.random() * 8;
      st.angle = Math.random() * Math.PI * 2;
      st.speed = 600 + Math.random() * 1800;
      st.x = cx + Math.cos(st.angle) * st.dist;
      st.y = cy + Math.sin(st.angle) * st.dist;
      st.alpha = 0;
    }
  }
  drawStreaks(ctx, s, alphaScale);
}

function drawStreaks(ctx, s, alphaScale = 1) {
  ctx.globalCompositeOperation = "lighter";
  ctx.lineCap = "round";
  const cx = s.jumpCenter ? s.jumpCenter.x : s.w / 2;
  const cy = s.jumpCenter ? s.jumpCenter.y : s.h / 2;
  for (const st of s.streaks) {
    if (st.alpha <= 0.01) continue;
    // Streak length grows with distance from center (parallax/depth)
    const len = Math.min(400, 8 + st.dist * 0.4 + st.speed * 0.04);
    const dx = Math.cos(st.angle), dy = Math.sin(st.angle);
    ctx.strokeStyle = `hsla(${st.hue}, ${st.sat}%, ${st.light}%, ${st.alpha * alphaScale})`;
    ctx.lineWidth = st.thickness;
    ctx.beginPath();
    ctx.moveTo(st.x - dx * len, st.y - dy * len);
    ctx.lineTo(st.x, st.y);
    ctx.stroke();
  }
  ctx.globalCompositeOperation = "source-over";
}

function drawCoreGlow(ctx, cx, cy, radius, intensity) {
  ctx.globalCompositeOperation = "lighter";
  const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
  g.addColorStop(0, `rgba(220,230,255,${intensity})`);
  g.addColorStop(0.4, `rgba(140,160,255,${intensity * 0.6})`);
  g.addColorStop(1, `rgba(60,30,120,0)`);
  ctx.fillStyle = g;
  ctx.fillRect(cx - radius, cy - radius, radius * 2, radius * 2);
  ctx.globalCompositeOperation = "source-over";
}

window.BlackHoleParticles = BlackHoleParticles;
