/**
 * A procedural, unbadged black Model X-style electric crossover.
 *
 * The asset is authored in metres with its origin on the road at the centre of
 * the wheelbase. It faces -Z at yaw zero, matching the rest of Tera; +X is the
 * vehicle's right. The broad, low nose, rising panoramic glass, cab-forward
 * roof and tapered tail carry the silhouette. Fine detail is deliberately
 * sparse because this car is normally read from a corridor or chase camera.
 *
 * Build one rig and clone it. Clones share geometry and materials while their
 * wheel and steering joints remain independent. Dispose the original only
 * after every clone has left the scene.
 */

import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";

export const MODEL_X_METRICS = {
  length: 5.04,
  width: 2.08,
  height: 1.68,
  wheelbase: 2.965,
  track: 1.78,
  wheelRadius: 0.405,
  tireWidth: 0.285,
  maxSteeringAngle: 0.62,
} as const;

export type ModelXDetail = "corridor" | "follow";

export interface ModelXMaterials {
  paint: THREE.Material;
  glass: THREE.Material;
  trim: THREE.Material;
  tire: THREE.Material;
  wheel: THREE.Material;
  brake: THREE.Material;
  headlight: THREE.Material;
  tailLight: THREE.Material;
}

export interface ModelXBuildOptions {
  detail?: ModelXDetail;
  /** Used only when `materials` is omitted. */
  paint?: THREE.ColorRepresentation;
  /** A complete externally owned skin, useful for a world-level material pool. */
  materials?: ModelXMaterials;
}

export interface ModelXWheel {
  /** Yaw this group to steer. Rear steering groups stay at zero. */
  steering: THREE.Group;
  /** Rotate this group around local X to roll the wheel. */
  spin: THREE.Group;
  tire: THREE.Mesh;
  rim: THREE.Mesh;
}

export interface ModelXWheels {
  frontLeft: ModelXWheel;
  frontRight: ModelXWheel;
  rearLeft: ModelXWheel;
  rearRight: ModelXWheel;
}

export interface ModelXRig {
  root: THREE.Group;
  /** Static bodywork; useful when a traffic renderer instances body meshes. */
  body: THREE.Group;
  wheels: ModelXWheels;
  /** True only on a prototype that created its own default material set. */
  readonly ownsMaterials: boolean;
}

export interface ModelXInstancePart {
  name: string;
  geometry: THREE.BufferGeometry;
  material: THREE.Material | THREE.Material[];
  /** Transform from the vehicle root to this drawable in the neutral pose. */
  matrix: THREE.Matrix4;
  castShadow: boolean;
  receiveShadow: boolean;
}

const UNIT_BOX = new THREE.BoxGeometry(1, 1, 1);
const UNIT_PLANE = new THREE.PlaneGeometry(1, 1);

/** Default materials are intentionally untextured: no binary art and no UV dependency. */
export function createModelXMaterials(
  paint: THREE.ColorRepresentation = 0x050607,
): ModelXMaterials {
  const body = new THREE.MeshPhysicalMaterial({
    name: "model-x.paint",
    color: paint,
    metalness: 0.72,
    roughness: 0.22,
    clearcoat: 1,
    clearcoatRoughness: 0.12,
  });
  return {
    paint: body,
    glass: new THREE.MeshPhysicalMaterial({
      name: "model-x.glass",
      color: 0x101b22,
      metalness: 0.12,
      roughness: 0.12,
      transparent: true,
      opacity: 0.86,
      side: THREE.DoubleSide,
    }),
    trim: new THREE.MeshStandardMaterial({
      name: "model-x.trim",
      color: 0x101214,
      metalness: 0.68,
      roughness: 0.3,
    }),
    tire: new THREE.MeshStandardMaterial({
      name: "model-x.tire",
      color: 0x111213,
      metalness: 0,
      roughness: 0.92,
    }),
    wheel: new THREE.MeshStandardMaterial({
      name: "model-x.wheel",
      color: 0x2d3135,
      metalness: 0.88,
      roughness: 0.25,
    }),
    brake: new THREE.MeshStandardMaterial({
      name: "model-x.brake",
      color: 0x72777a,
      metalness: 0.92,
      roughness: 0.32,
    }),
    headlight: new THREE.MeshStandardMaterial({
      name: "model-x.headlight",
      color: 0xd9f4ff,
      emissive: 0xb8eaff,
      emissiveIntensity: 2.4,
      roughness: 0.16,
    }),
    tailLight: new THREE.MeshStandardMaterial({
      name: "model-x.tail-light",
      color: 0xff2433,
      emissive: 0xd70918,
      emissiveIntensity: 1.9,
      roughness: 0.2,
    }),
  };
}

