/**
 * Map-scale buildings for destinations that deserve more than a pin.
 *
 * This is not a general building generator and it intentionally loads no kit.
 * At Tera's camera distances a one-metre façade module is sub-pixel; reproducing
 * it as geometry would turn three destinations into thousands of triangles and
 * several draw calls per material. What survives at map scale is the grammar:
 * silhouette, repeated window bays, a distinct ground-floor door and a roof
 * line. Those are generated here from a small data shape and one cached unit
 * box, with all windows in one InstancedMesh.
 *
 * The placement idea was researched against achrefelouafi's MIT-licensed
 * BasicProceduralBuilding, which classifies façade cells into ground, window,
 * corner and roof pieces. No source or binary asset from that project is copied
 * here: its GLB part kit and Blender-coordinate port solve a different-scale
 * problem. See ASSET_RESEARCH.md.
 */

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

export interface BuildingGlyphHandle {
  group: THREE.Group;
  /** Solid shell pieces only; windows do not need thousands of ray targets. */
  pickables: THREE.Object3D[];
  /** World-space height above the group's ground datum. */
  anchorY: number;
  dispose(): void;
}

export interface BuildingSegment {
  x: number;
  z: number;
  width: number;
  depth: number;
  base: number;
  height: number;
}

interface WindowPlacement {
  x: number;
  y: number;
  z: number;
  width: number;
  height: number;
  yaw: number;
  lit: boolean;
}

const DEFAULT_BODY = 0x8d9aa4;
/**
 * Pane and door thickness, in true metres.
 *
 * These were 0.012 and 0.028 **scene units**, chosen against the Bay Area
 * board (94.3 m to the unit) so a pane stood 1.13 m proud and a door 2.64 m.
 * On the merged board that is 23 m of glass and a 54 m door — measured on the
 * HQ glyph at Salesforce Tower while hunting the 700 m FiDi slab. Same class
 * of bug as the pin, the aeroplane, and the freeway ribbon: a constant in
 * scene units is a constant that assumes one board.
 */
const PANE_DEPTH_M = 1.13;
const DOOR_DEPTH_M = 2.64;
const WINDOW_DAY = new THREE.Color(0x334c5d);
const WINDOW_LIT = new THREE.Color(0xd7c68d);

/**
 * The silhouette in metres, kept pure so the contract can be unit-tested with
 * no WebGL context. Segments never overlap on the same vertical plane except
 * where a tower deliberately steps inward above its podium.
 */
export function layoutBuildingGlyph(glyph: BuildingGlyph): BuildingSegment[] {
  const width = Math.max(4, glyph.width);
  const depth = Math.max(4, glyph.depth);
  const height = Math.max(3, glyph.height);

  switch (glyph.profile) {
    case "tower": {
      const podium = height * 0.13;
      const shaft = height * 0.72;
      return [
        { x: 0, z: 0, width, depth, base: 0, height: podium },
        {
          x: 0,
          z: 0,
          width: width * 0.74,
          depth: depth * 0.78,
          base: podium,
          height: shaft,
        },
        {
          x: 0,
          z: 0,
          width: width * 0.56,
          depth: depth * 0.6,
          base: podium + shaft,
          height: height - podium - shaft,
        },
      ];
    }
    case "courtyard": {
      const wing = Math.max(3.5, Math.min(width, depth) * 0.28);
      return [
        { x: 0, z: -(depth - wing) / 2, width, depth: wing, base: 0, height },
        { x: 0, z: (depth - wing) / 2, width, depth: wing, base: 0, height },
        {
          x: -(width - wing) / 2,
          z: 0,
          width: wing,
          depth: Math.max(wing, depth - wing * 2),
          base: 0,
          height,
        },
        {
          x: (width - wing) / 2,
          z: 0,
          width: wing,
          depth: Math.max(wing, depth - wing * 2),
          base: 0,
          height,
        },
      ];
    }
    case "hangar":
      return [{ x: 0, z: 0, width, depth, base: 0, height: height * 0.72 }];
    case "block":
    default:
      return [{ x: 0, z: 0, width, depth, base: 0, height }];
  }
}

