import * as THREE from 'three';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { CARS, GRID } from '../config.js';
import { matBuilding } from './geometry.js';
import { mulberry32 } from '../utils.js';

function addColor(g, c) {
  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));
}

const CAR_COLORS = ['#d94f4f', '#e8e8e8', '#3d6fd9', '#44b78b', '#e0a63d', '#777f88', '#9b59b6', '#22262b'];

/**
 * Animated traffic agents that drive along the road graph.
 */
export class Traffic {
  constructor(scene, city) {
    this.city = city;
    this.scene = scene;

    // little car: body + cabin + wheels hint
    const parts = [];
    const mkBox = (w, h, d, x, y, z, c) => {
      const g = new THREE.BoxGeometry(w, h, d);
      g.translate(x, y + h / 2, z);
      addColor(g, c);
      parts.push(g);
    };
    const bodyC = [0.85, 0.85, 0.85];
    mkBox(0.34, 0.09, 0.18, 0, 0.02, 0, bodyC);           // body (placeholder color)
    mkBox(0.17, 0.08, 0.15, -0.02, 0.11, 0, [0.25, 0.28, 0.32]); // cabin
    this.bodyGeo = mergeGeometries(parts.map(p => p));

    this.mesh = new THREE.InstancedMesh(this.bodyGeo, matBuilding, CARS.max);
    this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.mesh.count = 0;
    this.mesh.castShadow = false;
    this.mesh.frustumCulled = false;
    this.mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(CARS.max * 3), 3);
    scene.add(this.mesh);

    this.agents = [];
    this.rng = mulberry32(city.seed ^ 0xcafe);
    this._t = 0;
    this.targetCount = 0;
    this._recalcTimer = 0;
    this._lastNodeCount = -1;
  }

  computeTarget() {
    let roads = 0;
    for (let i = 0; i < this.city.grid.n; i++) if (this.city.grid.struct[i] === 1) roads++;
    this.targetCount = Math.min(CARS.max, Math.floor(roads * CARS.perRoad + this.city.stats.pop * CARS.perPop));
  }

  spawn() {
    const graph = this.city.ensureRoadGraph();
    if (graph.nodes.length < 2) return;
    const a = Math.floor(this.rng() * graph.nodes.length);
    const nbrs = graph.nodes[a].nbrs;
    if (!nbrs.length) return;
    const b = nbrs[Math.floor(this.rng() * nbrs.length)];
    const ci = this.agents.length % CAR_COLORS.length;
    this.agents.push({
      a, b, t: this.rng(),
      speed: 1.4 + this.rng() * 1.2,
      side: 0.16,
      prev: -1
    });
    const col = new THREE.Color(CAR_COLORS[ci]);
    // tint only body-ish by brightening
    this.mesh.instanceColor.setXYZ(this.agents.length - 1, col.r, col.g, col.b);
  }

  nodeXZ(ni) {
    const i = this.city.roadGraph.nodes[ni].i;
    const S = GRID;
    const x = i % S, z = (i - x) / S;
    return [x - S / 2 + 0.5, z - S / 2 + 0.5];
  }

  update(dt) {
    const graph = this.city.ensureRoadGraph();
    // graph rebuilt (roads added/removed) → old agent indices are invalid
    if (graph.nodes.length !== this._lastNodeCount) {
      this.agents.length = 0;
      this._lastNodeCount = graph.nodes.length;
    }
    this._recalcTimer -= dt;
    if (this._recalcTimer <= 0) {
      this.computeTarget();
      this._recalcTimer = 2.0;
    }
    // adjust population (guarded — spawn() may legitimately fail on isolated roads)
    let guard = CARS.max * 2;
    while (this.agents.length < this.targetCount && guard-- > 0) this.spawn();
    while (this.agents.length > this.targetCount) this.agents.pop();

    const m = new THREE.Matrix4();
    const pos = new THREE.Vector3();
    const quat = new THREE.Quaternion();
    const scale = new THREE.Vector3(1, 1, 1);
    const up = new THREE.Vector3(0, 1, 0);

    for (let k = 0; k < this.agents.length; k++) {
      const car = this.agents[k];
      car.t += car.speed * dt;
      if (car.t >= 1) {
        // pick next edge
        const graph = this.city.roadGraph;
        const nbrs = graph.nodes[car.b].nbrs;
        let next = -1;
        if (nbrs.length > 1) {
          const forward = nbrs.filter(n => n !== car.a);
          const pool = forward.length ? forward : nbrs;
          // bias straight
          next = pool[Math.floor(this.rng() * pool.length)];
        } else {
          next = car.a;
        }
        car.prev = car.a; car.a = car.b; car.b = next; car.t -= 1;
      }
      const [ax, az] = this.nodeXZ(car.a);
      const [bx, bz] = this.nodeXZ(car.b);
      const dx = bx - ax, dz = bz - az;
      const len = Math.hypot(dx, dz) || 1;
      const ux = dx / len, uz = dz / len;
      // right-hand lane offset
      const px = ax + dx * car.t - uz * car.side;
      const pz = az + dz * car.t + ux * car.side;
      pos.set(px, 0.03, pz);
      quat.setFromAxisAngle(up, Math.atan2(ux, uz));
      m.compose(pos, quat, scale);
      this.mesh.setMatrixAt(k, m);
    }
    this.mesh.count = this.agents.length;
    this.mesh.instanceMatrix.needsUpdate = true;
    if (this.mesh.instanceColor) this.mesh.instanceColor.needsUpdate = true;
  }
}
