import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { GRID, STRUCT, ZONE, BUILDINGS } from '../config.js';
import { getGeoCache, matBuilding } from './geometry.js';
import { Traffic } from './cars.js';
import { mulberry32, clamp } from '../utils.js';

const WORLD = GRID;
const HALF = WORLD / 2;
const UP_AXIS = new THREE.Vector3(0, 1, 0);

// ============================================================
// Main 3D renderer: ground, water, instanced city pools,
// day/night cycle, camera controls and picking helpers.
// ============================================================
export class Renderer {
  constructor(container, city, settings) {
    this.city = city;
    this.settings = settings;

    const w = container.clientWidth, h = container.clientHeight;
    // preserveDrawingBuffer keeps the frame readable for user screenshots
    // (right-click save / "share your city") and headless captures.
    this.renderer3 = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
    this.renderer3.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer3.setSize(w, h);
    this.renderer3.shadowMap.enabled = !!settings.shadows;
    this.renderer3.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer3.toneMapping = THREE.ACESFilmicToneMapping;
    this.renderer3.toneMappingExposure = 1.12;
    container.appendChild(this.renderer3.domElement);
    this.domElement = this.renderer3.domElement;

    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color('#86b7dd');
    this.scene.fog = new THREE.Fog('#86b7dd', 70, 260);

    this.camera = new THREE.PerspectiveCamera(55, w / h, 0.1, 600);
    this.camera.position.set(0, 52, 58);

    this.controls = new OrbitControls(this.camera, this.renderer3.domElement);
    this.controls.enableDamping = true;
    this.controls.dampingFactor = 0.08;
    this.controls.minDistance = 9;
    this.controls.maxDistance = 170;
    this.controls.maxPolarAngle = 1.32;
    this.controls.target.set(0, 0, 0);
    this.controls.mouseButtons = {
      LEFT: -1,                   // left is reserved for painting tools (input.js)
      MIDDLE: THREE.MOUSE.PAN,
      RIGHT: THREE.MOUSE.ROTATE
    };
    this.controls.touches = { ONE: -1, TWO: THREE.TOUCH.DOLLY_ROTATE };
    this.controls.zoomSpeed = 0.9;
    this.controls.panSpeed = 0.9;

    // lights
    this.hemi = new THREE.HemisphereLight('#cfe8ff', '#5a7050', 0.85);
    this.scene.add(this.hemi);
    this.sun = new THREE.DirectionalLight('#fff3d6', 1.25);
    this.sun.position.set(40, 60, 20);
    if (settings.shadows) {
      this.sun.castShadow = true;
      this.sun.shadow.mapSize.set(2048, 2048);
      const sc = this.sun.shadow.camera;
      sc.left = -50; sc.right = 50; sc.top = 50; sc.bottom = -50;
      sc.near = 10; sc.far = 220;
      this.sun.shadow.bias = -0.0004;
    }
    this.scene.add(this.sun);

    // world
    this._buildGround();
    this._buildWater();
    this._buildRoadGeos();

    // zone overlay quad + materials
    const q = new THREE.PlaneGeometry(0.94, 0.94);
    q.rotateX(-Math.PI / 2);
    q.translate(0, 0.03, 0);
    this.quadGeo = q;
    const overlayMat = (hex) => new THREE.MeshBasicMaterial({
      map: makeZoneTexture(hex), transparent: true, depthWrite: false
    });
    this.zoneOverlayMats = {
      [ZONE.RES]: overlayMat('#35e08a'),
      [ZONE.COM]: overlayMat('#4aa8ff'),
      [ZONE.IND]: overlayMat('#ffb020')
    };

    // instanced pools
    this.geoCache = getGeoCache();
    this.pools = new Map();

    // scenery jitter table
    const srng = mulberry32(city.seed ^ 0x77aa11);
    this.scenJitter = new Float32Array(city.grid.n * 4);
    for (let i = 0; i < city.grid.n * 4; i++) this.scenJitter[i] = srng();

    // cursor helpers
    const hlPts = [
      new THREE.Vector3(-0.5, 0.09, -0.5), new THREE.Vector3(0.5, 0.09, -0.5),
      new THREE.Vector3(0.5, 0.09, 0.5), new THREE.Vector3(-0.5, 0.09, 0.5),
    ];
    this.hoverMesh = new THREE.LineLoop(
      new THREE.BufferGeometry().setFromPoints(hlPts),
      new THREE.LineBasicMaterial({ color: '#ffffff', transparent: true, opacity: 0.9 })
    );
    this.hoverMesh.visible = false;
    this.scene.add(this.hoverMesh);

    this.dragRect = new THREE.Mesh(
      new THREE.BoxGeometry(1, 0.05, 1),
      new THREE.MeshBasicMaterial({ color: '#4ad06a', transparent: true, opacity: 0.32, depthWrite: false })
    );
    this.dragRect.visible = false;
    this.scene.add(this.dragRect);

    this.ghost = null;
    this.ghostKey = null;

    // traffic
    this.traffic = new Traffic(this.scene, city);

    // scratch
    this.timeOfDay = 0.30;
    this._m = new THREE.Matrix4();
    this._q = new THREE.Quaternion();
    this._v = new THREE.Vector3();
    this._s = new THREE.Vector3();
    this.skyDay = new THREE.Color('#86b7dd');
    this.skyNight = new THREE.Color('#0b1026');
    this._skyTmp = new THREE.Color();

    this._ac = new AbortController();
    window.addEventListener('resize', () => this.resize(container), { signal: this._ac.signal });
  }