interface Placement {
  position?: readonly [number, number, number];
  scale?: readonly [number, number, number];
  rotation?: readonly [number, number, number];
}

function matrix(place: Placement): THREE.Matrix4 {
  const position = new THREE.Vector3(...(place.position ?? [0, 0, 0]));
  const quaternion = new THREE.Quaternion().setFromEuler(
    new THREE.Euler(...(place.rotation ?? [0, 0, 0]), "YXZ"),
  );
  const scale = new THREE.Vector3(...(place.scale ?? [1, 1, 1]));
  return new THREE.Matrix4().compose(position, quaternion, scale);
}

class StaticBatch {
  private readonly groups = new Map<THREE.Material, THREE.BufferGeometry[]>();

  add(geometry: THREE.BufferGeometry, material: THREE.Material, place: Placement = {}): void {
    const transformed = geometry.clone().applyMatrix4(matrix(place));
    const found = this.groups.get(material);
    if (found) found.push(transformed);
    else this.groups.set(material, [transformed]);
  }

  /** Add a one-off generated shape, then release its untransformed source. */
  addOwned(geometry: THREE.BufferGeometry, material: THREE.Material, place: Placement = {}): void {
    this.add(geometry, material, place);
    geometry.dispose();
  }

  box(material: THREE.Material, place: Placement): void {
    this.add(UNIT_BOX, material, place);
  }

  build(name: string): THREE.Group {
    const group = new THREE.Group();
    group.name = name;
    for (const [material, geometries] of this.groups) {
      const geometry =
        geometries.length === 1 ? geometries[0] : mergeGeometries(geometries, false);
      if (geometries.length > 1) for (const item of geometries) item.dispose();
      if (!geometry) continue;
      const mesh = new THREE.Mesh(geometry, material);
      mesh.name = `${name}:${material.name || "surface"}`;
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      group.add(mesh);
    }
    this.groups.clear();
    return group;
  }
}

interface BodySection {
  z: number;
  halfWidth: number;
  bottom: number;
  shoulder: number;
  belt: number;
  roof: number;
}

const BODY_SECTIONS: readonly BodySection[] = [
  { z: -2.52, halfWidth: 0.58, bottom: 0.55, shoulder: 0.68, belt: 0.79, roof: 0.84 },
  { z: -2.34, halfWidth: 0.88, bottom: 0.47, shoulder: 0.69, belt: 0.88, roof: 0.93 },
  { z: -1.58, halfWidth: 1.0, bottom: 0.4, shoulder: 0.72, belt: 0.98, roof: 1.08 },
  { z: -0.78, halfWidth: 0.99, bottom: 0.39, shoulder: 0.76, belt: 1.06, roof: 1.32 },
  { z: -0.12, halfWidth: 0.97, bottom: 0.39, shoulder: 0.78, belt: 1.12, roof: 1.58 },
  { z: 0.72, halfWidth: 0.96, bottom: 0.4, shoulder: 0.78, belt: 1.1, roof: 1.68 },
  { z: 1.48, halfWidth: 0.97, bottom: 0.41, shoulder: 0.77, belt: 1.06, roof: 1.52 },
  { z: 2.28, halfWidth: 0.89, bottom: 0.48, shoulder: 0.72, belt: 0.98, roof: 1.18 },
  { z: 2.52, halfWidth: 0.63, bottom: 0.57, shoulder: 0.69, belt: 0.82, roof: 0.9 },
] as const;

