// Quantum Cathedral standalone demo — intro + two cathedral variants + Thank You
const WIDTH = 1920;
const HEIGHT = 1080;
const INTRO_START = 0.0;
const INTRO_END = 6.0;
const INTRO_DURATION = INTRO_END - INTRO_START;
const INTRO_PACE = 1.2;
const SCENE1_START = INTRO_END;
const SCENE1_END = 14.0;
const SCENE1_DURATION = SCENE1_END - SCENE1_START;
const SCENE1_PACE = 2;
const SCENE_PC_START = SCENE1_END;
const SCENE_PC_END = 33.0;
const SCENE_PC_DURATION = SCENE_PC_END - SCENE_PC_START;
const SCENE_PC_PACE = 1.4;
const PC_SCATTER_END = 3.17;
const PC_BUILD_END = 9.5;
const PC_ROTATE_END = 11.88;
const PC_RABBIT_END = 14.25;
const SCENE2_START = SCENE_PC_END;
const SCENE2_END = 47.0;
const SCENE2_DURATION = SCENE2_END - SCENE2_START;
const SCENE2_PACE = 1.6;
const SCENE3_START = SCENE2_END;
const SCENE3_END = 65.0;
const SCENE3_DURATION = SCENE3_END - SCENE3_START;
const SCENE3_PACE = 1.1;
const OUTRO_ZOOM_DURATION = 4.9;
const OUTRO_HEART_DURATION = 4.9;
const OUTRO_CAT_HOLD_DURATION = 2.5;
const CAT_HEAD_SCALE = 1.5 * 1.3;
const FADE_OUT_DURATION = 2.0;
const FADE_IN_DURATION = 2.0;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.setPixelRatio(1);
document.body.appendChild(renderer.domElement);
document.body.style.cursor = 'none';

const clock = new THREE.Clock();

let audioStarted = false;
let audioAutoTryCount = 0;
const AUDIO_AUTO_TRY_LIMIT = 180;

async function ensureBackgroundAudio() {
  if (isBeatPlaying()) return true;

  const started = await startBackgroundAudio();
  if (started) audioStarted = true;
  return started;
}

function bindAudioUnlock() {
  const unlock = async () => {
    if (isBeatPlaying()) return;

    const started = await unlockBackgroundAudio();
    if (started) audioStarted = true;
  };

  renderer.domElement.addEventListener('pointerdown', unlock);
  window.addEventListener('pointerdown', unlock);
  window.addEventListener('touchstart', unlock);
  window.addEventListener('keydown', (event) => {
    if (event.key !== 'Escape') unlock();
  });
  window.addEventListener('focus', () => {
    ensureBackgroundAudio();
  });
}

let running = true;
let animationFrameId = null;
let activeSceneIndex = -1;
let activeScene = null;

function lerp(a, b, t) {
  return a + (b - a) * t;
}

function smoothstep(edge0, edge1, x) {
  const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
  return t * t * (3 - 2 * t);
}

function easeInOutCubic(t) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

function disposeMaterial(material) {
  if (!material) return;
  if (Array.isArray(material)) {
    material.forEach(disposeMaterial);
    return;
  }
  Object.keys(material).forEach((key) => {
    const value = material[key];
    if (value && typeof value.dispose === 'function') {
      value.dispose();
    }
  });
  material.dispose();
}

function clampFold(value, fold) {
  const clamped = Math.max(-fold, Math.min(fold, value));
  return clamped * 2 - value;
}

function buildCathedralGeometry() {
  const linePositions = [];
  const glowPositions = [];

  function addLine(ax, ay, az, bx, by, bz) {
    linePositions.push(ax, ay, az, bx, by, bz);
    glowPositions.push(ax, ay, az, bx, by, bz);
  }

  function addArch(columnX, z, span, apexHeight, segments) {
    const baseY = 0;
    const capitalY = apexHeight * 0.55;
    addLine(columnX, baseY, z, columnX, capitalY, z);

    const innerX = columnX > 0 ? columnX - span : columnX + span;
    for (let i = 0; i <= segments; i += 1) {
      const t = i / segments;
      const x = lerp(columnX, innerX, t);
      const arch = Math.sin(t * Math.PI);
      const y = lerp(capitalY, apexHeight, arch);
      const nextT = (i + 1) / segments;
      const nx = lerp(columnX, innerX, nextT);
      const nArch = Math.sin(nextT * Math.PI);
      const ny = lerp(capitalY, apexHeight, nArch);
      if (i < segments) {
        addLine(x, y, z, nx, ny, z);
      }
      if (i % 2 === 0) {
        glowPositions.push(x, y, z);
      }
    }
  }

  const bayCount = 5;
  const baySpacing = 8;
  const startZ = -((bayCount - 1) * baySpacing) / 2;

  for (let bay = 0; bay < bayCount; bay += 1) {
    const z = startZ + bay * baySpacing;
    addArch(-5.5, z, 2.2, 11.5, 18);
    addArch(5.5, z, 2.2, 11.5, 18);

    addLine(-5.5, 0, z, 5.5, 0, z);
    addLine(-5.5, 11.5, z, 5.5, 11.5, z);

    const ribPeakY = 13.5;
    for (let i = 0; i <= 12; i += 1) {
      const t = i / 12;
      const x = lerp(-5.5, 0, t);
      const y = lerp(11.5, ribPeakY, Math.sin(t * Math.PI * 0.5));
      const nx = lerp(-5.5, 0, (i + 1) / 12);
      const ny = lerp(11.5, ribPeakY, Math.sin(((i + 1) / 12) * Math.PI * 0.5));
      if (i < 12) {
        addLine(x, y, z, nx, ny, z);
      }
    }
    for (let i = 0; i <= 12; i += 1) {
      const t = i / 12;
      const x = lerp(5.5, 0, t);
      const y = lerp(11.5, ribPeakY, Math.sin(t * Math.PI * 0.5));
      const nx = lerp(5.5, 0, (i + 1) / 12);
      const ny = lerp(11.5, ribPeakY, Math.sin(((i + 1) / 12) * Math.PI * 0.5));
      if (i < 12) {
        addLine(x, y, z, nx, ny, z);
      }
    }

    addLine(0, ribPeakY, z, 0, 14.5, z - baySpacing * 0.35);
    addLine(0, ribPeakY, z, 0, 14.5, z + baySpacing * 0.35);
  }

  for (let bay = 0; bay < bayCount - 1; bay += 1) {
    const z = startZ + bay * baySpacing;
    const nextZ = z + baySpacing;
    addLine(-5.5, 11.5, z, -5.5, 11.5, nextZ);
    addLine(5.5, 11.5, z, 5.5, 11.5, nextZ);
    addLine(0, 14.5, z, 0, 14.5, nextZ);
  }

  addLine(-5.5, 0, startZ - 4, -5.5, 0, startZ + (bayCount - 1) * baySpacing + 4);
  addLine(5.5, 0, startZ - 4, 5.5, 0, startZ + (bayCount - 1) * baySpacing + 4);
  addLine(-5.5, 0, startZ - 4, 5.5, 0, startZ - 4);
  addLine(
    -5.5,
    0,
    startZ + (bayCount - 1) * baySpacing + 4,
    5.5,
    0,
    startZ + (bayCount - 1) * baySpacing + 4,
  );

  return {
    linePositions: new Float32Array(linePositions),
    glowPositions: new Float32Array(glowPositions),
  };
}

function buildPCWireSegments() {
  const segments = [];

  function addLine(ax, ay, az, bx, by, bz) {
    segments.push([ax, ay, az, bx, by, bz]);
  }

  function box(cx, cy, cz, w, h, d) {
    const x0 = cx - w / 2;
    const x1 = cx + w / 2;
    const y0 = cy - h / 2;
    const y1 = cy + h / 2;
    const z0 = cz - d / 2;
    const z1 = cz + d / 2;

    addLine(x0, y0, z0, x1, y0, z0);
    addLine(x1, y0, z0, x1, y1, z0);
    addLine(x1, y1, z0, x0, y1, z0);
    addLine(x0, y1, z0, x0, y0, z0);
    addLine(x0, y0, z1, x1, y0, z1);
    addLine(x1, y0, z1, x1, y1, z1);
    addLine(x1, y1, z1, x0, y1, z1);
    addLine(x0, y1, z1, x0, y0, z1);
    addLine(x0, y0, z0, x0, y0, z1);
    addLine(x1, y0, z0, x1, y0, z1);
    addLine(x1, y1, z0, x1, y1, z1);
    addLine(x0, y1, z0, x0, y1, z1);
  }

  box(0, 2.15, -0.15, 3.4, 2.0, 0.14);
  box(0, 2.15, -0.15, 3.1, 1.75, 0.08);
  addLine(0, 1.1, -0.15, 0, 0.72, 0.15);
  addLine(-0.45, 0.72, 0.15, 0.45, 0.72, 0.15);
  addLine(-0.45, 0.72, 0.15, -0.45, 0.72, 0.35);
  addLine(0.45, 0.72, 0.15, 0.45, 0.72, 0.35);
  addLine(-0.45, 0.72, 0.35, 0.45, 0.72, 0.35);

  box(-1.75, 0.95, 0.35, 0.62, 1.75, 0.72);
  box(-1.75, 0.95, 0.71, 0.58, 1.65, 0.04);
  for (let i = 0; i < 4; i += 1) {
    const y = 0.35 + i * 0.38;
    addLine(-1.75, y, 0.74, -1.48, y, 0.74);
  }
  addLine(-2.06, 0.95, 0.35, -2.06, 0.95, 0.71);
  addLine(-1.44, 0.95, 0.35, -1.44, 0.95, 0.71);

  box(0.35, 0.38, 0.95, 1.55, 0.1, 0.52);
  for (let i = 0; i < 5; i += 1) {
    const x = -0.28 + i * 0.28;
    addLine(x, 0.38, 0.72, x, 0.38, 1.18);
  }

  box(1.45, 0.4, 0.92, 0.28, 0.12, 0.38);
  addLine(1.45, 0.46, 0.92, 1.45, 0.52, 0.92);

  addLine(-2.6, 0.55, 1.05, 2.6, 0.55, 1.05);
  addLine(-2.6, 0.55, 1.05, -2.6, 0.55, -0.5);
  addLine(2.6, 0.55, 1.05, 2.6, 0.55, -0.5);
  addLine(-2.6, 0.55, -0.5, 2.6, 0.55, -0.5);

  box(0.1, 1.55, 0.55, 0.85, 0.55, 0.45);
  addLine(-0.5, 0.12, 0.35, -0.5, 0.12, 1.05);
  addLine(0.7, 0.12, 0.35, 0.7, 0.12, 1.05);

  return segments;
}