export function createBuildingGlyph(
  world: World,
  glyph: BuildingGlyph,
  accentColor: number,
): BuildingGlyphHandle {
  const group = new THREE.Group();
  group.name = `building-glyph:${glyph.profile}`;
  // A positive three.js yaw turns local north west. Compass headings increase
  // eastward, so the sign is reversed once at the world/building boundary.
  const heading = Number.isFinite(glyph.heading) ? glyph.heading : 0;
  group.rotation.y = -(heading * Math.PI) / 180;

  const pickables: THREE.Object3D[] = [];
  const segments = layoutBuildingGlyph(glyph);
  const horizontal = 1 / world.metresPerUnit;
  const vertical = (m: number) => world.metres(m);
  const bodyColor = new THREE.Color(glyph.bodyColor ?? DEFAULT_BODY);
  const body = new THREE.MeshLambertMaterial({ color: bodyColor });
  body.name = "building shell";
  const trim = new THREE.MeshLambertMaterial({ color: bodyColor.clone().multiplyScalar(0.72) });
  trim.name = "building roof and trim";
  const glass = new THREE.MeshLambertMaterial({
    // Instanced colours are multiplied by the base material colour, so white
    // is the neutral carrier for the cool and warm pane colours below.
    color: 0xffffff,
    emissive: new THREE.Color(accentColor).multiplyScalar(0.34),
    emissiveIntensity: 0.36,
  });
  glass.name = "building windows";
  const accent = new THREE.MeshLambertMaterial({
    color: accentColor,
    emissive: accentColor,
    emissiveIntensity: 0.42,
  });
  accent.name = "building door";

  const unit = new THREE.BoxGeometry(1, 1, 1);
  unit.translate(0, 0.5, 0);
  const windows: WindowPlacement[] = [];
  const random = mulberry32(glyph.seed ?? hashGlyph(glyph));

  for (const segment of segments) {
    const shell = new THREE.Mesh(unit, body);
    shell.name = "building shell segment";
    shell.position.set(segment.x * horizontal, vertical(segment.base), segment.z * horizontal);
    shell.scale.set(segment.width * horizontal, vertical(segment.height), segment.depth * horizontal);
    shell.castShadow = true;
    shell.receiveShadow = true;
    group.add(shell);
    pickables.push(shell);

    // A slightly proud cap makes each setback legible from above, where the
    // camera spends nearly all its time. It is one centimetre in the data and a
    // deliberately larger fraction of a pixel after vertical exaggeration.
    const cap = new THREE.Mesh(unit, trim);
    cap.name = "building roof line";
    cap.position.set(
      segment.x * horizontal,
      vertical(segment.base + segment.height),
      segment.z * horizontal,
    );
    cap.scale.set(
      (segment.width + 0.7) * horizontal,
      Math.max(0.012, vertical(0.28)),
      (segment.depth + 0.7) * horizontal,
    );
    cap.castShadow = true;
    group.add(cap);
    pickables.push(cap);

    collectWindows(windows, segment, glyph, horizontal, vertical, random);
  }

  const windowGeometry = new THREE.BoxGeometry(1, 1, 1);
  const windowMesh = new THREE.InstancedMesh(windowGeometry, glass, windows.length);
  windowMesh.name = "building window bays";
  windowMesh.castShadow = false;
  windowMesh.receiveShadow = false;
  const matrix = new THREE.Matrix4();
  const quaternion = new THREE.Quaternion();
  const position = new THREE.Vector3();
  const scale = new THREE.Vector3();
  const up = new THREE.Vector3(0, 1, 0);
  windows.forEach((window, i) => {
    position.set(window.x, window.y, window.z);
    quaternion.setFromAxisAngle(up, window.yaw);
    scale.set(window.width, window.height, PANE_DEPTH_M * horizontal);
    matrix.compose(position, quaternion, scale);
    windowMesh.setMatrixAt(i, matrix);
    // A few warm panes stop the repeated grid reading as graph paper. The
    // choice is seeded, so the same office has the same lights after a reload.
    windowMesh.setColorAt(i, window.lit ? WINDOW_LIT : WINDOW_DAY);
  });
  windowMesh.instanceMatrix.needsUpdate = true;
  windowMesh.instanceColor!.needsUpdate = true;
  windowMesh.computeBoundingSphere();
  group.add(windowMesh);

  let gable: THREE.BufferGeometry | null = null;
  if (glyph.profile === "hangar") {
    const bodyHeight = glyph.height * 0.72;
    const rise = glyph.height - bodyHeight;
    // The triangular prism completes the upper 28% of the silhouette; its two
    // sloped faces are what turn a low grey block into a hangar at map scale.
    gable = gableGeometry(
      Math.max(4, glyph.width) * horizontal,
      Math.max(4, glyph.depth) * horizontal,
      vertical(rise),
    );
    const roof = new THREE.Mesh(gable, trim);
    roof.name = "hangar roof";
    roof.position.y = vertical(bodyHeight);
    roof.castShadow = true;
    roof.receiveShadow = true;
    group.add(roof);
    pickables.push(roof);
  }

  // One bright address on the south/front façade. It is navigation, not a
  // physically accurate entrance schedule, and stays readable after the
  // repeated bays have merged into their average at distance.
  const first = segments[0] as BuildingSegment;
  const door = new THREE.Mesh(unit, accent);
  door.name = "building entrance";
  const doorWidth = glyph.profile === "hangar" ? first.width * 0.42 : Math.min(5, first.width * 0.22);
  const doorHeight = glyph.profile === "hangar" ? first.height * 0.6 : Math.min(5, first.height * 0.55);
  door.position.set(
    first.x * horizontal,
    vertical(first.base),
    (first.z + first.depth / 2 + 0.18) * horizontal,
  );
  door.scale.set(doorWidth * horizontal, vertical(doorHeight), DOOR_DEPTH_M * horizontal);
  door.castShadow = false;
  group.add(door);
  pickables.push(door);

  const anchorY = vertical(glyph.height) + Math.max(0.18, vertical(2));

  return {
    group,
    pickables,
    anchorY,
    dispose() {
      windowMesh.dispose();
      unit.dispose();
      windowGeometry.dispose();
      gable?.dispose();
      body.dispose();
      trim.dispose();
      glass.dispose();
      accent.dispose();
      pickables.length = 0;
      group.clear();
    },
  };
}

