// surface.js — landed planet scene: terrain, sky, lighting, flora, crystals, particles.
import * as THREE from 'three';
import { Noise, smoothstep, lerp, dotCanvas } from './util.js';
import { scatterFlora, makeCrystalField } from './flora.js';
import { makeSpacePlanet } from './planet.js';

export class Surface {
  constructor(planet, otherPlanets = []) {
    this.planet = planet;
    this.scene = new THREE.Scene();
    const c = planet.colors;

    // fog / haze
    this.scene.fog = new THREE.FogExp2(c.fog.clone().getHex(), planet.fogDensity);

    // ---- height field ----
    const t = planet.terrain;
    this.noise = new Noise(t.seed);
    this._t = t;
    this.heightAt = (x, z) => this._heightAt(x, z);

    // sun direction (shared by sky shader + light) — low for long shadows & drama
    this.sunDir = new THREE.Vector3(0.62, 0.24, 0.42).normalize();

    // ---- terrain mesh ----
    this.size = 440;
    this.buildTerrain();

    // ---- sky dome + sun disc ----
    this.buildSky();

    // ---- lighting ----
    this.sun = new THREE.DirectionalLight(c.sun.getHex(), planet.sunIntensity);
    this.sun.position.copy(this.sunDir.clone().multiplyScalar(120));
    this.sun.castShadow = true;
    this.sun.shadow.mapSize.set(2048, 2048);
    const cam = this.sun.shadow.camera;
    cam.near = 1; cam.far = 320; cam.left = -90; cam.right = 90; cam.top = 90; cam.bottom = -90;
    this.sun.shadow.bias = -0.0004;
    this.scene.add(this.sun);
    this.scene.add(this.sun.target);

    this.hemi = new THREE.HemisphereLight(c.skyHorizon.getHex(), c.groundLow.getHex(), planet.ambientIntensity);
    this.scene.add(this.hemi);
    this.fill = new THREE.AmbientLight(c.ambient.getHex(), planet.ambientIntensity * 0.35);
    this.scene.add(this.fill);

    // ---- flora + crystals ----
    this.flora = scatterFlora(this.scene, planet, this.heightAt, { area: this.size * 0.82, count: 420 });
    this.crystals = makeCrystalField(this.scene, planet, this.heightAt, { area: this.size * 0.8, count: 80 });

    // ---- drifting spore particles ----
    this.buildParticles();

    // ---- sky planets (the system's other worlds) ----
    this.skyPlanets = [];
    this.buildSkyPlanets(otherPlanets);

    // ---- passive drifting jelly (stretch flourish) ----
    this.buildJellies();

    this.shipGroup = null;
    this.time = 0;
  }

  _heightAt(x, z) {
    const t = this._t;
    const nx = x * 0.01 * t.freq, nz = z * 0.01 * t.freq;
    const roll = this.noise.fbm2(nx, nz, t.oct);
    const ridg = this.noise.ridged2(nx * 0.7 + 11, nz * 0.7 - 7, t.oct);
    let h = lerp(roll, ridg, t.ridged) * t.amp;
    // big gentle swell for parallax hills
    h += this.noise.fbm2(nx * 0.25 + 30, nz * 0.25 - 20, 3) * t.amp * 0.8;
    // distant ridge silhouettes for horizon drama (kicks in away from spawn)
    const d = Math.sqrt(x * x + z * z);
    const farRidge = (this.noise.ridged2(nx * 0.16 + 50, nz * 0.16 - 40, 3) * 0.5 + 0.5);
    h += farRidge * farRidge * t.amp * 1.1 * smoothstep(40, 150, d);
    // flatten near landing origin
    const flat = smoothstep(7, 34, d);
    h *= 0.28 + 0.72 * flat;
    return h;
  }