function segmentsToLineArray(segments) {
  const arr = new Float32Array(segments.length * 6);
  segments.forEach((seg, index) => {
    const offset = index * 6;
    arr[offset] = seg[0];
    arr[offset + 1] = seg[1];
    arr[offset + 2] = seg[2];
    arr[offset + 3] = seg[3];
    arr[offset + 4] = seg[4];
    arr[offset + 5] = seg[5];
  });
  return arr;
}

function resampleLineArray(sourceSegments, targetSegmentCount) {
  const arr = new Float32Array(targetSegmentCount * 6);
  for (let i = 0; i < targetSegmentCount; i += 1) {
    const seg = sourceSegments[i % sourceSegments.length];
    const offset = i * 6;
    arr[offset] = seg[0];
    arr[offset + 1] = seg[1];
    arr[offset + 2] = seg[2];
    arr[offset + 3] = seg[3];
    arr[offset + 4] = seg[4];
    arr[offset + 5] = seg[5];
  }
  return arr;
}

function buildScatterLineArray(sourceArray, spread) {
  const out = new Float32Array(sourceArray.length);
  const segmentCount = sourceArray.length / 6;

  for (let s = 0; s < segmentCount; s += 1) {
    const hash = (n) => {
      const v = Math.sin(n * 12.9898 + s * 78.233) * 43758.5453;
      return v - Math.floor(v);
    };
    const cx = (hash(s * 1.7) - 0.5) * spread * 2;
    const cy = (hash(s * 2.9) - 0.5) * spread * 1.4 + 2;
    const cz = (hash(s * 4.1) - 0.5) * spread * 2;
    const offset = s * 6;

    for (let v = 0; v < 2; v += 1) {
      const i = offset + v * 3;
      const jx = (hash(i * 5.3) - 0.5) * spread * 0.5;
      const jy = (hash(i * 6.7) - 0.5) * spread * 0.5;
      const jz = (hash(i * 7.9) - 0.5) * spread * 0.5;
      out[i] = cx + jx;
      out[i + 1] = cy + jy;
      out[i + 2] = cz + jz;
    }
  }

  return out;
}

function buildRedStickExplosion(sourceArray, spread) {
  const out = new Float32Array(sourceArray.length);
  const segmentCount = sourceArray.length / 6;
  const cx = 0;
  const cy = 1.15;
  const cz = 0.45;

  for (let s = 0; s < segmentCount; s += 1) {
    const hash = (n) => {
      const v = Math.sin(n * 12.9898 + s * 78.233) * 43758.5453;
      return v - Math.floor(v);
    };
    const offset = s * 6;
    const midX = (sourceArray[offset] + sourceArray[offset + 3]) * 0.5;
    const midY = (sourceArray[offset + 1] + sourceArray[offset + 4]) * 0.5;
    const midZ = (sourceArray[offset + 2] + sourceArray[offset + 5]) * 0.5;
    const dirX = midX - cx + (hash(s * 1.9) - 0.5) * 1.2;
    const dirY = midY - cy + (hash(s * 2.7) - 0.5) * 1.2;
    const dirZ = midZ - cz + (hash(s * 3.5) - 0.5) * 1.2;
    const len = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ) || 1;
    const push = spread * (0.65 + hash(s * 4.3) * 0.85);
    const nx = dirX / len;
    const ny = dirY / len;
    const nz = dirZ / len;
    const stickLen = 0.35 + hash(s * 5.1) * 0.55;

    for (let v = 0; v < 2; v += 1) {
      const i = offset + v * 3;
      const along = v === 0 ? -stickLen * 0.5 : stickLen * 0.5;
      out[i] = midX + nx * push + nx * along;
      out[i + 1] = midY + ny * push + ny * along;
      out[i + 2] = midZ + nz * push + nz * along;
    }
  }

  return out;
}

function buildRabbitHeadWireSegments() {
  const segments = [];

  function addLine(ax, ay, az, bx, by, bz) {
    segments.push([ax, ay, az, bx, by, bz]);
  }

  function ring(cx, cy, cz, rx, ry, count) {
    for (let i = 0; i < count; i += 1) {
      const a0 = (i / count) * Math.PI * 2;
      const a1 = ((i + 1) / count) * Math.PI * 2;
      addLine(
        cx + Math.cos(a0) * rx,
        cy + Math.sin(a0) * ry,
        cz,
        cx + Math.cos(a1) * rx,
        cy + Math.sin(a1) * ry,
        cz,
      );
    }
  }

  const cy = 1.15;

  ring(0, cy, 0, 0.38, 0.4, 22);
  addLine(-0.1, cy + 0.28, 0, -0.15, cy + 0.95, 0);
  addLine(-0.15, cy + 0.95, 0, -0.06, cy + 0.3, 0);
  addLine(0.1, cy + 0.28, 0, 0.15, cy + 0.95, 0);
  addLine(0.15, cy + 0.95, 0, 0.06, cy + 0.3, 0);
  addLine(-0.12, cy + 0.48, 0.02, -0.14, cy + 0.78, 0.02);
  addLine(0.12, cy + 0.48, 0.02, 0.14, cy + 0.78, 0.02);
  ring(-0.13, cy + 0.06, 0.06, 0.055, 0.075, 10);
  ring(0.13, cy + 0.06, 0.06, 0.055, 0.075, 10);
  addLine(0, cy - 0.06, 0.1, -0.035, cy - 0.12, 0.1);
  addLine(0, cy - 0.06, 0.1, 0.035, cy - 0.12, 0.1);
  addLine(-0.05, cy - 0.14, 0.09, 0.05, cy - 0.14, 0.09);
  addLine(0.28, cy + 0.02, 0.08, 0.62, cy - 0.01, 0.08);
  addLine(0.28, cy - 0.06, 0.08, 0.62, cy - 0.09, 0.08);
  addLine(-0.28, cy + 0.02, 0.08, -0.62, cy - 0.01, 0.08);
  addLine(-0.28, cy - 0.06, 0.08, -0.62, cy - 0.09, 0.08);

  return segments;
}

function lerpLineArrays(from, to, amount, out) {
  for (let i = 0; i < from.length; i += 1) {
    out[i] = lerp(from[i], to[i], amount);
  }
}

