// dendrite-hero.jsx — Animated neuron / dendrite network background.
// Performance strategy:
//  • Static branches are RASTERIZED ONCE into an offscreen canvas per layer
//    (with built-in blur for far layers). The render loop just blits these.
//  • Per-frame work is only: clear, blit static layers, stamp soma glow
//    sprites, stamp pulse sprites. No expensive ctx.filter blur in loop,
//    no per-segment radial gradients, no double-pass strokes.
//  • Soma glow + pulse use cached pre-rendered radial-gradient sprites
//    so they render as a single drawImage per soma/pulse.
//  • Lit segments (currently traveling pulse trail) drawn in a small
//    additive layer on top — only the lit ones, not all.

(function () {
  function DendriteNetwork() {
    const canvasRef = React.useRef(null);

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

      let W = 0, H = 0;
      const DPR = Math.min(window.devicePixelRatio || 1, 1.4);

      let seed = 0xC0FFEE;
      const rand = () => {
        seed = (seed * 1664525 + 1013904223) >>> 0;
        return seed / 0xFFFFFFFF;
      };
      const rrange = (a, b) => a + rand() * (b - a);

      // ── Accent ──
      let ACCENT_RGB = [124, 196, 232], ACCENT2_RGB = [183, 157, 255];
      function parseRgb(c) {
        const tmp = document.createElement("span");
        tmp.style.color = c;
        document.body.appendChild(tmp);
        const cs = getComputedStyle(tmp).color;
        document.body.removeChild(tmp);
        const m = cs.match(/rgba?\(([^)]+)\)/);
        return m ? m[1].split(",").slice(0, 3).map((v) => parseFloat(v) | 0) : [124, 196, 232];
      }
      const refreshAccents = () => {
        const cs = getComputedStyle(document.documentElement);
        const a1 = cs.getPropertyValue("--accent").trim();
        const a2 = cs.getPropertyValue("--accent-2").trim();
        if (a1) ACCENT_RGB = parseRgb(a1);
        if (a2) ACCENT2_RGB = parseRgb(a2);
      };
      refreshAccents();
      // No interval needed — accents rarely change. Re-rasterize on accent
      // change is expensive; do it lazily only on rebuild.

      const rgba = (rgb, a) => `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`;
      const mix = (a, b, t) => [
        a[0] + (b[0] - a[0]) * t | 0,
        a[1] + (b[1] - a[1]) * t | 0,
        a[2] + (b[2] - a[2]) * t | 0,
      ];

      // ── Branch model ──
      let segments = [];
      let somata = [];
      let pulses = [];
      let staticLayers = []; // pre-rendered offscreen canvases per layer
      let glowSprite = null;
      let coreSprite = null;
      let pulseSprite = null;

      const LAYERS = [
        { z: 0.30, somaR: [4, 6],   primary: [3, 4], depthMax: 3, lenMul: 0.55, alpha: 0.30, blur: 1.6, hueMix: 0.55 },
        { z: 0.55, somaR: [6, 10],  primary: [3, 5], depthMax: 4, lenMul: 0.85, alpha: 0.55, blur: 0.6, hueMix: 0.30 },
        { z: 1.00, somaR: [9, 14],  primary: [4, 6], depthMax: 5, lenMul: 1.05, alpha: 0.92, blur: 0,   hueMix: 0.10 },
      ];

      function growBranch(soma, x, y, ang, len, depth, layer, parent) {
        const SUB = 2;
        let cx = x, cy = y, cAng = ang;
        let lastId = parent;
        for (let s = 0; s < SUB; s++) {
          const subLen = len / SUB;
          cAng += rrange(-0.25, 0.25);
          const nx = cx + Math.cos(cAng) * subLen;
          const ny = cy + Math.sin(cAng) * subLen;
          const id = segments.length;
          segments.push({
            x0: cx, y0: cy, x1: nx, y1: ny,
            parent: lastId, children: [],
            depth, layerIdx: LAYERS.indexOf(layer),
            lit: 0,
          });
          if (lastId >= 0) segments[lastId].children.push(id);
          else soma.segIds.push(id);
          lastId = id;
          cx = nx; cy = ny;
        }
        if (depth >= layer.depthMax) return;
        const nChildren = depth === 0 ? 1 : (rand() < 0.55 ? 2 : (rand() < 0.85 ? 1 : 3));
        for (let i = 0; i < nChildren; i++) {
          const spread = rrange(0.35, 0.95);
          const childAng = cAng + (i - (nChildren - 1) / 2) * spread + rrange(-0.18, 0.18);
          const childLen = len * rrange(0.55, 0.78);
          if (childLen < 8) continue;
          growBranch(soma, cx, cy, childAng, childLen, depth + 1, layer, lastId);
        }
      }

      function makeNeuron(layer, x, y) {
        const r = rrange(layer.somaR[0], layer.somaR[1]);
        const soma = {
          x, y, layer, layerIdx: LAYERS.indexOf(layer),
          r,
          phase: rand() * Math.PI * 2,
          segIds: [],
          lastFire: -999,
        };
        somata.push(soma);
        const primary = Math.floor(rrange(layer.primary[0], layer.primary[1] + 1));
        const baseAng = rand() * Math.PI * 2;
        for (let i = 0; i < primary; i++) {
          const ang = baseAng + (i / primary) * Math.PI * 2 + rrange(-0.4, 0.4);
          const len = rrange(60, 130) * layer.lenMul;
          growBranch(soma, x, y, ang, len, 0, layer, -1);
        }
      }

      // ── Pre-render glow/core/pulse sprites ──
      function makeSprites() {
        const a = ACCENT_RGB;

        // Soma glow sprite — radial; reused at all sizes via drawImage scale
        const gs = 128;
        const gc = document.createElement("canvas");
        gc.width = gc.height = gs;
        const gctx = gc.getContext("2d");
        const gg = gctx.createRadialGradient(gs / 2, gs / 2, 0, gs / 2, gs / 2, gs / 2);
        gg.addColorStop(0,   rgba(a, 0.85));
        gg.addColorStop(0.25, rgba(a, 0.40));
        gg.addColorStop(0.55, rgba(a, 0.10));
        gg.addColorStop(1,   rgba(a, 0));
        gctx.fillStyle = gg;
        gctx.fillRect(0, 0, gs, gs);
        glowSprite = gc;

        // Bright core sprite (white center)
        const cs = 64;
        const cc = document.createElement("canvas");
        cc.width = cc.height = cs;
        const cctx = cc.getContext("2d");
        const white = [255, 255, 255];
        const mid = mix(white, a, 0.4);
        const cg = cctx.createRadialGradient(cs / 2, cs / 2, 0, cs / 2, cs / 2, cs / 2);
        cg.addColorStop(0,   rgba(white, 1.0));
        cg.addColorStop(0.35, rgba(mid, 0.95));
        cg.addColorStop(0.7, rgba(a, 0.4));
        cg.addColorStop(1,   rgba(a, 0));
        cctx.fillStyle = cg;
        cctx.fillRect(0, 0, cs, cs);
        coreSprite = cc;

        // Pulse sprite
        const ps = 64;
        const pc = document.createElement("canvas");
        pc.width = pc.height = ps;
        const pctx = pc.getContext("2d");
        const pg = pctx.createRadialGradient(ps / 2, ps / 2, 0, ps / 2, ps / 2, ps / 2);
        pg.addColorStop(0,   rgba(white, 1.0));
        pg.addColorStop(0.2, rgba(mix(white, a, 0.3), 0.9));
        pg.addColorStop(0.5, rgba(a, 0.5));
        pg.addColorStop(1,   rgba(a, 0));
        pctx.fillStyle = pg;
        pctx.fillRect(0, 0, ps, ps);
        pulseSprite = pc;
      }

      // ── Bake static branches into per-layer offscreen canvases ──
      function bakeStaticLayers() {
        staticLayers = LAYERS.map((layer, li) => {
          const off = document.createElement("canvas");
          off.width = Math.floor(W * DPR);
          off.height = Math.floor(H * DPR);
          const octx = off.getContext("2d");
          octx.setTransform(DPR, 0, 0, DPR, 0, 0);
          octx.lineCap = "round";
          octx.lineJoin = "round";

          // single pass: faint cool stroke for the branch network
          const baseColor = mix(ACCENT_RGB, [10, 22, 36], layer.hueMix);
          const a = layer.alpha;

          for (const seg of segments) {
            if (seg.layerIdx !== li) continue;
            const baseW = Math.max(0.35, (layer.depthMax - seg.depth) * 0.5);
            octx.lineWidth = baseW;
            octx.strokeStyle = rgba(baseColor, a * 0.85);
            octx.beginPath();
            octx.moveTo(seg.x0, seg.y0);
            octx.lineTo(seg.x1, seg.y1);
            octx.stroke();
          }

          // Apply a real CSS blur on the offscreen canvas itself only for
          // far layers. We do this by drawing the canvas through a blurred
          // intermediate. Cheaper than per-frame ctx.filter.
          if (layer.blur > 0) {
            const blurred = document.createElement("canvas");
            blurred.width = off.width;
            blurred.height = off.height;
            const bctx = blurred.getContext("2d");
            bctx.filter = `blur(${layer.blur * DPR}px)`;
            bctx.drawImage(off, 0, 0);
            return blurred;
          }
          return off;
        });
      }

      function rebuild() {
        seed = 0xC0FFEE;
        segments = [];
        somata = [];
        pulses = [];

        const area = W * H;
        const COUNTS = [
          Math.round(area / 70000),  // far
          Math.round(area / 110000), // mid
          Math.round(area / 200000), // near
        ];

        for (let li = 0; li < LAYERS.length; li++) {
          const layer = LAYERS[li];
          const N = Math.max(2, COUNTS[li]);
          const cols = Math.ceil(Math.sqrt(N * (W / H)));
          const rows = Math.ceil(N / cols);
          const cellW = W / cols, cellH = H / rows;
          let placed = 0;
          for (let r = 0; r < rows && placed < N; r++) {
            for (let c = 0; c < cols && placed < N; c++) {
              const cx = (c + 0.5) * cellW + rrange(-cellW * 0.4, cellW * 0.4);
              const cy = (r + 0.5) * cellH + rrange(-cellH * 0.4, cellH * 0.4);
              if (li === 2) {
                const dx = cx - W / 2, dy = cy - H / 2;
                const ex = (dx / (W * 0.30)) ** 2 + (dy / (H * 0.22)) ** 2;
                if (ex < 1) { placed++; continue; }
              }
              makeNeuron(layer, cx, cy);
              placed++;
            }
          }
        }

        makeSprites();
        bakeStaticLayers();
      }

      const resize = () => {
        const r = canvas.getBoundingClientRect();
        W = Math.max(1, Math.floor(r.width));
        H = Math.max(1, Math.floor(r.height));
        canvas.width = Math.floor(W * DPR);
        canvas.height = Math.floor(H * DPR);
        ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
        rebuild();
      };

      // ── Pulses ──
      function spawnPulse(soma) {
        if (!soma.segIds.length) return;
        if (pulses.length > 60) return; // hard cap
        const startId = soma.segIds[Math.floor(rand() * soma.segIds.length)];
        pulses.push({
          segId: startId,
          u: 0,
          speed: rrange(0.45, 0.85),
          life: 1.0,
          layerIdx: soma.layerIdx,
          color2: rand() < 0.85,
        });
      }
      function advancePulse(p, dt) {
        p.u += p.speed * dt;
        const seg = segments[p.segId];
        if (!seg) { p.life = 0; return; }
        if (seg.lit < 1) seg.lit = 1;
        if (p.u >= 1) {
          const kids = seg.children;
          if (!kids.length || rand() < 0.20) { p.life = 0; return; }
          p.segId = kids[Math.floor(rand() * kids.length)];
          p.u = p.u - 1;
          p.life *= 0.92;
          if (p.life < 0.15) p.life = 0;
        }
      }

      // ── Drift ──
      let mx = 0, my = 0, smx = 0, smy = 0;
      const onMove = (e) => {
        mx = (e.clientX / window.innerWidth) * 2 - 1;
        my = (e.clientY / window.innerHeight) * 2 - 1;
      };
      window.addEventListener("pointermove", onMove);

      let last = performance.now();
      let visible = !document.hidden;
      const onVis = () => { visible = !document.hidden; };
      document.addEventListener("visibilitychange", onVis);

      // Frame skipping for smoothness on slow devices
      let frameAcc = 0;
      const TARGET_DT = 1 / 60;

      function render(now) {
        if (cancelled) return;
        raf = requestAnimationFrame(render);
        const dt = Math.min(0.05, (now - last) / 1000);
        last = now;
        if (!visible) return;

        smx += (mx - smx) * 0.04;
        smy += (my - smy) * 0.04;

        const t = now / 1000;

        // Trigger random firings
        for (let i = 0; i < somata.length; i++) {
          const s = somata[i];
          if (rand() < 0.10 * s.layer.z * dt * 60) {
            s.lastFire = t;
            const n = 1 + (rand() < 0.5 ? 0 : 1);
            for (let k = 0; k < n; k++) spawnPulse(s);
          }
        }

        // Advance pulses
        for (let i = 0; i < pulses.length; i++) advancePulse(pulses[i], dt);
        if (pulses.length) pulses = pulses.filter((p) => p.life > 0);

        // Decay segment lit
        const decay = Math.pow(0.18, dt);
        for (let i = 0; i < segments.length; i++) {
          if (segments[i].lit > 0) {
            segments[i].lit *= decay;
            if (segments[i].lit < 0.02) segments[i].lit = 0;
          }
        }

        // ── Draw ──
        ctx.clearRect(0, 0, W, H);

        // Per-layer
        for (let li = 0; li < LAYERS.length; li++) {
          const layer = LAYERS[li];
          const dx = -smx * 14 * layer.z;
          const dy = -smy * 14 * layer.z;

          // Static branches (single drawImage)
          if (staticLayers[li]) {
            ctx.drawImage(staticLayers[li], dx, dy, W, H);
          }

          // Lit segments overlay (additive) — only draw the lit ones
          ctx.save();
          ctx.translate(dx, dy);
          ctx.globalCompositeOperation = "lighter";
          ctx.lineCap = "round";
          for (let i = 0; i < segments.length; i++) {
            const seg = segments[i];
            if (seg.layerIdx !== li || seg.lit <= 0) continue;
            const lit = seg.lit;
            const baseW = Math.max(0.5, (layer.depthMax - seg.depth) * 0.55);
            ctx.lineWidth = baseW + lit * 1.8;
            ctx.strokeStyle = rgba([255, 255, 255], lit * 0.55);
            ctx.beginPath();
            ctx.moveTo(seg.x0, seg.y0);
            ctx.lineTo(seg.x1, seg.y1);
            ctx.stroke();
          }
          ctx.restore();

          // Somata glow + core (drawImage of sprites — cheap)
          ctx.save();
          ctx.translate(dx, dy);
          ctx.globalCompositeOperation = "lighter";
          for (let i = 0; i < somata.length; i++) {
            const s = somata[i];
            if (s.layerIdx !== li) continue;
            const breath = 0.55 + 0.45 * Math.sin(s.phase + t * 1.4);
            const fireAge = t - s.lastFire;
            const fire = fireAge < 0.6 ? Math.pow(1 - fireAge / 0.6, 2) : 0;

            // Glow (large)
            const glowR = s.r * (3.4 + fire * 4.0);
            const ga = layer.alpha * (0.35 + 0.45 * breath) + fire * 0.55;
            ctx.globalAlpha = Math.min(1, ga);
            ctx.drawImage(glowSprite, s.x - glowR, s.y - glowR, glowR * 2, glowR * 2);

            // Core (small bright)
            const coreR = s.r * (0.85 + 0.15 * breath + fire * 0.6);
            ctx.globalAlpha = Math.min(1, 0.85 + fire * 0.15);
            ctx.drawImage(coreSprite, s.x - coreR, s.y - coreR, coreR * 2, coreR * 2);
          }
          ctx.globalAlpha = 1;
          ctx.restore();

          // Pulses
          ctx.save();
          ctx.translate(dx, dy);
          ctx.globalCompositeOperation = "lighter";
          for (let i = 0; i < pulses.length; i++) {
            const p = pulses[i];
            if (p.layerIdx !== li) continue;
            const seg = segments[p.segId];
            if (!seg) continue;
            const px = seg.x0 + (seg.x1 - seg.x0) * p.u;
            const py = seg.y0 + (seg.y1 - seg.y0) * p.u;
            const r = (li === 2 ? 14 : li === 1 ? 10 : 7) * (0.6 + p.life * 0.6);
            ctx.globalAlpha = Math.min(1, p.life);
            ctx.drawImage(pulseSprite, px - r, py - r, r * 2, r * 2);
          }
          ctx.globalAlpha = 1;
          ctx.restore();
        }
      }
      raf = requestAnimationFrame(render);

      const ro = new ResizeObserver(resize);
      ro.observe(canvas);
      resize();

      return () => {
        cancelled = true;
        cancelAnimationFrame(raf);
        ro.disconnect();
        document.removeEventListener("visibilitychange", onVis);
        window.removeEventListener("pointermove", onMove);
      };
    }, []);

    return <canvas ref={canvasRef} className="dn-canvas" aria-hidden="true"/>;
  }

  window.DendriteNetwork = DendriteNetwork;
})();