function collectWindows(
  out: WindowPlacement[],
  segment: BuildingSegment,
  glyph: BuildingGlyph,
  horizontal: number,
  vertical: (m: number) => number,
  random: () => number,
) {
  const floorShare = segment.height / Math.max(1, glyph.height);
  const rows =
    glyph.profile === "hangar"
      ? 1
      : Math.max(1, Math.min(18, Math.round(Math.max(1, glyph.storeys) * floorShare)));
  const rowPitch = segment.height / (rows + 0.35);
  const windowHeightM = Math.max(1.1, rowPitch * (glyph.profile === "tower" ? 0.5 : 0.58));
  const y0 = segment.base + rowPitch * 0.6;

  const addFacade = (
    spanM: number,
    face: "north" | "south" | "east" | "west",
  ) => {
    const cols = Math.max(2, Math.min(10, Math.round(spanM / 6.5)));
    const bay = spanM / cols;
    const width = bay * (0.5 + random() * 0.1) * horizontal;
    for (let row = 0; row < rows; row += 1) {
      for (let col = 0; col < cols; col += 1) {
        const along = -spanM / 2 + bay * (col + 0.5);
        const y = vertical(y0 + rowPitch * row + windowHeightM / 2);
        const lit = random() > 0.77;
        switch (face) {
          case "north":
            out.push({
              x: (segment.x + along) * horizontal,
              y,
              z: (segment.z - segment.depth / 2 - 0.08) * horizontal,
              width,
              height: vertical(windowHeightM),
              yaw: 0,
              lit,
            });
            break;
          case "south":
            out.push({
              x: (segment.x - along) * horizontal,
              y,
              z: (segment.z + segment.depth / 2 + 0.08) * horizontal,
              width,
              height: vertical(windowHeightM),
              yaw: Math.PI,
              lit,
            });
            break;
          case "east":
            out.push({
              x: (segment.x + segment.width / 2 + 0.08) * horizontal,
              y,
              z: (segment.z + along) * horizontal,
              width,
              height: vertical(windowHeightM),
              yaw: Math.PI / 2,
              lit,
            });
            break;
          case "west":
            out.push({
              x: (segment.x - segment.width / 2 - 0.08) * horizontal,
              y,
              z: (segment.z - along) * horizontal,
              width,
              height: vertical(windowHeightM),
              yaw: -Math.PI / 2,
              lit,
            });
            break;
        }
      }
    }
  };

  addFacade(segment.width, "north");
  addFacade(segment.width, "south");
  addFacade(segment.depth, "east");
  addFacade(segment.depth, "west");
}

/** A triangular prism whose ridge runs along local X. */
function gableGeometry(width: number, depth: number, rise: number): THREE.BufferGeometry {
  const x = width / 2;
  const z = depth / 2;
  const positions = new Float32Array([
    -x, 0, -z,
    -x, 0, z,
    -x, rise, 0,
    x, 0, -z,
    x, 0, z,
    x, rise, 0,
  ]);
  const indices = [
    0, 1, 2,
    3, 5, 4,
    0, 3, 4, 0, 4, 1,
    1, 4, 5, 1, 5, 2,
    2, 5, 3, 2, 3, 0,
  ];
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();
  return geometry;
}

function hashGlyph(glyph: BuildingGlyph): number {
  const text = `${glyph.profile}:${glyph.width}:${glyph.depth}:${glyph.height}:${glyph.heading}`;
  let hash = 0x811c9dc5;
  for (let i = 0; i < text.length; i += 1) {
    hash ^= text.charCodeAt(i);
    hash = Math.imul(hash, 0x01000193);
  }
  return hash >>> 0;
}

function mulberry32(seed: number): () => number {
  let value = seed >>> 0;
  return () => {
    value = (value + 0x6d2b79f5) >>> 0;
    let next = Math.imul(value ^ (value >>> 15), 1 | value);
    next = (next + Math.imul(next ^ (next >>> 7), 61 | next)) ^ next;
    return ((next ^ (next >>> 14)) >>> 0) / 4294967296;
  };
}