function createPCBuildScene() {
  const threeScene = new THREE.Scene();
  threeScene.background = new THREE.Color(0x020208);
  threeScene.fog = new THREE.FogExp2(0x020208, 0.022);

  const camera = new THREE.PerspectiveCamera(58, WIDTH / HEIGHT, 0.1, 200);
  const cameraTarget = new THREE.Vector3(0, 1.2, 0.4);

  const disposables = [];
  let wireLines = null;
  let cathedralPositions = null;
  let scatterPositions = null;
  let pcPositions = null;
  let rabbitHeadPositions = null;
  let redStickPositions = null;
  let workPositions = null;
  let segmentCount = 0;
  let spinAngle = 0;

  return {
    name: 'Wire PC Build',
    startTime: SCENE_PC_START,
    endTime: SCENE_PC_END,
    threeScene,
    camera,
    initialized: false,

    init() {
      console.log('[Scene Manager] Wire PC Build init');

      const ambient = new THREE.AmbientLight(0x101028, 0.35);
      threeScene.add(ambient);
      disposables.push(ambient);

      const keyLight = new THREE.PointLight(0x55ccff, 2.2, 60, 2);
      keyLight.position.set(4, 6, 8);
      threeScene.add(keyLight);
      disposables.push(keyLight);

      const fillLight = new THREE.PointLight(0xaa44ff, 1.4, 50, 2);
      fillLight.position.set(-6, 3, -4);
      threeScene.add(fillLight);
      disposables.push(fillLight);

      const cathedralData = buildCathedralGeometry();
      cathedralPositions = cathedralData.linePositions.slice();
      segmentCount = cathedralPositions.length / 6;

      const pcSegments = buildPCWireSegments();
      pcPositions = resampleLineArray(pcSegments, segmentCount);
      rabbitHeadPositions = resampleLineArray(buildRabbitHeadWireSegments(), segmentCount);
      scatterPositions = buildScatterLineArray(cathedralPositions, 14);
      redStickPositions = buildRedStickExplosion(rabbitHeadPositions, 16);
      workPositions = new Float32Array(cathedralPositions.length);

      const lineGeometry = new THREE.BufferGeometry();
      lineGeometry.setAttribute('position', new THREE.BufferAttribute(cathedralPositions.slice(), 3));
      const lineMaterial = new THREE.LineBasicMaterial({
        color: 0x55ccff,
        transparent: true,
        opacity: 0.6,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
      });
      wireLines = new THREE.LineSegments(lineGeometry, lineMaterial);
      threeScene.add(wireLines);
      disposables.push(lineGeometry, lineMaterial, wireLines);

      camera.position.set(0, 2.5, 16);
      camera.lookAt(cameraTarget);

      this.initialized = true;
    },

    update(sceneTime, totalTime) {
      const t = sceneTime * SCENE_PC_PACE;
      const fadeIn = smoothstep(0, FADE_IN_DURATION, sceneTime);
      const fadeOut = 1 - smoothstep(SCENE_PC_DURATION - FADE_OUT_DURATION, SCENE_PC_DURATION, sceneTime);
      const sceneFade = fadeIn * fadeOut;

      const scatterEnd = PC_SCATTER_END;
      const buildEnd = PC_BUILD_END;
      const rotateEnd = PC_ROTATE_END;
      const rabbitEnd = PC_RABBIT_END;
      const breakStart = PC_RABBIT_END;
      const positions = wireLines.geometry.attributes.position.array;
      const cyanColor = new THREE.Color(0x55ccff);
      const redColor = new THREE.Color(0xff3355);

      if (sceneTime < scatterEnd) {
        const scatterT = easeInOutCubic(sceneTime / scatterEnd);
        lerpLineArrays(cathedralPositions, scatterPositions, scatterT, workPositions);
        for (let s = 0; s < segmentCount; s += 1) {
          const wobble = Math.sin(t * 1.8 + s * 0.35) * 0.35 * scatterT;
          const i = s * 6;
          workPositions[i + 1] += wobble;
          workPositions[i + 4] += wobble * 0.8;
        }
        wireLines.material.color.copy(cyanColor);
        spinAngle = Math.sin(t * 0.08) * 0.06;
      } else if (sceneTime < buildEnd) {
        const buildT = easeInOutCubic((sceneTime - scatterEnd) / (buildEnd - scatterEnd));
        for (let s = 0; s < segmentCount; s += 1) {
          const delay = (s / segmentCount) * 0.55;
          const localT = smoothstep(delay, delay + 0.45, buildT);
          const i = s * 6;
          for (let v = 0; v < 6; v += 1) {
            workPositions[i + v] = lerp(scatterPositions[i + v], pcPositions[i + v], localT);
          }
          if (localT > 0 && localT < 1) {
            workPositions[i + 1] += Math.sin(t * 3.5 + s) * 0.12 * (1 - localT);
            workPositions[i + 4] += Math.sin(t * 3.5 + s + 1) * 0.12 * (1 - localT);
          }
        }
        wireLines.material.color.copy(cyanColor);
        spinAngle = Math.sin(t * 0.08) * 0.06;
      } else if (sceneTime < rotateEnd) {
        workPositions.set(pcPositions);
        spinAngle += 0.032;
        wireLines.material.color.copy(cyanColor);
      } else if (sceneTime < rabbitEnd) {
        const morphT = easeInOutCubic((sceneTime - rotateEnd) / (rabbitEnd - rotateEnd));
        lerpLineArrays(pcPositions, rabbitHeadPositions, morphT, workPositions);
        for (let s = 0; s < segmentCount; s += 1) {
          if (morphT > 0.05 && morphT < 0.95) {
            const wobble = Math.sin(t * 4.0 + s * 0.4) * 0.08 * (1 - Math.abs(morphT - 0.5) * 2);
            const i = s * 6;
            workPositions[i + 1] += wobble;
            workPositions[i + 4] += wobble * 0.7;
          }
        }
        wireLines.material.color.copy(cyanColor);
        spinAngle += 0.015 * (1 - morphT);
      } else {
        const breakDuration = SCENE_PC_DURATION - FADE_OUT_DURATION - breakStart;
        const breakT = easeInOutCubic((sceneTime - breakStart) / breakDuration);
        lerpLineArrays(rabbitHeadPositions, redStickPositions, breakT, workPositions);

        for (let s = 0; s < segmentCount; s += 1) {
          const hash = Math.sin(s * 17.17 + t * 2.4) * 0.5 + 0.5;
          const flutter = breakT * hash * 0.35;
          const i = s * 6;
          workPositions[i + 1] += flutter;
          workPositions[i + 4] += flutter * 0.7;
        }

        wireLines.material.color.copy(cyanColor).lerp(redColor, breakT);
        spinAngle += 0.018 * (1 - breakT * 0.5);
      }

      positions.set(workPositions);
      wireLines.geometry.attributes.position.needsUpdate = true;
      wireLines.material.opacity = 0.62 * sceneFade;
      wireLines.rotation.y = spinAngle;

      const camOrbit = t * 0.14;
      const camPhase = Math.min(sceneTime / SCENE_PC_DURATION, 1);
      camera.position.set(
        Math.sin(camOrbit) * lerp(14, 9, camPhase),
        lerp(2.5, 1.8, camPhase) + Math.sin(t * 0.2) * 0.4,
        Math.cos(camOrbit) * lerp(14, 9, camPhase),
      );
      cameraTarget.set(0, lerp(1.4, 1.1, camPhase), 0.45);
      camera.lookAt(cameraTarget);
    },

    destroy() {
      disposables.forEach((item) => {
        threeScene.remove(item);
        if (item.geometry) item.geometry.dispose();
        if (item.material) disposeMaterial(item.material);
      });

      disposables.length = 0;
      wireLines = null;
      cathedralPositions = null;
      scatterPositions = null;
      pcPositions = null;
      rabbitHeadPositions = null;
      redStickPositions = null;
      workPositions = null;
      spinAngle = 0;
      threeScene.fog = null;
      this.initialized = false;
    },

    cleanup() {
      this.destroy();
    },
  };
}

function createSnowParticles(count) {
  const positions = new Float32Array(count * 3);
  const seeds = new Float32Array(count * 5);

  for (let i = 0; i < count; i += 1) {
    const hash = (n) => {
      const value = Math.sin(n * 12.9898 + i * 78.233) * 43758.5453;
      return value - Math.floor(value);
    };

    const baseX = (hash(i * 1.17) - 0.5) * 18;
    const baseZ = (hash(i * 2.31) - 0.5) * 36;
    const phase = hash(i * 3.71);
    const fallSpeed = 1.2 + hash(i * 5.91) * 2.2;
    const driftPhase = hash(i * 7.43) * Math.PI * 2;

    positions[i * 3] = baseX;
    positions[i * 3 + 1] = 15 + phase * 8;
    positions[i * 3 + 2] = baseZ;

    seeds[i * 5] = baseX;
    seeds[i * 5 + 1] = baseZ;
    seeds[i * 5 + 2] = phase;
    seeds[i * 5 + 3] = fallSpeed;
    seeds[i * 5 + 4] = driftPhase;
  }

  return { positions, seeds };
}

function updateFractalDeform(fractalMesh, fractalBasePositions, t, bassPulse, pulseScale, twistSpeed) {
  const positions = fractalMesh.geometry.attributes.position.array;
  const foldLimit = 0.85 + bassPulse * 0.15;
  const foldScale = 2.1 + Math.sin(t * 0.4) * 0.15;

  for (let i = 0; i < positions.length; i += 3) {
    let x = fractalBasePositions[i];
    let y = fractalBasePositions[i + 1];
    let z = fractalBasePositions[i + 2];

    x = clampFold(x, foldLimit);
    y = clampFold(y, foldLimit);
    z = clampFold(z, foldLimit);

    const radiusSq = x * x + y * y + z * z;
    const sphereFold = 1.15 / Math.max(radiusSq, 0.25);
    x *= sphereFold;
    y *= sphereFold;
    z *= sphereFold;

    x *= foldScale;
    y *= foldScale;
    z *= foldScale;

    const twist = t * twistSpeed;
    const tx = x * Math.cos(twist) - z * Math.sin(twist);
    const tz = x * Math.sin(twist) + z * Math.cos(twist);
    x = tx;
    z = tz;

    const wobble =
      1 +
      0.12 * Math.sin(y * 3.1 + t * 2.2) * Math.cos(x * 2.7 - t * 1.6);
    positions[i] = x * pulseScale * wobble;
    positions[i + 1] = y * pulseScale * wobble;
    positions[i + 2] = z * pulseScale * wobble;
  }

  fractalMesh.geometry.attributes.position.needsUpdate = true;
  fractalMesh.geometry.computeVertexNormals();
}