/** A low-poly longitudinal loft with deliberately strong shoulder highlights. */
function bodyLoft(): THREE.BufferGeometry {
  const positions: number[] = [];
  const indices: number[] = [];
  const ring = 10;

  for (const s of BODY_SECTIONS) {
    const points: readonly [number, number][] = [
      [-s.halfWidth * 0.7, s.bottom],
      [-s.halfWidth, s.shoulder],
      [-s.halfWidth * 0.98, s.belt],
      [-s.halfWidth * 0.83, s.roof - 0.08],
      [-s.halfWidth * 0.58, s.roof],
      [s.halfWidth * 0.58, s.roof],
      [s.halfWidth * 0.83, s.roof - 0.08],
      [s.halfWidth * 0.98, s.belt],
      [s.halfWidth, s.shoulder],
      [s.halfWidth * 0.7, s.bottom],
    ];
    for (const [x, y] of points) positions.push(x, y, s.z);
  }

  for (let z = 0; z < BODY_SECTIONS.length - 1; z++) {
    for (let i = 0; i < ring; i++) {
      const next = (i + 1) % ring;
      const a = z * ring + i;
      const b = z * ring + next;
      const c = (z + 1) * ring + next;
      const d = (z + 1) * ring + i;
      indices.push(a, b, d, b, c, d);
    }
  }

  const frontCenter = positions.length / 3;
  positions.push(0, 0.72, BODY_SECTIONS[0]!.z);
  const rearCenter = positions.length / 3;
  positions.push(0, 0.74, BODY_SECTIONS[BODY_SECTIONS.length - 1]!.z);
  for (let i = 0; i < ring; i++) {
    const next = (i + 1) % ring;
    indices.push(frontCenter, next, i);
    const base = (BODY_SECTIONS.length - 1) * ring;
    indices.push(rearCenter, base + i, base + next);
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();
  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();
  geometry.name = "model-x.body-loft";
  return geometry;
}

function quad(points: readonly [number, number, number][]): THREE.BufferGeometry {
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(points.flat(), 3));
  geometry.setIndex([0, 1, 2, 0, 2, 3]);
  geometry.computeVertexNormals();
  return geometry;
}

function buildBody(materials: ModelXMaterials, detail: ModelXDetail): THREE.Group {
  const batch = new StaticBatch();
  batch.addOwned(bodyLoft(), materials.paint);

  // The windows are fitted panels rather than holes. Against black paint their
  // blue-grey reflectance is what separates the greenhouse from the body.
  batch.addOwned(
    quad([
      [-0.77, 1.04, -0.79],
      [0.77, 1.04, -0.79],
      [0.63, 1.56, -0.14],
      [-0.63, 1.56, -0.14],
    ]),
    materials.glass,
  );
  batch.addOwned(
    quad([
      [-0.61, 1.58, -0.1],
      [0.61, 1.58, -0.1],
      [0.58, 1.64, 1.18],
      [-0.58, 1.64, 1.18],
    ]),
    materials.glass,
  );
  batch.addOwned(
    quad([
      [-0.65, 1.5, 1.45],
      [0.65, 1.5, 1.45],
      [0.72, 1.04, 2.12],
      [-0.72, 1.04, 2.12],
    ]),
    materials.glass,
  );

  for (const side of [-1, 1]) {
    const x = side * 0.956;
    batch.addOwned(
      quad([
        [x, 1.06, -0.71],
        [x, 1.55, -0.08],
        [x, 1.59, 0.3],
        [x, 1.06, 0.3],
      ]),
      materials.glass,
    );
    batch.addOwned(
      quad([
        [x, 1.06, 0.34],
        [x, 1.59, 0.34],
        [side * 0.94, 1.49, 1.34],
        [side * 0.96, 1.05, 1.46],
      ]),
      materials.glass,
    );

    // Slim pillars and the falcon-door roof seam survive a chase camera while
    // keeping the side glass readable as two doors rather than one dark strip.
    batch.box(materials.trim, {
      position: [x, 1.31, 0.32],
      scale: [0.028, 0.56, 0.045],
    });
    batch.box(materials.trim, {
      position: [side * 0.76, 1.625, 0.73],
      scale: [0.022, 0.025, 0.86],
      rotation: [0, 0, side * -0.08],
    });

    // Flush black handles: relief and a highlight, never a badge.
    if (detail === "follow") {
      for (const z of [-0.24, 0.82]) {
        batch.box(materials.trim, {
          position: [side * 0.987, 1.025, z],
          scale: [0.018, 0.035, 0.22],
        });
      }
    }

    // Headlights sweep back into the fender; taillights wrap the rear corner.
    batch.box(materials.headlight, {
      position: [side * 0.65, 0.88, -2.39],
      scale: [0.56, 0.085, 0.035],
      rotation: [0, side * 0.13, side * 0.04],
    });
    batch.box(materials.tailLight, {
      position: [side * 0.65, 0.97, 2.35],
      scale: [0.58, 0.075, 0.04],
      rotation: [0, side * -0.12, side * -0.03],
    });
  }

  // Lower aero surfaces stop the black shell dissolving into the road.
  batch.box(materials.trim, {
    position: [0, 0.39, -1.92],
    scale: [1.82, 0.13, 1.02],
  });
  batch.box(materials.trim, {
    position: [0, 0.42, 2.32],
    scale: [1.5, 0.15, 0.3],
  });
  batch.add(UNIT_PLANE, materials.trim, {
    position: [0, 1.685, 0.68],
    scale: [1.18, 0.72, 1],
    rotation: [-Math.PI / 2, 0, 0],
  });

  const body = batch.build("model-x.body");
  body.userData.kind = "vehicle-body";
  return body;
}

