"use client";

import { useEffect, useRef } from "react";
import rawParticles from "../data/particles.json";
import { type AppSettings } from "../types";

const PARTICLE_COUNT = 7000;
const shapes = ["circle", "triangle", "diamond", "square"] as const;

type Shape = (typeof shapes)[number];

interface ParticleData {
  x: number;
  y: number;
  vx: number;
  vy: number;
  size: number;
  colorIndex: number;
  shape: Shape;
  opacity: number;
  springFactor: number;
  damping: number;
  driftOffset: number;
  scaleFactor: number;
}

interface ParticleCanvasProps {
  settings: AppSettings;
}

function randomRange(min: number, max: number) {
  return Math.random() * (max - min) + min;
}

function lerp(start: number, end: number, t: number) {
  return start * (1 - t) + end * t;
}

export default function ParticleCanvas({ settings }: ParticleCanvasProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const settingsRef = useRef<AppSettings>(settings);

  // Sync settings updates to ref without re-triggering main useEffect hook
  useEffect(() => {
    settingsRef.current = settings;
  }, [settings]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    let W = window.innerWidth;
    let H = window.innerHeight;
    canvas.width = W;
    canvas.height = H;

    const mouse = { x: -1000, y: -1000, active: false };
    let mouseInfluenceX = 0;
    let mouseInfluenceY = 0;
    let mouseVx = 0;
    let mouseVy = 0;
    let prevMouseX = -1000;
    let prevMouseY = -1000;

    let gyroX = 0;
    let gyroY = 0;
    let hasGyro = false;

    // ─── 3D Coordinates Setup ──────────────────────────────────────────────
    const sortedBrain = [...rawParticles.brain].slice(0, PARTICLE_COUNT);
    // Expand lightbulb by 1.25 and rotate -45 degrees around X-axis
    const angle45 = -Math.PI / 4; // -45 degrees in radians
    const cos45 = Math.cos(angle45);
    const sin45 = Math.sin(angle45);
    const sortedLightbulb = [...rawParticles.lightbulb].slice(0, PARTICLE_COUNT).map((p) => {
      const ex = p.x * 1.25;
      const ey = p.y * 1.25;
      const ez = p.z * 1.25;
      return {
        x: ex,
        y: ey * cos45 - ez * sin45,
        z: ey * sin45 + ez * cos45,
      };
    });
    // Programmatic 3D Sphere Generator (100% outline as alternating latitude/longitude lines, no face/surface fill)
    const sortedSphere: { x: number; y: number; z: number }[] = new Array(PARTICLE_COUNT);
    const sphereRadius = 0.45;

    const N_lat = 10; // 10 latitude lines
    const N_lon = 12; // 12 longitude lines

    for (let c = 0; c < 4; c++) {
      const bandStart = c * 1750;

      // Generate all 1750 particles as outlines
      for (let i = 0; i < 1750; i++) {
        let x = 0, y = 0, z = 0;

        if (c === 0 || c === 1) {
          // Latitudes: Color 0 even lines, Color 1 odd lines
          const availableIndices = [];
          for (let k = 1; k <= N_lat; k++) {
            if (c === 0 && k % 2 === 0) availableIndices.push(k);
            if (c === 1 && k % 2 !== 0) availableIndices.push(k);
          }
          const k = availableIndices[i % availableIndices.length];
          const phi = (k / (N_lat + 1)) * Math.PI;
          const theta = Math.random() * Math.PI * 2;

          x = Math.sin(phi) * Math.cos(theta);
          y = Math.cos(phi); // Y is vertical axis
          z = Math.sin(phi) * Math.sin(theta);
        } else {
          // Longitudes: Color 2 even lines, Color 3 odd lines
          const availableIndices = [];
          for (let j = 0; j < N_lon; j++) {
            if (c === 2 && j % 2 === 0) availableIndices.push(j);
            if (c === 3 && j % 2 !== 0) availableIndices.push(j);
          }
          const j = availableIndices[i % availableIndices.length];
          const theta = (j / N_lon) * Math.PI * 2;
          const phi = Math.random() * Math.PI;

          x = Math.sin(phi) * Math.cos(theta);
          y = Math.cos(phi); // Y is vertical axis
          z = Math.sin(phi) * Math.sin(theta);
        }

        // Add noise
        const noise = 0.015;
        x += randomRange(-noise, noise);
        y += randomRange(-noise, noise);
        z += randomRange(-noise, noise);

        sortedSphere[bandStart + i] = {
          x: x * sphereRadius,
          y: y * sphereRadius,
          z: z * sphereRadius
        };
      }
    }

    // 1. Programmatic 3D Cube Generator (hollow, with distinct edge outlines and solid-colored edges)
    const sortedCube: { x: number; y: number; z: number }[] = new Array(PARTICLE_COUNT);
    const fillIndices: number[] = [];
    const fillCoordinates: { x: number; y: number; z: number }[] = [];

    // Define 4 alternate (non-adjacent) corners of the cube to assign adjacent edges to colors
    const corners = [
      { x: 1.0, y: 1.0, z: 1.0 },   // Corner 0
      { x: 1.0, y: -1.0, z: -1.0 }, // Corner 1
      { x: -1.0, y: 1.0, z: -1.0 }, // Corner 2
      { x: -1.0, y: -1.0, z: 1.0 }  // Corner 3
    ];

    // Generate edge and fill coordinates
    for (let c = 0; c < 4; c++) {
      const bandStart = c * 1750;
      const corner = corners[c];

      // A: Generate edge particles (1312 particles)
      for (let i = 0; i < 1312; i++) {
        const edgeIndex = i % 3;
        const t = randomRange(-1.0, 1.0);
        let x = 0, y = 0, z = 0;

        if (edgeIndex === 0) {
          x = t;
          y = corner.y;
          z = corner.z;
        } else if (edgeIndex === 1) {
          x = corner.x;
          y = t;
          z = corner.z;
        } else {
          x = corner.x;
          y = corner.y;
          z = t;
        }

        // Add noise
        const noise = 0.025;
        x += randomRange(-noise, noise);
        y += randomRange(-noise, noise);
        z += randomRange(-noise, noise);

        sortedCube[bandStart + i] = {
          x: x * 0.42,
          y: y * 0.42,
          z: z * 0.42
        };
      }

      // B: Generate fill particles (438 particles)
      for (let i = 1312; i < 1750; i++) {
        const idx = bandStart + i;
        fillIndices.push(idx);

        const face = i % 6;
        let x = randomRange(-1.0, 1.0);
        let y = randomRange(-1.0, 1.0);
        let z = randomRange(-1.0, 1.0);
        if (face === 0) x = -1.0;
        else if (face === 1) x = 1.0;
        else if (face === 2) y = -1.0;
        else if (face === 3) y = 1.0;
        else if (face === 4) z = -1.0;
        else if (face === 5) z = 1.0;

        // Add noise
        const noise = 0.025;
        x += randomRange(-noise, noise);
        y += randomRange(-noise, noise);
        z += randomRange(-noise, noise);

        fillCoordinates.push({
          x: x * 0.42,
          y: y * 0.42,
          z: z * 0.42
        });
      }
    }

    // Shuffle the fill coordinates so that fill colors are randomly mixed and scattered
    for (let i = fillCoordinates.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = fillCoordinates[i];
      fillCoordinates[i] = fillCoordinates[j];
      fillCoordinates[j] = temp;
    }

    // Put shuffled fill coordinates back into their reserved slots in sortedCube
    for (let i = 0; i < fillIndices.length; i++) {
      sortedCube[fillIndices[i]] = fillCoordinates[i];
    }

    // 2. Programmatic 3D Torus Generator (with slight organic noise)
    const sortedTorus: { x: number; y: number; z: number }[] = [];
    const R_torus = 0.75;
    const r_torus = 0.22;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.random() * Math.PI * 2;
      let x = (R_torus + r_torus * Math.cos(phi)) * Math.cos(theta);
      let y = (R_torus + r_torus * Math.cos(phi)) * Math.sin(theta);
      let z = r_torus * Math.sin(phi);

      // Add noise
      const noise = 0.02;
      x += randomRange(-noise, noise);
      y += randomRange(-noise, noise);
      z += randomRange(-noise, noise);

      // Rotate by 90 degrees around Y-axis sideways (swap x and z)
      sortedTorus.push({ x: z, y: y * 0.95, z: -x });
    }
    // Shuffle the torus coordinates so that colors are randomly mixed and scattered rather than height-banded
    for (let i = sortedTorus.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = sortedTorus[i];
      sortedTorus[i] = sortedTorus[j];
      sortedTorus[j] = temp;
    }

    // 3. Programmatic DNA Double Helix Generator
    const sortedDNA: { x: number; y: number; z: number }[] = [];
    const helixRadius = 0.52;
    const helixHeight = 1.6;
    const turns = 2.0;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const isStrand = Math.random() < 0.7;
      const strandIndex = i % 2;
      const pct = Math.random();
      const y = pct * helixHeight - (helixHeight / 2);
      const angle = pct * turns * Math.PI * 2 + (strandIndex * Math.PI);
      let x = 0;
      let z = 0;
      if (isStrand) {
        x = Math.cos(angle) * helixRadius;
        z = Math.sin(angle) * helixRadius;
      } else {
        const t_rung = randomRange(-1.0, 1.0);
        x = Math.cos(pct * turns * Math.PI * 2) * helixRadius * t_rung;
        z = Math.sin(pct * turns * Math.PI * 2) * helixRadius * t_rung;
      }
      const n = 0.02;
      sortedDNA.push({
        x: x + randomRange(-n, n),
        y: y + randomRange(-n, n),
        z: z + randomRange(-n, n),
      });
    }

    // 4. Programmatic 3D Octahedron/Pyramid Generator (with distinct edge outlines and solid-colored edges)
    const sortedPyramid: { x: number; y: number; z: number }[] = new Array(PARTICLE_COUNT);
    const pyrScale = 0.85;

    // Vertices of the Octahedron
    const vertices = [
      { x: 1.0, y: 0.0, z: 0.0 },   // V0
      { x: -1.0, y: 0.0, z: 0.0 },  // V1
      { x: 0.0, y: 1.0, z: 0.0 },   // V2
      { x: 0.0, y: -1.0, z: 0.0 },  // V3
      { x: 0.0, y: 0.0, z: 1.0 },   // V4
      { x: 0.0, y: 0.0, z: -1.0 }   // V5
    ];

    // Partition edges into 4 groups of 3 adjacent edges meeting at a vertex:
    const octahedronColorGroups = [
      [[4, 0], [4, 1], [4, 2]], // Group 0 (meets at V4)
      [[5, 0], [5, 1], [5, 3]], // Group 1 (meets at V5)
      [[2, 1], [2, 0], [2, 5]], // Group 2 (meets at V2)
      [[3, 0], [3, 1], [3, 4]]  // Group 3 (meets at V3)
    ];

    const pyrFillIndices: number[] = [];
    const pyrFillCoordinates: { x: number; y: number; z: number }[] = [];

    // Generate edge and fill coordinates for Octahedron
    for (let c = 0; c < 4; c++) {
      const bandStart = c * 1750;

      // A: Generate edge particles (1312 particles)
      for (let i = 0; i < 1312; i++) {
        const edge = octahedronColorGroups[c][i % 3];
        const v1 = vertices[edge[0]];
        const v2 = vertices[edge[1]];
        const t = Math.random();
        let x = v1.x + t * (v2.x - v1.x);
        let y = v1.y + t * (v2.y - v1.y);
        let z = v1.z + t * (v2.z - v1.z);

        // Add noise
        const n = 0.025;
        x += randomRange(-n, n);
        y += randomRange(-n, n);
        z += randomRange(-n, n);

        sortedPyramid[bandStart + i] = {
          x: x * pyrScale,
          y: y * pyrScale,
          z: z * pyrScale
        };
      }

      // B: Generate fill particles (438 particles)
      for (let i = 1312; i < 1750; i++) {
        const idx = bandStart + i;
        pyrFillIndices.push(idx);

        const face = i % 8;
        const xSign = (face & 1) ? 1.0 : -1.0;
        const ySign = (face & 2) ? 1.0 : -1.0;
        const zSign = (face & 4) ? 1.0 : -1.0;
        const A = { x: xSign, y: 0.0, z: 0.0 };
        const B = { x: 0.0, y: ySign, z: 0.0 };
        const C = { x: 0.0, y: 0.0, z: zSign };
        let r1 = Math.random();
        let r2 = Math.random();
        if (r1 + r2 > 1.0) {
          r1 = 1.0 - r1;
          r2 = 1.0 - r2;
        }
        let x = A.x + r1 * (B.x - A.x) + r2 * (C.x - A.x);
        let y = A.y + r1 * (B.y - A.y) + r2 * (C.y - A.y);
        let z = A.z + r1 * (B.z - A.z) + r2 * (C.z - A.z);

        // Add noise
        const n = 0.025;
        x += randomRange(-n, n);
        y += randomRange(-n, n);
        z += randomRange(-n, n);

        pyrFillCoordinates.push({
          x: x * pyrScale,
          y: y * pyrScale,
          z: z * pyrScale
        });
      }
    }

    // Shuffle the fill coordinates so that fill colors are randomly mixed and scattered
    for (let i = pyrFillCoordinates.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = pyrFillCoordinates[i];
      pyrFillCoordinates[i] = pyrFillCoordinates[j];
      pyrFillCoordinates[j] = temp;
    }

    // Put shuffled fill coordinates back into their reserved slots in sortedPyramid
    for (let i = 0; i < pyrFillIndices.length; i++) {
      sortedPyramid[pyrFillIndices[i]] = pyrFillCoordinates[i];
    }


    // 5. Programmatic Trefoil Knot Generator
    const sortedTrefoil: { x: number; y: number; z: number }[] = [];
    const trefoilScale = 0.38;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const t = (i / PARTICLE_COUNT) * Math.PI * 2 * 3;
      const x = Math.sin(t) + 2.0 * Math.sin(2.0 * t);
      const y = Math.cos(t) - 2.0 * Math.cos(2.0 * t);
      const z = -Math.sin(3.0 * t);
      const n = 0.02;
      sortedTrefoil.push({
        x: (x + randomRange(-n, n)) * trefoilScale,
        y: (y + randomRange(-n, n)) * trefoilScale,
        z: (z + randomRange(-n, n)) * trefoilScale,
      });
    }

    // 6. Programmatic 3D Astroid Star Generator
    const sortedAstroid: { x: number; y: number; z: number }[] = [];
    const astroidScale = 0.95;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const u = randomRange(-Math.PI / 2, Math.PI / 2);
      const v = randomRange(-Math.PI, Math.PI);
      const cosU = Math.cos(u);
      const sinU = Math.sin(u);
      const cosV = Math.cos(v);
      const sinV = Math.sin(v);
      const x = cosU * cosU * cosU * cosV * cosV * cosV;
      const y = sinU * sinU * sinU * cosV * cosV * cosV;
      const z = sinV * sinV * sinV;
      const n = 0.02;
      sortedAstroid.push({
        x: (x + randomRange(-n, n)) * astroidScale,
        y: (y + randomRange(-n, n)) * astroidScale,
        z: (z + randomRange(-n, n)) * astroidScale,
      });
    }

    // 10. Programmatic Scattered Generator
    const sortedScattered: { x: number; y: number; z: number }[] = [];
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      sortedScattered.push({
        x: randomRange(-2.0, 2.0),
        y: randomRange(-1.2, 1.2),
        z: randomRange(-0.8, 0.8),
      });
    }
    // Shuffle the scattered coordinates so that colors are randomly mixed and scattered rather than height-banded
    for (let i = sortedScattered.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = sortedScattered[i];
      sortedScattered[i] = sortedScattered[j];
      sortedScattered[j] = temp;
    }

    // ─── Polyhedron Sampling Helpers ──────────────────────────────────────────
    // Helper to sample inside a triangle
    function sampleTriangle(
      a: { x: number; y: number; z: number },
      b: { x: number; y: number; z: number },
      c: { x: number; y: number; z: number }
    ) {
      const r1 = Math.random();
      const r2 = Math.random();
      const sqrtR1 = Math.sqrt(r1);
      const u = 1 - sqrtR1;
      const v = r2 * sqrtR1;
      const w = 1 - u - v;
      return {
        x: u * a.x + v * b.x + w * c.x,
        y: u * a.y + v * b.y + w * c.y,
        z: u * a.z + v * b.z + w * c.z,
      };
    }

    // Helper to sample inside a face (triangle, pentagon or pentagram)
    function sampleFace(
      vertices: { x: number; y: number; z: number }[],
      faceIndices: number[],
      isStar: boolean
    ) {
      if (faceIndices.length === 3) {
        return sampleTriangle(vertices[faceIndices[0]], vertices[faceIndices[1]], vertices[faceIndices[2]]);
      } else if (faceIndices.length === 5) {
        const v = faceIndices.map((idx) => vertices[idx]);
        const cx = (v[0].x + v[1].x + v[2].x + v[3].x + v[4].x) / 5;
        const cy = (v[0].y + v[1].y + v[2].y + v[3].y + v[4].y) / 5;
        const cz = (v[0].z + v[1].z + v[2].z + v[3].z + v[4].z) / 5;
        const O = { x: cx, y: cy, z: cz };

        const idx = Math.floor(Math.random() * 5);
        if (isStar) {
          // Pentagram: Connect center O to tips in star winding
          return sampleTriangle(O, v[idx], v[(idx + 2) % 5]);
        } else {
          // Pentagon: Connect center O to adjacent vertices
          return sampleTriangle(O, v[idx], v[(idx + 1) % 5]);
        }
      }
      return { x: 0, y: 0, z: 0 };
    }

    // Helper to sort face indices circularly on a plane in 3D
    function sortFaceCircular(
      face: number[],
      verts: { x: number; y: number; z: number }[]
    ): number[] {
      const p0 = verts[face[0]];
      const p1 = verts[face[1]];
      const p2 = verts[face[2]];

      const ux = p1.x - p0.x, uy = p1.y - p0.y, uz = p1.z - p0.z;
      const vx = p2.x - p0.x, vy = p2.y - p0.y, vz = p2.z - p0.z;

      let nx = uy * vz - uz * vy;
      let ny = uz * ux - ux * vz;
      let nz = ux * vy - uy * vx;
      const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
      nx /= len; ny /= len; nz /= len;

      let tx = 1, ty = 0, tz = 0;
      if (Math.abs(nx) > 0.9) {
        tx = 0; ty = 1;
      }
      const dot = tx * nx + ty * ny + tz * nz;
      const ux_orth = tx - dot * nx;
      const uy_orth = ty - dot * ny;
      const uz_orth = tz - dot * nz;
      const lenU = Math.sqrt(ux_orth * ux_orth + uy_orth * uy_orth + uz_orth * uz_orth);
      const ux_n = ux_orth / lenU, uy_n = uy_orth / lenU, uz_n = uz_orth / lenU;

      const vx_n = ny * uz_n - nz * uy_n;
      const vy_n = nz * ux_n - nx * uz_n;
      const vz_n = nx * uy_n - ny * ux_n;

      let cx = 0, cy = 0, cz = 0;
      for (const idx of face) {
        cx += verts[idx].x;
        cy += verts[idx].y;
        cz += verts[idx].z;
      }
      cx /= 5; cy /= 5; cz /= 5;

      return [...face].sort((a, b) => {
        const va = verts[a];
        const vb = verts[b];
        const ax = (va.x - cx) * ux_n + (va.y - cy) * uy_n + (va.z - cz) * uz_n;
        const ay = (va.x - cx) * vx_n + (va.y - cy) * vy_n + (va.z - cz) * vz_n;
        const bx = (vb.x - cx) * ux_n + (vb.y - cy) * uy_n + (vb.z - cz) * uz_n;
        const by = (vb.x - cx) * vx_n + (vb.y - cy) * vy_n + (vb.z - cz) * vz_n;
        return Math.atan2(ay, ax) - Math.atan2(by, bx);
      });
    }

    // Helper to sort 5 vertices circularly using edge graph cycle (specifically for icosahedron neighbor sets)
    function sortCircular(neighbors: number[], icoEdges: number[][]): number[] {
      const sorted: number[] = [neighbors[0]];
      let current = neighbors[0];
      while (sorted.length < 5) {
        const next = neighbors.find(n =>
          !sorted.includes(n) &&
          icoEdges.some(e => (e[0] === current && e[1] === n) || (e[0] === n && e[1] === current))
        );
        if (next !== undefined) {
          sorted.push(next);
          current = next;
        } else {
          break;
        }
      }
      return sorted;
    }

    // Master Polyhedron Generator
    function generatePolyhedron(
      vertices: { x: number; y: number; z: number }[],
      edges: number[][],
      faces: number[][],
      scale: number,
      isStar: boolean,
      noise: number,
      edgePercent: number
    ): { x: number; y: number; z: number }[] {
      const points: { x: number; y: number; z: number }[] = new Array(7000);

      // Edge and face allocations per band (total 1750 particles per band)
      const bandConfigs: { edgeCounts: number[]; faceCount: number; bandStart: number }[] = [];

      if (edgePercent === 100) {
        // Band 0: 8 edges. 6 with 219, 2 with 218. Face = 0.
        bandConfigs.push({ edgeCounts: [219, 219, 219, 219, 219, 219, 218, 218], faceCount: 0, bandStart: 0 });
        // Band 1: 8 edges. 6 with 219, 2 with 218. Face = 0.
        bandConfigs.push({ edgeCounts: [219, 219, 219, 219, 219, 219, 218, 218], faceCount: 0, bandStart: 1750 });
        // Band 2: 7 edges. 7 with 250. Face = 0.
        bandConfigs.push({ edgeCounts: [250, 250, 250, 250, 250, 250, 250], faceCount: 0, bandStart: 3500 });
        // Band 3: 7 edges. 7 with 250. Face = 0.
        bandConfigs.push({ edgeCounts: [250, 250, 250, 250, 250, 250, 250], faceCount: 0, bandStart: 5250 });
      } else if (edgePercent === 95) {
        // Band 0: 8 edges (6 with 208, 2 with 207 -> 1662). Face = 88.
        bandConfigs.push({ edgeCounts: [208, 208, 208, 208, 208, 208, 207, 207], faceCount: 88, bandStart: 0 });
        // Band 1: 8 edges (6 with 208, 2 with 207 -> 1662). Face = 88.
        bandConfigs.push({ edgeCounts: [208, 208, 208, 208, 208, 208, 207, 207], faceCount: 88, bandStart: 1750 });
        // Band 2: 7 edges (2 with 238, 5 with 237 -> 1663). Face = 87.
        bandConfigs.push({ edgeCounts: [238, 238, 237, 237, 237, 237, 237], faceCount: 87, bandStart: 3500 });
        // Band 3: 7 edges (2 with 238, 5 with 237 -> 1663). Face = 87.
        bandConfigs.push({ edgeCounts: [238, 238, 237, 237, 237, 237, 237], faceCount: 87, bandStart: 5250 });
      } else {
        // Default 75%
        // Band 0: 8 edges * 175 = 1400. Face = 350.
        bandConfigs.push({ edgeCounts: [175, 175, 175, 175, 175, 175, 175, 175], faceCount: 350, bandStart: 0 });
        // Band 1: 8 edges * 175 = 1400. Face = 350.
        bandConfigs.push({ edgeCounts: [175, 175, 175, 175, 175, 175, 175, 175], faceCount: 350, bandStart: 1750 });
        // Band 2: 7 edges * 175 = 1225. Face = 525.
        bandConfigs.push({ edgeCounts: [175, 175, 175, 175, 175, 175, 175], faceCount: 525, bandStart: 3500 });
        // Band 3: 7 edges * 175 = 1225. Face = 525.
        bandConfigs.push({ edgeCounts: [175, 175, 175, 175, 175, 175, 175], faceCount: 525, bandStart: 5250 });
      }

      let edgeIdx = 0;
      let faceIdx = 0;

      for (let c = 0; c < 4; c++) {
        const config = bandConfigs[c];

        // 1. Populate Edge Particles
        let offset = config.bandStart;
        for (let e = 0; e < config.edgeCounts.length; e++) {
          const edge = edges[edgeIdx++];
          const v1 = vertices[edge[0]];
          const v2 = vertices[edge[1]];
          const count = config.edgeCounts[e];
          for (let i = 0; i < count; i++) {
            const t = Math.random();
            points[offset + i] = {
              x: (v1.x + t * (v2.x - v1.x) + randomRange(-noise, noise)) * scale,
              y: (v1.y + t * (v2.y - v1.y) + randomRange(-noise, noise)) * scale,
              z: (v1.z + t * (v2.z - v1.z) + randomRange(-noise, noise)) * scale,
            };
          }
          offset += count;
        }

        // 2. Populate Face Particles
        if (config.faceCount > 0) {
          let subFaceCount = 0;
          let subFaceParticleCounts: number[] = [];
          if (faces.length === 20) {
            if (c < 2) {
              subFaceCount = 4;
              subFaceParticleCounts = config.faceCount === 88 ? [22, 22, 22, 22] : [88, 88, 87, 87];
            } else {
              subFaceCount = 6;
              subFaceParticleCounts = config.faceCount === 87 ? [15, 15, 15, 14, 14, 14] : [88, 88, 88, 87, 87, 87];
            }
          } else if (faces.length === 12) {
            subFaceCount = 3;
            if (config.faceCount === 88) subFaceParticleCounts = [30, 29, 29];
            else if (config.faceCount === 87) subFaceParticleCounts = [29, 29, 29];
            else if (config.faceCount === 350) subFaceParticleCounts = [117, 117, 116];
            else if (config.faceCount === 525) subFaceParticleCounts = [175, 175, 175];
          }

          for (let f = 0; f < subFaceCount; f++) {
            const face = faces[faceIdx++];
            const count = subFaceParticleCounts[f];
            for (let i = 0; i < count; i++) {
              const pt = sampleFace(vertices, face, isStar);
              points[offset + i] = {
                x: (pt.x + randomRange(-noise, noise)) * scale,
                y: (pt.y + randomRange(-noise, noise)) * scale,
                z: (pt.z + randomRange(-noise, noise)) * scale,
              };
            }
            offset += count;
          }
        }
      }

      return points;
    }

    // 13. Programmatic Geodesic Icosahedron Generator
    const phi_ico = (1 + Math.sqrt(5)) / 2;
    const icoScale = 0.65;
    const icoLen = Math.sqrt(1 + phi_ico * phi_ico);
    const icoVerts = [
      { x: 0, y: 1 / icoLen, z: phi_ico / icoLen }, // 0
      { x: 0, y: -1 / icoLen, z: phi_ico / icoLen }, // 1
      { x: 0, y: 1 / icoLen, z: -phi_ico / icoLen }, // 2
      { x: 0, y: -1 / icoLen, z: -phi_ico / icoLen }, // 3
      { x: 1 / icoLen, y: phi_ico / icoLen, z: 0 }, // 4
      { x: -1 / icoLen, y: phi_ico / icoLen, z: 0 }, // 5
      { x: 1 / icoLen, y: -phi_ico / icoLen, z: 0 }, // 6
      { x: -1 / icoLen, y: -phi_ico / icoLen, z: 0 }, // 7
      { x: phi_ico / icoLen, y: 0, z: 1 / icoLen }, // 8
      { x: -phi_ico / icoLen, y: 0, z: 1 / icoLen }, // 9
      { x: phi_ico / icoLen, y: 0, z: -1 / icoLen }, // 10
      { x: -phi_ico / icoLen, y: 0, z: -1 / icoLen }, // 11
    ];
    const icoEdges = [
      [0, 1], [0, 4], [0, 5], [0, 8], [0, 9],
      [1, 6], [1, 7], [1, 8], [1, 9],
      [2, 3], [2, 4], [2, 5], [2, 10], [2, 11],
      [3, 6], [3, 7], [3, 10], [3, 11],
      [4, 8], [4, 10], [5, 9], [5, 11],
      [6, 8], [6, 10],
      [7, 9], [7, 11], [8, 10], [9, 11], [6, 7], [4, 5]
    ];
    // Programmatically discover the 20 faces of the icosahedron
    const icoFaces: number[][] = [];
    for (let i = 0; i < 12; i++) {
      for (let j = i + 1; j < 12; j++) {
        for (let k = j + 1; k < 12; k++) {
          const hasIJ = icoEdges.some(e => (e[0] === i && e[1] === j) || (e[0] === j && e[1] === i));
          const hasJK = icoEdges.some(e => (e[0] === j && e[1] === k) || (e[0] === k && e[1] === j));
          const hasKI = icoEdges.some(e => (e[0] === k && e[1] === i) || (e[0] === i && e[1] === k));
          if (hasIJ && hasJK && hasKI) {
            icoFaces.push([i, j, k]);
          }
        }
      }
    }
    const sortedIcosahedron = generatePolyhedron(icoVerts, icoEdges, icoFaces, icoScale, false, 0.018, 75);

    // 15. Programmatic Hyperboloid Generator (single-sheet)
    const sortedHyperboloid: { x: number; y: number; z: number }[] = [];
    const hyper_a = 0.28;
    const hyper_b = 0.55;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const u = randomRange(-1.1, 1.1);
      const v = Math.random() * Math.PI * 2;
      const x = hyper_a * Math.cosh(u) * Math.cos(v);
      const y = hyper_b * Math.sinh(u);
      const z = hyper_a * Math.cosh(u) * Math.sin(v);
      const n = 0.015;
      sortedHyperboloid.push({
        x: x + randomRange(-n, n),
        y: y + randomRange(-n, n),
        z: z + randomRange(-n, n),
      });
    }
    // Shuffle the hyperboloid coordinates for random particle distribution
    for (let i = sortedHyperboloid.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = sortedHyperboloid[i];
      sortedHyperboloid[i] = sortedHyperboloid[j];
      sortedHyperboloid[j] = temp;
    }

    // 17. Programmatic Klein Bottle Generator (3D immersion approximation)
    // Uses the standard parametric immersion: splits into outer tube (u∈[0,π]) and inner tube (u∈[π,2π])
    const sortedKlein: { x: number; y: number; z: number }[] = [];
    const kleinScale = 0.10;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      const u = Math.random() * Math.PI * 2;
      const v = Math.random() * Math.PI * 2;
      let x: number, y: number, z: number;
      if (u < Math.PI) {
        x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(u) * Math.cos(v);
        y = 8 * Math.sin(u) + (2 * (1 - Math.cos(u) / 2)) * Math.sin(u) * Math.cos(v);
        z = (2 * (1 - Math.cos(u) / 2)) * Math.sin(v);
      } else {
        x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(v + Math.PI);
        y = 8 * Math.sin(u);
        z = (2 * (1 - Math.cos(u) / 2)) * Math.sin(v);
      }
      const n = 0.04;
      sortedKlein.push({
        x: (x + randomRange(-n, n)) * kleinScale,
        y: (y + randomRange(-n, n)) * kleinScale,
        z: (z + randomRange(-n, n)) * kleinScale,
      });
    }
    sortedKlein.sort((a, b) => a.y - b.y);

    // ─── Kepler-Poinsot Polyhedra (4 Star Polyhedra) ──────────────────────────
    // All derived from the golden ratio φ ≈ 1.618
    const kp_phi = (1 + Math.sqrt(5)) / 2;
    const kp_inv = 1 / kp_phi;                      // = φ−1 ≈ 0.618
    const kp_icoR = Math.sqrt(1 + kp_phi * kp_phi); // circumradius of (0,1,φ)
    const kp_dodR = Math.sqrt(3);                    // circumradius of dodecahedron

    // 12 normalised icosahedral vertices (used by SSD, GD, GI) as {x,y,z}
    const kpIco = [
      { x: 0, y: 1 / kp_icoR, z: kp_phi / kp_icoR },  // 0
      { x: 0, y: 1 / kp_icoR, z: -kp_phi / kp_icoR },  // 1
      { x: 0, y: -1 / kp_icoR, z: kp_phi / kp_icoR },  // 2
      { x: 0, y: -1 / kp_icoR, z: -kp_phi / kp_icoR },  // 3
      { x: 1 / kp_icoR, y: kp_phi / kp_icoR, z: 0 },  // 4
      { x: 1 / kp_icoR, y: -kp_phi / kp_icoR, z: 0 },  // 5
      { x: -1 / kp_icoR, y: kp_phi / kp_icoR, z: 0 },  // 6
      { x: -1 / kp_icoR, y: -kp_phi / kp_icoR, z: 0 },  // 7
      { x: kp_phi / kp_icoR, y: 0, z: 1 / kp_icoR },  // 8
      { x: kp_phi / kp_icoR, y: 0, z: -1 / kp_icoR },  // 9
      { x: -kp_phi / kp_icoR, y: 0, z: 1 / kp_icoR },  // 10
      { x: -kp_phi / kp_icoR, y: 0, z: -1 / kp_icoR },  // 11
    ];

    // 20 normalised dodecahedral vertices (used by GSD) as {x,y,z}
    const kpDod = [
      { x: 1 / kp_dodR, y: 1 / kp_dodR, z: 1 / kp_dodR },   // 0  C0
      { x: 1 / kp_dodR, y: 1 / kp_dodR, z: -1 / kp_dodR },   // 1  C1
      { x: 1 / kp_dodR, y: -1 / kp_dodR, z: 1 / kp_dodR },   // 2  C2
      { x: 1 / kp_dodR, y: -1 / kp_dodR, z: -1 / kp_dodR },   // 3  C3
      { x: -1 / kp_dodR, y: 1 / kp_dodR, z: 1 / kp_dodR },   // 4  C4
      { x: -1 / kp_dodR, y: 1 / kp_dodR, z: -1 / kp_dodR },   // 5  C5
      { x: -1 / kp_dodR, y: -1 / kp_dodR, z: 1 / kp_dodR },   // 6  C6
      { x: -1 / kp_dodR, y: -1 / kp_dodR, z: -1 / kp_dodR },   // 7  C7
      { x: 0, y: kp_inv / kp_dodR, z: kp_phi / kp_dodR },   // 8  Z0
      { x: 0, y: kp_inv / kp_dodR, z: -kp_phi / kp_dodR },   // 9  Z1
      { x: 0, y: -kp_inv / kp_dodR, z: kp_phi / kp_dodR },   // 10 Z2
      { x: 0, y: -kp_inv / kp_dodR, z: -kp_phi / kp_dodR },   // 11 Z3
      { x: kp_inv / kp_dodR, y: kp_phi / kp_dodR, z: 0 },  // 12 X0
      { x: kp_inv / kp_dodR, y: -kp_phi / kp_dodR, z: 0 },  // 13 X1
      { x: -kp_inv / kp_dodR, y: kp_phi / kp_dodR, z: 0 },  // 14 X2
      { x: -kp_inv / kp_dodR, y: -kp_phi / kp_dodR, z: 0 },  // 15 X3
      { x: kp_phi / kp_dodR, y: 0, z: kp_inv / kp_dodR },  // 16 Y0
      { x: kp_phi / kp_dodR, y: 0, z: -kp_inv / kp_dodR },  // 17 Y1
      { x: -kp_phi / kp_dodR, y: 0, z: kp_inv / kp_dodR },  // 18 Y2
      { x: -kp_phi / kp_dodR, y: 0, z: -kp_inv / kp_dodR },  // 19 Y3
    ];

    // SSD and GD share edges
    const ssdEdges: number[][] = [
      [0, 1], [0, 5], [0, 7], [0, 9], [0, 11],
      [1, 5], [1, 7], [1, 8], [1, 10],
      [2, 3], [2, 4], [2, 6], [2, 9], [2, 11],
      [3, 4], [3, 6], [3, 8], [3, 10],
      [4, 5], [4, 10], [4, 11],
      [5, 10], [5, 11],
      [6, 7], [6, 8], [6, 9],
      [7, 8], [7, 9],
      [8, 10], [9, 11],
    ];

    // Find neighbors of each vertex in the icosahedron to form faces for SSD and GD
    const neighborSets: number[][] = [];
    for (let i = 0; i < 12; i++) {
      const neighbors: number[] = [];
      for (let j = 0; j < 12; j++) {
        if (icoEdges.some(e => (e[0] === i && e[1] === j) || (e[0] === j && e[1] === i))) {
          neighbors.push(j);
        }
      }
      neighborSets.push(sortCircular(neighbors, icoEdges));
    }

    const ssdFaces = neighborSets;
    const gdFaces = neighborSets;

    // GSD edges
    const gsdEdges: number[][] = [
      [0, 8], [0, 12], [0, 16], [1, 9], [1, 12], [1, 17], [2, 10], [2, 13],
      [2, 16], [3, 11], [3, 13], [3, 17], [4, 8], [4, 14], [4, 18],
      [5, 9], [5, 14], [5, 19], [6, 10], [6, 15], [6, 18], [7, 11],
      [7, 15], [7, 19],
      [8, 10], [9, 11], [12, 14], [13, 15], [16, 17], [18, 19],
    ];

    // Find GSD faces (12 coplanar faces of dodecahedron)
    const gsdFaces: number[][] = [];
    for (let i = 0; i < 20; i++) {
      for (let j = i + 1; j < 20; j++) {
        for (let k = j + 1; k < 20; k++) {
          const vi = kpDod[i];
          const vj = kpDod[j];
          const vk = kpDod[k];
          const ux = vj.x - vi.x, uy = vj.y - vi.y, uz = vj.z - vi.z;
          const vx = vk.x - vi.x, vy = vk.y - vi.y, vz = vk.z - vi.z;
          const nx = uy * vz - uz * vy;
          const ny = uz * vx - ux * vz;
          const nz = ux * vy - uy * vx;
          const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
          if (len < 1e-5) continue;
          const ndx = nx / len, ndy = ny / len, ndz = nz / len;
          const d = ndx * vi.x + ndy * vi.y + ndz * vi.z;

          const planeVerts: number[] = [i, j, k];
          for (let m = 0; m < 20; m++) {
            if (m === i || m === j || m === k) continue;
            const vm = kpDod[m];
            const dist = Math.abs(ndx * vm.x + ndy * vm.y + ndz * vm.z - d);
            if (dist < 1e-4) {
              planeVerts.push(m);
            }
          }

          if (planeVerts.length === 5) {
            const sortedFace = [...planeVerts].sort((a, b) => a - b);
            if (!gsdFaces.some(f => [...f].sort((a, b) => a - b).join(',') === sortedFace.join(','))) {
              gsdFaces.push(sortFaceCircular(planeVerts, kpDod));
            }
          }
        }
      }
    }

    // GI faces (20 triangular faces formed by ssdEdges)
    const giFaces: number[][] = [];
    for (let i = 0; i < 12; i++) {
      for (let j = i + 1; j < 12; j++) {
        for (let k = j + 1; k < 12; k++) {
          const hasIJ = ssdEdges.some(e => (e[0] === i && e[1] === j) || (e[0] === j && e[1] === i));
          const hasJK = ssdEdges.some(e => (e[0] === j && e[1] === k) || (e[0] === k && e[1] === j));
          const hasKI = ssdEdges.some(e => (e[0] === k && e[1] === i) || (e[0] === i && e[1] === k));
          if (hasIJ && hasJK && hasKI) {
            giFaces.push([i, j, k]);
          }
        }
      }
    }

    // SSD Option A (Spatial Quadrants, 100% edges, 0% faces)
    const ssdScale = 0.60;
    const ssdEdges_A = [...ssdEdges].sort((e1, e2) => {
      const m1x = (kpIco[e1[0]].x + kpIco[e1[1]].x) / 2;
      const m1y = (kpIco[e1[0]].y + kpIco[e1[1]].y) / 2;
      const m2x = (kpIco[e2[0]].x + kpIco[e2[1]].x) / 2;
      const m2y = (kpIco[e2[0]].y + kpIco[e2[1]].y) / 2;
      return Math.atan2(m1y, m1x) - Math.atan2(m2y, m2x);
    });
    const sortedSSD = generatePolyhedron(kpIco, ssdEdges_A, ssdFaces, ssdScale, true, 0.014, 100);

    // GSD (95% edges, 5% faces)
    const gsdScale = 0.625;
    const sortedGSD = generatePolyhedron(kpDod, gsdEdges, gsdFaces, gsdScale, true, 0.014, 95);

    // GD Option A (Tri-Axial alignment, 100% edges, 0% faces)
    const gdScale = 0.576;
    const gdEdges_A = [...ssdEdges].sort((e1, e2) => {
      const d1x = kpIco[e1[1]].x - kpIco[e1[0]].x, d1y = kpIco[e1[1]].y - kpIco[e1[0]].y, d1z = kpIco[e1[1]].z - kpIco[e1[0]].z;
      const d2x = kpIco[e2[1]].x - kpIco[e2[0]].x, d2y = kpIco[e2[1]].y - kpIco[e2[0]].y, d2z = kpIco[e2[1]].z - kpIco[e2[0]].z;
      const score1 = Math.abs(d1x) + Math.abs(d1y) * 10 + Math.abs(d1z) * 100;
      const score2 = Math.abs(d2x) + Math.abs(d2y) * 10 + Math.abs(d2z) * 100;
      return score1 - score2;
    });
    const sortedGD = generatePolyhedron(kpIco, gdEdges_A, gdFaces, gdScale, false, 0.012, 100);

    // GI (100% edges, 0% faces, outer/inner concentric shell split)
    const giScaleOuter = 0.59;
    const giScaleInner = giScaleOuter * kp_inv;
    const sortedGI: { x: number; y: number; z: number }[] = new Array(7000);
    const giNoise = 0.012;

    const giOuterEdgeCounts = [117, 117, 117, 117, 117, 117, 117, 117, 117, 117, 116, 116, 116, 116, 116]; // sum = 1750
    let giOffset = 0;

    // Outer Shell Color 0 (edges 0 to 14)
    for (let e = 0; e < 15; e++) {
      const edge = ssdEdges[e];
      const v1 = kpIco[edge[0]];
      const v2 = kpIco[edge[1]];
      const count = giOuterEdgeCounts[e];
      for (let i = 0; i < count; i++) {
        const t = Math.random();
        sortedGI[giOffset++] = {
          x: (v1.x + t * (v2.x - v1.x) + randomRange(-giNoise, giNoise)) * giScaleOuter,
          y: (v1.y + t * (v2.y - v1.y) + randomRange(-giNoise, giNoise)) * giScaleOuter,
          z: (v1.z + t * (v2.z - v1.z) + randomRange(-giNoise, giNoise)) * giScaleOuter,
        };
      }
    }

    // Outer Shell Color 1 (edges 15 to 29)
    for (let e = 0; e < 15; e++) {
      const edge = ssdEdges[15 + e];
      const v1 = kpIco[edge[0]];
      const v2 = kpIco[edge[1]];
      const count = giOuterEdgeCounts[e];
      for (let i = 0; i < count; i++) {
        const t = Math.random();
        sortedGI[giOffset++] = {
          x: (v1.x + t * (v2.x - v1.x) + randomRange(-giNoise, giNoise)) * giScaleOuter,
          y: (v1.y + t * (v2.y - v1.y) + randomRange(-giNoise, giNoise)) * giScaleOuter,
          z: (v1.z + t * (v2.z - v1.z) + randomRange(-giNoise, giNoise)) * giScaleOuter,
        };
      }
    }

    // Inner Shell Color 2 (edges 0 to 14)
    for (let e = 0; e < 15; e++) {
      const edge = ssdEdges[e];
      const v1 = kpIco[edge[0]];
      const v2 = kpIco[edge[1]];
      const count = giOuterEdgeCounts[e];
      for (let i = 0; i < count; i++) {
        const t = Math.random();
        sortedGI[giOffset++] = {
          x: (v1.x + t * (v2.x - v1.x) + randomRange(-giNoise, giNoise)) * giScaleInner,
          y: (v1.y + t * (v2.y - v1.y) + randomRange(-giNoise, giNoise)) * giScaleInner,
          z: (v1.z + t * (v2.z - v1.z) + randomRange(-giNoise, giNoise)) * giScaleInner,
        };
      }
    }

    // Inner Shell Color 3 (edges 15 to 29)
    for (let e = 0; e < 15; e++) {
      const edge = ssdEdges[15 + e];
      const v1 = kpIco[edge[0]];
      const v2 = kpIco[edge[1]];
      const count = giOuterEdgeCounts[e];
      for (let i = 0; i < count; i++) {
        const t = Math.random();
        sortedGI[giOffset++] = {
          x: (v1.x + t * (v2.x - v1.x) + randomRange(-giNoise, giNoise)) * giScaleInner,
          y: (v1.y + t * (v2.y - v1.y) + randomRange(-giNoise, giNoise)) * giScaleInner,
          z: (v1.z + t * (v2.z - v1.z) + randomRange(-giNoise, giNoise)) * giScaleInner,
        };
      }
    }

    // 11. Programmatic 3D Email/Envelope Generator
    const sortedEnvelope: { x: number; y: number; z: number }[] = new Array(PARTICLE_COUNT);
    const envFillIndices: number[] = [];
    const envFillCoordinates: { x: number; y: number; z: number }[] = [];

    // Group 0: Flap Diagonals (\/) - Color 1
    const group0Segments = [
      [{ x: -0.7, y: 0.35, z: 0.05 }, { x: 0.0, y: -0.1, z: 0.05 }],
      [{ x: 0.7, y: 0.35, z: 0.05 }, { x: 0.0, y: -0.1, z: 0.05 }],
      [{ x: -0.7, y: 0.35, z: -0.05 }, { x: 0.0, y: -0.1, z: -0.05 }],
      [{ x: 0.7, y: 0.35, z: -0.05 }, { x: 0.0, y: -0.1, z: -0.05 }],
      [{ x: 0.0, y: -0.1, z: -0.05 }, { x: 0.0, y: -0.1, z: 0.05 }]
    ];

    // Group 1: Side Verticals (| |) - Color 2
    const group1Segments = [
      [{ x: -0.7, y: -0.45, z: 0.05 }, { x: -0.7, y: 0.35, z: 0.05 }],
      [{ x: 0.7, y: -0.45, z: 0.05 }, { x: 0.7, y: 0.35, z: 0.05 }],
      [{ x: -0.7, y: -0.45, z: -0.05 }, { x: -0.7, y: 0.35, z: -0.05 }],
      [{ x: 0.7, y: -0.45, z: -0.05 }, { x: 0.7, y: 0.35, z: -0.05 }]
    ];

    // Group 2: Top Outline - Color 3
    const group2Segments = [
      [{ x: -0.7, y: 0.35, z: 0.05 }, { x: 0.7, y: 0.35, z: 0.05 }],
      [{ x: -0.7, y: 0.35, z: -0.05 }, { x: 0.7, y: 0.35, z: -0.05 }],
      [{ x: -0.7, y: 0.35, z: -0.05 }, { x: -0.7, y: 0.35, z: 0.05 }],
      [{ x: 0.7, y: 0.35, z: -0.05 }, { x: 0.7, y: 0.35, z: 0.05 }]
    ];

    // Group 3: Bottom Outline - Color 4
    const group3Segments = [
      [{ x: -0.7, y: -0.45, z: 0.05 }, { x: 0.7, y: -0.45, z: 0.05 }],
      [{ x: -0.7, y: -0.45, z: -0.05 }, { x: 0.7, y: -0.45, z: -0.05 }],
      [{ x: -0.7, y: -0.45, z: -0.05 }, { x: -0.7, y: -0.45, z: 0.05 }],
      [{ x: 0.7, y: -0.45, z: -0.05 }, { x: 0.7, y: -0.45, z: 0.05 }]
    ];

    const allGroupSegments = [group0Segments, group1Segments, group2Segments, group3Segments];

    const tiltAngle = (25 * Math.PI) / 180; // 25 degrees
    const cosTilt = Math.cos(tiltAngle);
    const sinTilt = Math.sin(tiltAngle);

    for (let c = 0; c < 4; c++) {
      const bandStart = c * 1750;
      const segments = allGroupSegments[c];

      // A: Generate outline particles (1312 particles)
      for (let i = 0; i < 1312; i++) {
        const segIndex = i % segments.length;
        const [p1, p2] = segments[segIndex];
        const t_val = Math.random();
        let x = p1.x + t_val * (p2.x - p1.x);
        let y = p1.y + t_val * (p2.y - p1.y);
        let z = p1.z + t_val * (p2.z - p1.z);

        // Add noise
        const noise = 0.015;
        x += randomRange(-noise, noise);
        y += randomRange(-noise, noise);
        z += randomRange(-noise, noise);

        // Swap x and z, then apply Y rotation tilt of 30 degrees so it starts rotated
        sortedEnvelope[bandStart + i] = {
          x: (z * cosTilt + x * sinTilt) * 0.95,
          y: y * 0.95,
          z: (z * sinTilt - x * cosTilt) * 0.95
        };
      }

      // B: Generate body/scattered particles (438 particles)
      for (let i = 1312; i < 1750; i++) {
        const idx = bandStart + i;
        envFillIndices.push(idx);

        // Sample inside the envelope box volume
        let x = randomRange(-0.7, 0.7);
        let y = randomRange(-0.45, 0.35);
        let z = randomRange(-0.05, 0.05);

        // Add scattering noise
        const noise = 0.04;
        x += randomRange(-noise, noise);
        y += randomRange(-noise, noise);
        z += randomRange(-noise, noise);

        envFillCoordinates.push({
          x: (z * cosTilt + x * sinTilt) * 0.95,
          y: y * 0.95,
          z: (z * sinTilt - x * cosTilt) * 0.95
        });
      }
    }

    // Shuffle the fill coordinates so that fill colors are randomly mixed and scattered
    for (let i = envFillCoordinates.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = envFillCoordinates[i];
      envFillCoordinates[i] = envFillCoordinates[j];
      envFillCoordinates[j] = temp;
    }

    // Put shuffled fill coordinates back into their reserved slots in sortedEnvelope
    for (let i = 0; i < envFillIndices.length; i++) {
      sortedEnvelope[envFillIndices[i]] = envFillCoordinates[i];
    }

    // Sort coordinates by Y (bottom-to-top) so colors blend beautifully
    sortedBrain.sort((a, b) => a.y - b.y);
    sortedLightbulb.sort((a, b) => a.y - b.y);
    sortedDNA.sort((a, b) => a.y - b.y);
    sortedTrefoil.sort((a, b) => a.y - b.y);
    sortedAstroid.sort((a, b) => a.y - b.y);

    // ─── Init Particles ───────────────────────────────────────────────────
    const particles: ParticleData[] = [];
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      particles.push({
        x: randomRange(0, W),
        y: randomRange(0, H),
        vx: 0,
        vy: 0,
        size: randomRange(1.8, 4.2),
        colorIndex: Math.floor(Math.random() * 4),
        shape: shapes[Math.floor(Math.random() * shapes.length)],
        opacity: randomRange(0.2, 0.95),
        springFactor: randomRange(0.015, 0.04),
        damping: randomRange(0.85, 0.92),
        driftOffset: randomRange(0, 1000),
        scaleFactor: 1.0,
      });
    }

    // Sort particles by color group index to align with depth bands
    particles.sort((a, b) => a.colorIndex - b.colorIndex);

    // Sprite Cache (recreated dynamically when settings.colors change)
    const sprites: { [color: string]: HTMLCanvasElement } = {};
    const spriteSize = 32;
    let activeColors = ["", "", "", ""];

    let lastFrameTime = Date.now();
    const localTimes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    let animId: number;

    function animate() {
      ctx!.clearRect(0, 0, W, H);
      const now = Date.now();
      const delta = now - lastFrameTime;
      lastFrameTime = now;

      const scrollTop = window.scrollY || document.documentElement.scrollTop;
      const scrollHeight =
        document.documentElement.scrollHeight - window.innerHeight;
      const scrollRatio = scrollHeight > 0 ? scrollTop / scrollHeight : 0;

      const isMobile = W < 768;

      // Real-time configurations from ref
      const currentCount = Math.min(particles.length, settingsRef.current.particleCount);
      // Resolve white/black colors depending on theme to prevent them from disappearing
      const theme = settingsRef.current.theme;
      const currentColors = settingsRef.current.colors.map((c) => {
        const lower = c.trim().toLowerCase();
        if (theme === "white") {
          // If light theme, convert complete white to premium red (#ef4444)
          if (lower === "#ffffff" || lower === "#fff" || lower === "rgb(255,255,255)" || lower === "white") {
            return "#ef4444";
          }
        } else if (theme === "black") {
          // If dark theme, convert complete black to white
          if (lower === "#000000" || lower === "#000" || lower === "rgb(0,0,0)" || lower === "black") {
            return "#ffffff";
          }
        }
        return c;
      }) as [string, string, string, string];
      const stiffnessMultiplier = settingsRef.current.springStiffness / 0.03;
      const dampingMultiplier = settingsRef.current.damping / 0.90;
      const sizeMultiplier = settingsRef.current.particleSize / 3.0;

      // ─── Sprite Cache Sync ────────────────────────────────────────────────
      let colorsChanged = false;
      for (let c = 0; c < 4; c++) {
        if (currentColors[c] !== activeColors[c]) {
          colorsChanged = true;
          break;
        }
      }

      if (colorsChanged) {
        activeColors = [...currentColors];
        currentColors.forEach((color) => {
          const offscreen = sprites[color] || document.createElement("canvas");
          offscreen.width = spriteSize;
          offscreen.height = spriteSize;
          const octx = offscreen.getContext("2d");
          if (octx) {
            octx.clearRect(0, 0, spriteSize, spriteSize);
            const r = spriteSize / 2;
            const grad = octx.createRadialGradient(
              r - r * 0.3,
              r - r * 0.3,
              r * 0.1,
              r,
              r,
              r
            );
            grad.addColorStop(0, "#ffffff");
            grad.addColorStop(0.15, color);
            grad.addColorStop(0.95, color);
            grad.addColorStop(1.0, "rgba(0, 0, 0, 0)");

            octx.fillStyle = grad;
            octx.beginPath();
            octx.arc(r, r, r, 0, Math.PI * 2);
            octx.fill();
          }
          sprites[color] = offscreen;
        });
      }

      const configs = [
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 400 : 550 }, // 0:  Brain (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 320 : 445 }, // 1:  Lightbulb (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 300 : 420 }, // 2:  DNA Helix (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 372 : 528 }, // 3:  SSD / Nebula Spindle Star (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 300 : 420 }, // 4:  Octahedron (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 220 : 330 }, // 5:  Cube (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 360 : 528 }, // 6:  GD / Intersecting Aegis (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 240 : 360 }, // 7:  Torus (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 300 : 420 }, // 8:  Trefoil Knot (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 360 : 525 }, // 9:  GSD / Nova Spire Lattice (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 300 : 420 }, // 10: Astroid Star (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 320 : 445 }, // 11: Envelope (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 363 : 522 }, // 12: GI / Quantum Dual Shell (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 340 : 490 }, // 13: Icosahedron (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.72, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 280 : 400 }, // 14: Hyperboloid (Right, layout-left)
        { cx: isMobile ? W * 0.5 : W * 0.32, cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 260 : 380 }, // 15: Klein Bottle (Left, layout-right)
        { cx: isMobile ? W * 0.5 : W * 0.5,  cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 380 : 520 }, // 16: Sphere (Center)
        { cx: isMobile ? W * 0.5 : W * 0.5,  cy: isMobile ? H * 0.45 : H * 0.5, scale: isMobile ? 320 : 445 }, // 17: Scattered (Center)
      ];

      // ─── Math for Multisection Morphing ──────────────────────────────────
      const shapesList = [
        sortedBrain,
        sortedLightbulb,
        sortedDNA,
        sortedSSD,
        sortedPyramid,
        sortedCube,
        sortedGD,
        sortedTorus,
        sortedTrefoil,
        sortedGSD,
        sortedAstroid,
        sortedEnvelope,
        sortedGI,
        sortedIcosahedron,
        sortedHyperboloid,
        sortedKlein,
        sortedSphere,
        sortedScattered
      ];
      const K = shapesList.length; // 18
      const N = K - 1; // 17

      const scaledRatio = scrollRatio * N;
      let index = Math.floor(scaledRatio);
      let t = scaledRatio - index;

      if (index >= N) {
        index = N - 1;
        t = 1.0;
      } else if (index < 0) {
        index = 0;
        t = 0.0;
      }

      // Scroll Snap Dead Zone curve
      if (settingsRef.current.deadZoneEnabled) {
        const p = settingsRef.current.deadZonePercentage / 100;
        const halfP = p / 2;
        if (t < halfP) {
          t = 0;
        } else if (t > 1 - halfP) {
          t = 1;
        } else {
          t = (t - halfP) / (1 - p);
        }
      }

      // Interpolate center and scale between the two active shapes
      const conf1 = configs[index];
      const conf2 = configs[index + 1] || conf1;

      const cx = lerp(conf1.cx, conf2.cx, t);
      const cy = lerp(conf1.cy, conf2.cy, t);
      const baseScale = lerp(conf1.scale, conf2.scale, t);

      // Track mouse velocity
      if (mouse.active) {
        if (prevMouseX !== -1000) {
          mouseVx = mouse.x - prevMouseX;
          mouseVy = mouse.y - prevMouseY;
        }
        prevMouseX = mouse.x;
        prevMouseY = mouse.y;
      } else {
        mouseVx = 0;
        mouseVy = 0;
        prevMouseX = -1000;
        prevMouseY = -1000;
      }

      let targetInfluenceX = 0;
      let targetInfluenceY = 0;
      if (hasGyro && settingsRef.current.gyroEnabled) {
        targetInfluenceX = gyroX;
        targetInfluenceY = gyroY;
      } else if (mouse.active) {
        targetInfluenceX = ((mouse.x - W / 2) / (W / 2)) * 0.30;
        targetInfluenceY = ((mouse.y - H / 2) / (H / 2)) * 0.15;
      }
      mouseInfluenceX += (targetInfluenceX - mouseInfluenceX) * 0.05;
      mouseInfluenceY += (targetInfluenceY - mouseInfluenceY) * 0.05;

      // Offsets for [rotateX, rotateY, rotateZ] for each of the 18 shapes:
      const shapeOffsets = [
        { rx: 0, ry: 0, rz: 0 }, // 0:  Brain
        { rx: 0, ry: -0.5236, rz: 0 }, // 1:  Lightbulb
        { rx: 0, ry: 0, rz: 0 }, // 2:  DNA
        { rx: 0, ry: 0.5236, rz: 0 }, // 3:  SSD / Nebula Spindle Star
        { rx: 0, ry: 0, rz: 0 }, // 4:  Octahedron
        { rx: 0.2618, ry: 0.5236, rz: 0 }, // 5:  Cube
        { rx: 0, ry: 0.7854, rz: 0 }, // 6:  GD / Intersecting Aegis
        { rx: 0, ry: 0.4363, rz: 0 }, // 7:  Torus
        { rx: 0, ry: 1.7453, rz: 0 }, // 8:  Trefoil Knot
        { rx: 0, ry: 0.3927, rz: 0 }, // 9:  GSD / Nova Spire Lattice
        { rx: 0, ry: 0, rz: 0 }, // 10: Astroid Star
        { rx: 0, ry: 0, rz: 0 }, // 11: Envelope
        { rx: 0, ry: 0.5236, rz: 0 }, // 12: GI / Quantum Dual Shell
        { rx: 0, ry: 0.5236, rz: 0 }, // 13: Icosahedron
        { rx: 0, ry: 0.7854, rz: 0 }, // 14: Hyperboloid
        { rx: 0.3491, ry: 1.5708, rz: 0 }, // 15: Klein Bottle
        { rx: 0, ry: 0, rz: 0 }, // 16: Sphere
        { rx: 0, ry: 0, rz: 0 }, // 17: Scattered
      ];

      // Update local timers for all shapes based on active/inactive states
      for (let i = 0; i < 18; i++) {
        const active = (i === index && t < 1) || (i === index + 1 && t > 0);
        if (active) {
          localTimes[i] += delta;
        } else {
          localTimes[i] = 0;
        }
      }

      const off1 = shapeOffsets[index];
      const off2 = shapeOffsets[index + 1] || off1;

      const t1 = localTimes[index] * 0.00015 * settingsRef.current.autoRotateSpeed;
      const ry1 = 1.60 + off1.ry + t1 * 0.12 + mouseInfluenceX;
      const rx1 = 0.25 + off1.rx + Math.sin(t1 * 0.15) * 0.05 + mouseInfluenceY;
      const rz1 = off1.rz + Math.cos(t1 * 0.12) * 0.03;

      const t2 = localTimes[index + 1] * 0.00015 * settingsRef.current.autoRotateSpeed;
      const ry2 = 1.60 + off2.ry + t2 * 0.12 + mouseInfluenceX;
      const rx2 = 0.25 + off2.rx + Math.sin(t2 * 0.15) * 0.05 + mouseInfluenceY;
      const rz2 = off2.rz + Math.cos(t2 * 0.12) * 0.03;

      const rotateY = lerp(ry1, ry2, t);
      const rotateX = lerp(rx1, rx2, t);
      const rotateZ = lerp(rz1, rz2, t);

      // ─── Update Loop ──────────────────────────────────────────────────────
      for (let i = 0; i < currentCount; i++) {
        const p = particles[i];

        const pt1 = shapesList[index][i] || { x: 0, y: 0, z: 0 };
        const pt2 = shapesList[index + 1]?.[i] || pt1;

        let rx = lerp(pt1.x, pt2.x, t);
        let ry = lerp(pt1.y, pt2.y, t);
        let rz = lerp(pt1.z, pt2.z, t);

        // 3D Rotation Matrices
        const cosY = Math.cos(rotateY);
        const sinY = Math.sin(rotateY);
        let x1 = rx * cosY - rz * sinY;
        let z1 = rx * sinY + rz * cosY;

        const cosX = Math.cos(rotateX);
        const sinX = Math.sin(rotateX);
        let y1 = ry * cosX - z1 * sinX;
        let z2 = ry * sinX + z1 * cosX;

        const cosZ = Math.cos(rotateZ);
        const sinZ = Math.sin(rotateZ);
        let x2 = x1 * cosZ - y1 * sinZ;
        let y2 = x1 * sinZ + y1 * cosZ;

        // Perspective Projection
        const fov = 400;
        const perspective = fov / Math.max(50, fov + z2 * 250);
        p.scaleFactor = perspective;

        const scale = baseScale * perspective;
        let targetX = cx + x2 * scale;
        let targetY = cy - y2 * scale;

        // Wave drift in Scattered phase (Index 4 to 5 transition)
        if (index === N - 1) {
          const waveTime = Date.now() * 0.001 + p.driftOffset;
          targetY += Math.sin(waveTime) * 15 * t;
        }

        // Spring Physics
        const ax = (targetX - p.x) * p.springFactor * stiffnessMultiplier;
        const ay = (targetY - p.y) * p.springFactor * stiffnessMultiplier;
        p.vx = (p.vx + ax) * p.damping * dampingMultiplier;
        p.vy = (p.vy + ay) * p.damping * dampingMultiplier;

        p.x += p.vx;
        p.y += p.vy;

        // Proximity-based Mouse Interaction Modes
        if (mouse.active && settingsRef.current.interactionMode !== "disabled") {
          const dx = p.x - mouse.x;
          const dy = p.y - mouse.y;
          const dist = Math.sqrt(dx * dx + dy * dy);
          const radius = settingsRef.current.interactionRadius;

          if (dist < radius && dist > 0) {
            const factor = (radius - dist) / radius;
            const smoothFactor = factor * factor * (3 - 2 * factor);
            const force = smoothFactor * settingsRef.current.interactionForce;

            const mode = settingsRef.current.interactionMode;
            if (mode === "repel") {
              p.vx += (dx / dist) * force * 0.4;
              p.vy += (dy / dist) * force * 0.4;
            } else if (mode === "attract") {
              p.vx -= (dx / dist) * force * 0.4;
              p.vy -= (dy / dist) * force * 0.4;
            } else if (mode === "swarm") {
              // Orbit velocity + slight gravity pull towards center
              p.vx += (-dy / dist) * force * 0.5 - (dx / dist) * force * 0.08;
              p.vy += (dx / dist) * force * 0.5 - (dy / dist) * force * 0.08;
            } else if (mode === "ripple") {
              // Proximity Breathing wave ripple
              const waveTime = Date.now() * 0.005 + p.driftOffset;
              const waveX = Math.sin(waveTime) * (1.0 + Math.abs(mouseVx) * 0.8) * force * smoothFactor;
              const waveY = Math.cos(waveTime) * (1.0 + Math.abs(mouseVy) * 0.8) * force * smoothFactor;
              p.x += waveX;
              p.y += waveY;
            }
          }
        }
      }

      // ─── Render circular particles (spheres sprites) ──────────────────────
      for (let i = 0; i < currentCount; i++) {
        const p = particles[i];
        if (p.shape !== "circle") continue;

        ctx!.globalAlpha = p.opacity * settingsRef.current.particleOpacity;
        const size = p.size * p.scaleFactor * sizeMultiplier;
        const colorHex = currentColors[p.colorIndex];
        const sprite = sprites[colorHex];
        if (sprite) {
          ctx!.drawImage(
            sprite,
            p.x - size / 2,
            p.y - size / 2,
            size,
            size
          );
        }
      }

      // ─── Render vector particles (triangle, diamond, square) ──────────────
      ctx!.globalAlpha = 0.68 * settingsRef.current.particleOpacity;

      for (let c = 0; c < 4; c++) {
        const colorHex = currentColors[c];

        shapes.forEach((shape) => {
          if (shape === "circle") return;

          ctx!.fillStyle = colorHex;
          ctx!.beginPath();

          for (let i = 0; i < currentCount; i++) {
            const p = particles[i];
            if (p.colorIndex !== c || p.shape !== shape) continue;

            const size = p.size * p.scaleFactor * sizeMultiplier;
            const halfSize = size / 2;

            switch (shape) {
              case "triangle":
                ctx!.moveTo(p.x, p.y - halfSize);
                ctx!.lineTo(p.x - halfSize, p.y + halfSize);
                ctx!.lineTo(p.x + halfSize, p.y + halfSize);
                ctx!.closePath();
                break;
              case "diamond":
                ctx!.moveTo(p.x, p.y - halfSize);
                ctx!.lineTo(p.x + halfSize, p.y);
                ctx!.lineTo(p.x, p.y + halfSize);
                ctx!.lineTo(p.x - halfSize, p.y);
                ctx!.closePath();
                break;
              case "square":
                ctx!.rect(p.x - halfSize, p.y - halfSize, size, size);
                break;
            }
          }

          ctx!.fill();
        });
      }

      ctx!.globalAlpha = 1.0;
      animId = requestAnimationFrame(animate);
    }

    animId = requestAnimationFrame(animate);

    // ─── Event Listeners ──────────────────────────────────────────────────
    const handleResize = () => {
      W = window.innerWidth;
      H = window.innerHeight;
      canvas.width = W;
      canvas.height = H;
    };

    const handleMouseMove = (e: MouseEvent) => {
      mouse.x = e.clientX;
      mouse.y = e.clientY;
      mouse.active = true;
    };

    const handleMouseLeave = () => {
      mouse.x = -1000;
      mouse.y = -1000;
      mouse.active = false;
    };

    const handleOrientation = (e: DeviceOrientationEvent) => {
      if (!settingsRef.current.gyroEnabled) return;
      if (e.gamma !== null && e.beta !== null) {
        hasGyro = true;
        const gammaClamped = Math.max(-30, Math.min(30, e.gamma));
        const betaClamped = Math.max(-30, Math.min(30, e.beta - 60));

        gyroX = (gammaClamped / 30) * 0.30 * settingsRef.current.gyroSensitivity;
        gyroY = (betaClamped / 30) * 0.20 * settingsRef.current.gyroSensitivity;
      }
    };

    const handleTouchStart = (e: TouchEvent) => {
      if (e.touches.length > 0) {
        mouse.x = e.touches[0].clientX;
        mouse.y = e.touches[0].clientY;
        mouse.active = true;
      }
    };

    const handleTouchMove = (e: TouchEvent) => {
      if (e.touches.length > 0) {
        mouse.x = e.touches[0].clientX;
        mouse.y = e.touches[0].clientY;
        mouse.active = true;
      }
    };

    const handleTouchEnd = () => {
      mouse.x = -1000;
      mouse.y = -1000;
      mouse.active = false;
    };

    window.addEventListener("resize", handleResize);
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseleave", handleMouseLeave);
    window.addEventListener("deviceorientation", handleOrientation);
    window.addEventListener("touchstart", handleTouchStart, { passive: true });
    window.addEventListener("touchmove", handleTouchMove, { passive: true });
    window.addEventListener("touchend", handleTouchEnd, { passive: true });

    return () => {
      cancelAnimationFrame(animId);
      window.removeEventListener("resize", handleResize);
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseleave", handleMouseLeave);
      window.removeEventListener("deviceorientation", handleOrientation);
      window.removeEventListener("touchstart", handleTouchStart);
      window.removeEventListener("touchmove", handleTouchMove);
      window.removeEventListener("touchend", handleTouchEnd);
    };
  }, []);

  return (
    <div className="canvas-container">
      <canvas
        ref={canvasRef}
        id="particle-canvas"
        role="img"
        aria-label="Interactive 3D particle physics visualization canvas"
      />
    </div>
  );
}