function computeDeformedPositions(basePositions, out, t, bassPulse, pulseScale, twistSpeed) {
  const foldLimit = 0.85 + bassPulse * 0.15;
  const foldScale = 2.1 + Math.sin(t * 0.4) * 0.15;

  for (let i = 0; i < basePositions.length; i += 3) {
    let x = basePositions[i];
    let y = basePositions[i + 1];
    let z = basePositions[i + 2];

    x = clampFold(x, foldLimit);
    y = clampFold(y, foldLimit);
    z = clampFold(z, foldLimit);

    const radiusSq = x * x + y * y + z * z;
    const sphereFold = 1.15 / Math.max(radiusSq, 0.25);
    x *= sphereFold;
    y *= sphereFold;
    z *= sphereFold;

    x *= foldScale;
    y *= foldScale;
    z *= foldScale;

    const twist = t * twistSpeed;
    const tx = x * Math.cos(twist) - z * Math.sin(twist);
    const tz = x * Math.sin(twist) + z * Math.cos(twist);
    x = tx;
    z = tz;

    const wobble =
      1 +
      0.12 * Math.sin(y * 3.1 + t * 2.2) * Math.cos(x * 2.7 - t * 1.6);
    out[i] = x * pulseScale * wobble;
    out[i + 1] = y * pulseScale * wobble;
    out[i + 2] = z * pulseScale * wobble;
  }
}

function lerpPositionArrays(from, to, amount, out) {
  for (let i = 0; i < from.length; i += 1) {
    out[i] = lerp(from[i], to[i], amount);
  }
}

function buildHeartPositions(basePositions) {
  const positions = new Float32Array(basePositions.length);
  const count = basePositions.length / 3;

  for (let i = 0; i < count; i += 1) {
    const u = (i / count) * Math.PI * 2;
    const layer = ((i * 7 + 3) % count) / count;
    const x2d = 16 * Math.pow(Math.sin(u), 3);
    const y2d =
      13 * Math.cos(u) -
      5 * Math.cos(2 * u) -
      2 * Math.cos(3 * u) -
      Math.cos(4 * u);
    const scale = 0.075 * 1.5 * (1.1 + layer * 0.25);
    const depth = (layer - 0.5) * 1.6;
    const bx = basePositions[i * 3];
    const by = basePositions[i * 3 + 1];
    const bz = basePositions[i * 3 + 2];

    positions[i * 3] = x2d * scale + bx * 0.12;
    positions[i * 3 + 1] = y2d * scale + 0.3 + by * 0.12;
    positions[i * 3 + 2] = depth + bz * 0.08;
  }

  return positions;
}

function buildCatHeadPositions(basePositions) {
  const wire = [];

  function ring(cx, cy, cz, rx, ry, segments) {
    for (let i = 0; i < segments; i += 1) {
      const a = (i / segments) * Math.PI * 2;
      wire.push([cx + Math.cos(a) * rx, cy + Math.sin(a) * ry, cz]);
    }
  }

  function addLine(a, b, steps) {
    for (let i = 0; i <= steps; i += 1) {
      const t = i / steps;
      wire.push([
        lerp(a[0], b[0], t),
        lerp(a[1], b[1], t),
        lerp(a[2], b[2], t),
      ]);
    }
  }

  const cy = 0.35;

  ring(0, cy, 0, 0.44, 0.4, 32);
  addLine([-0.24, cy + 0.3, 0], [-0.08, cy + 0.68, 0], 5);
  addLine([-0.08, cy + 0.68, 0], [0.04, cy + 0.3, 0], 4);
  addLine([0.24, cy + 0.3, 0], [0.08, cy + 0.68, 0], 5);
  addLine([0.08, cy + 0.68, 0], [-0.04, cy + 0.3, 0], 4);
  addLine([-0.15, cy + 0.42, 0.03], [-0.08, cy + 0.56, 0.03], 3);
  addLine([0.15, cy + 0.42, 0.03], [0.08, cy + 0.56, 0.03], 3);
  ring(0, cy - 0.14, 0.08, 0.15, 0.11, 16);
  ring(-0.17, cy + 0.1, 0.07, 0.075, 0.095, 12);
  ring(0.17, cy + 0.1, 0.07, 0.075, 0.095, 12);
  addLine([0, cy - 0.02, 0.11], [-0.045, cy - 0.1, 0.11], 2);
  addLine([0, cy - 0.02, 0.11], [0.045, cy - 0.1, 0.11], 2);
  addLine([-0.045, cy - 0.1, 0.11], [0.045, cy - 0.1, 0.11], 2);
  addLine([0.34, cy + 0.06, 0.09], [0.78, cy + 0.02, 0.09], 4);
  addLine([0.34, cy - 0.04, 0.09], [0.78, cy - 0.08, 0.09], 4);
  addLine([-0.34, cy + 0.06, 0.09], [-0.78, cy + 0.02, 0.09], 4);
  addLine([-0.34, cy - 0.04, 0.09], [-0.78, cy - 0.08, 0.09], 4);
  for (let i = 0; i <= 14; i += 1) {
    const a = Math.PI * 0.2 + (i / 14) * Math.PI * 0.6;
    wire.push([Math.cos(a) * 0.36, cy - 0.34 + Math.sin(a) * 0.07, 0]);
  }

  const positions = new Float32Array(basePositions.length);
  const count = basePositions.length / 3;

  for (let i = 0; i < count; i += 1) {
    const p = wire[i % wire.length];
    const bx = basePositions[i * 3];
    const by = basePositions[i * 3 + 1];
    const bz = basePositions[i * 3 + 2];

    positions[i * 3] = p[0] * CAT_HEAD_SCALE + bx * 0.06;
    positions[i * 3 + 1] = p[1] * CAT_HEAD_SCALE + by * 0.06;
    positions[i * 3 + 2] = p[2] * CAT_HEAD_SCALE + bz * 0.06;
  }

  return positions;
}