interface WheelGeometry {
  tire: THREE.BufferGeometry;
  rim: THREE.BufferGeometry;
  brake: THREE.BufferGeometry;
}

function createWheelGeometry(detail: ModelXDetail): WheelGeometry {
  const radialSegments = detail === "follow" ? 24 : 16;
  const tubularSegments = detail === "follow" ? 12 : 8;
  const tire = new THREE.TorusGeometry(0.315, 0.09, tubularSegments, radialSegments);
  tire.rotateY(Math.PI / 2);
  tire.name = "model-x.wheel.tire";

  const rimParts: THREE.BufferGeometry[] = [];
  const lip = new THREE.TorusGeometry(0.218, 0.024, 6, radialSegments);
  lip.rotateY(Math.PI / 2);
  rimParts.push(lip);
  const hub = new THREE.CylinderGeometry(0.067, 0.067, 0.1, 12, 1);
  hub.rotateZ(Math.PI / 2);
  rimParts.push(hub);
  for (let i = 0; i < 5; i++) {
    const angle = (i / 5) * Math.PI * 2;
    const spoke = UNIT_BOX.clone().applyMatrix4(
      matrix({
        position: [0, Math.cos(angle) * 0.13, Math.sin(angle) * 0.13],
        scale: [0.055, 0.255, 0.035],
        rotation: [angle, 0, 0],
      }),
    );
    rimParts.push(spoke);
  }
  const rim = mergeGeometries(rimParts, false);
  for (const part of rimParts) part.dispose();
  if (!rim) throw new Error("model-x: could not build wheel rim");
  rim.name = "model-x.wheel.rim";

  const brake = new THREE.CylinderGeometry(0.17, 0.17, MODEL_X_METRICS.tireWidth + 0.018, 20, 1);
  brake.rotateZ(Math.PI / 2);
  brake.name = "model-x.wheel.brake";
  return { tire, rim, brake };
}

function buildWheel(
  name: string,
  x: number,
  z: number,
  geometries: WheelGeometry,
  materials: ModelXMaterials,
): ModelXWheel {
  const steering = new THREE.Group();
  steering.name = `${name}.steering`;
  steering.position.set(x, MODEL_X_METRICS.wheelRadius, z);

  const spin = new THREE.Group();
  spin.name = `${name}.spin`;
  steering.add(spin);

  const brake = new THREE.Mesh(geometries.brake, materials.brake);
  brake.name = `${name}.brake`;
  brake.castShadow = true;
  spin.add(brake);

  const rim = new THREE.Mesh(geometries.rim, materials.wheel);
  rim.name = `${name}.rim`;
  rim.castShadow = true;
  spin.add(rim);

  const tire = new THREE.Mesh(geometries.tire, materials.tire);
  tire.name = `${name}.tire`;
  tire.castShadow = true;
  tire.receiveShadow = true;
  spin.add(tire);

  return { steering, spin, tire, rim };
}

const WHEEL_KEYS = ["frontLeft", "frontRight", "rearLeft", "rearRight"] as const;

function resolveRig(root: THREE.Group, ownsMaterials: boolean): ModelXRig {
  const body = root.getObjectByName("model-x.body");
  if (!(body instanceof THREE.Group)) throw new Error("model-x: body group is missing");

  const wheels = {} as Record<(typeof WHEEL_KEYS)[number], ModelXWheel>;
  for (const key of WHEEL_KEYS) {
    const steering = root.getObjectByName(`${key}.steering`);
    const spin = root.getObjectByName(`${key}.spin`);
    const tire = root.getObjectByName(`${key}.tire`);
    const rim = root.getObjectByName(`${key}.rim`);
    if (!(steering instanceof THREE.Group) || !(spin instanceof THREE.Group)) {
      throw new Error(`model-x: wheel rig "${key}" is incomplete`);
    }
    if (!(tire instanceof THREE.Mesh) || !(rim instanceof THREE.Mesh)) {
      throw new Error(`model-x: wheel meshes for "${key}" are missing`);
    }
    wheels[key] = { steering, spin, tire, rim };
  }
  return { root, body, wheels, ownsMaterials };
}