  dispose() {
    try {
      this._ac.abort();
      this.renderer3.dispose();
      this.domElement.remove();
      for (const [, p] of this.pools) p.mesh.dispose();
      this.scene.traverse(o => {
        if (o.geometry) o.geometry.dispose();
        if (o.material && o.material !== matBuilding) o.material.dispose?.();
      });
    } catch { /* best effort */ }
  }

  resize(container) {
    const w = container.clientWidth, h = container.clientHeight;
    this.camera.aspect = w / h;
    this.camera.updateProjectionMatrix();
    this.renderer3.setSize(w, h);
  }

  // ---------------- static world ----------------

  _buildGround() {
    const segs = WORLD;
    const geo = new THREE.PlaneGeometry(WORLD, WORLD, segs, segs);
    geo.rotateX(-Math.PI / 2);
    const pos = geo.attributes.position;
    const colors = new Float32Array(pos.count * 3);
    const g = this.city.grid;
    const noise = mulberry32(this.city.seed ^ 0x55);
    const shade = [];
    for (let i = 0; i < 64; i++) shade.push(noise());
    const cGrassA = new THREE.Color('#69a84f'), cGrassB = new THREE.Color('#82bd63');
    const cSand = new THREE.Color('#d9c98e'), cWaterDeep = new THREE.Color('#17456e');
    const cSkirt = new THREE.Color('#2c62a8');
    const tmp = new THREE.Color();
    for (let vi = 0; vi < pos.count; vi++) {
      const x = Math.round(pos.getX(vi) + HALF - 0.5);
      const z = Math.round(pos.getZ(vi) + HALF - 0.5);
      if (!g.inB(x, z)) { tmp.copy(cSkirt); }
      else {
        const i = g.idx(x, z);
        if (g.terrain[i] === 1) tmp.copy(cWaterDeep);
        else {
          const n = shade[(x * 7 + z * 13) & 63];
          tmp.lerpColors(cGrassA, cGrassB, n);
          let nearWater = false;
          for (const [dx, dz] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
            const nx = x + dx, nz = z + dz;
            if (g.inB(nx, nz) && g.terrain[g.idx(nx, nz)] === 1) { nearWater = true; break; }
          }
          if (nearWater) tmp.lerp(cSand, 0.65);
        }
      }
      colors[vi * 3] = tmp.r; colors[vi * 3 + 1] = tmp.g; colors[vi * 3 + 2] = tmp.b;
    }
    geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95, metalness: 0 }));
    mesh.receiveShadow = !!this.settings.shadows;
    this.scene.add(mesh);
  }

  _buildWater() {
    const g = this.city.grid;
    const parts = [];
    for (let z = 0; z < WORLD; z++) {
      for (let x = 0; x < WORLD; x++) {
        if (g.terrain[g.idx(x, z)] !== 1) continue;
        const q = new THREE.PlaneGeometry(1.002, 1.002);
        q.rotateX(-Math.PI / 2);
        q.translate(x - HALF + 0.5, 0.07, z - HALF + 0.5);
        parts.push(q);
      }
    }
    if (!parts.length) return;
    const merged = mergeGeometries(parts.map(p => (p.index ? p.toNonIndexed() : p)));
    parts.forEach(p => p.dispose());
    const mat = new THREE.MeshStandardMaterial({
      color: '#2f6fb3', transparent: true, opacity: 0.92,
      roughness: 0.28, metalness: 0.15
    });
    this.waterMesh = new THREE.Mesh(merged, mat);
    this.scene.add(this.waterMesh);
  }

  _buildRoadGeos() {
    const mk = (w, h, d, c, x, y, z) => coloredBox(w, h, d, c, x, y, z);
    this.geoRoad = mergeGeometries([
      mk(0.96, 0.05, 0.96, rgbHex('#3b3e44'), 0, 0, 0),
      mk(0.08, 0.052, 0.96, rgbHex('#5a5e66'), -0.44, 0, 0),
      mk(0.08, 0.052, 0.96, rgbHex('#5a5e66'), 0.44, 0, 0)
    ].map(p => (p.index ? p.toNonIndexed() : p)));

    this.geoRoadBridge = mergeGeometries([
      mk(0.98, 0.07, 0.98, rgbHex('#84878c'), 0, 0, 0),
      mk(0.98, 0.05, 0.05, rgbHex('#9aa0a6'), 0, 0.09, -0.46),
      mk(0.98, 0.05, 0.05, rgbHex('#9aa0a6'), 0, 0.09, 0.46),
      mk(0.08, 0.45, 0.08, rgbHex('#6f7378'), -0.3, -0.45, 0),
      mk(0.08, 0.45, 0.08, rgbHex('#6f7378'), 0.3, -0.45, 0)
    ].map(p => (p.index ? p.toNonIndexed() : p)));

    this.geoDash = mk(0.36, 0.006, 0.07, [1.5, 1.45, 1.2], 0, 0.055, 0);
  }

  // ---------------- instanced pools ----------------

  _ensurePool(key, geometry, material, count) {
    let p = this.pools.get(key);
    if (!p || p.cap < count || p.cap > count * 3 + 24) {
      if (p) { this.scene.remove(p.mesh); p.mesh.dispose(); }
      const cap = Math.max(count, 8);
      const mesh = new THREE.InstancedMesh(geometry, material, cap);
      mesh.count = count;
      mesh.castShadow = !!this.settings.shadows && key !== 'dash' && !key.startsWith('zoneov');
      mesh.receiveShadow = !!this.settings.shadows;
      mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(cap * 3).fill(1), 3);
      mesh.frustumCulled = false;
      this.scene.add(mesh);
      p = { mesh, cap };
      this.pools.set(key, p);
    } else {
      p.mesh.count = count;
    }
    return p;
  }

  tileWorld(x, z) { return [x - HALF + 0.5, z - HALF + 0.5]; }

  /** Rebuild all instanced content from the grid. Runs on any city change. */
  sync() {
    const city = this.city, g = city.grid, S = g.size;
    const buckets = new Map();
    // entry layout: [wx, wz, rotY, sx, sy, sz, tr, tg, tb]
    const push = (key, wx, wz, rotY = 0, sx = 1, sy = 1, sz = 1, tr = 1, tg = 1, tb = 1) => {
      let arr = buckets.get(key);
      if (!arr) { arr = []; buckets.set(key, arr); }
      arr.push(wx, wz, rotY, sx, sy, sz, tr, tg, tb);
    };
    const TINT_BURN = [2.1, 0.85, 0.3];
    const TINT_ABANDONED = [0.42, 0.4, 0.38];
    const TINT_NOPOWER = [0.72, 0.74, 0.82];
    let burningCount = 0;

    for (let i = 0; i < g.n; i++) {
      const s = g.struct[i];
      const x = i % S, z = (i - x) / S;
      const [wx, wz] = this.tileWorld(x, z);

      if (s !== 0 && s !== STRUCT.ROAD && g.anchor[i] !== i) continue;

      let tint = null;
      if (g.burning[i]) { burningCount++; tint = TINT_BURN; }
      else if (g.isDeveloped(i)) {
        const abandoned = g.badMonths[i] >= 2 && !g.powered[i];
        if (abandoned) tint = TINT_ABANDONED;
        else if (!g.powered[i]) tint = TINT_NOPOWER;
      }

      if (g.rubble[i]) { push('rubble', wx, wz, 0, 1, 1, 1); continue; }

      if (s === STRUCT.ROAD) {
        const bridge = g.terrain[i] === 1;
        if (bridge) push('road_bridge', wx, wz);
        else push('road', wx, wz);
        if (!bridge) {
          const N = g.inB(x, z - 1) && g.struct[g.idx(x, z - 1)] === STRUCT.ROAD;
          const So = g.inB(x, z + 1) && g.struct[g.idx(x, z + 1)] === STRUCT.ROAD;
          const E = g.inB(x + 1, z) && g.struct[g.idx(x + 1, z)] === STRUCT.ROAD;
          const W = g.inB(x - 1, z) && g.struct[g.idx(x - 1, z)] === STRUCT.ROAD;
          if ((N && So && !E && !W)) push('dash', wx, wz, 0);
          else if ((E && W && !N && !So)) push('dash', wx, wz, Math.PI / 2);
        }
        continue;
      }

      if (s !== 0) {
        const a = g.anchor[i];
        const ax = a % S, az = (a - ax) / S;
        const meta = BUILDINGS[s];
        const cx = ax + (meta.w - 1) / 2, cz = az + (meta.h - 1) / 2;
        const [cwx, cwz] = this.tileWorld(cx, cz);
        push(meta.id, cwx, cwz, 0, 1, 1, 1, ...(tint || ONES));
        continue;
      }

      if (g.zone[i] !== ZONE.NONE) {
        if (g.level[i] > 0) {
          const nameKey = ['res', 'com', 'ind'][g.zone[i] - 1];
          push(`${nameKey}_${g.level[i]}_${g.variant[i] % 3}`, wx, wz, 0, 1, 1, 1, ...(tint || ONES));
        } else {
          push(`zoneov_${g.zone[i]}`, wx, wz);
        }
        continue;
      }

      // untouched land scenery
      if (g.terrain[i] === 0 && !g.burning[i] && g.scenery[i]) {
        const sc = g.scenery[i];
        const j = i * 4, jt = this.scenJitter;
        const jx = (jt[j] - 0.5) * 0.34, jz = (jt[j + 1] - 0.5) * 0.34;
        const rot = jt[j + 2] * Math.PI * 2;
        const scl = 0.85 + jt[j + 3] * 0.4;
        const key = sc === 3 ? 'rock' : sc === 2 ? 'tree2' : 'tree';
        push(key, wx + jx, wz + jz, rot, scl, scl, scl);
      }
    }
    this.burningTilesCount = burningCount;

    for (const [key, flat] of buckets) {
      const count = flat.length / 9;
      let geometry, material = matBuilding;
      if (key.startsWith('zoneov_')) {
        geometry = this.quadGeo;
        material = this.zoneOverlayMats[+key.slice(7)];
      } else if (key === 'road') geometry = this.geoRoad;
      else if (key === 'road_bridge') geometry = this.geoRoadBridge;
      else if (key === 'dash') geometry = this.geoDash;
      else geometry = this.geoCache[key];
      if (!geometry) continue;

      const p = this._ensurePool(key, geometry, material, count);
      const mesh = p.mesh;
      for (let k = 0; k < count; k++) {
        const o = k * 9;
        this._v.set(flat[o], 0, flat[o + 1]);
        this._q.setFromAxisAngle(UP_AXIS, flat[o + 2]);
        this._s.set(flat[o + 3], flat[o + 4], flat[o + 5]);
        this._m.compose(this._v, this._q, this._s);
        mesh.setMatrixAt(k, this._m);
        mesh.instanceColor.setXYZ(k, flat[o + 6], flat[o + 7], flat[o + 8]);
      }
      mesh.instanceMatrix.needsUpdate = true;
      mesh.instanceColor.needsUpdate = true;
    }
    for (const [key, p] of this.pools) {
      if (!buckets.has(key)) {
        p.mesh.count = 0;
        p.mesh.instanceMatrix.needsUpdate = true;
      }
    }
  }

  // ---------------- frame ----------------

  frame(dt, simSpeedIdx, paused) {
    const speedMul = paused ? 0 : ([0, 1, 2.6, 5.2][simSpeedIdx] || 1);
    this.timeOfDay = (this.timeOfDay + dt * speedMul / 300) % 1;
    this._updateSky();

    this.controls.update();
    const carMul = paused ? 0 : ([0, 1, 1.7, 2.6][simSpeedIdx] || 1);
    this.traffic.update(dt * carMul);

    this._adaptiveQuality(dt);
    this.renderer3.render(this.scene, this.camera);
  }

  /** Drop internal resolution when frames are consistently expensive
   *  (software GL / weak GPUs), restore it when there is headroom. */
  _adaptiveQuality(dt) {
    this._emaDt = (this._emaDt ?? dt) * 0.95 + dt * 0.05;
    this._qCheck = (this._qCheck ?? 0) + dt;
    if (this._qCheck < 2) return;          // evaluate every ~2 s
    this._qCheck = 0;
    const level = this._qualityLevel ?? 0;  // 0 = native, 3 = quarter
    if (this._emaDt > 0.09 && level < 3) {
      this._setQualityLevel(level + 1);
    } else if (this._emaDt < 0.03 && level > 0) {
      this._setQualityLevel(level - 1);
    }
  }

  _setQualityLevel(level) {
    this._qualityLevel = level;
    const scale = [1, 0.75, 0.6, 0.45][level];
    this.renderer3.setPixelRatio(Math.min(window.devicePixelRatio, 2) * scale);
    console.info(`PolyCity: render scale ${(scale * 100) | 0}%`);
  }

  _updateSky() {
    const a = this.timeOfDay * Math.PI * 2;
    const sunH = Math.sin(a);
    const daylight = smooth01((sunH + 0.15) / 0.45);
    this._skyTmp.copy(this.skyNight).lerp(this.skyDay, daylight);
    this.scene.background.copy(this._skyTmp);
    this.scene.fog.color.copy(this._skyTmp);
    this.hemi.intensity = 0.22 + 0.78 * daylight;
    this.sun.intensity = 1.35 * daylight;
    const R = 90;
    this.sun.position.set(Math.cos(a) * R * 0.7, Math.max(6, sunH * R), 30);
    if (this.waterMesh) {
      this.waterMesh.material.emissive.set(daylight > 0.4 ? '#0a2033' : '#101c30');
      this.waterMesh.material.emissiveIntensity = 0.5 + 0.35 * Math.sin(performance.now() / 900);
    }
  }

  // ---------------- picking / cursors ----------------

  pickTile(clientX, clientY) {
    const rect = this.domElement.getBoundingClientRect();
    const ndc = new THREE.Vector2(
      ((clientX - rect.left) / rect.width) * 2 - 1,
      -((clientY - rect.top) / rect.height) * 2 + 1
    );
    const ray = new THREE.Raycaster();
    ray.setFromCamera(ndc, this.camera);
    const dy = ray.ray.direction.y || -1e-9;
    const t = dy < 0 ? -ray.ray.origin.y / dy : -1;
    if (t <= 0) return null;
    const p = ray.ray.origin.clone().addScaledVector(ray.ray.direction, t);
    const x = Math.floor(p.x + HALF);
    const z = Math.floor(p.z + HALF);
    if (x < 0 || z < 0 || x >= WORLD || z >= WORLD) return null;
    return { x, z };
  }

  setHover(tile, visible = true) {
    this.hoverMesh.visible = visible && !!tile;
    if (tile) this.hoverMesh.position.set(tile.x - HALF + 0.5, 0, tile.z - HALF + 0.5);
  }

  showDragRect(x0, z0, x1, z1, ok = true) {
    const minX = Math.min(x0, x1), maxX = Math.max(x0, x1);
    const minZ = Math.min(z0, z1), maxZ = Math.max(z0, z1);
    this.dragRect.visible = true;
    this.dragRect.scale.set(maxX - minX + 1, 1, maxZ - minZ + 1);
    this.dragRect.position.set((minX + maxX) / 2 - HALF + 0.5, 0.06, (minZ + maxZ) / 2 - HALF + 0.5);
    this.dragRect.material.color.set(ok ? '#4ad06a' : '#e05555');
  }

  hideDragRect() { this.dragRect.visible = false; }

  setGhost(structId, tile, valid) {
    if (!structId || !tile) { this.clearGhost(); return; }
    const meta = BUILDINGS[structId];
    const geo = this.geoCache[meta.id];
    if (!geo) { this.clearGhost(); return; }
    if (!this.ghost || this.ghostKey !== meta.id) {
      this.clearGhost();
      this.ghost = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ transparent: true, opacity: 0.55, depthWrite: false }));
      this.ghostKey = meta.id;
      this.scene.add(this.ghost);
    }
    const cx = tile.x + (meta.w - 1) / 2, cz = tile.z + (meta.h - 1) / 2;
    this.ghost.position.set(cx - HALF + 0.5, 0.02, cz - HALF + 0.5);
    this.ghost.material.color.set(valid ? '#39d353' : '#ff5252');
    this.ghost.visible = true;
  }

  clearGhost() {
    if (this.ghost) { this.scene.remove(this.ghost); this.ghost = null; this.ghostKey = null; }
  }

  setShadows(on) {
    this.settings.shadows = on;
    this.renderer3.shadowMap.enabled = on;
    this.sun.castShadow = on;
    for (const [key, p] of this.pools) {
      p.mesh.castShadow = on && key !== 'dash' && !key.startsWith('zoneov');
    }
  }
}

