/**
 * Bridges and roads — the lines that tie the landmasses together and give the
 * grid something to hang off.
 *
 * Roads follow the terrain: each path is resampled far more finely than it is
 * written in the city pack, and every sample takes its height from the ground,
 * so a street climbs out of the flats instead of burrowing through the hill.
 */

import * as THREE from "three";
import type { Bridge, LatLng } from "./types.ts";
import type { World } from "./world.ts";

/** Resample a lat/lng path into scene-space points that ride the ground. */
function drapePath(world: World, path: LatLng[], samplesPerLeg = 14, lift = 0.14): THREE.Vector3[] {
  const out: THREE.Vector3[] = [];
  for (let i = 0; i < path.length - 1; i++) {
    const from = path[i];
    const to = path[i + 1];
    if (!from || !to) continue;
    const [lat0, lng0] = from;
    const [lat1, lng1] = to;
    const steps = i === path.length - 2 ? samplesPerLeg : samplesPerLeg - 1;
    for (let s = 0; s <= steps; s++) {
      const t = s / samplesPerLeg;
      const lat = lat0 + (lat1 - lat0) * t;
      const lng = lng0 + (lng1 - lng0) * t;
      const [x, z] = world.project(lat, lng);
      out.push(new THREE.Vector3(x, world.groundAt(lat, lng) + lift, z));
    }
  }
  return out;
}

function ribbon(points: THREE.Vector3[], width: number, color: number): THREE.Mesh {
  const curve = new THREE.CatmullRomCurve3(points);
  const geo = new THREE.TubeGeometry(curve, points.length * 2, width / 2, 4, false);
  const mesh = new THREE.Mesh(geo, new THREE.MeshLambertMaterial({ color }));
  mesh.receiveShadow = true;
  return mesh;
}

/** A draped, flat road deck. A tube turns a freeway into a raised pipeline. */
function roadRibbon(
  points: readonly THREE.Vector3[],
  width: number,
  color: number,
  lift = 0,
): THREE.Mesh {
  const positions: number[] = [];
  const normals: number[] = [];
  const indices: number[] = [];
  const half = width / 2;

  for (let index = 0; index < points.length; index += 1) {
    const point = points[index];
    const previous = points[Math.max(0, index - 1)];
    const next = points[Math.min(points.length - 1, index + 1)];
    if (!point || !previous || !next) continue;
    const dx = next.x - previous.x;
    const dz = next.z - previous.z;
    const length = Math.hypot(dx, dz) || 1;
    const nx = -dz / length;
    const nz = dx / length;
    positions.push(
      point.x + nx * half, point.y + lift, point.z + nz * half,
      point.x - nx * half, point.y + lift, point.z - nz * half,
    );
    normals.push(0, 1, 0, 0, 1, 0);
    if (index < points.length - 1) {
      const a = index * 2;
      indices.push(a, a + 2, a + 1, a + 1, a + 2, a + 3);
    }
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
  geometry.setIndex(indices);
  geometry.computeBoundingSphere();
  const mesh = new THREE.Mesh(
    geometry,
    new THREE.MeshLambertMaterial({ color, side: THREE.DoubleSide }),
  );
  mesh.receiveShadow = true;
  return mesh;
}

export function createRoads(world: World): THREE.Group {
  const group = new THREE.Group();
  group.name = "roads";
  for (const road of world.city.roads) {
    const color = road.kind === "freeway" ? 0x7d7166 : 0x8b8578;
    const path = drapePath(world, road.path);
    group.add(roadRibbon(path, road.width, color));
    if (road.kind === "freeway") {
      // One warm median stroke is enough at corridor scale to read as divided
      // highway without spending a textured asset or a draw call per lane.
      group.add(roadRibbon(path, Math.max(0.025, road.width * 0.035), 0xd7c27c, 0.012));
    }
  }
  return group;
}

/**
 * A suspension bridge: deck, towers, and a main cable sagging between them.
 *
 * The cable is the detail worth the code. Two orange towers with a straight
 * line between them read as a trestle; the catenary is what makes the shape at
 * the mouth of the bay unmistakably the Golden Gate.
 */
export function createBridge(world: World, bridge: Bridge): THREE.Group {
  const group = new THREE.Group();
  group.name = bridge.name;

  const deckY = world.metres(bridge.deckHeight);
  const towerY = world.metres(bridge.towerHeight);
  const material = () => new THREE.MeshLambertMaterial({ color: bridge.color });

  const deckPoints = bridge.path.map(([lat, lng]) => {
    const [x, z] = world.project(lat, lng);
    return new THREE.Vector3(x, deckY, z);
  });

  const deck = ribbon(deckPoints, 0.5, bridge.color);
  deck.castShadow = true;
  group.add(deck);

  const towerTops: THREE.Vector3[] = [];
  for (const [lat, lng] of bridge.towers) {
    const [x, z] = world.project(lat, lng);
    const geo = new THREE.BoxGeometry(0.34, towerY, 0.34);
    geo.translate(0, towerY / 2, 0);
    const tower = new THREE.Mesh(geo, material());
    tower.position.set(x, 0, z);
    tower.castShadow = true;
    group.add(tower);

    // Cross-braces, which is most of what you see of a tower at distance.
    for (const frac of [0.55, 0.82]) {
      const brace = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.16, 0.4), material());
      brace.position.set(x, towerY * frac, z);
      group.add(brace);
    }
    towerTops.push(new THREE.Vector3(x, towerY, z));
  }

  const anchors = [deckPoints[0], ...towerTops, deckPoints[deckPoints.length - 1]];
  for (let i = 0; i < anchors.length - 1; i++) {
    const a = anchors[i];
    const b = anchors[i + 1];
    if (!a || !b) continue;
    const isMainSpan = i > 0 && i < anchors.length - 2;
    const sag = bridge.sag * towerY * (isMainSpan ? 1 : 0.42);

    const pts: THREE.Vector3[] = [];
    for (let s = 0; s <= 18; s++) {
      const t = s / 18;
      const p = a.clone().lerp(b, t);
      p.y -= Math.sin(t * Math.PI) * sag;
      pts.push(p);
    }
    group.add(
      new THREE.Mesh(
        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 24, 0.055, 5, false),
        material(),
      ),
    );

    // Vertical hangers down to the deck.
    for (let s = 2; s < 18; s += 2) {
      const t = s / 18;
      const p = a.clone().lerp(b, t);
      const top = p.y - Math.sin(t * Math.PI) * sag;
      if (top <= deckY + 0.2) continue;
      const h = top - deckY;
      const geo = new THREE.BoxGeometry(0.035, h, 0.035);
      geo.translate(0, h / 2, 0);
      const hanger = new THREE.Mesh(geo, material());
      hanger.position.set(p.x, deckY, p.z);
      group.add(hanger);
    }
  }

  return group;
}

export function createBridges(world: World): THREE.Group {
  const group = new THREE.Group();
  group.name = "bridges";
  for (const b of world.city.bridges) group.add(createBridge(world, b));
  return group;
}
