bostrom.network/src/components/CyberlinkVisualizer.tsx

import { useState, useRef, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Zap, Link2 } from 'lucide-react';
import bostromLogo from '@/assets/bostrom-logo.png';
import { triggerRebalance } from '@/hooks/useWeightCounter';

interface BackgroundNode {
  x: number;
  y: number;
  vx: number;
  vy: number;
  radius: number;
  color: string;
}

interface BackgroundEdge {
  from: number;
  to: number;
}

interface LabeledParticle {
  id: string;
  label: string;
  x: number;
  y: number;
  angle: number;
  color: string;
}

interface CyberlinkResult {
  from: string;
  to: string;
  result: boolean;
  timestamp: number;
}

const BG_COLORS = [
  'hsl(130, 100%, 50%)', // acid green
  'hsl(180, 100%, 50%)', // cyan
  'hsl(300, 100%, 60%)', // magenta
  'hsl(60, 100%, 50%)',  // yellow
  'hsl(30, 100%, 50%)',  // orange
];

const LABELED_PARTICLES: LabeledParticle[] = [
  { id: 'triangle', label: 'triangle', x: 0, y: 0, angle: -Math.PI / 2, color: 'hsl(180, 100%, 50%)' },
  { id: 'pink', label: 'pink', x: 0, y: 0, angle: -Math.PI / 2 + (2 * Math.PI / 5), color: 'hsl(300, 100%, 60%)' },
  { id: 'angle', label: 'angle', x: 0, y: 0, angle: -Math.PI / 2 + (4 * Math.PI / 5), color: 'hsl(130, 100%, 50%)' },
  { id: 'shape', label: 'shape', x: 0, y: 0, angle: -Math.PI / 2 + (6 * Math.PI / 5), color: 'hsl(60, 100%, 50%)' },
  { id: 'neon', label: 'neon', x: 0, y: 0, angle: -Math.PI / 2 + (8 * Math.PI / 5), color: 'hsl(30, 100%, 50%)' },
];

interface ParticleConnection {
  from: string;
  to: string;
}

// Initial connections: neon→pink, angle→shape
const INITIAL_CONNECTIONS: ParticleConnection[] = [
  { from: 'neon', to: 'pink' },
  { from: 'angle', to: 'shape' },
];