function createFractalFocusScene(options = {}) {
  const startTime = options.startTime ?? INTRO_START;
  const endTime = options.endTime ?? INTRO_END;
  const duration = options.duration ?? INTRO_DURATION;
  const pace = options.pace ?? INTRO_PACE;
  const name = options.name ?? 'Fractal Focus';
  const variant = options.variant ?? 'purple';
  const isRed = variant === 'red';

  const threeScene = new THREE.Scene();
  threeScene.background = new THREE.Color(isRed ? 0x0a0406 : 0x010104);

  const camera = new THREE.PerspectiveCamera(55, WIDTH / HEIGHT, 0.1, 200);
  const cameraTarget = new THREE.Vector3(0, 0, 0);

  const disposables = [];
  let fractalMesh = null;
  let fractalBasePositions = null;
  let heartPositions = null;
  let catHeadPositions = null;
  let morphBuffer = null;
  let coreLight = null;
  let rimLight = null;

  return {
    name,
    startTime,
    endTime,
    threeScene,
    camera,
    initialized: false,

    init() {
      console.log(`[Scene Manager] ${name} init (${variant})`);

      const ambient = new THREE.AmbientLight(isRed ? 0x180810 : 0x0a0618, isRed ? 0.18 : 0.15);
      threeScene.add(ambient);
      disposables.push(ambient);

      coreLight = new THREE.PointLight(isRed ? 0xffaa55 : 0x8844ff, 3.2, 50, 2);
      coreLight.position.set(0, isRed ? 3 : 2, 4);
      threeScene.add(coreLight);
      disposables.push(coreLight);

      rimLight = new THREE.PointLight(isRed ? 0xff4488 : 0x44aaff, 1.4, 60, 2);
      rimLight.position.set(isRed ? 5 : -5, -2, -6);
      threeScene.add(rimLight);
      disposables.push(rimLight);

      const fractalGeometry = new THREE.IcosahedronGeometry(isRed ? 1.8 : 1.4, 3);
      fractalBasePositions = fractalGeometry.attributes.position.array.slice();
      const fractalMaterial = new THREE.MeshStandardMaterial({
        color: isRed ? 0xffcc66 : 0x6622cc,
        emissive: isRed ? 0x661133 : 0x4400aa,
        emissiveIntensity: isRed ? 1.4 : 1.6,
        wireframe: true,
        metalness: 0.9,
        roughness: 0.12,
        transparent: true,
        opacity: 0.95,
      });
      fractalMesh = new THREE.Mesh(fractalGeometry, fractalMaterial);
      threeScene.add(fractalMesh);
      disposables.push(fractalGeometry, fractalMaterial, fractalMesh);

      if (isRed) {
        heartPositions = buildHeartPositions(fractalBasePositions);
        catHeadPositions = buildCatHeadPositions(fractalBasePositions);
        morphBuffer = new Float32Array(fractalBasePositions.length);
      }

      camera.position.set(0, isRed ? 0.6 : 0.4, isRed ? 2.4 : 2.8);
      camera.lookAt(cameraTarget);

      this.initialized = true;
    },

    update(sceneTime, totalTime) {
      const t = sceneTime * pace;
      const fadeIn = smoothstep(0, FADE_IN_DURATION * 0.6, sceneTime);
      const fadeOut = 1 - smoothstep(duration - FADE_OUT_DURATION, duration, sceneTime);
      const sceneFade = fadeIn * fadeOut;

      const simulatedPulse =
        0.5 +
        0.5 *
          Math.sin(t * 2.0) *
          Math.sin(t * 0.55) *
          Math.sin(t * 1.1 + 0.4);
      const bassPulse = audioState.bassPulse > 0 ? audioState.bassPulse : simulatedPulse;
      const pulseScale = 1 + bassPulse * (isRed ? 0.22 : 0.18);
      const twistSpeed = isRed ? 0.28 : 0.42;
      const startScale = isRed ? 3.4 : 3.2;
      const endScale = isRed ? 1.2 : 1.15;
      const startCamZ = isRed ? 2.4 : 2.8;
      const endCamZ = isRed ? 24 : 22;

      if (fractalMesh && fractalBasePositions) {
        if (isRed && sceneTime >= OUTRO_ZOOM_DURATION) {
          const heartEnd = OUTRO_ZOOM_DURATION + OUTRO_HEART_DURATION;
          const catMorphEnd = duration - OUTRO_CAT_HOLD_DURATION - FADE_OUT_DURATION;
          const zoomTEnd = OUTRO_ZOOM_DURATION * pace;

          if (sceneTime < heartEnd) {
            const morphT = easeInOutCubic(
              (sceneTime - OUTRO_ZOOM_DURATION) / OUTRO_HEART_DURATION,
            );
            const positions = fractalMesh.geometry.attributes.position.array;
            computeDeformedPositions(
              fractalBasePositions,
              morphBuffer,
              zoomTEnd,
              bassPulse,
              pulseScale,
              twistSpeed,
            );
            lerpPositionArrays(morphBuffer, heartPositions, morphT, positions);
            fractalMesh.geometry.attributes.position.needsUpdate = true;
            fractalMesh.geometry.computeVertexNormals();

            const beat = 1 + bassPulse * 0.06 * Math.sin(t * 3.0);
            fractalMesh.scale.setScalar(lerp(endScale, 1.05 * 1.5, morphT) * beat);
            fractalMesh.rotation.set(
              lerp(zoomTEnd * 0.15, 0, morphT),
              lerp(zoomTEnd * 0.22, 0, morphT),
              0,
            );
          } else if (sceneTime < catMorphEnd) {
            const morphT = easeInOutCubic((sceneTime - heartEnd) / (catMorphEnd - heartEnd));
            const positions = fractalMesh.geometry.attributes.position.array;
            lerpPositionArrays(heartPositions, catHeadPositions, morphT, positions);
            fractalMesh.geometry.attributes.position.needsUpdate = true;
            fractalMesh.geometry.computeVertexNormals();

            const idle = 1 + Math.sin(t * 1.6) * 0.025 + bassPulse * 0.04;
            fractalMesh.scale.setScalar(1.05 * 1.3 * idle);
            fractalMesh.rotation.set(
              Math.sin(t * 0.35) * 0.06 + 0.12,
              t * 0.55,
              Math.sin(t * 0.18) * 0.05,
            );
          } else {
            const positions = fractalMesh.geometry.attributes.position.array;
            positions.set(catHeadPositions);
            fractalMesh.geometry.attributes.position.needsUpdate = true;
            fractalMesh.geometry.computeVertexNormals();

            const idle = 1 + Math.sin(t * 1.6) * 0.025 + bassPulse * 0.04;
            fractalMesh.scale.setScalar(1.05 * 1.3 * idle);
            fractalMesh.rotation.set(
              Math.sin(t * 0.35) * 0.06 + 0.12,
              t * 0.55,
              Math.sin(t * 0.18) * 0.05,
            );
          }

          fractalMesh.material.opacity = 0.95 * sceneFade;
          fractalMesh.material.emissiveIntensity = ((isRed ? 1.2 : 1.4) + bassPulse * 0.5) * sceneFade;
        } else {
          const zoomDuration = isRed ? OUTRO_ZOOM_DURATION : duration;
          const zoomProgress = easeInOutCubic(Math.min(sceneTime / zoomDuration, 1));

          updateFractalDeform(fractalMesh, fractalBasePositions, t, bassPulse, pulseScale, twistSpeed);
          fractalMesh.rotation.y = t * (isRed ? 0.22 : 0.28);
          fractalMesh.rotation.x = t * (isRed ? 0.15 : 0.12);

          const meshScale = lerp(startScale, endScale, zoomProgress);
          fractalMesh.scale.setScalar(meshScale);
          fractalMesh.material.opacity = 0.95 * sceneFade;
          fractalMesh.material.emissiveIntensity = ((isRed ? 1.2 : 1.4) + bassPulse * 0.5) * sceneFade;
        }
      }

      let camZ;
      let camY;
      let camX;

      if (isRed && sceneTime >= OUTRO_ZOOM_DURATION) {
        camZ = endCamZ + Math.sin(t * 0.12) * 0.6;
        camY = 2.0 + Math.sin(t * 0.2) * 0.25;
        camX = Math.sin(t * 0.15) * 0.5;
      } else {
        const zoomDuration = isRed ? OUTRO_ZOOM_DURATION : duration;
        const zoomProgress = easeInOutCubic(Math.min(sceneTime / zoomDuration, 1));
        camZ = lerp(startCamZ, endCamZ, zoomProgress);
        camY = lerp(isRed ? 0.6 : 0.4, isRed ? 2.2 : 1.8, zoomProgress) +
          Math.sin(t * 0.25) * 0.15 * (1 - zoomProgress);
        camX = Math.sin(t * 0.18) * 0.35 * zoomProgress;
      }

      camera.position.set(camX, camY, camZ);
      camera.lookAt(cameraTarget);

      if (coreLight) {
        coreLight.intensity = (2.8 + bassPulse * 1.4) * sceneFade;
      }
      if (rimLight) {
        rimLight.intensity = (1.2 + bassPulse * 0.4) * sceneFade;
      }
    },

    destroy() {
      disposables.forEach((item) => {
        threeScene.remove(item);
        if (item.geometry) {
          item.geometry.dispose();
        }
        if (item.material) {
          disposeMaterial(item.material);
        }
      });

      disposables.length = 0;
      fractalMesh = null;
      fractalBasePositions = null;
      heartPositions = null;
      catHeadPositions = null;
      morphBuffer = null;
      coreLight = null;
      rimLight = null;
      this.initialized = false;
    },

    cleanup() {
      this.destroy();
    },
  };
}

