// space.js — star system flight: starfield, sun, orbital planets, ship + boost VFX.
import * as THREE from 'three';
import { makeSpacePlanet } from './planet.js';

export class Space {
  constructor(planetDescs, shipObj) {
    this.scene = new THREE.Scene();
    this.descs = planetDescs;
    this.ship = shipObj;

    // ---- ambient + sun light ----
    // ~perpendicular to the hero framing so planets show a clear day/night terminator
    this.sunDir = new THREE.Vector3(0.78, 0.33, -0.38).normalize();
    this.sunLight = new THREE.DirectionalLight(0xfff4e0, 3.0);
    this.sunLight.position.copy(this.sunDir.clone().multiplyScalar(1000));
    this.scene.add(this.sunLight);
    this.scene.add(new THREE.AmbientLight(0x223044, 0.6));

    // ---- starfield ----
    this.buildStars();

    // ---- sun ----
    this.buildSun();

    // ---- planets in a system layout ----
    this.planets = [];
    const layout = [
      new THREE.Vector3(-1400, 120, -2600),
      new THREE.Vector3(1700, -200, -1200),
      new THREE.Vector3(300, 400, -4200),
    ];
    const radii = [320, 260, 380];
    planetDescs.forEach((d, i) => {
      const pl = makeSpacePlanet(d, radii[i % radii.length]);
      pl.group.position.copy(layout[i % layout.length]);
      pl.setSun(this.sunDir.clone());
      pl.radius = radii[i % radii.length];
      this.scene.add(pl.group);
      this.planets.push(pl);
    });

    // ---- ship rig ----
    this.rig = new THREE.Group();        // position + orientation of the ship
    this.scene.add(this.rig);
    this.ship.group.scale.setScalar(1.0);
    this.rig.add(this.ship.group);
    this.rig.position.set(layout[0].x + 200, layout[0].y + 120, layout[0].z + 900);
    this.rig.lookAt(layout[0]);

    this.vel = new THREE.Vector3();
    this.boost = 0;

    // ---- speed lines (warp streaks) ----
    this.buildSpeedLines();

    // ---- entry overlay (atmosphere wash on the cockpit) ----
    this.buildEntryOverlay();

    this.tmp = new THREE.Vector3();
  }

