// Planet data with real NASA/Solar System Scope texture URLs
const PLANETS = [
  {
    id: "sun",
    name: "Sun",
    type: "Star · G2V",
    blurb:
      "A middle-aged yellow dwarf burning four million tonnes of mass into pure light every second. Source of all life in the system.",
    radius: 56,
    detailRadius: 320,
    orbitRadius: 0,
    period: 0,
    distance: "0 AU",
    day: "25 Earth days",
    year: "—",
    moons: 0,
    temp: "5,500 °C surface · 15M °C core",
    gravity: "274 m/s²",
    composition: "Hydrogen 73% · Helium 25% · trace metals",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/sunmap.jpg",
    color: "#FFD66B",
    emissive: "#FF7B2A",
    glowColor: "#FFB347",
    isStar: true,
  },
  {
    id: "mercury",
    name: "Mercury",
    type: "Terrestrial planet",
    blurb:
      "A scarred, sun-baked rock with no atmosphere. Days reach 430 °C; nights plunge to -180 °C. Pocked with billions of craters.",
    radius: 9,
    detailRadius: 280,
    orbitRadius: 110,
    period: 8,
    distance: "0.39 AU",
    day: "59 Earth days",
    year: "88 Earth days",
    moons: 0,
    temp: "-180 to 430 °C",
    gravity: "3.7 m/s²",
    composition: "Iron core, silicate crust",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/mercurymap.jpg",
    color: "#a89078",
  },
  {
    id: "venus",
    name: "Venus",
    type: "Terrestrial planet",
    blurb:
      "Wrapped in a suffocating shroud of sulfuric clouds. Hottest world in the system — a runaway greenhouse with crushing pressure.",
    radius: 13,
    detailRadius: 300,
    orbitRadius: 160,
    period: 14,
    distance: "0.72 AU",
    day: "243 Earth days",
    year: "225 Earth days",
    moons: 0,
    temp: "465 °C",
    gravity: "8.87 m/s²",
    composition: "CO₂ atmosphere, sulfuric acid clouds",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/venusmap.jpg",
    color: "#e0b878",
    glowColor: "#F2C77A",
  },
  {
    id: "earth",
    name: "Earth",
    type: "Terrestrial planet",
    blurb:
      "The pale blue dot. A water world with a thin breathable veil — the only known cradle of life in the universe.",
    radius: 14,
    detailRadius: 310,
    orbitRadius: 215,
    period: 22,
    distance: "1.00 AU",
    day: "23h 56m",
    year: "365.25 days",
    moons: 1,
    temp: "-88 to 58 °C",
    gravity: "9.81 m/s²",
    composition: "78% N₂, 21% O₂, liquid water oceans",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/earthmap1k.jpg",
    cloudTexture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/earthcloudmap.jpg",
    color: "#5b8fc4",
    glowColor: "#7AB8FF",
  },
  {
    id: "mars",
    name: "Mars",
    type: "Terrestrial planet",
    blurb:
      "The rust-red desert world. Home to Olympus Mons (largest volcano in the system) and Valles Marineris (a 4,000 km canyon).",
    radius: 11,
    detailRadius: 290,
    orbitRadius: 280,
    period: 32,
    distance: "1.52 AU",
    day: "24h 37m",
    year: "687 Earth days",
    moons: 2,
    temp: "-140 to 30 °C",
    gravity: "3.72 m/s²",
    composition: "Iron oxide crust, thin CO₂ atmosphere",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/marsmap1k.jpg",
    color: "#c46838",
    glowColor: "#D87050",
  },
  {
    id: "jupiter",
    name: "Jupiter",
    type: "Gas giant",
    blurb:
      "The king of planets. Its Great Red Spot is a storm two Earths wide that has raged for at least 350 years.",
    radius: 34,
    detailRadius: 360,
    orbitRadius: 360,
    period: 62,
    distance: "5.20 AU",
    day: "9h 56m",
    year: "11.86 years",
    moons: 95,
    temp: "-145 °C cloud tops",
    gravity: "24.79 m/s²",
    composition: "H₂, He, ammonia & water ice clouds",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/jupitermap.jpg",
    color: "#c89870",
    glowColor: "#D8B080",
  },
  {
    id: "saturn",
    name: "Saturn",
    type: "Gas giant",
    blurb:
      "The jeweled planet. Its rings span 280,000 km but are only ten metres thick — cosmic vinyl of ice and dust shepherded by moons.",
    radius: 28,
    detailRadius: 300,
    orbitRadius: 440,
    period: 88,
    distance: "9.58 AU",
    day: "10h 33m",
    year: "29.46 years",
    moons: 146,
    temp: "-178 °C",
    gravity: "10.44 m/s²",
    composition: "H₂, He, ammonia ice; ring system",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/saturnmap.jpg",
    ringTexture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/saturnringcolor.jpg",
    ringAlpha:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/saturnringpattern.gif",
    color: "#d4b878",
    glowColor: "#D8C088",
    rings: true,
    ringInner: 1.3,
    ringOuter: 2.3,
    tilt: 26.7,
  },
  {
    id: "uranus",
    name: "Uranus",
    type: "Ice giant",
    blurb:
      "The sideways planet. Tilted 98° on its axis, it rolls around the Sun like a wheel through pale cyan methane haze.",
    radius: 20,
    detailRadius: 290,
    orbitRadius: 510,
    period: 124,
    distance: "19.2 AU",
    day: "17h 14m",
    year: "84 Earth years",
    moons: 27,
    temp: "-224 °C",
    gravity: "8.69 m/s²",
    composition: "Water, methane, ammonia ices",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/uranusmap.jpg",
    color: "#9ed4ce",
    glowColor: "#8ED4CC",
    tilt: 98,
    rings: "thin",
    ringInner: 1.4,
    ringOuter: 1.7,
  },
  {
    id: "neptune",
    name: "Neptune",
    type: "Ice giant",
    blurb:
      "The deep blue wanderer. Supersonic winds tear across its atmosphere at 2,100 km/h — the fastest in the solar system.",
    radius: 19,
    detailRadius: 290,
    orbitRadius: 580,
    period: 168,
    distance: "30.1 AU",
    day: "16h 6m",
    year: "165 Earth years",
    moons: 14,
    temp: "-218 °C",
    gravity: "11.15 m/s²",
    composition: "Water, methane, ammonia ices",
    texture:
      "https://cdn.jsdelivr.net/gh/jeromeetienne/threex.planets@master/images/neptunemap.jpg",
    color: "#3a5fb8",
    glowColor: "#5A8AE0",
  },
];