function createQuantumCathedralScene(options = {}) {
  const startTime = options.startTime ?? SCENE1_START;
  const endTime = options.endTime ?? SCENE1_END;
  const duration = options.duration ?? SCENE1_DURATION;
  const name = options.name ?? 'Quantum Cathedral';
  const pace = options.pace ?? SCENE1_PACE;
  const variant = options.variant ?? 'ascent';
  const threeScene = new THREE.Scene();
  const isOrbit = variant === 'orbit';

  threeScene.background = new THREE.Color(isOrbit ? 0x0a0208 : 0x02020a);
  threeScene.fog = new THREE.FogExp2(isOrbit ? 0x12040c : 0x02020a, isOrbit ? 0.035 : 0.028);

  const camera = new THREE.PerspectiveCamera(isOrbit ? 68 : 60, WIDTH / HEIGHT, 0.1, 200);
  const cameraTarget = new THREE.Vector3(0, isOrbit ? 7 : 6, 0);

  const disposables = [];
  const contentGroup = new THREE.Group();
  let cathedralLines = null;
  let cathedralGlow = null;
  let fractalMesh = null;
  let fractalBasePositions = null;
  let snowPoints = null;
  let snowPositions = null;
  let snowSeeds = null;
  let lights = [];

  function updateAscent(sceneTime, t, progress, easedProgress, sceneFade, bassPulse, pulseScale) {
    updateFractalDeform(fractalMesh, fractalBasePositions, t, bassPulse, pulseScale, 0.55);
    fractalMesh.rotation.y = t * 0.35;
    fractalMesh.rotation.z = t * 0.18;
    fractalMesh.material.opacity = 0.92 * sceneFade;

    const floorY = -1.5;
    const ceilingY = 15;
    const fallSpan = ceilingY - floorY + 8;

    for (let i = 0; i < snowPositions.length / 3; i += 1) {
      const baseX = snowSeeds[i * 5];
      const baseZ = snowSeeds[i * 5 + 1];
      const phase = snowSeeds[i * 5 + 2];
      const fallSpeed = snowSeeds[i * 5 + 3] * pace;
      const driftPhase = snowSeeds[i * 5 + 4];

      snowPositions[i * 3] = baseX + Math.sin(t * 0.3 + driftPhase) * 0.45;
      snowPositions[i * 3 + 1] = ceilingY - ((t * fallSpeed + phase * fallSpan) % fallSpan);
      snowPositions[i * 3 + 2] = baseZ + Math.cos(t * 0.25 + driftPhase) * 0.45;
    }

    snowPoints.geometry.attributes.position.needsUpdate = true;
    snowPoints.material.opacity = 0.65 * sceneFade;
    snowPoints.material.color.setRGB(
      lerp(170 / 255, 1.0, easedProgress),
      lerp(221 / 255, 0.38, easedProgress),
      lerp(1.0, 0.58, easedProgress),
    );

    cathedralLines.material.opacity = 0.55 * sceneFade;
    cathedralGlow.material.opacity = 0.85 * sceneFade;
    cathedralGlow.rotation.y = Math.sin(t * 0.08) * 0.04;

    const camY = lerp(1.2, 9.5, easedProgress);
    const camZ = lerp(20, 11, easedProgress);
    const lookY = lerp(3.5, 14.5, easedProgress);
    camera.position.set(
      Math.sin(t * 0.12) * 1.5 * (1 - easedProgress * 0.4),
      camY,
      camZ,
    );
    cameraTarget.set(0, lookY, 0);
    camera.lookAt(cameraTarget);
  }

  function updateOrbit(sceneTime, t, progress, easedProgress, sceneFade, bassPulse, pulseScale) {
    updateFractalDeform(fractalMesh, fractalBasePositions, t, bassPulse, pulseScale, 0.28);
    fractalMesh.rotation.x = t * 0.22;
    fractalMesh.rotation.y = t * 0.15;
    fractalMesh.material.opacity = 0.88 * sceneFade;

    contentGroup.rotation.y = t * 0.06;

    for (let i = 0; i < snowPositions.length / 3; i += 1) {
      const baseX = snowSeeds[i * 5];
      const baseZ = snowSeeds[i * 5 + 1];
      const phase = snowSeeds[i * 5 + 2];
      const driftPhase = snowSeeds[i * 5 + 4];
      const orbitRadius = 4 + Math.abs(baseX) * 0.55 + Math.abs(baseZ) * 0.18;
      const orbitAngle = t * 0.45 * pace + driftPhase + phase * Math.PI * 2;

      snowPositions[i * 3] = Math.cos(orbitAngle) * orbitRadius;
      snowPositions[i * 3 + 1] = 2.5 + Math.sin(t * 0.7 + phase * 5.0) * 6.5;
      snowPositions[i * 3 + 2] = Math.sin(orbitAngle) * orbitRadius;
    }

    snowPoints.geometry.attributes.position.needsUpdate = true;
    snowPoints.material.opacity = 0.55 * sceneFade;

    cathedralLines.material.opacity = 0.48 * sceneFade;
    cathedralGlow.material.opacity = 0.9 * sceneFade;
    cathedralGlow.rotation.y = t * 0.2;

    const orbitRadius = lerp(18, 11, easedProgress);
    const orbitAngle = t * 0.32 + easedProgress * Math.PI * 0.5;
    const camY = lerp(2.5, 10, easedProgress) + Math.sin(t * 0.35) * 1.4;
    camera.position.set(
      Math.cos(orbitAngle) * orbitRadius,
      camY,
      Math.sin(orbitAngle) * orbitRadius,
    );
    cameraTarget.set(
      Math.sin(t * 0.18) * 1.2,
      lerp(6, 11, easedProgress),
      Math.cos(t * 0.14) * 1.2,
    );
    camera.lookAt(cameraTarget);
  }

  return {
    name,
    startTime,
    endTime,
    threeScene,
    camera,
    initialized: false,

    init() {
      console.log(`[Scene Manager] ${name} init (${variant})`);

      threeScene.add(contentGroup);
      disposables.push(contentGroup);

      const ambient = new THREE.AmbientLight(isOrbit ? 0x201018 : 0x101030, isOrbit ? 0.42 : 0.35);
      threeScene.add(ambient);
      disposables.push(ambient);

      const fractalLight = new THREE.PointLight(isOrbit ? 0xffaa55 : 0x44ddff, 2.4, 40, 2);
      fractalLight.position.set(0, isOrbit ? 6 : 4, 4);
      threeScene.add(fractalLight);
      lights.push(fractalLight);
      disposables.push(fractalLight);

      const columnLightA = new THREE.PointLight(isOrbit ? 0xff4488 : 0xaa44ff, 1.6, 55, 2);
      columnLightA.position.set(-6, 8, 0);
      threeScene.add(columnLightA);
      lights.push(columnLightA);
      disposables.push(columnLightA);

      const columnLightB = new THREE.PointLight(isOrbit ? 0xff6644 : 0x6633ff, 1.4, 55, 2);
      columnLightB.position.set(6, 8, 0);
      threeScene.add(columnLightB);
      lights.push(columnLightB);
      disposables.push(columnLightB);

      const ceilingLight = new THREE.PointLight(isOrbit ? 0xffcc66 : 0x33ccff, 1.0, 45, 2);
      ceilingLight.position.set(0, isOrbit ? 4 : 15, 0);
      threeScene.add(ceilingLight);
      lights.push(ceilingLight);
      disposables.push(ceilingLight);

      const cathedralData = buildCathedralGeometry();

      const lineGeometry = new THREE.BufferGeometry();
      lineGeometry.setAttribute('position', new THREE.BufferAttribute(cathedralData.linePositions, 3));
      const lineMaterial = new THREE.LineBasicMaterial({
        color: isOrbit ? 0xff5599 : 0x55ccff,
        transparent: true,
        opacity: 0.55,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
      });
      cathedralLines = new THREE.LineSegments(lineGeometry, lineMaterial);
      if (isOrbit) {
        cathedralLines.rotation.x = Math.PI;
        cathedralLines.position.y = 12;
        cathedralLines.scale.setScalar(0.92);
      }
      contentGroup.add(cathedralLines);
      disposables.push(lineGeometry, lineMaterial, cathedralLines);

      const glowGeometry = new THREE.BufferGeometry();
      glowGeometry.setAttribute('position', new THREE.BufferAttribute(cathedralData.glowPositions, 3));
      const glowMaterial = new THREE.PointsMaterial({
        color: isOrbit ? 0xffaa44 : 0xbb66ff,
        size: isOrbit ? 0.28 : 0.22,
        transparent: true,
        opacity: 0.85,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        sizeAttenuation: true,
      });
      cathedralGlow = new THREE.Points(glowGeometry, glowMaterial);
      if (isOrbit) {
        cathedralGlow.rotation.x = Math.PI;
        cathedralGlow.position.y = 12;
        cathedralGlow.scale.setScalar(0.92);
      }
      contentGroup.add(cathedralGlow);
      disposables.push(glowGeometry, glowMaterial, cathedralGlow);

      const fractalGeometry = new THREE.IcosahedronGeometry(isOrbit ? 1.8 : 1.4, 3);
      fractalBasePositions = fractalGeometry.attributes.position.array.slice();
      const fractalMaterial = new THREE.MeshStandardMaterial({
        color: isOrbit ? 0xffcc66 : 0x22eeff,
        emissive: isOrbit ? 0x661133 : 0x4400aa,
        emissiveIntensity: 1.2,
        wireframe: true,
        metalness: 0.85,
        roughness: 0.15,
        transparent: true,
        opacity: 0.92,
      });
      fractalMesh = new THREE.Mesh(fractalGeometry, fractalMaterial);
      fractalMesh.position.set(0, isOrbit ? 5.5 : 2, 0);
      if (isOrbit) {
        fractalMesh.scale.setScalar(1.15);
      }
      contentGroup.add(fractalMesh);
      disposables.push(fractalGeometry, fractalMaterial, fractalMesh);

      const snow = createSnowParticles(3500);
      snowPositions = snow.positions;
      snowSeeds = snow.seeds;
      const snowGeometry = new THREE.BufferGeometry();
      snowGeometry.setAttribute('position', new THREE.BufferAttribute(snowPositions, 3));
      const snowMaterial = new THREE.PointsMaterial({
        color: isOrbit ? 0xffeedd : 0xaaddff,
        size: isOrbit ? 0.1 : 0.08,
        transparent: true,
        opacity: 0.65,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        sizeAttenuation: true,
      });
      snowPoints = new THREE.Points(snowGeometry, snowMaterial);
      contentGroup.add(snowPoints);
      disposables.push(snowGeometry, snowMaterial, snowPoints);

      if (isOrbit) {
        contentGroup.rotation.z = 0.08;
        camera.position.set(16, 3, 0);
        cameraTarget.set(0, 7, 0);
      } else {
        camera.position.set(0, 1.2, 20);
        cameraTarget.set(0, 3.5, 0);
      }
      camera.lookAt(cameraTarget);

      this.initialized = true;
    },

    update(sceneTime, totalTime) {
      const t = sceneTime * pace;
      const progress = Math.min(t / duration, 1);
      const easedProgress = easeInOutCubic(progress);
      const fadeIn = smoothstep(0, FADE_IN_DURATION, sceneTime);
      const fadeOut = 1 - smoothstep(duration - FADE_OUT_DURATION, duration, sceneTime);
      const sceneFade = fadeIn * fadeOut;

      const simulatedPulse =
        0.5 +
        0.5 *
          Math.sin(t * 2.4) *
          Math.sin(t * 0.65) *
          Math.sin(t * 1.35 + 0.5);
      const bassPulse = audioState.bassPulse > 0 ? audioState.bassPulse : simulatedPulse;
      const pulseScale = 1 + bassPulse * (isOrbit ? 0.28 : 0.22);

      if (fractalMesh && fractalBasePositions) {
        if (isOrbit) {
          updateOrbit(sceneTime, t, progress, easedProgress, sceneFade, bassPulse, pulseScale);
        } else {
          updateAscent(sceneTime, t, progress, easedProgress, sceneFade, bassPulse, pulseScale);
        }
      }

      lights[0].intensity = (2.0 + bassPulse * 1.2) * sceneFade;
      lights[1].intensity = (1.4 + bassPulse * 0.5) * sceneFade;
      lights[2].intensity = (1.2 + bassPulse * 0.4) * sceneFade;
      lights[3].intensity = (0.9 + bassPulse * 0.3) * sceneFade;
    },

    destroy() {
      disposables.forEach((item) => {
        threeScene.remove(item);
        if (item.geometry) {
          item.geometry.dispose();
        }
        if (item.material) {
          disposeMaterial(item.material);
        }
      });

      disposables.length = 0;
      cathedralLines = null;
      cathedralGlow = null;
      fractalMesh = null;
      fractalBasePositions = null;
      snowPoints = null;
      snowPositions = null;
      snowSeeds = null;
      lights = [];
      threeScene.fog = null;
      this.initialized = false;
    },

    cleanup() {
      this.destroy();
    },
  };
}