export const CyberlinkVisualizer = () => {
  const bgCanvasRef = useRef<HTMLCanvasElement>(null);
  const graphCanvasRef = useRef<HTMLCanvasElement>(null);
  const bgNodesRef = useRef<BackgroundNode[]>([]);
  const bgEdgesRef = useRef<BackgroundEdge[]>([]);
  const bgAnimationRef = useRef<number>();
  const graphAnimationRef = useRef<number>();
  const particlesRef = useRef<LabeledParticle[]>([...LABELED_PARTICLES]);
  const mouseRef = useRef({ x: 0, y: 0 });
  const isRebalancingRef = useRef(false);
  
  const [toText, setToText] = useState('');
  const [isProcessing, setIsProcessing] = useState(false);
  const [results, setResults] = useState<CyberlinkResult[]>([]);
  const [showResult, setShowResult] = useState<boolean | null>(null);
  const [selectedParticles, setSelectedParticles] = useState<string[]>([]);
  const [particleConnections, setParticleConnections] = useState<ParticleConnection[]>([...INITIAL_CONNECTIONS]);
  const [particleCount, setParticleCount] = useState(LABELED_PARTICLES.length);
  const connectionsRef = useRef<ParticleConnection[]>([...INITIAL_CONNECTIONS]);
  const selectedParticlesRef = useRef<string[]>([]);

  // Sync connections ref with state
  useEffect(() => {
    connectionsRef.current = particleConnections;
  }, [particleConnections]);

  // Sync selected particles ref with state
  useEffect(() => {
    selectedParticlesRef.current = selectedParticles;
  }, [selectedParticles]);

  // Initialize background nodes (like KnowledgeGraph)
  useEffect(() => {
    const canvas = bgCanvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const resize = () => {
      const container = canvas.parentElement;
      if (!container) return;
      canvas.width = container.clientWidth;
      canvas.height = container.clientHeight;
      initBgNodes();
    };

    const initBgNodes = () => {
      const nodeCount = Math.min(80, Math.floor((canvas.width * canvas.height) / 8000));
      bgNodesRef.current = [];
      bgEdgesRef.current = [];

      for (let i = 0; i < nodeCount; i++) {
        bgNodesRef.current.push({
          x: Math.random() * canvas.width,
          y: Math.random() * canvas.height,
          vx: (Math.random() - 0.5) * 0.3,
          vy: (Math.random() - 0.5) * 0.3,
          radius: Math.random() * 2 + 1,
          color: BG_COLORS[Math.floor(Math.random() * BG_COLORS.length)],
        });
      }

      // Create edges between nearby nodes
      for (let i = 0; i < nodeCount; i++) {
        for (let j = i + 1; j < nodeCount; j++) {
          if (Math.random() < 0.08) {
            bgEdgesRef.current.push({ from: i, to: j });
          }
        }
      }
    };

    const handleMouseMove = (e: MouseEvent) => {
      const rect = canvas.getBoundingClientRect();
      mouseRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    };

    const animate = () => {
      if (!ctx || !canvas) return;

      ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      const nodes = bgNodesRef.current;
      const edges = bgEdgesRef.current;
      const mouse = mouseRef.current;

      // Update and draw nodes
      nodes.forEach((node) => {
        // Mouse attraction
        const dx = mouse.x - node.x;
        const dy = mouse.y - node.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < 150 && dist > 0) {
          node.vx += (dx / dist) * 0.015;
          node.vy += (dy / dist) * 0.015;
        }

        // Update position
        node.x += node.vx;
        node.y += node.vy;

        // Damping
        node.vx *= 0.99;
        node.vy *= 0.99;

        // Bounce off edges
        if (node.x < 0 || node.x > canvas.width) node.vx *= -1;
        if (node.y < 0 || node.y > canvas.height) node.vy *= -1;

        // Keep in bounds
        node.x = Math.max(0, Math.min(canvas.width, node.x));
        node.y = Math.max(0, Math.min(canvas.height, node.y));
      });

      // Draw edges
      edges.forEach((edge) => {
        const from = nodes[edge.from];
        const to = nodes[edge.to];
        const dist = Math.sqrt((from.x - to.x) ** 2 + (from.y - to.y) ** 2);

        if (dist < 150) {
          const alpha = (1 - dist / 150) * 0.25;
          ctx.strokeStyle = `rgba(0, 255, 65, ${alpha})`;
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(from.x, from.y);
          ctx.lineTo(to.x, to.y);
          ctx.stroke();
        }
      });

      // Draw nodes with minimal glow
      nodes.forEach((node) => {
        // Small glow
        const gradient = ctx.createRadialGradient(
          node.x, node.y, 0,
          node.x, node.y, node.radius * 2
        );
        gradient.addColorStop(0, node.color);
        gradient.addColorStop(0.6, node.color.replace(')', ', 0.2)').replace('hsl', 'hsla'));
        gradient.addColorStop(1, 'transparent');

        ctx.fillStyle = gradient;
        ctx.beginPath();
        ctx.arc(node.x, node.y, node.radius * 2, 0, Math.PI * 2);
        ctx.fill();

        // Core
        ctx.fillStyle = node.color;
        ctx.beginPath();
        ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
        ctx.fill();
      });

      bgAnimationRef.current = requestAnimationFrame(animate);
    };

    resize();
    window.addEventListener('resize', resize);
    canvas.addEventListener('mousemove', handleMouseMove);
    animate();

    return () => {
      window.removeEventListener('resize', resize);
      canvas.removeEventListener('mousemove', handleMouseMove);
      if (bgAnimationRef.current) {
        cancelAnimationFrame(bgAnimationRef.current);
      }
    };
  }, []);

  // Graph animation (central core + 5 labeled particles)
  useEffect(() => {
    const canvas = graphCanvasRef.current;
    if (!canvas) return;
    
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const coreImage = new Image();
    coreImage.src = bostromLogo;

    const resize = () => {
      const container = canvas.parentElement;
      if (!container) return;
      canvas.width = container.clientWidth;
      canvas.height = container.clientHeight;
    };

    const animate = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      const centerX = canvas.width / 2;
      const centerY = canvas.height / 2;
      const orbitRadius = Math.min(canvas.width, canvas.height) * 0.32;
      const particles = particlesRef.current;
      const isRebalancing = isRebalancingRef.current;

      // Update particle positions
      particles.forEach((p, i) => {
        if (isRebalancing) {
          p.angle += 0.05 + Math.random() * 0.02;
        } else {
          p.angle += 0.002;
        }
        p.x = centerX + Math.cos(p.angle) * orbitRadius;
        p.y = centerY + Math.sin(p.angle) * orbitRadius;
      });

      // Draw connections from particles to core
      particles.forEach((p) => {
        const gradient = ctx.createLinearGradient(centerX, centerY, p.x, p.y);
        gradient.addColorStop(0, 'hsla(130, 100%, 50%, 0.3)');
        gradient.addColorStop(1, p.color.replace(')', ', 0.5)').replace('hsl', 'hsla'));
        
        ctx.strokeStyle = gradient;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.lineTo(p.x, p.y);
        ctx.stroke();
      });

      // Draw core (Bostrom logo)
      const coreSize = Math.min(canvas.width, canvas.height) * 0.18;
      if (coreImage.complete) {
        // Glow behind core
        const coreGlow = ctx.createRadialGradient(centerX, centerY, coreSize * 0.3, centerX, centerY, coreSize * 0.8);
        coreGlow.addColorStop(0, 'hsla(130, 100%, 50%, 0.2)');
        coreGlow.addColorStop(1, 'transparent');
        ctx.fillStyle = coreGlow;
        ctx.beginPath();
        ctx.arc(centerX, centerY, coreSize * 0.8, 0, Math.PI * 2);
        ctx.fill();

        ctx.drawImage(
          coreImage,
          centerX - coreSize / 2,
          centerY - coreSize / 2,
          coreSize,
          coreSize
        );
      }

      // Draw inter-particle connections (after core so they're visible)
      connectionsRef.current.forEach((conn) => {
        const fromP = particles.find(p => p.id === conn.from);
        const toP = particles.find(p => p.id === conn.to);
        if (!fromP || !toP) return;

        const gradient = ctx.createLinearGradient(fromP.x, fromP.y, toP.x, toP.y);
        gradient.addColorStop(0, fromP.color.replace(')', ', 0.8)').replace('hsl', 'hsla'));
        gradient.addColorStop(1, toP.color.replace(')', ', 0.8)').replace('hsl', 'hsla'));
        
        ctx.strokeStyle = gradient;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(fromP.x, fromP.y);
        ctx.lineTo(toP.x, toP.y);
        ctx.stroke();
      });

      // Draw labeled particles (small glow)
      const selected = selectedParticlesRef.current;
      particles.forEach((p) => {
        const size = 12;
        const isSelected = selected.includes(p.id);

        // Selection ring (if selected)
        if (isSelected) {
          ctx.strokeStyle = 'hsl(300, 100%, 60%)';
          ctx.lineWidth = 3;
          ctx.beginPath();
          ctx.arc(p.x, p.y, size + 6, 0, Math.PI * 2);
          ctx.stroke();
          
          // Pulsing glow for selected
          const pulseGlow = ctx.createRadialGradient(p.x, p.y, size, p.x, p.y, size * 2.5);
          pulseGlow.addColorStop(0, 'hsla(300, 100%, 60%, 0.4)');
          pulseGlow.addColorStop(1, 'transparent');
          ctx.fillStyle = pulseGlow;
          ctx.beginPath();
          ctx.arc(p.x, p.y, size * 2.5, 0, Math.PI * 2);
          ctx.fill();
        }

        // Minimal glow
        const glowGradient = ctx.createRadialGradient(p.x, p.y, size * 0.5, p.x, p.y, size * 1.5);
        glowGradient.addColorStop(0, p.color.replace(')', ', 0.3)').replace('hsl', 'hsla'));
        glowGradient.addColorStop(1, 'transparent');
        
        ctx.fillStyle = glowGradient;
        ctx.beginPath();
        ctx.arc(p.x, p.y, size * 1.5, 0, Math.PI * 2);
        ctx.fill();

        // 3D sphere
        const sphereGradient = ctx.createRadialGradient(
          p.x - size * 0.3, p.y - size * 0.3, size * 0.1,
          p.x, p.y, size
        );
        sphereGradient.addColorStop(0, 'hsl(0, 0%, 90%)');
        sphereGradient.addColorStop(0.3, p.color);
        sphereGradient.addColorStop(1, p.color.replace('100%', '30%').replace('60%', '20%'));
        
        ctx.fillStyle = sphereGradient;
        ctx.beginPath();
        ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
        ctx.fill();

        // Label
        ctx.font = '11px Orbitron, sans-serif';
        ctx.textAlign = 'center';
        ctx.fillStyle = isSelected ? 'hsl(300, 100%, 60%)' : '#ffffff';
        ctx.fillText(p.label, p.x, p.y + size + 16);
      });

      graphAnimationRef.current = requestAnimationFrame(animate);
    };

    resize();
    window.addEventListener('resize', resize);
    animate();

    return () => {
      window.removeEventListener('resize', resize);
      if (graphAnimationRef.current) {
        cancelAnimationFrame(graphAnimationRef.current);
      }
    };
  }, []);

  const reorganizeParticles = useCallback(() => {
    isRebalancingRef.current = true;
    
    // Trigger global counter reset (syncs with AnimatedCounter)
    triggerRebalance();
    
    // After animation, redistribute particles evenly around the orbit
    setTimeout(() => {
      isRebalancingRef.current = false;
      const particles = particlesRef.current;
      const count = particles.length;
      particles.forEach((p, i) => {
        // Evenly distribute starting from top (-PI/2)
        p.angle = -Math.PI / 2 + (2 * Math.PI * i) / count;
      });
    }, 2000);
  }, []);

  const handleCanvasClick = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
    const canvas = graphCanvasRef.current;
    if (!canvas) return;

    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const clickX = (e.clientX - rect.left) * scaleX;
    const clickY = (e.clientY - rect.top) * scaleY;

    const particles = particlesRef.current;
    const clickRadius = 25; // Hit area for particles

    // Check if clicked on a particle
    for (const p of particles) {
      const dist = Math.sqrt((clickX - p.x) ** 2 + (clickY - p.y) ** 2);
      if (dist < clickRadius) {
        setSelectedParticles(prev => {
          if (prev.includes(p.id)) {
            // Deselect if already selected
            return prev.filter(id => id !== p.id);
          } else if (prev.length < 2) {
            // Add to selection (max 2)
            return [...prev, p.id];
          } else {
            // Replace first selection
            return [prev[1], p.id];
          }
        });
        return;
      }
    }

    // Clicked on empty space - clear selection
    setSelectedParticles([]);
  }, []);

  const handleCreateLink = useCallback(() => {
    if (selectedParticles.length !== 2) return;

    const [from, to] = selectedParticles;
    
    // Check if connection already exists
    const exists = particleConnections.some(
      conn => (conn.from === from && conn.to === to) || (conn.from === to && conn.to === from)
    );

    if (!exists) {
      const newConnection = { from, to };
      setParticleConnections(prev => [...prev, newConnection]);
      reorganizeParticles();
    }

    setSelectedParticles([]);
  }, [selectedParticles, particleConnections, reorganizeParticles]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!toText.trim()) return;

    setIsProcessing(true);
    setShowResult(null);
    
    // Add new particle on the LEFT side of the graph (angle = PI)
    const newParticle: LabeledParticle = {
      id: `particle-${Date.now()}`,
      label: toText.trim(),
      x: 0,
      y: 0,
      angle: Math.PI, // Left side of the orbit
      color: BG_COLORS[particlesRef.current.length % BG_COLORS.length],
    };
    particlesRef.current.push(newParticle);
    setParticleCount(particlesRef.current.length);
    
    reorganizeParticles();
    
    await new Promise(resolve => setTimeout(resolve, 2000));

    const result = Math.random() > 0.3;
    setShowResult(result);
    
    setResults(prev => [{
      from: '△',
      to: toText,
      result,
      timestamp: Date.now(),
    }, ...prev.slice(0, 4)]);

    setIsProcessing(false);
    setToText('');
  };

  return (
    <section className="py-20 relative overflow-hidden min-h-[700px]">
      {/* Background canvas (like main page) */}
      <canvas
        ref={bgCanvasRef}
        className="absolute inset-0 w-full h-full"
        style={{ background: 'transparent' }}
      />
      
      {/* Gradient overlay */}
      <div className="absolute inset-0 bg-gradient-to-b from-transparent via-background/30 to-background pointer-events-none" />
      
      <div className="container mx-auto px-6 relative z-10">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.6 }}
          className="text-center mb-8"
        >
          <h2 className="text-3xl md:text-4xl font-orbitron font-bold text-glow-primary mb-4">
            Cyberlink Simulator
          </h2>
          <p className="text-muted-foreground max-w-2xl mx-auto">
            Connect knowledge particles and watch the graph reorganize in real-time
          </p>
        </motion.div>

        <div className="grid lg:grid-cols-2 gap-8 items-center">
          {/* Graph Canvas */}
          <div className="relative aspect-square max-h-[500px] rounded-xl border border-primary/30 overflow-hidden box-glow-primary bg-background/50 backdrop-blur-sm">
            <canvas
              ref={graphCanvasRef}
              className="w-full h-full cursor-pointer"
              style={{ background: 'transparent' }}
              onClick={handleCanvasClick}
            />

            {/* Selected particles info & create link button */}
            <AnimatePresence>
              {selectedParticles.length > 0 && (
                <motion.div
                  initial={{ opacity: 0, y: 20 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: 20 }}
                  className="absolute bottom-4 left-0 right-0 mx-auto w-fit bg-card/90 backdrop-blur-sm rounded-lg px-4 py-3 border border-[hsl(300,100%,60%)]/50"
                >
                  <div className="flex items-center gap-3">
                    <div className="flex items-center gap-2">
                      <span className="text-sm text-[hsl(300,100%,60%)] font-orbitron">
                        {selectedParticles[0]}
                      </span>
                      {selectedParticles.length === 2 && (
                        <>
                          <Link2 className="w-4 h-4 text-[hsl(300,100%,60%)]" />
                          <span className="text-sm text-[hsl(300,100%,60%)] font-orbitron">
                            {selectedParticles[1]}
                          </span>
                        </>
                      )}
                    </div>
                    {selectedParticles.length === 2 && (
                      <Button
                        size="sm"
                        onClick={handleCreateLink}
                        className="font-orbitron bg-[hsl(300,100%,60%)] hover:bg-[hsl(300,100%,50%)] text-white"
                      >
                        <Zap className="w-3 h-3 mr-1" />
                        Cyberlink
                      </Button>
                    )}
                  </div>
                  {selectedParticles.length === 1 && (
                    <p className="text-xs text-muted-foreground mt-1">Select another particle</p>
                  )}
                </motion.div>
              )}
            </AnimatePresence>
            

            {/* Processing overlay */}
            <AnimatePresence>
              {isProcessing && (
                <motion.div
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                  className="absolute inset-0 bg-black/50 flex items-center justify-center"
                >
                  <div className="text-center">
                    <motion.div
                      animate={{ rotate: 360 }}
                      transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
                      className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full mx-auto mb-4"
                    />
                    <p className="font-orbitron text-primary text-glow-primary">
                      Recalculating weights...
                    </p>
                  </div>
                </motion.div>
              )}
            </AnimatePresence>

          </div>

          {/* Input Form */}
          <div className="space-y-6">
            <form onSubmit={handleSubmit} className="space-y-6">
              {/* Neon Pink Triangle */}
              <div className="flex flex-col items-center gap-4">
                <div className="relative">
                  <svg 
                    width="80" 
                    height="70" 
                    viewBox="0 0 80 70" 
                    className="drop-shadow-[0_0_15px_hsl(300,100%,60%)]"
                  >
                    <polygon 
                      points="40,5 75,65 5,65" 
                      fill="transparent"
                      stroke="hsl(300, 100%, 60%)"
                      strokeWidth="3"
                      className="animate-pulse-slow"
                    />
                    <polygon 
                      points="40,5 75,65 5,65" 
                      fill="hsla(300, 100%, 60%, 0.1)"
                    />
                  </svg>
                  <div className="absolute inset-0 flex items-center justify-center">
                    <div className="w-4 h-4 rounded-full bg-[hsl(300,100%,60%)] blur-md opacity-60" />
                  </div>
                </div>
                <span className="text-xs text-muted-foreground font-play uppercase tracking-wider">Particle</span>
              </div>

              <div className="flex justify-center">
                <Link2 className="w-6 h-6 text-[hsl(300,100%,60%)] rotate-90" />
              </div>

              <div className="space-y-2">
                <label className="text-sm text-muted-foreground font-play">Connect to</label>
                <div className="relative">
                  <Input
                    value={toText}
                    onChange={(e) => setToText(e.target.value)}
                    placeholder="Enter knowledge..."
                    className="bg-card/50 border-primary/30 focus:border-primary text-foreground placeholder:text-muted-foreground"
                    disabled={isProcessing}
                  />
                </div>
              </div>

              <Button
                type="submit"
                disabled={isProcessing || !toText.trim()}
                className="w-full font-orbitron box-glow-primary"
              >
                <Zap className="w-4 h-4 mr-2" />
                {isProcessing ? 'Processing...' : 'Create Cyberlink'}
              </Button>
            </form>


            {/* Stats */}
            <div className="grid grid-cols-2 gap-4">
              <div className="bg-card/30 rounded-lg p-4 border border-secondary/30">
                <div className="text-xs text-muted-foreground mb-1">Connections</div>
                <div className="font-orbitron text-secondary text-glow-secondary">
                  {particleCount + particleConnections.length}
                </div>
              </div>
              <div className="bg-card/30 rounded-lg p-4 border border-accent/30">
                <div className="text-xs text-muted-foreground mb-1">Knowledge Particles</div>
                <div className="font-orbitron text-accent text-glow-accent">
                  {particleCount + 1}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

Neighbours