// ---------------- helpers ----------------
const ONES = [1, 1, 1];

function smooth01(v) {
  const x = clamp(v, 0, 1);
  return x * x * (3 - 2 * x);
}

function rgbHex(hex) {
  const c = new THREE.Color(hex);
  return [c.r, c.g, c.b];
}

function coloredBox(w, h, d, c, x, y, z) {
  const g = new THREE.BoxGeometry(w, h, d);
  g.translate(x, y + h / 2, z);
  const n = g.attributes.position.count;
  const arr = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) { arr[i * 3] = c[0]; arr[i * 3 + 1] = c[1]; arr[i * 3 + 2] = c[2]; }
  g.setAttribute('color', new THREE.BufferAttribute(arr, 3));
  return g;
}

function makeZoneTexture(hex) {
  const cv = document.createElement('canvas');
  cv.width = cv.height = 64;
  const ctx = cv.getContext('2d');
  ctx.globalAlpha = 0.20;
  ctx.fillStyle = hex;
  ctx.fillRect(0, 0, 64, 64);
  ctx.globalAlpha = 0.95;
  ctx.strokeStyle = hex;
  ctx.lineWidth = 7;
  ctx.strokeRect(3.5, 3.5, 57, 57);
  ctx.globalAlpha = 0.5;
  ctx.lineWidth = 2;
  ctx.setLineDash([6, 5]);
  ctx.strokeRect(10, 10, 44, 44);
  const tex = new THREE.CanvasTexture(cv);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}