  buildTerrain() {
    const seg = 220;
    const geo = new THREE.PlaneGeometry(this.size, this.size, seg, seg);
    geo.rotateX(-Math.PI / 2);
    const pos = geo.attributes.position;
    const c = this.planet.colors;
    const colors = new Float32Array(pos.count * 3);
    const tmp = new THREE.Color();
    let maxH = 1;
    for (let i = 0; i < pos.count; i++) maxH = Math.max(maxH, this._heightAt(pos.getX(i), pos.getZ(i)));
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i), z = pos.getZ(i);
      const y = this._heightAt(x, z);
      pos.setY(i, y);
      const e = 1.2;
      const slope = (Math.abs(this._heightAt(x + e, z) - this._heightAt(x - e, z)) +
        Math.abs(this._heightAt(x, z + e) - this._heightAt(x, z - e))) / (4 * e);
      const hn = THREE.MathUtils.clamp(y / maxH, 0, 1);
      tmp.copy(c.groundLow).lerp(c.groundHigh, smoothstep(0.15, 0.85, hn));
      tmp.lerp(c.rock, smoothstep(0.45, 1.1, slope));
      // subtle noise mottling
      const mott = 0.92 + 0.16 * this.noise.fbm2(x * 0.08, z * 0.08, 2);
      tmp.multiplyScalar(mott);
      colors[i * 3] = tmp.r; colors[i * 3 + 1] = tmp.g; colors[i * 3 + 2] = tmp.b;
    }
    geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geo.computeVertexNormals();
    const mat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95, metalness: 0.0 });
    this.terrain = new THREE.Mesh(geo, mat);
    this.terrain.receiveShadow = true;
    this.terrain.castShadow = false;
    this.scene.add(this.terrain);
    this.maxH = maxH;
  }

  buildSky() {
    const c = this.planet.colors;
    const mat = new THREE.ShaderMaterial({
      side: THREE.BackSide, fog: false, depthWrite: false,
      uniforms: {
        uTop: { value: c.skyTop.clone() },
        uHorizon: { value: c.skyHorizon.clone() },
        uCloud: { value: c.skyTop.clone().lerp(new THREE.Color(0xffffff), 0.35) },
        uSunDir: { value: this.sunDir.clone() },
        uSunColor: { value: c.sun.clone() },
        uTime: { value: 0 },
      },
      vertexShader: /* glsl */`
        varying vec3 vDir;
        void main(){ vDir = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
      `,
      fragmentShader: /* glsl */`
        varying vec3 vDir; uniform vec3 uTop, uHorizon, uCloud, uSunDir, uSunColor; uniform float uTime;
        float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
        float vnoise(vec2 p){ vec2 i=floor(p), f=fract(p); f=f*f*(3.0-2.0*f);
          return mix(mix(hash(i),hash(i+vec2(1,0)),f.x), mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x), f.y); }
        float fbm(vec2 p){ float a=0.5,s=0.0; for(int i=0;i<4;i++){ s+=a*vnoise(p); p*=2.0; a*=0.5;} return s; }
        void main(){
          vec3 dir = normalize(vDir);
          float h = clamp(dir.y*1.15+0.05, 0.0, 1.0);
          vec3 col = mix(uHorizon, uTop, pow(h, 0.7));
          // drifting wispy cloud bands, strongest in the mid-sky, fading at zenith & horizon
          vec2 sp = vec2(atan(dir.z, dir.x)*1.1, dir.y*2.6);
          float cl = fbm(sp*2.0 + vec2(uTime*0.012, 0.0));
          float band = smoothstep(0.40, 0.72, cl) * smoothstep(0.02, 0.32, h) * smoothstep(0.95, 0.45, h);
          col = mix(col, uCloud, band*0.7);
          // sun glow — tight so it doesn't wash the sky
          float s = max(dot(dir, normalize(uSunDir)), 0.0);
          col += uSunColor * pow(s, 380.0) * 3.2;      // disc
          col += uSunColor * pow(s, 40.0) * 0.32;      // tight halo
          // gentle horizon band
          col += uHorizon * pow(1.0 - h, 3.0) * 0.22;
          gl_FragColor = vec4(col, 1.0);
        }
      `,
    });
    this.skyMat = mat;
    this.sky = new THREE.Mesh(new THREE.SphereGeometry(2200, 32, 24), mat);
    this.scene.add(this.sky);
  }

  buildParticles() {
    const n = 1400;
    const geo = new THREE.BufferGeometry();
    const pos = new Float32Array(n * 3);
    this._pSpan = 120;
    for (let i = 0; i < n; i++) {
      pos[i * 3] = (Math.random() - 0.5) * this._pSpan;
      pos[i * 3 + 1] = Math.random() * 40;
      pos[i * 3 + 2] = (Math.random() - 0.5) * this._pSpan;
    }
    geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    const tex = new THREE.CanvasTexture(dotCanvas());
    const mat = new THREE.PointsMaterial({
      map: tex, color: this.planet.colors.crystal.clone().lerp(new THREE.Color(0xffffff), 0.3),
      size: 0.7, transparent: true, opacity: 0.5,
      blending: THREE.AdditiveBlending, depthWrite: false, fog: true, sizeAttenuation: true,
    });
    this.particles = new THREE.Points(geo, mat);
    this.particles.frustumCulled = false;
    this.scene.add(this.particles);
  }

  buildSkyPlanets(others) {
    others.slice(0, 2).forEach((pd, i) => {
      const pl = makeSpacePlanet(pd, 60 + i * 25);
      const ang = -0.6 + i * 1.1;
      pl.group.position.set(Math.sin(ang) * 900, 380 + i * 90, -Math.cos(ang) * 900 - 200);
      pl.setSun(this.sunDir.clone());
      // disable fog on these by marking materials (shader already fog:false)
      this.scene.add(pl.group);
      this.skyPlanets.push(pl);
    });
  }

  buildJellies() {
    this.jellies = [];
    const mat = new THREE.MeshStandardMaterial({
      color: this.planet.colors.atmosphere.clone(), transparent: true, opacity: 0.4,
      emissive: this.planet.colors.atmosphere.clone(), emissiveIntensity: 1.2, roughness: 0.4, depthWrite: false,
    });
    for (let i = 0; i < 3; i++) {
      const g = new THREE.Group();
      const dome = new THREE.Mesh(new THREE.SphereGeometry(1.6, 16, 12, 0, Math.PI * 2, 0, Math.PI * 0.6), mat);
      dome.scale.set(1, 0.7, 1);
      g.add(dome);
      for (let k = 0; k < 6; k++) {
        const ang = (k / 6) * Math.PI * 2;
        const tent = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.12, 3, 5), mat);
        tent.position.set(Math.cos(ang) * 0.8, -1.4, Math.sin(ang) * 0.8);
        g.add(tent);
      }
      const x = (Math.random() - 0.5) * 120, z = (Math.random() - 0.5) * 120;
      g.position.set(x, this._heightAt(x, z) + 12 + Math.random() * 8, z);
      g.userData = { phase: Math.random() * 6.28, baseY: g.position.y, sx: x, sz: z };
      this.scene.add(g);
      this.jellies.push(g);
    }
  }

  setShip(group) {
    if (this.shipGroup) this.scene.remove(this.shipGroup);
    this.shipGroup = group;
    if (group) this.scene.add(group);
  }

  get scannables() {
    return [...this.flora.scannables, ...this.crystals.scannables];
  }

  update(dt, camPos) {
    this.time += dt;
    if (this.skyMat) this.skyMat.uniforms.uTime.value = this.time;
    // particle drift + recycle around camera
    if (this.particles && camPos) {
      const p = this.particles.geometry.attributes.position;
      const span = this._pSpan, half = span / 2;
      for (let i = 0; i < p.count; i++) {
        let x = p.getX(i) + Math.sin(this.time * 0.3 + i) * 0.01 + dt * 1.2;
        let y = p.getY(i) + dt * 0.6;
        let z = p.getZ(i) + Math.cos(this.time * 0.2 + i) * 0.01;
        if (y > 42) y = 0;
        // wrap relative to camera
        if (x - camPos.x > half) x -= span; else if (x - camPos.x < -half) x += span;
        if (z - camPos.z > half) z -= span; else if (z - camPos.z < -half) z += span;
        p.setXYZ(i, x, y, z);
      }
      p.needsUpdate = true;
    }
    this.skyPlanets.forEach((pl) => pl.update(dt));
    for (const j of this.jellies) {
      const u = j.userData;
      u.phase += dt * 0.5;
      j.position.y = u.baseY + Math.sin(u.phase) * 1.5;
      j.position.x = u.sx + Math.sin(u.phase * 0.3) * 6;
      j.rotation.y += dt * 0.1;
    }
  }
}