function createThankYouScene(startTime) {
  const threeScene = new THREE.Scene();
  threeScene.background = new THREE.Color(0x050510);
  const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

  const canvas = document.createElement('canvas');
  canvas.width = WIDTH;
  canvas.height = HEIGHT;
  const ctx = canvas.getContext('2d');

  const texture = new THREE.CanvasTexture(canvas);
  texture.minFilter = THREE.LinearFilter;
  texture.magFilter = THREE.LinearFilter;

  const material = new THREE.MeshBasicMaterial({
    map: texture,
    transparent: true,
    depthWrite: false,
  });
  const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
  threeScene.add(mesh);

  const ENTER_DURATION = 0.9;
  const HOLD_DURATION = 2.0;
  const EXIT_DURATION = 0.8;
  const credits = [
    { text: 'Thank You', size: 88, weight: '600', font: 'Cinzel, Georgia, serif', glow: true, hue: 195 },
    { text: '', size: 24 },
    { text: 'Cursor', size: 42, weight: '500', font: '"JetBrains Mono", monospace', hue: 280 },
    {
      lines: [
        'Mira, a really nice AI bot',
        'working for customer engagement',
        'and demo ideas',
      ],
      size: 42,
      weight: '500',
      font: 'Quicksand, Helvetica, sans-serif',
      hue: 330,
    },
    { text: 'Three.js', size: 32, weight: '400', font: 'Orbitron, sans-serif', dim: true, hue: 45 },
    { text: 'Web Audio API', size: 32, weight: '400', font: '"Source Code Pro", monospace', dim: true, hue: 120 },
    { text: 'And You', size: 48, weight: '400', font: 'Pacifico, cursive', glow: true, hue: 180 },
  ];

  const entryDirections = [
    { x: 0, y: -1 },
    { x: 1, y: 0 },
    { x: 0, y: 1 },
    { x: -1, y: 0 },
    { x: 0.85, y: -0.55 },
    { x: -0.75, y: 0.65 },
    { x: 0.6, y: 0.85 },
    { x: -0.9, y: -0.45 },
  ];

  const particles = [];
  for (let i = 0; i < 220; i += 1) {
    const hash = (n) => {
      const v = Math.sin(n * 12.9898 + i * 78.233) * 43758.5453;
      return v - Math.floor(v);
    };
    particles.push({
      x: hash(i * 1.7) * WIDTH,
      y: hash(i * 2.9) * HEIGHT,
      size: 1.5 + hash(i * 4.1) * 4,
      speedX: (hash(i * 5.3) - 0.5) * 40,
      speedY: (hash(i * 6.7) - 0.5) * 30,
      hue: hash(i * 7.9) * 360,
      phase: hash(i * 8.2) * Math.PI * 2,
    });
  }

  const lineHeight = 72;
  const textCredits = credits.filter((line) => line.text || line.lines);
  const blockHeight = textCredits.length * lineHeight;
  let layoutY = HEIGHT / 2 - blockHeight / 2 + lineHeight / 2;
  let textIndex = 0;

  credits.forEach((line, index) => {
    if (!line.text && !line.lines) return;

    const contentLines = line.lines || [line.text];
    line.subLineHeight = Math.round(line.size * 1.12);

    line.targetX = WIDTH / 2;
    line.targetY = layoutY;
    line.entryDir = entryDirections[(index * 5 + textIndex * 3) % entryDirections.length];
    line.exitDir = entryDirections[(index * 7 + textIndex * 5 + 2) % entryDirections.length];
    line.delay = textIndex * 1.4;
    line.travel = 480 + (textIndex % 4) * 90;
    layoutY += lineHeight;
    textIndex += 1;
  });

  const END_FADE_TO_BLACK = 2.0;
  const creditsEndTime =
    textCredits[textCredits.length - 1].delay +
    ENTER_DURATION +
    HOLD_DURATION +
    EXIT_DURATION;
  const thankYouDuration = creditsEndTime + END_FADE_TO_BLACK;

  function drawBackground(sceneTime, fade) {
    const hueShift = sceneTime * 18;

    const bg = ctx.createLinearGradient(0, 0, WIDTH, HEIGHT);
    bg.addColorStop(0, `hsla(${hueShift % 360}, 70%, 12%, ${fade})`);
    bg.addColorStop(0.45, `hsla(${(hueShift + 80) % 360}, 65%, 8%, ${fade})`);
    bg.addColorStop(1, `hsla(${(hueShift + 200) % 360}, 75%, 14%, ${fade})`);
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, WIDTH, HEIGHT);

    const glow = ctx.createRadialGradient(
      WIDTH * 0.5 + Math.sin(sceneTime * 0.4) * 120,
      HEIGHT * 0.45 + Math.cos(sceneTime * 0.35) * 80,
      20,
      WIDTH * 0.5,
      HEIGHT * 0.5,
      WIDTH * 0.55,
    );
    glow.addColorStop(0, `hsla(${(hueShift + 160) % 360}, 90%, 55%, ${0.18 * fade})`);
    glow.addColorStop(1, 'rgba(0, 0, 0, 0)');
    ctx.fillStyle = glow;
    ctx.fillRect(0, 0, WIDTH, HEIGHT);
  }

  function drawParticles(sceneTime, fade) {
    particles.forEach((p, i) => {
      const x = (p.x + p.speedX * sceneTime + Math.sin(sceneTime * 0.8 + p.phase) * 30) % WIDTH;
      const y = (p.y + p.speedY * sceneTime + Math.cos(sceneTime * 0.6 + p.phase) * 24) % HEIGHT;
      const px = x < 0 ? x + WIDTH : x;
      const py = y < 0 ? y + HEIGHT : y;
      const pulse = 0.5 + 0.5 * Math.sin(sceneTime * 2.2 + p.phase);
      const hue = (p.hue + sceneTime * 25 + i * 3) % 360;

      ctx.beginPath();
      ctx.fillStyle = `hsla(${hue}, 85%, 68%, ${(0.25 + pulse * 0.35) * fade})`;
      ctx.arc(px, py, p.size * (0.8 + pulse * 0.4), 0, Math.PI * 2);
      ctx.fill();
    });
  }

  function drawCredits(sceneTime, fade, blackOverlay = 0) {
    ctx.clearRect(0, 0, WIDTH, HEIGHT);
    drawBackground(sceneTime, fade);
    drawParticles(sceneTime, fade);

    credits.forEach((line) => {
      if (!line.text && !line.lines) return;

      const contentLines = line.lines || [line.text];
      const elapsed = sceneTime - line.delay;
      if (elapsed < 0) return;

      const enterEnd = ENTER_DURATION;
      const holdEnd = enterEnd + HOLD_DURATION;
      const exitEnd = holdEnd + EXIT_DURATION;

      if (elapsed >= exitEnd) return;

      let x = line.targetX;
      let y = line.targetY;
      let alpha = fade;

      if (elapsed < enterEnd) {
        const enterT = elapsed / ENTER_DURATION;
        const eased = 1 - Math.pow(1 - enterT, 3);
        const offset = line.travel * (1 - eased);
        x = line.targetX + line.entryDir.x * offset;
        y = line.targetY + line.entryDir.y * offset;
        alpha = fade * eased;
      } else if (elapsed < holdEnd) {
        x = line.targetX;
        y = line.targetY;
        alpha = fade;
      } else {
        const exitT = (elapsed - holdEnd) / EXIT_DURATION;
        const eased = exitT * exitT;
        const offset = line.travel * eased;
        x = line.targetX + line.exitDir.x * offset;
        y = line.targetY + line.exitDir.y * offset;
        alpha = fade * (1 - exitT);
      }

      const textHue = (line.hue + sceneTime * 8) % 360;
      ctx.font = `${line.weight || '400'} ${line.size}px ${line.font || 'Helvetica, Arial, sans-serif'}`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';

      if (line.glow) {
        ctx.shadowColor = `hsla(${textHue}, 90%, 65%, ${0.75 * alpha})`;
        ctx.shadowBlur = 32;
      } else {
        ctx.shadowBlur = 0;
      }

      if (line.dim) {
        ctx.fillStyle = `hsla(${textHue}, 45%, 78%, ${0.7 * alpha})`;
      } else {
        ctx.fillStyle = `hsla(${textHue}, 75%, 88%, ${alpha})`;
      }

      const subLineHeight = line.subLineHeight || line.size;
      const startY = y - ((contentLines.length - 1) * subLineHeight) / 2;
      contentLines.forEach((text, lineIndex) => {
        ctx.fillText(text, x, startY + lineIndex * subLineHeight);
      });
      ctx.shadowBlur = 0;
    });

    if (blackOverlay > 0) {
      ctx.fillStyle = `rgba(0, 0, 0, ${blackOverlay})`;
      ctx.fillRect(0, 0, WIDTH, HEIGHT);
    }
  }

  let endAudioFadeStarted = false;

  return {
    name: 'Thank You',
    startTime,
    endTime: startTime + thankYouDuration,
    threeScene,
    camera,
    initialized: false,

    init() {
      console.log('[Scene Manager] Thank You — credits');
      document.fonts.ready.then(() => {
        drawCredits(0, 0);
        texture.needsUpdate = true;
      });
      this.initialized = true;
    },

    update(sceneTime, totalTime) {
      const fadeIn = smoothstep(0, FADE_IN_DURATION, sceneTime);
      const drawTime = Math.min(sceneTime, creditsEndTime);
      let blackOverlay = 0;

      if (sceneTime > creditsEndTime) {
        blackOverlay = smoothstep(0, END_FADE_TO_BLACK, sceneTime - creditsEndTime);
        if (!endAudioFadeStarted) {
          endAudioFadeStarted = true;
          fadeOutBackgroundAudio(END_FADE_TO_BLACK);
        }
      }

      const fade = fadeIn * (1 - blackOverlay);

      drawCredits(drawTime, fade, blackOverlay);
      texture.needsUpdate = true;
      material.opacity = fadeIn * (1 - blackOverlay * 0.35);

      if (sceneTime >= thankYouDuration) {
        stopLoop();
      }
    },

    destroy() {
      threeScene.remove(mesh);
      mesh.geometry.dispose();
      disposeMaterial(material);
      texture.dispose();
      this.initialized = false;
    },

    cleanup() {
      this.destroy();
    },
  };
}

