import * as THREE from 'three';
import { terrainHeight, createPlanetMesh, makePlanetSpec } from './planet.js';
import { createFlora, createMinerals } from './flora.js';
import { fbm2, valueNoise2 } from './noise.js';

function particleTex() {
  const c = document.createElement('canvas'); c.width = c.height = 32;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
  g.addColorStop(0, 'rgba(255,255,255,1)'); g.addColorStop(0.5, 'rgba(255,255,255,0.4)'); g.addColorStop(1, 'rgba(255,255,255,0)');
  ctx.fillStyle = g; ctx.fillRect(0, 0, 32, 32);
  const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t;
}

function skyDome(spec, sunDir) {
  const mat = new THREE.ShaderMaterial({
    uniforms: {
      uTop: { value: spec.palette.skyTop },
      uHorizon: { value: spec.palette.skyHorizon },
      uSunDir: { value: sunDir.clone() },
      uSunCol: { value: new THREE.Color().copy(spec.palette.sun).lerp(new THREE.Color(0xffffff), 0.5) },
      uExp: { value: 0.0 }, // entry-blend (0=normal)
    },
    vertexShader: `varying vec3 vDir;
      void main(){ vDir = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
    fragmentShader: `
      uniform vec3 uTop; uniform vec3 uHorizon; uniform vec3 uSunDir; uniform vec3 uSunCol;
      varying vec3 vDir;
      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); vec2 u=f*f*(3.0-2.0*f);
        return mix(mix(hash(i),hash(i+vec2(1,0)),u.x), mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),u.x), u.y); }
      float fbm(vec2 p){ float s=0.0,a=0.5; for(int i=0;i<5;i++){ s+=a*vnoise(p); p*=2.03; a*=0.5; } return s; }
      void main(){
        vec3 dir = normalize(vDir);
        float t = clamp(dir.y, 0.0, 1.0);
        vec3 col = mix(uHorizon, uTop, pow(t, 0.65));
        float sd = max(dot(dir, normalize(uSunDir)), 0.0);
        // procedural cloud bands projected onto the dome
        vec2 cuv = dir.xz / (dir.y + 0.28);
        float cloud = fbm(cuv * 2.2 + vec2(3.0, 1.0));
        cloud = smoothstep(0.52, 0.95, cloud) * smoothstep(0.02, 0.4, t) * (1.0 - t * 0.5);
        vec3 cloudCol = mix(uHorizon, uSunCol, 0.5) * 1.25;
        col = mix(col, cloudCol, cloud * 0.7);
        col += uSunCol * pow(sd, 220.0) * 1.8;          // sun disc
        col += uSunCol * pow(sd, 6.0) * 0.30;           // sun haze
        col += uSunCol * cloud * pow(sd, 2.0) * 0.25;   // sun-lit cloud edge
        col = mix(col, uHorizon * 1.12, pow(1.0 - t, 4.0) * 0.45); // horizon glow
        gl_FragColor = vec4(col, 1.0);
      }`,
    side: THREE.BackSide, depthWrite: false, fog: false,
  });
  const dome = new THREE.Mesh(new THREE.SphereGeometry(3200, 32, 20), mat);
  dome.name = 'sky';
  return dome;
}

export function createSurface(spec, systemSpecs, sunDir) {
  const scene = new THREE.Scene();
  const pal = spec.palette;
  const heightAt = (x, z) => terrainHeight(spec, x, z);

  // Haze matches the horizon sky so distant terrain dissolves into the horizon (not a teal wall).
  scene.fog = new THREE.FogExp2(pal.skyHorizon.clone().lerp(pal.fog, 0.4), spec.fogDensity);

  // --- sky ---
  const sky = skyDome(spec, sunDir);
  scene.add(sky);

  // --- terrain ---
  const SIZE = 640, SEG = 200;
  const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEG, SEG);
  geo.rotateX(-Math.PI / 2);
  const pos = geo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i), z = pos.getZ(i);
    pos.setY(i, heightAt(x, z));
  }
  geo.computeVertexNormals();
  // vertex colors from height + slope
  const colors = new Float32Array(pos.count * 3);
  const nrm = geo.attributes.normal;
  const c = new THREE.Color();
  let minH = Infinity, maxH = -Infinity;
  for (let i = 0; i < pos.count; i++) { const y = pos.getY(i); if (y < minH) minH = y; if (y > maxH) maxH = y; }
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i), z = pos.getZ(i), y = pos.getY(i);
    const t = (y - minH) / Math.max(0.001, maxH - minH);
    const slope = 1 - nrm.getY(i); // 0 flat, ->1 steep
    c.copy(pal.groundLow).lerp(pal.groundMid, Math.min(1, t * 1.6));
    c.lerp(pal.groundHigh, Math.max(0, t - 0.55) * 2.0);
    c.lerp(pal.rock, Math.min(1, slope * 2.4));
    // subtle per-vertex variation
    const v = valueNoise2(x * 0.08, z * 0.08, spec.seed + 5) * 0.18 - 0.09;
    c.offsetHSL(0, 0, v);
    colors[i * 3] = c.r; colors[i * 3 + 1] = c.g; colors[i * 3 + 2] = c.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  const terrainMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95, metalness: 0.02 });
  const terrain = new THREE.Mesh(geo, terrainMat);
  terrain.receiveShadow = true;
  terrain.name = 'terrain';
  scene.add(terrain);

  // --- lighting (strong sun + sky-tinted ambient, warm/cool) ---
  const sun = new THREE.DirectionalLight(new THREE.Color().copy(pal.sun).lerp(new THREE.Color(0xffffff), 0.55), 3.3);
  sun.position.copy(sunDir).multiplyScalar(300);
  sun.castShadow = true;
  sun.shadow.mapSize.set(2048, 2048);
  sun.shadow.camera.near = 10; sun.shadow.camera.far = 600;
  const sc = 160;
  sun.shadow.camera.left = -sc; sun.shadow.camera.right = sc;
  sun.shadow.camera.top = sc; sun.shadow.camera.bottom = -sc;
  sun.shadow.bias = -0.0004;
  scene.add(sun);
  scene.add(sun.target);
  // keep the sun dominant so ground keeps its own hue; soft sky bounce only
  const hemi = new THREE.HemisphereLight(pal.skyHorizon.clone().lerp(new THREE.Color(0xffffff), 0.25), pal.groundLow.clone(), 0.32);
  scene.add(hemi);
  scene.add(new THREE.AmbientLight(pal.ambient.clone().lerp(new THREE.Color(0xffffff), 0.3), 0.16));

  // --- sibling planets in the sky ---
  const skyPlanets = new THREE.Group();
  let added = 0;
  systemSpecs.forEach((s, i) => {
    if (s.seed === spec.seed) return;
    if (added >= 2) return;
    const r = 70 + added * 18;
    const pm = createPlanetMesh(s, r);
    pm.traverse((o) => { if (o.material) o.material.fog = false; });
    const ang = (added === 0 ? 0.8 : -1.4);
    const elev = added === 0 ? 0.5 : 0.34;
    pm.position.set(Math.cos(ang) * 1400, 360 + elev * 500, Math.sin(ang) * 1400);
    skyPlanets.add(pm);
    added++;
  });
  scene.add(skyPlanets);

  // --- drifting spores ---
  const SP = 520;
  const sp = new Float32Array(SP * 3);
  const sv = new Float32Array(SP * 3);
  for (let i = 0; i < SP; i++) {
    sp[i * 3] = (Math.random() - 0.5) * 180;
    sp[i * 3 + 1] = Math.random() * 40 + 2;
    sp[i * 3 + 2] = (Math.random() - 0.5) * 180;
    sv[i * 3] = (Math.random() - 0.5) * 1.2;
    sv[i * 3 + 1] = Math.random() * 0.6 + 0.1;
    sv[i * 3 + 2] = (Math.random() - 0.5) * 1.2;
  }
  const spGeo = new THREE.BufferGeometry();
  spGeo.setAttribute('position', new THREE.BufferAttribute(sp, 3));
  const spMat = new THREE.PointsMaterial({
    size: 0.6, map: particleTex(), color: pal.crystalEmissive.clone().lerp(new THREE.Color(0xffffff), 0.4),
    transparent: true, opacity: 0.8, depthWrite: false, blending: THREE.AdditiveBlending, sizeAttenuation: true,
  });
  const spores = new THREE.Points(spGeo, spMat);
  spores.frustumCulled = false;
  scene.add(spores);

  // --- flora + minerals ---
  const flora = createFlora(spec, heightAt, { radius: 235, inner: 7, clusters: 205 });
  scene.add(flora);
  const minerals = createMinerals(spec, heightAt, { radius: 215, inner: 9, count: 58 });
  scene.add(minerals);

  // --- parked ship placed on terrain ---
  return {
    scene, heightAt, terrain, sky, sun, spores, flora, minerals, skyPlanets,
    sporeData: { pos: sp, vel: sv, geo: spGeo },
    update(dt, camPos) {
      // spores drift + wrap around camera
      const p = this.sporeData.pos, v = this.sporeData.vel;
      for (let i = 0; i < SP; i++) {
        p[i * 3] += v[i * 3] * dt; p[i * 3 + 1] += v[i * 3 + 1] * dt; p[i * 3 + 2] += v[i * 3 + 2] * dt;
        if (p[i * 3 + 1] > 46) p[i * 3 + 1] = 1;
        const dx = p[i * 3] - camPos.x, dz = p[i * 3 + 2] - camPos.z;
        if (dx > 90) p[i * 3] -= 180; else if (dx < -90) p[i * 3] += 180;
        if (dz > 90) p[i * 3 + 2] -= 180; else if (dz < -90) p[i * 3 + 2] += 180;
      }
      this.sporeData.geo.attributes.position.needsUpdate = true;
      sky.position.copy(camPos);
      for (const o of skyPlanets.children) {
        if (o.userData.surf) o.userData.surf.rotation.y += o.userData.spec.rotSpeed * dt;
      }
    },
  };
}

// First-person walker on the heightfield.
export class FPController {
  constructor(heightAt, gravity) {
    this.heightAt = heightAt;
    this.gravity = gravity;
    this.pos = new THREE.Vector3();
    this.yaw = 0; this.pitch = 0;
    this.vy = 0;
    this.eye = 1.75;
    this.grounded = true;
    this.speed = 9.5;
    this.tmp = new THREE.Vector3();
  }
  setSpawn(x, z, yaw) {
    this.pos.set(x, this.heightAt(x, z) + this.eye, z);
    this.yaw = yaw || 0; this.pitch = -0.05;
  }
  look(dx, dy) {
    this.yaw -= dx * 0.0022;
    this.pitch -= dy * 0.0022;
    this.pitch = Math.max(-1.4, Math.min(1.3, this.pitch));
  }
  update(dt, input) {
    const fwd = new THREE.Vector3(-Math.sin(this.yaw), 0, -Math.cos(this.yaw));
    const right = new THREE.Vector3(Math.cos(this.yaw), 0, -Math.sin(this.yaw));
    const move = this.tmp.set(0, 0, 0);
    if (input.f) move.add(fwd);
    if (input.b) move.sub(fwd);
    if (input.l) move.sub(right);
    if (input.r) move.add(right);
    if (move.lengthSq() > 0) move.normalize().multiplyScalar(this.speed * dt);
    this.pos.x += move.x; this.pos.z += move.z;

    const ground = this.heightAt(this.pos.x, this.pos.z) + this.eye;
    if (this.grounded) {
      this.pos.y = ground;
      if (input.jump) { this.vy = Math.sqrt(2 * this.gravity * 2.6); this.grounded = false; }
    } else {
      this.vy -= this.gravity * dt;
      this.pos.y += this.vy * dt;
      if (this.pos.y <= ground) { this.pos.y = ground; this.vy = 0; this.grounded = true; }
    }
  }
  applyTo(camera) {
    camera.position.copy(this.pos);
    camera.rotation.set(0, 0, 0);
    camera.rotateY(this.yaw);
    camera.rotateX(this.pitch);
  }
}
