/**
 * 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 { buildFreewayWorldPlan } from "../transport/freewayWorld.ts";
import type { TransportPack } from "../transport/types.ts";
import { buildRoutePath, sampleRoute } from "../transport/vehicleSim.ts";
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;
}

function offsetPath(points: readonly THREE.Vector3[], offset: number): THREE.Vector3[] {
  return points.map((point, index) => {
    const previous = points[Math.max(0, index - 1)] ?? point;
    const next = points[Math.min(points.length - 1, index + 1)] ?? point;
    const dx = next.x - previous.x;
    const dz = next.z - previous.z;
    const length = Math.hypot(dx, dz) || 1;
    return new THREE.Vector3(point.x + (-dz / length) * offset, point.y, point.z + (dx / length) * offset);
  });
}

/** Merge alternating path spans into one dashed marking mesh. */
function dashedRibbon(
  points: readonly THREE.Vector3[],
  offset: number,
  width: number,
  color: number,
): THREE.Mesh {
  const shifted = offsetPath(points, offset);
  const positions: number[] = [];
  const indices: number[] = [];
  for (let index = 0; index < shifted.length - 1; index += 2) {
    const a = shifted[index];
    const spanEnd = shifted[Math.min(index + 1, shifted.length - 1)];
    if (!a || !spanEnd) continue;
    const b = a.clone().lerp(spanEnd, 0.44);
    const dx = b.x - a.x;
    const dz = b.z - a.z;
    const length = Math.hypot(dx, dz) || 1;
    const nx = (-dz / length) * width / 2;
    const nz = (dx / length) * width / 2;
    const base = positions.length / 3;
    positions.push(
      a.x + nx, a.y + 0.035, a.z + nz,
      a.x - nx, a.y + 0.035, a.z - nz,
      b.x + nx, b.y + 0.035, b.z + nz,
      b.x - nx, b.y + 0.035, b.z - nz,
    );
    indices.push(base, base + 2, base + 1, base + 1, base + 2, base + 3);
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();
  const mesh = new THREE.Mesh(
    geometry,
    new THREE.MeshBasicMaterial({ color, toneMapped: false, side: THREE.DoubleSide }),
  );
  mesh.name = "freeway:lane-dashes";
  return mesh;
}

function makeShieldMaterial(identity: "us-highway" | "interstate", shield: string): THREE.Material {
  if (typeof document === "undefined") {
    return new THREE.MeshBasicMaterial({ color: identity === "interstate" ? 0x2d5b8c : 0xe8edf0 });
  }
  const canvas = document.createElement("canvas");
  canvas.width = 256;
  canvas.height = 192;
  const context = canvas.getContext("2d");
  if (!context) return new THREE.MeshBasicMaterial({ color: 0xe8edf0 });
  context.fillStyle = identity === "interstate" ? "#174b80" : "#f4f5ef";
  context.fillRect(6, 6, 244, 180);
  context.lineWidth = 12;
  context.strokeStyle = identity === "interstate" ? "#f3f5f7" : "#151b20";
  context.strokeRect(6, 6, 244, 180);
  context.fillStyle = identity === "interstate" ? "#f3f5f7" : "#151b20";
  context.font = "700 52px ui-monospace, monospace";
  context.textAlign = "center";
  context.fillText(identity === "interstate" ? "INTERSTATE" : "US", 128, 63);
  context.font = "800 86px ui-monospace, monospace";
  context.fillText(shield, 128, 151);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return new THREE.MeshBasicMaterial({ map: texture, toneMapped: false, side: THREE.DoubleSide });
}

/**
 * Browser-feasible authored freeway world. Geometry is deliberately batched by
 * road/marking class: the corridor gains lane-scale readability without one
 * draw call per reflector, tree, or roadside prop.
 */
export function createFreewayWorld(world: World, pack: TransportPack): THREE.Group {
  const group = new THREE.Group();
  group.name = "freeway-world-v2";
  const plan = buildFreewayWorldPlan(pack);
  group.userData.planSeed = plan.seed;

  const asphalt = [0x353a3d, 0x303538];
  const shoulder = [0x555759, 0x4e5153];
  const berm = [0x64705c, 0x74674c];
  const barrierMaterial = new THREE.MeshLambertMaterial({ color: 0xb6b4aa });
  const guardMaterial = new THREE.MeshStandardMaterial({ color: 0x9fa8aa, metalness: 0.64, roughness: 0.42 });
  const reflectorMaterial = new THREE.MeshBasicMaterial({ color: 0xf7e3a0, toneMapped: false });
  const reflectorGeometry = new THREE.BoxGeometry(0.018, 0.01, 0.028);
  const treeTrunkMaterial = new THREE.MeshLambertMaterial({ color: 0x66513b });
  const treeCrownMaterials = [
    new THREE.MeshLambertMaterial({ color: 0x3f5942 }),
    new THREE.MeshLambertMaterial({ color: 0x687347 }),
  ];
  const trunkGeometry = new THREE.CylinderGeometry(0.045, 0.06, 0.45, 5);
  const crownGeometry = new THREE.IcosahedronGeometry(0.28, 0);
  const poleGeometry = new THREE.CylinderGeometry(0.022, 0.03, 0.72, 5);
  const siloGeometry = new THREE.CylinderGeometry(0.14, 0.16, 0.55, 8);
  const roadsideFeatures = plan.routes.reduce((sum, route) => sum + route.roadside.length, 0);
  const trunks = new THREE.InstancedMesh(trunkGeometry, treeTrunkMaterial, roadsideFeatures);
  const coastalCrowns = new THREE.InstancedMesh(crownGeometry, treeCrownMaterials[0]!, roadsideFeatures);
  const orchardCrowns = new THREE.InstancedMesh(crownGeometry, treeCrownMaterials[1]!, roadsideFeatures);
  const poles = new THREE.InstancedMesh(poleGeometry, guardMaterial, roadsideFeatures);
  const silos = new THREE.InstancedMesh(siloGeometry, barrierMaterial, roadsideFeatures);
  trunks.name = "freeway:roadside-trunks";
  coastalCrowns.name = "freeway:coastal-oaks";
  orchardCrowns.name = "freeway:orchards";
  poles.name = "freeway:power-poles";
  silos.name = "freeway:valley-silos";
  let trunkCount = 0;
  let coastalCrownCount = 0;
  let orchardCrownCount = 0;
  let poleCount = 0;
  let siloCount = 0;
  const dummy = new THREE.Object3D();

  world.city.roads.forEach((road, roadIndex) => {
    if (road.kind !== "freeway") return;
    const path = drapePath(world, road.path, 52, 0.115);
    const route = plan.routes[roadIndex];
    const identityIndex = route?.identity === "interstate" ? 1 : 0;
    const routePath = route ? buildRoutePath(pack, route.routeId) : null;
    // Broad earthwork under separate decks makes grade and curve changes read.
    group.add(roadRibbon(path, 2.75, berm[identityIndex] ?? berm[0]!, -0.09));
    for (const side of [-1, 1] as const) {
      group.add(roadRibbon(offsetPath(path, side * 0.64), 1.18, shoulder[identityIndex] ?? shoulder[0]!, 0.004));
      group.add(roadRibbon(offsetPath(path, side * 0.64), 1.03, asphalt[identityIndex] ?? asphalt[0]!, 0.012));
      // Inner yellow edge, two lane dividers, outer white shoulder edge.
      group.add(roadRibbon(offsetPath(path, side * 0.12), 0.026, 0xf0c84f, 0.038));
      group.add(roadRibbon(offsetPath(path, side * 1.16), 0.026, 0xe8ece8, 0.038));
      group.add(dashedRibbon(path, side * 0.47, 0.022, 0xf4f4ec));
      group.add(dashedRibbon(path, side * 0.81, 0.022, 0xf4f4ec));
      const guardPath = offsetPath(path, side * 1.27);
      const guard = new THREE.Mesh(
        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(guardPath), Math.max(24, guardPath.length * 2), 0.025, 5, false),
        guardMaterial,
      );
      guard.name = "freeway:outer-guardrail";
      guard.castShadow = true;
      group.add(guard);
    }
    // Low concrete median walls keep both carriageways visually independent.
    for (const side of [-1, 1] as const) {
      const medianPath = offsetPath(path, side * 0.075).map((point) => point.clone().setY(point.y + 0.065));
      const median = new THREE.Mesh(
        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(medianPath), Math.max(24, medianPath.length * 2), 0.055, 4, false),
        barrierMaterial,
      );
      median.name = "freeway:median-barrier";
      group.add(median);
    }
    // Retroreflectors are instanced and restrained, never roadside light blobs.
    const reflectorPoints = path.filter((_, index) => index % 2 === 0);
    const reflectors = new THREE.InstancedMesh(reflectorGeometry, reflectorMaterial, reflectorPoints.length * 4);
    reflectors.name = "freeway:reflectors";
    let reflectorIndex = 0;
    for (const pointIndex of reflectorPoints.keys()) {
      const point = reflectorPoints[pointIndex];
      if (!point) continue;
      for (const offset of [-0.81, -0.47, 0.47, 0.81]) {
        const shifted = offsetPath(path, offset)[pointIndex * 2] ?? point;
        dummy.position.set(shifted.x, shifted.y + 0.055, shifted.z);
        dummy.rotation.set(0, 0, 0);
        dummy.scale.setScalar(1);
        dummy.updateMatrix();
        reflectors.setMatrixAt(reflectorIndex++, dummy.matrix);
      }
    }
    reflectors.count = reflectorIndex;
    group.add(reflectors);

    if (!route || !routePath) return;
    const shieldMaterial = makeShieldMaterial(route.identity, route.shield);
    for (const feature of route.roadside) {
      const sample = sampleRoute(routePath, feature.distanceM);
      const [x, z] = world.project(sample.lat, sample.lng);
      const heading = (sample.headingDeg * Math.PI) / 180;
      const sceneSetback = feature.kind === "route-sign" ? 1.42 : 1.7 + feature.setbackM * 0.014;
      const px = x + Math.cos(heading) * sceneSetback * feature.side;
      const pz = z + Math.sin(heading) * sceneSetback * feature.side;
      const ground = world.groundAt(sample.lat, sample.lng);
      if (feature.kind === "route-sign") {
        const sign = new THREE.Group();
        sign.name = `freeway:sign:${route.shield}`;
        const post = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.62, 0.035), guardMaterial);
        post.position.y = 0.31;
        const board = new THREE.Mesh(new THREE.PlaneGeometry(0.42, 0.31), shieldMaterial);
        board.position.y = 0.69;
        board.rotation.y = -heading + (feature.side === 1 ? Math.PI : 0);
        sign.add(post, board);
        sign.position.set(px, ground + 0.08, pz);
        sign.userData.routeId = route.routeId;
        group.add(sign);
        continue;
      }
      const visualScale = feature.scale * 0.58;
      const halfHeight = feature.kind === "power-pole" ? 0.36 : feature.kind === "silo" ? 0.275 : 0.225;
      dummy.position.set(px, ground + halfHeight * visualScale, pz);
      dummy.rotation.set(0, heading + feature.scale, 0);
      dummy.scale.setScalar(visualScale);
      dummy.updateMatrix();
      if (feature.kind === "power-pole") poles.setMatrixAt(poleCount++, dummy.matrix);
      else if (feature.kind === "silo") silos.setMatrixAt(siloCount++, dummy.matrix);
      else {
        trunks.setMatrixAt(trunkCount++, dummy.matrix);
        dummy.position.y += 0.25 * feature.scale;
        dummy.scale.set(feature.scale * 0.7, feature.scale * 0.5, feature.scale * 0.62);
        dummy.updateMatrix();
        if (feature.kind === "oak") coastalCrowns.setMatrixAt(coastalCrownCount++, dummy.matrix);
        else orchardCrowns.setMatrixAt(orchardCrownCount++, dummy.matrix);
      }
    }
  });
  trunks.count = trunkCount;
  poles.count = poleCount;
  silos.count = siloCount;
  coastalCrowns.count = coastalCrownCount;
  orchardCrowns.count = orchardCrownCount;
  group.add(trunks, coastalCrowns, orchardCrowns, poles, silos);
  return group;
}

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;
}