export function buildModelX(options: ModelXBuildOptions = {}): ModelXRig {
  const detail = options.detail ?? "follow";
  const materials = options.materials ?? createModelXMaterials(options.paint);
  const root = new THREE.Group();
  root.name = "model-x";
  root.userData.kind = "vehicle";
  root.userData.vehicleModel = "model-x";
  root.userData.forwardAxis = "-Z";

  root.add(buildBody(materials, detail));
  const geometry = createWheelGeometry(detail);
  const halfTrack = MODEL_X_METRICS.track / 2;
  const halfWheelbase = MODEL_X_METRICS.wheelbase / 2;
  const wheels: ModelXWheels = {
    frontLeft: buildWheel("frontLeft", -halfTrack, -halfWheelbase, geometry, materials),
    frontRight: buildWheel("frontRight", halfTrack, -halfWheelbase, geometry, materials),
    rearLeft: buildWheel("rearLeft", -halfTrack, halfWheelbase, geometry, materials),
    rearRight: buildWheel("rearRight", halfTrack, halfWheelbase, geometry, materials),
  };
  for (const key of WHEEL_KEYS) root.add(wheels[key].steering);
  return { root, body: root.getObjectByName("model-x.body") as THREE.Group, wheels, ownsMaterials: !options.materials };
}

/** Clone the hierarchy while sharing all immutable geometry and material resources. */
export function cloneModelX(source: ModelXRig): ModelXRig {
  return resolveRig(source.root.clone(true), false);
}

export function setModelXWheelRotation(rig: ModelXRig, radians: number): void {
  for (const key of WHEEL_KEYS) rig.wheels[key].spin.rotation.x = radians;
}

export function advanceModelXWheels(rig: ModelXRig, distanceMetres: number): void {
  const delta = -distanceMetres / MODEL_X_METRICS.wheelRadius;
  for (const key of WHEEL_KEYS) rig.wheels[key].spin.rotation.x += delta;
}

/** Set both front wheels, clamped to the physical steering envelope. */
export function setModelXSteering(rig: ModelXRig, radians: number): void {
  const angle = THREE.MathUtils.clamp(
    radians,
    -MODEL_X_METRICS.maxSteeringAngle,
    MODEL_X_METRICS.maxSteeringAngle,
  );
  rig.wheels.frontLeft.steering.rotation.y = angle;
  rig.wheels.frontRight.steering.rotation.y = angle;
}

/**
 * Describe neutral-pose drawables for a traffic renderer that groups matching
 * pieces into InstancedMesh batches. Matrices are cloned and safe to retain.
 */
export function modelXInstanceParts(rig: ModelXRig): ModelXInstancePart[] {
  rig.root.updateMatrixWorld(true);
  const inverseRoot = rig.root.matrixWorld.clone().invert();
  const result: ModelXInstancePart[] = [];
  rig.root.traverse((object) => {
    if (!(object instanceof THREE.Mesh)) return;
    result.push({
      name: object.name,
      geometry: object.geometry,
      material: object.material,
      matrix: inverseRoot.clone().multiply(object.matrixWorld),
      castShadow: object.castShadow,
      receiveShadow: object.receiveShadow,
    });
  });
  return result;
}

/**
 * Dispose the prototype's unique geometry, and its default materials if owned.
 * Never dispose a clone: it references the same GPU resources as the prototype.
 */
export function disposeModelX(
  rig: ModelXRig,
  options: { disposeMaterials?: boolean } = {},
): void {
  const geometries = new Set<THREE.BufferGeometry>();
  const materials = new Set<THREE.Material>();
  rig.root.traverse((object) => {
    if (!(object instanceof THREE.Mesh)) return;
    geometries.add(object.geometry);
    if (Array.isArray(object.material)) {
      for (const material of object.material) materials.add(material);
    } else materials.add(object.material);
  });
  for (const geometry of geometries) geometry.dispose();
  if (options.disposeMaterials ?? rig.ownsMaterials) {
    for (const material of materials) material.dispose();
  }
}