  buildStars() {
    // two layers: dense faint field + sparse bright stars
    const make = (n, size, baseL, satMax) => {
      const pos = new Float32Array(n * 3);
      const col = new Float32Array(n * 3);
      const c = new THREE.Color();
      for (let i = 0; i < n; i++) {
        const v = new THREE.Vector3().randomDirection().multiplyScalar(6500);
        pos[i * 3] = v.x; pos[i * 3 + 1] = v.y; pos[i * 3 + 2] = v.z;
        const h = 0.55 + Math.random() * 0.18;
        c.setHSL(h, satMax * Math.random(), baseL + Math.random() * (1 - baseL));
        col[i * 3] = c.r; col[i * 3 + 1] = c.g; col[i * 3 + 2] = c.b;
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
      geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
      const mat = new THREE.PointsMaterial({ size, vertexColors: true, transparent: true, opacity: 0.95, sizeAttenuation: false, depthWrite: false });
      return new THREE.Points(geo, mat);
    };
    this.stars = make(3200, 1.5, 0.55, 0.4);
    this.scene.add(this.stars);
    this.starsBright = make(420, 3.2, 0.8, 0.3);
    this.scene.add(this.starsBright);

    // faint nebula backdrop
    const nebGeo = new THREE.SphereGeometry(7000, 32, 24);
    const nebMat = new THREE.ShaderMaterial({
      side: THREE.BackSide, depthWrite: false,
      uniforms: { uA: { value: new THREE.Color(0x140a2a) }, uB: { value: new THREE.Color(0x06121f) } },
      vertexShader: `varying vec3 v; void main(){ v=normalize(position); gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
      fragmentShader: `varying vec3 v; uniform vec3 uA,uB;
        float h(vec3 p){return fract(sin(dot(p,vec3(12.9,78.2,37.7)))*43758.5);}
        void main(){ float n=h(floor(v*8.0))*0.5+h(floor(v*3.0))*0.5; vec3 c=mix(uB,uA, n*0.6+pow(max(v.y,0.0),2.0)*0.3); gl_FragColor=vec4(c,1.0);}`,
    });
    this.scene.add(new THREE.Mesh(nebGeo, nebMat));
  }

  buildSun() {
    const g = new THREE.Group();
    const core = new THREE.Mesh(new THREE.SphereGeometry(220, 32, 24),
      new THREE.MeshBasicMaterial({ color: 0xfff1c4 }));
    g.add(core);
    const glowMat = new THREE.ShaderMaterial({
      transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.BackSide,
      uniforms: { uColor: { value: new THREE.Color(0xffd27a) } },
      vertexShader: `varying vec3 vN; varying vec3 vV; void main(){ vN=normalize(normalMatrix*normal); vec4 mv=modelViewMatrix*vec4(position,1.0); vV=normalize(-mv.xyz); gl_Position=projectionMatrix*mv;}`,
      fragmentShader: `varying vec3 vN; varying vec3 vV; uniform vec3 uColor; void main(){ float f=pow(1.0-abs(dot(vN,vV)),2.0); gl_FragColor=vec4(uColor*f*2.0, f);}`,
    });
    g.add(new THREE.Mesh(new THREE.SphereGeometry(420, 32, 24), glowMat));
    g.position.copy(this.sunDir.clone().multiplyScalar(5200));
    this.scene.add(g);
    this.sunMesh = g;
  }

  buildSpeedLines() {
    const n = 600;
    const geo = new THREE.BufferGeometry();
    const pos = new Float32Array(n * 6); // start+end per line
    this.speedData = [];
    for (let i = 0; i < n; i++) {
      this.speedData.push(this.randStreak());
    }
    geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    const mat = new THREE.LineBasicMaterial({ color: 0xbfe6ff, transparent: true, opacity: 0.0, blending: THREE.AdditiveBlending, depthWrite: false });
    this.speed = new THREE.LineSegments(geo, mat);
    this.speed.frustumCulled = false;
    // attach to camera space by adding to rig later; we keep in world but reposition each frame
    this.scene.add(this.speed);
  }
  randStreak() {
    const a = Math.random() * Math.PI * 2;
    const r = 6 + Math.random() * 70;
    return { x: Math.cos(a) * r, y: Math.sin(a) * r, z: -20 - Math.random() * 160 };
  }

  buildEntryOverlay() {
    // a quad parented to the camera, filled with the target planet's sky wash + shimmer
    this.entryMat = new THREE.ShaderMaterial({
      transparent: true, depthTest: false, depthWrite: false,
      uniforms: {
        uColor: { value: new THREE.Color(0xff8844) },
        uColor2: { value: new THREE.Color(0xffffff) },
        uAmt: { value: 0 }, uTime: { value: 0 },
      },
      vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0);}`,
      fragmentShader: `varying vec2 vUv; uniform vec3 uColor,uColor2; uniform float uAmt,uTime;
        float h(vec2 p){return fract(sin(dot(p,vec2(12.9,78.2)))*43758.5);}
        void main(){
          vec2 d=vUv-0.5;
          float r=length(d);
          float shimmer = (h(floor(vUv*vec2(60.0,30.0))+floor(uTime*16.0))-0.5)*0.35;
          float ring = smoothstep(0.12, 0.82, r);      // 0 centre -> 1 edge
          float a = clamp(uAmt,0.0,1.0);
          vec3 sky = mix(uColor2, uColor, ring);
          vec3 hot = vec3(1.0, 0.55, 0.28);            // friction heat at the leading edge
          vec3 col = mix(sky, hot, pow(ring,3.0)*0.45) + shimmer*0.25;
          // centre stays clear so the planet + speed-lines read; atmosphere closes in from edges
          float alpha = a * (ring*0.92 + 0.10 + max(shimmer,0.0)*a*0.6);
          gl_FragColor = vec4(col, clamp(alpha,0.0,1.0));
        }`,
    });
    this.entryQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.entryMat);
    this.entryQuad.frustumCulled = false;
    this.entryQuad.renderOrder = 999;
    this.scene.add(this.entryQuad);
  }