window.PLANETS = PLANETS;

// ---------- Three.js planet renderer ----------
// Renders into a given container. Returns a dispose() function.
function renderPlanet3D(container, planet, opts = {}) {
  const { detail = false, autoRotate = true, allowOrbit = false, size } = opts;

  const w = container.clientWidth;
  const h = container.clientHeight;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(40, w / h, 0.1, 1000);
  // Pull the camera back far enough that the ring outer edge fits inside
  // the vertical FOV with a 12% margin. Without this Saturn's rings
  // (outer = 2.3 sphere-radii) clip at the default z = 3.2.
  const _ringOuter = planet.rings ? planet.ringOuter || 2.3 : 1;
  const _fovRad = (40 * Math.PI) / 180;
  const _requiredZ = (Math.max(1, _ringOuter) * 1.12) / Math.tan(_fovRad / 2);
  camera.position.set(0, 0, Math.max(3.2, _requiredZ));

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

  // Lights
  if (planet.isStar) {
    scene.add(new THREE.AmbientLight(0xffffff, 1.2));
  } else {
    scene.add(new THREE.AmbientLight(0xffffff, 0.18));
    const sunLight = new THREE.DirectionalLight(0xffffff, 1.6);
    sunLight.position.set(5, 2, 4);
    scene.add(sunLight);
    // soft fill from opposite side so dark side isn't pitch black in detail view
    if (detail) {
      const fill = new THREE.DirectionalLight(0x4060a0, 0.18);
      fill.position.set(-4, -1, -3);
      scene.add(fill);
    }
  }

  const loader = new THREE.TextureLoader();
  loader.crossOrigin = "anonymous";
  // Track every texture we successfully load so dispose() can free GPU memory
  // and so async loads that finish after teardown can drop their texture
  // instead of attaching it to a disposed material.
  const loadedTextures = [];
  let disposed = false;

  // Planet sphere
  const geo = new THREE.SphereGeometry(1, detail ? 96 : 48, detail ? 96 : 48);
  let mat;
  if (planet.isStar) {
    mat = new THREE.MeshBasicMaterial({ color: 0xffffff });
  } else {
    mat = new THREE.MeshPhongMaterial({
      color: 0xffffff,
      shininess: planet.id === "earth" ? 12 : 4,
      specular: planet.id === "earth" ? 0x223344 : 0x111111,
    });
  }
  const sphere = new THREE.Mesh(geo, mat);
  sphere.rotation.z = ((planet.tilt || 23.4) * Math.PI) / 180;
  scene.add(sphere);

  // Load texture (with fallback color while loading)
  mat.color.set(planet.color);
  loader.load(
    planet.texture,
    (tex) => {
      if (disposed) {
        tex.dispose();
        return;
      }
      tex.colorSpace = THREE.SRGBColorSpace;
      mat.map = tex;
      mat.color.set(0xffffff);
      mat.needsUpdate = true;
      loadedTextures.push(tex);
    },
    undefined,
    () => {
      if (disposed) return;
      // Keep color fallback visible on error
      mat.color.set(planet.color);
      mat.needsUpdate = true;
    },
  );

  // Earth clouds
  let clouds = null;
  if (planet.cloudTexture && detail) {
    const cloudMat = new THREE.MeshPhongMaterial({
      transparent: true,
      opacity: 0.6,
      depthWrite: false,
    });
    clouds = new THREE.Mesh(new THREE.SphereGeometry(1.012, 64, 64), cloudMat);
    sphere.add(clouds);
    loader.load(planet.cloudTexture, (tex) => {
      if (disposed) {
        tex.dispose();
        return;
      }
      tex.colorSpace = THREE.SRGBColorSpace;
      cloudMat.map = tex;
      cloudMat.alphaMap = tex;
      cloudMat.needsUpdate = true;
      loadedTextures.push(tex);
    });
  }

  // Atmosphere glow (back-side fresnel-ish via shader)
  let atmosphere = null;
  if (planet.glowColor && detail && !planet.isStar) {
    const atmMat = new THREE.ShaderMaterial({
      uniforms: { glow: { value: new THREE.Color(planet.glowColor) } },
      vertexShader: `
        varying vec3 vNormal; varying vec3 vPos;
        void main() {
          vNormal = normalize(normalMatrix * normal);
          vec4 mv = modelViewMatrix * vec4(position, 1.0);
          vPos = -mv.xyz;
          gl_Position = projectionMatrix * mv;
        }
      `,
      fragmentShader: `
        uniform vec3 glow; varying vec3 vNormal; varying vec3 vPos;
        void main() {
          float intensity = pow(0.85 - dot(vNormal, normalize(vPos)), 3.0);
          gl_FragColor = vec4(glow, 1.0) * intensity;
        }
      `,
      side: THREE.BackSide,
      blending: THREE.AdditiveBlending,
      transparent: true,
      depthWrite: false,
    });
    atmosphere = new THREE.Mesh(new THREE.SphereGeometry(1.1, 48, 48), atmMat);
    scene.add(atmosphere);
  }

  // Sun corona/glow sprite. In detail view the sprite can reveal its square
  // texture bounds when the Sun fills the screen, so detail relies on the
  // sphere plus page-level radial light instead.
  let coronaSprite = null;
  let coronaTex = null;
  if (planet.isStar && !detail) {
    const canvas = document.createElement("canvas");
    canvas.width = 256;
    canvas.height = 256;
    const ctx = canvas.getContext("2d");
    const grad = ctx.createRadialGradient(128, 128, 0, 128, 128, 128);
    grad.addColorStop(0, "rgba(255,224,138,1)");
    grad.addColorStop(0.3, "rgba(255,147,38,0.7)");
    grad.addColorStop(0.62, "rgba(255,91,26,0.18)");
    grad.addColorStop(0.84, "rgba(80,22,0,0.035)");
    grad.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, 256, 256);
    coronaTex = new THREE.CanvasTexture(canvas);
    const spriteMat = new THREE.SpriteMaterial({
      map: coronaTex,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
    });
    coronaSprite = new THREE.Sprite(spriteMat);
    coronaSprite.scale.set(detail ? 4.2 : 2.6, detail ? 4.2 : 2.6, 1);
    scene.add(coronaSprite);
  }

  // Saturn / Uranus rings
  let ring = null;
  if (planet.rings) {
    const inner = planet.ringInner || 1.3;
    const outer = planet.ringOuter || 2.3;
    const ringGeo = new THREE.RingGeometry(inner, outer, 128, 1);
    // Re-map UVs so a horizontal texture maps radially
    const pos = ringGeo.attributes.position;
    const uv = ringGeo.attributes.uv;
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i),
        y = pos.getY(i);
      const r = Math.sqrt(x * x + y * y);
      const u = (r - inner) / (outer - inner);
      uv.setXY(i, u, 0.5);
    }
    let ringMat;
    if (planet.ringTexture) {
      ringMat = new THREE.MeshBasicMaterial({
        color: planet.glowColor || planet.color,
        transparent: true,
        side: THREE.DoubleSide,
        opacity: 0.95,
        depthWrite: false,
      });
      loader.load(planet.ringTexture, (tex) => {
        if (disposed) {
          tex.dispose();
          return;
        }
        tex.colorSpace = THREE.SRGBColorSpace;
        ringMat.map = tex;
        ringMat.color.set(0xffffff);
        ringMat.needsUpdate = true;
        loadedTextures.push(tex);
      });
      if (planet.ringAlpha) {
        loader.load(planet.ringAlpha, (tex) => {
          if (disposed) {
            tex.dispose();
            return;
          }
          ringMat.alphaMap = tex;
          ringMat.needsUpdate = true;
          loadedTextures.push(tex);
        });
      }
    } else {
      ringMat = new THREE.MeshBasicMaterial({
        color: planet.glowColor || planet.color,
        transparent: true,
        opacity: 0.5,
        side: THREE.DoubleSide,
        depthWrite: false,
      });
    }
    ring = new THREE.Mesh(ringGeo, ringMat);
    ring.rotation.x = Math.PI / 2;
    sphere.add(ring);
  }

  // Pointer interaction (drag to rotate in detail view)
  let dragState = {
    down: false,
    moved: false,
    x: 0,
    y: 0,
    rx: sphere.rotation.x,
    ry: sphere.rotation.y,
  };
  let userInteracted = false;
  if (allowOrbit) {
    const dom = renderer.domElement;
    dom.style.cursor = "grab";
    dom.addEventListener("pointerdown", (e) => {
      e.stopPropagation();
      dragState = {
        down: true,
        moved: false,
        x: e.clientX,
        y: e.clientY,
        rx: sphere.rotation.x,
        ry: sphere.rotation.y,
      };
      userInteracted = true;
      dom.style.cursor = "grabbing";
      dom.setPointerCapture(e.pointerId);
    });
    dom.addEventListener("pointermove", (e) => {
      if (!dragState.down) return;
      const dx = (e.clientX - dragState.x) / 200;
      const dy = (e.clientY - dragState.y) / 200;
      if (Math.hypot(e.clientX - dragState.x, e.clientY - dragState.y) > 4) {
        dragState.moved = true;
      }
      sphere.rotation.y = dragState.ry + dx;
      sphere.rotation.x = Math.max(-1.2, Math.min(1.2, dragState.rx + dy));
    });
    dom.addEventListener("pointerup", (e) => {
      e.stopPropagation();
      dragState.down = false;
      dom.style.cursor = "grab";
      try {
        dom.releasePointerCapture(e.pointerId);
      } catch {}
    });
    dom.addEventListener("pointercancel", (e) => {
      e.stopPropagation();
      dragState.down = false;
      dom.style.cursor = "grab";
    });
    dom.addEventListener("click", (e) => {
      if (!dragState.moved) return;
      e.preventDefault();
      e.stopPropagation();
      dragState.moved = false;
    });
    dom.addEventListener(
      "wheel",
      (e) => {
        e.preventDefault();
        // Don't let the user zoom in past the point that clips the rings.
        const minZ = Math.max(1.6, _requiredZ * 0.7);
        const maxZ = Math.max(6, _requiredZ * 1.8);
        camera.position.z = Math.max(
          minZ,
          Math.min(maxZ, camera.position.z + e.deltaY * 0.002),
        );
      },
      { passive: false },
    );
  }

  // Animation loop
  let raf;
  const speed = planet.isStar ? 0.0015 : 0.002;
  function tick() {
    if (autoRotate && !dragState.down) {
      sphere.rotation.y += speed;
    }
    if (clouds && !dragState.down) clouds.rotation.y += speed * 0.4;
    renderer.render(scene, camera);
    raf = requestAnimationFrame(tick);
  }
  tick();

  // Handle container resize
  const ro = new ResizeObserver(() => {
    const nw = container.clientWidth;
    const nh = container.clientHeight;
    if (nw && nh) {
      renderer.setSize(nw, nh);
      camera.aspect = nw / nh;
      camera.updateProjectionMatrix();
    }
  });
  ro.observe(container);

  return () => {
    disposed = true;
    cancelAnimationFrame(raf);
    ro.disconnect();
    // Walk the scene to free every geometry/material we created. Texture maps
    // are tracked separately because Three.js doesn't auto-dispose maps when a
    // material is disposed.
    scene.traverse((obj) => {
      if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose();
      const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
      mats.forEach((m) => {
        if (!m) return;
        if (m.map && m.map.dispose) m.map.dispose();
        if (m.alphaMap && m.alphaMap.dispose) m.alphaMap.dispose();
        if (m.dispose) m.dispose();
      });
    });
    loadedTextures.forEach((tex) => tex && tex.dispose && tex.dispose());
    if (coronaTex && coronaTex.dispose) coronaTex.dispose();
    renderer.dispose();
    if (renderer.domElement.parentNode)
      renderer.domElement.parentNode.removeChild(renderer.domElement);
  };
}

window.renderPlanet3D = renderPlanet3D;
