// world-cosmos.jsx — Cinematic procedural Earth, clouds, atmosphere, stars.

function initCosmos(container) {
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x000000);

  const camera = new THREE.PerspectiveCamera(32, 1, 0.01, 5000);
  camera.position.set(0, 0, 5.6);

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setClearColor(0x000000, 1);
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  container.appendChild(renderer.domElement);

  // ── Sun direction (warm key light) ──────────────────────────────────────
  const sunDir = new THREE.Vector3(0.85, 0.25, 0.45).normalize();

  // ══════════════════════════════════════════════════════════════════════════
  // STARFIELD — distance-graded, twinkling, with bright anchor stars
  // ══════════════════════════════════════════════════════════════════════════
  const starGeo = new THREE.BufferGeometry();
  const STAR_N = 20000;
  const sPos = new Float32Array(STAR_N * 3);
  const sSize = new Float32Array(STAR_N);
  const sBri  = new Float32Array(STAR_N);
  const sCol  = new Float32Array(STAR_N * 3);
  for (let i = 0; i < STAR_N; i++) {
    let x, y, z, r2;
    do {
      x = Math.random()*2-1; y = Math.random()*2-1; z = Math.random()*2-1;
      r2 = x*x + y*y + z*z;
    } while (r2 > 1 || r2 < 0.001);
    const r = 1800 * (0.55 + Math.random()*0.6);
    const s = Math.sqrt(r2);
    sPos[i*3] = (x/s)*r; sPos[i*3+1] = (y/s)*r; sPos[i*3+2] = (z/s)*r;
    // Three populations: dust (small dim), main sequence (medium), giants (rare bright)
    const p = Math.random();
    if (p < 0.78)       sSize[i] = 0.40 + Math.random()*0.75;     // dust
    else if (p < 0.985) sSize[i] = 1.20 + Math.random()*1.80;     // mid
    else                sSize[i] = 3.20 + Math.random()*5.00;     // anchor / giant
    // Brightness follows a Pareto-ish distribution so a handful pop
    sBri[i]  = 0.40 + Math.pow(Math.random(), 2.4) * 1.10;
    // Stellar color temperatures — warm white majority, with cool blue and
    // red/orange giants. More saturated than before so the field feels alive.
    const ct = Math.random();
    if      (ct < 0.55) { sCol[i*3]=1.00; sCol[i*3+1]=0.97; sCol[i*3+2]=0.92; } // warm white
    else if (ct < 0.78) { sCol[i*3]=0.74; sCol[i*3+1]=0.84; sCol[i*3+2]=1.00; } // blue
    else if (ct < 0.92) { sCol[i*3]=1.00; sCol[i*3+1]=0.72; sCol[i*3+2]=0.55; } // amber
    else                { sCol[i*3]=1.00; sCol[i*3+1]=0.55; sCol[i*3+2]=0.50; } // red giant
  }
  starGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
  starGeo.setAttribute('size', new THREE.BufferAttribute(sSize, 1));
  starGeo.setAttribute('aBri', new THREE.BufferAttribute(sBri, 1));
  starGeo.setAttribute('aCol', new THREE.BufferAttribute(sCol, 3));
  const starMat = new THREE.ShaderMaterial({
    uniforms: { uTime: { value: 0 } },
    transparent: true, depthWrite: false,
    blending: THREE.AdditiveBlending,
    vertexShader: `
      attribute float size; attribute float aBri; attribute vec3 aCol;
      varying float vBri; varying vec3 vCol; varying float vSize;
      uniform float uTime;
      void main(){
        vBri = aBri; vCol = aCol; vSize = size;
        vec4 mv = modelViewMatrix * vec4(position,1.0);
        // Two-frequency twinkle keyed off brightness for variety
        float tw = 0.78
                 + 0.16 * sin(uTime*1.8 + aBri*52.0)
                 + 0.06 * sin(uTime*4.7 + aBri*113.0);
        gl_PointSize = size * (340.0 / -mv.z) * tw;
        gl_Position = projectionMatrix * mv;
      }
    `,
    fragmentShader: `
      varying float vBri; varying vec3 vCol; varying float vSize;
      void main(){
        vec2 c = gl_PointCoord - 0.5;
        float d = length(c);
        // Soft round core with a halo that bleeds out
        float core = smoothstep(0.42, 0.0, d);
        float halo = smoothstep(0.50, 0.10, d) * 0.45;
        // Diffraction spikes — only really visible on the largest stars
        float spikeMask = smoothstep(2.0, 4.0, vSize);
        float fx = smoothstep(0.5,0.0,abs(c.x)*9.0)*smoothstep(0.03,0.0,abs(c.y));
        float fy = smoothstep(0.5,0.0,abs(c.y)*9.0)*smoothstep(0.03,0.0,abs(c.x));
        float spikes = (fx + fy) * 0.35 * spikeMask;
        float a = (core + halo + spikes) * vBri;
        gl_FragColor = vec4(vCol * a, a);
      }
    `,
  });
  const stars = new THREE.Points(starGeo, starMat);
  scene.add(stars);

  // ── Milky Way band (subtle, painted on a backside sphere) ────────────────
  const milkyGeo = new THREE.SphereGeometry(1300, 32, 32);
  const milkyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
    uniforms: { uTime: { value: 0 } },
    vertexShader: `varying vec3 vP; void main(){ vP=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
    fragmentShader: `
      varying vec3 vP;
      uniform float uTime;
      ${NOISE_GLSL}
      void main(){
        vec3 d = normalize(vP);
        // Band along an inclined plane
        vec3 axis = normalize(vec3(0.6, 0.25, 0.4));
        float band = pow(1.0 - abs(dot(d, axis)), 18.0);
        float n = fbm(d*5.0);
        float dust = smoothstep(0.45, 0.85, n);
        vec3 col = vec3(0.0);
        gl_FragColor = vec4(col, 0.0);
      }
    `,
  });
  const milky = new THREE.Mesh(milkyGeo, milkyMat);
  scene.add(milky);

  // ══════════════════════════════════════════════════════════════════════════
  // EARTH — loaded from earth.glb (replaces the procedural shader earth).
  // We wrap the loaded scene in a Group so we can rotate/translate it without
  // depending on its internal structure, and normalize its scale so it fits
  // inside the cloud sphere (r=1.018) and atmosphere shell (r=1.13).
  // ══════════════════════════════════════════════════════════════════════════
  const earth = new THREE.Group();
  scene.add(earth);
  // Lumière ambiante douce uniquement — pas de directionnelle pour éviter
  // le reflet spéculaire du "soleil" sur la surface de la terre.
  const earthFill = new THREE.AmbientLight(0xfff8ee, 0.9);
  scene.add(earthFill);

  let earthModel = null;
  const earthOriginalMats = new Map();
  let earthCurrentStyle = 'realistic';
  function applyEarthStyle(style) {
    earthCurrentStyle = style;
    if (!earthModel) return;
    earthModel.traverse((obj) => {
      if (!obj.isMesh) return;
      if (!earthOriginalMats.has(obj)) earthOriginalMats.set(obj, obj.material);
      const orig = earthOriginalMats.get(obj);
      if (style === 'wireframe') {
        obj.material = new THREE.MeshBasicMaterial({ color: 0xc9b896, wireframe: true });
      } else if (style === 'stylized') {
        obj.material = new THREE.MeshLambertMaterial({ color: 0xb5a98c });
      } else {
        obj.material = orig;
      }
    });
  }

  const dracoLoader = new THREE.DRACOLoader();
  dracoLoader.setDecoderPath('https://unpkg.com/three@0.147.0/examples/js/libs/draco/');
  const gltfLoader = new THREE.GLTFLoader();
  gltfLoader.setDRACOLoader(dracoLoader);
  gltfLoader.load('earth.glb', (gltf) => {
    earthModel = gltf.scene;
    // Hide common "atmosphere / halo / cloud / specular" layers many earth
    // GLBs include. Also log every mesh name so we can target ones we miss.
    console.log('[earth] mesh tree:');
    earthModel.traverse((obj) => {
      if (!obj.isMesh) return;
      const matName = obj.material?.name || '';
      const isTransparent = !!obj.material?.transparent;
      console.log(`  · "${obj.name}"  material="${matName}"  transparent=${isTransparent}`);
      const name = (obj.name + ' ' + matName).toLowerCase();
      if (name.includes('atmo') || name.includes('halo') ||
          name.includes('cloud') || name.includes('glow') ||
          name.includes('water') || name.includes('ocean') ||
          name.includes('specular') || name.includes('reflect') ||
          name.includes('shine')) {
        obj.visible = false;
        console.log('  ↳ HIDDEN');
      }
    });
    // Normalize using the bounding box's longest axis so the planet sphere
    // fills the cloud shell instead of being shrunk by stray geometry (rings,
    // halos) that would otherwise inflate the bounding sphere.
    const box = new THREE.Box3().setFromObject(earthModel);
    const size = new THREE.Vector3();
    box.getSize(size);
    const maxAxis = Math.max(size.x, size.y, size.z);
    const s = 2.0 / Math.max(maxAxis, 1e-3); // target diameter ≈ 2 (radius ≈ 1)
    earthModel.scale.setScalar(s);
    // Recenter on origin
    const center = new THREE.Vector3();
    box.getCenter(center);
    earthModel.position.sub(center.multiplyScalar(s));
    earth.add(earthModel);
    applyEarthStyle(earthCurrentStyle);
  }, undefined, (err) => {
    console.error('Failed to load earth.glb', err);
  });

  // ══════════════════════════════════════════════════════════════════════════
  // CLOUDS — separate sphere slightly above the surface, drifting
  // ══════════════════════════════════════════════════════════════════════════
  const cloudGeo = new THREE.SphereGeometry(1.018, 128, 128);
  const cloudMat = new THREE.ShaderMaterial({
    transparent: true,
    depthWrite: false,
    uniforms: {
      uTime: { value: 0 },
      uSunDir: { value: sunDir.clone() },
    },
    vertexShader: `
      varying vec3 vN; varying vec3 vWN; varying vec3 vL; varying vec3 vW;
      void main(){
        vN = normalize(normalMatrix * normal);
        vWN = normalize(mat3(modelMatrix) * normal);
        vL = position;
        vW = (modelMatrix * vec4(position,1.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
      }
    `,
    fragmentShader: `
      varying vec3 vN; varying vec3 vWN; varying vec3 vL; varying vec3 vW;
      uniform float uTime;
      uniform vec3  uSunDir;
      ${NOISE_GLSL}
      void main(){
        vec3 p = normalize(vL);
        // Drifting clouds — matched to the surface shadow sampling
        float c = fbm5(p*3.2 + vec3(uTime*0.014, 0.0, uTime*0.008));
        // Streaky high-altitude wisps
        float wisp = fbm(p*6.0 + vec3(uTime*0.04,0.0,0.0));
        float mask = smoothstep(0.50, 0.74, c) + smoothstep(0.62, 0.78, wisp)*0.4;
        mask = clamp(mask, 0.0, 1.0);
        // Soft falloff at edges
        float edge = smoothstep(0.50, 0.62, c);
        mask *= edge;

        // Light
        float lit = max(dot(vWN, uSunDir), 0.0);
        float wrap = lit*0.7 + 0.3;
        vec3 col = vec3(1.0, 0.98, 0.94) * wrap;
        // Rim — clouds glow at the limb
        vec3 V = normalize(cameraPosition - vW);
        float rim = pow(1.0 - max(dot(V, vWN), 0.0), 1.6);
        col += vec3(0.8,0.85,1.0) * rim * 0.25;

        float alpha = mask * (0.85 * wrap + 0.15);
        // Reduce on night side so clouds disappear into shadow
        alpha *= smoothstep(-0.2, 0.4, dot(vWN, uSunDir))*0.9 + 0.1;
        gl_FragColor = vec4(col, alpha);
      }
    `,
  });
  // Procedural cloud sphere disabled — the earth.glb has its own cloud layer.
  const clouds = new THREE.Mesh(cloudGeo, cloudMat);
  clouds.visible = false;

  // ══════════════════════════════════════════════════════════════════════════
  // ATMOSPHERE — additive backside sphere with Rayleigh-ish falloff
  // ══════════════════════════════════════════════════════════════════════════
  // Atmosphere shell, kept close to the surface (was 1.13 — too large/glowy)
  const atmoGeo = new THREE.SphereGeometry(1.045, 96, 96);
  const atmoMat = new THREE.ShaderMaterial({
    transparent: true, side: THREE.BackSide, depthWrite: false,
    blending: THREE.AdditiveBlending,
    uniforms: { uSunDir: { value: sunDir.clone() } },
    vertexShader: `
      varying vec3 vWN; varying vec3 vW;
      void main(){
        vWN = normalize(mat3(modelMatrix) * normal);
        vW = (modelMatrix * vec4(position,1.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
      }
    `,
    fragmentShader: `
      varying vec3 vWN; varying vec3 vW;
      uniform vec3 uSunDir;
      void main(){
        vec3 V = normalize(cameraPosition - vW);
        // Tighter rim falloff so the halo is thinner now that the shell is closer
        float rim = pow(1.0 - max(dot(V, vWN), 0.0), 3.4);
        float sun = max(dot(normalize(vW), uSunDir), 0.0);
        float lit = sun * 0.7 + 0.3;
        vec3 dayCol = vec3(0.28, 0.50, 0.95);
        vec3 termCol= vec3(0.95, 0.50, 0.32);
        float term = smoothstep(0.0, 0.25, sun) * smoothstep(0.6, 0.25, sun);
        vec3 col = mix(dayCol*lit, termCol, term*0.7);
        gl_FragColor = vec4(col, rim * 0.70 * (0.3 + 0.7*lit));
      }
    `,
  });
  // Procedural atmosphere shell also disabled — the earth.glb has its own halo.
  const atmo = new THREE.Mesh(atmoGeo, atmoMat);
  atmo.visible = false;

  // ── Camera path: orbit + descend toward atmosphere ───────────────────────
  const update = (p, t) => {
    p = clamp01(p);
    const e = ease(p);
    const dist = lerp(6.4, 1.45, e);
    const orbit = lerp(0.0, 0.7, p);
    const tilt  = lerp(0.04, 0.22, p);
    camera.position.x = Math.sin(orbit + t*0.04) * dist;
    camera.position.y = tilt * dist + Math.sin(t*0.07) * 0.05;
    camera.position.z = Math.cos(orbit + t*0.04) * dist;
    camera.lookAt(0, 0, 0);

    earth.rotation.y  = Math.PI / 2 + t * 0.022 + p * 0.5;
    clouds.rotation.y = earth.rotation.y * 1.06 + t * 0.004;
    atmo.rotation.y   = earth.rotation.y;
    stars.rotation.y  = t * 0.002;
    milky.rotation.y  = t * 0.0015;
    starMat.uniforms.uTime.value = t;
    milkyMat.uniforms.uTime.value = t;
    cloudMat.uniforms.uTime.value = t;
  };

  const setStyle = (style) => {
    applyEarthStyle(style);
    // Procedural clouds + atmosphere remain hidden regardless of style;
    // the earth.glb already contains its own cloud / halo layers.
  };

  const resize = () => {
    const r = container.getBoundingClientRect();
    camera.aspect = r.width / r.height;
    camera.updateProjectionMatrix();
    renderer.setSize(r.width, r.height, false);
  };
  resize();

  const dispose = () => {
    renderer.dispose();
    if (renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
    if (earthModel) earthModel.traverse((o) => {
      if (o.isMesh) {
        o.geometry?.dispose?.();
        if (Array.isArray(o.material)) o.material.forEach((m) => m.dispose?.());
        else o.material?.dispose?.();
      }
    });
    cloudGeo.dispose(); cloudMat.dispose();
    atmoGeo.dispose();  atmoMat.dispose();
    starGeo.dispose();  starMat.dispose();
    milkyGeo.dispose(); milkyMat.dispose();
  };

  return { scene, camera, renderer, update, resize, dispose, setStyle };
}

window.initCosmos = initCosmos;