  nearest(camPos) {
    let best = null, bd = Infinity;
    for (const pl of this.planets) {
      const d = pl.group.position.distanceTo(camPos) - pl.radius;
      if (d < bd) { bd = d; best = pl; }
    }
    return { planet: best, dist: bd };
  }
  planetIndex(pl) { return this.planets.indexOf(pl); }

  // flight integration. input: {yaw,pitch,thrust(0/1),boost(0/1)}
  update(dt, input, camera) {
    // orientation: apply yaw/pitch as local rotations
    const q = this.rig.quaternion;
    const yawQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), input.yaw * dt * 1.1);
    const pitchQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), input.pitch * dt * 1.1);
    q.multiply(yawQ).multiply(pitchQ);
    // roll toward turn for flavor
    const rollQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -input.yaw * dt * 0.6);
    q.multiply(rollQ);

    this.boost = THREE.MathUtils.lerp(this.boost, input.boost ? 1 : 0, dt * 4);
    const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(q);
    const accel = (input.thrust ? 1 : 0.06) * (140 + this.boost * 520);
    this.vel.addScaledVector(fwd, accel * dt);
    this.vel.multiplyScalar(1 - dt * 0.9); // drag
    this.rig.position.addScaledVector(this.vel, dt);

    this.ship.setBoost(this.boost);

    // chase camera: behind + above the ship, looking ahead
    const camOff = new THREE.Vector3(0, 1.5, 7.0).applyQuaternion(q);
    const desired = this.rig.position.clone().add(camOff);
    camera.position.lerp(desired, 1 - Math.pow(0.0009, dt));
    const lookAt = this.rig.position.clone().add(fwd.clone().multiplyScalar(20));
    camera.lookAt(lookAt);
    // boost FOV kick
    camera.fov = THREE.MathUtils.lerp(camera.fov, 62 + this.boost * 16, dt * 3);
    camera.updateProjectionMatrix();

    this.updateSpeedLines(dt, camera);
    this.planets.forEach((p) => p.update(dt));
    this.entryMat.uniforms.uTime.value += dt;
    if (this.stars) { this.stars.position.copy(camera.position); this.starsBright.position.copy(camera.position); }
  }

  updateSpeedLines(dt, camera) {
    const intensity = Math.max(this.boost, this.vel.length() / 600);
    this.speed.material.opacity = THREE.MathUtils.clamp(intensity * 0.9, 0, 0.85);
    // place streaks in camera space, stretched along -z by intensity
    const len = 2 + intensity * 60;
    const pos = this.speed.geometry.attributes.position.array;
    const m = camera.matrixWorld;
    const a = this._sa || (this._sa = new THREE.Vector3());
    const b = this._sb || (this._sb = new THREE.Vector3());
    for (let i = 0; i < this.speedData.length; i++) {
      const s = this.speedData[i];
      s.z += (40 + intensity * 600) * dt;
      if (s.z > -2) Object.assign(s, this.randStreak());
      a.set(s.x, s.y, s.z).applyMatrix4(m);
      b.set(s.x, s.y, s.z - len).applyMatrix4(m);
      pos[i * 6] = a.x; pos[i * 6 + 1] = a.y; pos[i * 6 + 2] = a.z;
      pos[i * 6 + 3] = b.x; pos[i * 6 + 4] = b.y; pos[i * 6 + 5] = b.z;
    }
    this.speed.geometry.attributes.position.needsUpdate = true;
  }
}