function buildSceneManager() {
  const contentScenes = [
    createFractalFocusScene({
      startTime: INTRO_START,
      endTime: INTRO_END,
      duration: INTRO_DURATION,
      pace: INTRO_PACE,
      variant: 'purple',
      name: 'Fractal Intro',
    }),
    createQuantumCathedralScene({
      startTime: SCENE1_START,
      endTime: SCENE1_END,
      duration: SCENE1_DURATION,
      pace: SCENE1_PACE,
      variant: 'ascent',
      name: 'Quantum Cathedral — Ascent',
    }),
    createPCBuildScene(),
    createQuantumCathedralScene({
      startTime: SCENE2_START,
      endTime: SCENE2_END,
      duration: SCENE2_DURATION,
      pace: SCENE2_PACE,
      variant: 'orbit',
      name: 'Quantum Cathedral — Orbit',
    }),
    createFractalFocusScene({
      startTime: SCENE3_START,
      endTime: SCENE3_END,
      duration: SCENE3_DURATION,
      pace: SCENE3_PACE,
      variant: 'red',
      name: 'Fractal Outro',
    }),
  ];

  const lastContentEnd = contentScenes[contentScenes.length - 1].endTime;
  const thankYouScene = createThankYouScene(lastContentEnd);

  return [...contentScenes, thankYouScene];
}

const sceneManager = buildSceneManager();

const whooshCueState = {
  introStart: false,
  pcBuildStart: false,
  rabbitBreak: false,
};

function getSceneIndexForTime(totalTime) {
  for (let i = sceneManager.length - 1; i >= 0; i -= 1) {
    if (totalTime >= sceneManager[i].startTime) {
      return i;
    }
  }
  return 0;
}

function activateScene(index) {
  if (activeSceneIndex === index) {
    return;
  }

  if (activeSceneIndex >= 0) {
    const nextScene = sceneManager[index];
    if (nextScene.name === 'Thank You') {
      playThankYouBlims();
    } else if (nextScene.name !== 'Fractal Outro') {
      playSceneWhoosh();
    }
  }

  if (activeScene) {
    activeScene.destroy();
  }

  activeSceneIndex = index;
  activeScene = sceneManager[index];
  activeScene.init();
}

function stopLoop() {
  if (!running) {
    return;
  }

  running = false;

  if (animationFrameId !== null) {
    cancelAnimationFrame(animationFrameId);
    animationFrameId = null;
  }

  if (audioStarted) {
    stopBackgroundAudio();
  }

  if (activeScene) {
    activeScene.destroy();
    activeScene = null;
    activeSceneIndex = -1;
  }

  document.body.style.cursor = 'default';
  renderer.domElement.style.cursor = 'default';
}

function getTotalTime() {
  return clock.getElapsedTime();
}

function updateWhooshCues(totalTime) {
  const scene = sceneManager[getSceneIndexForTime(totalTime)];
  if (!scene) return;

  const sceneTime = totalTime - scene.startTime;

  if (scene.name === 'Fractal Intro' && sceneTime >= 0.05 && !whooshCueState.introStart) {
    whooshCueState.introStart = true;
    playLongSceneWhoosh();
  }

  if (scene.name === 'Wire PC Build') {
    if (sceneTime >= PC_SCATTER_END && !whooshCueState.pcBuildStart) {
      whooshCueState.pcBuildStart = true;
      playLongSceneWhoosh();
    }
    if (sceneTime >= PC_RABBIT_END && !whooshCueState.rabbitBreak) {
      whooshCueState.rabbitBreak = true;
      playLongSceneWhoosh();
    }
  }
}

function isBeatStoppedPhase(totalTime) {
  const scene = sceneManager[getSceneIndexForTime(totalTime)];
  if (!scene) return false;
  if (scene.name === 'Thank You') return true;
  if (scene.name === 'Wire PC Build') return true;

  if (scene.name === 'Fractal Outro') {
    const sceneTime = totalTime - scene.startTime;
    const heartEnd = OUTRO_ZOOM_DURATION + OUTRO_HEART_DURATION;
    return sceneTime >= heartEnd;
  }

  return false;
}

function isSoloElementPhase(totalTime) {
  const scene = sceneManager[getSceneIndexForTime(totalTime)];
  if (!scene || scene.name === 'Thank You') return false;

  const sceneTime = totalTime - scene.startTime;

  if (scene.name === 'Fractal Intro') {
    return true;
  }

  if (scene.name === 'Quantum Cathedral — Ascent') {
    return true;
  }

  if (scene.name === 'Fractal Outro') {
    const heartEnd = OUTRO_ZOOM_DURATION + OUTRO_HEART_DURATION;
    if (sceneTime >= OUTRO_ZOOM_DURATION && sceneTime < heartEnd) return true;
  }

  return false;
}

function animate() {
  if (!running) {
    return;
  }

  animationFrameId = requestAnimationFrame(animate);

  updateAudio();
  const totalTime = getTotalTime();
  const beatStopped = isBeatStoppedPhase(totalTime);
  setBeatActive(!beatStopped);
  if (!beatStopped) {
    setBeatPace(isSoloElementPhase(totalTime));
  }
  if (!isBeatPlaying() && audioAutoTryCount < AUDIO_AUTO_TRY_LIMIT) {
    audioAutoTryCount += 1;
    ensureBackgroundAudio();
  }
  const sceneForAudio = sceneManager[getSceneIndexForTime(totalTime)];
  if (sceneForAudio) {
    const sceneDuration = sceneForAudio.endTime - sceneForAudio.startTime;
    updateSceneAudio(sceneForAudio.name, totalTime - sceneForAudio.startTime, sceneDuration);
  }
  updateWhooshCues(totalTime);
  const nextSceneIndex = getSceneIndexForTime(totalTime);

  if (nextSceneIndex !== activeSceneIndex) {
    activateScene(nextSceneIndex);
  }

  const sceneTime = totalTime - activeScene.startTime;
  activeScene.update(sceneTime, totalTime);
  renderer.render(activeScene.threeScene, activeScene.camera);
}

window.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') {
    stopLoop();
  }
});

bindAudioUnlock();

window.addEventListener('load', () => {
  ensureBackgroundAudio();
});

clock.start();
activateScene(0);
animate();
ensureBackgroundAudio();
