import {
  AmbientLight,
  BoxGeometry,
  Color,
  DirectionalLight,
  Fog,
  Group,
  Mesh,
  MeshLambertMaterial,
  PlaneGeometry,
  PointLight,
  Scene,
  Vector2,
  Vector3,
} from "three";
import { ConvexGeometry } from "three/examples/jsm/geometries/ConvexGeometry.js";
import type { InteractableDef, RoomDef, StaticGeometryDef } from "@psx/shared";
import type { PhysicsWorld } from "../engine/PhysicsWorld.js";
import { applyPSXMaterial } from "../post/PSXMaterial.js";
import { INTERACTABLE_PREFIX } from "../systems/InteractionSystem.js";

/**
 * Turns a declarative `RoomDef` into three.js objects plus box3d bodies.
 *
 * Visual and collider are authored separately rather than derived from each
 * other. That is the PSX-correct arrangement — collision was always coarser
 * than the art — and it keeps the mover's ray probes cheap.
 *
 * Materials are MeshLambert, not MeshStandard: the era had no PBR, and
 * per-vertex diffuse lighting is closer to what the hardware actually did.
 */

const STATIC_PREFIX = "geo:";

export interface BuiltRoom {
  group: Group;
  dispose(): void;
}

export class RoomBuilder {
  private readonly meshesByGeometryId = new Map<string, Mesh>();
  private readonly interactableMeshes = new Map<string, Mesh>();
  private group = new Group();
  private readonly snapResolution = new Vector2(320, 240);

  constructor(
    private readonly scene: Scene,
    private readonly physics: PhysicsWorld,
  ) {}

  setSnapResolution(width: number, height: number): void {
    this.snapResolution.set(width, height);
  }

  build(room: RoomDef, activeFlags: ReadonlySet<string>): BuiltRoom {
    this.group = new Group();
    this.group.name = room.id;

    this.scene.fog = new Fog(room.fog.color, room.fog.near, room.fog.far);
    this.scene.background = new Color(room.fog.color);

    this.buildLights(room);

    for (const def of room.geometry) {
      if (def.removeOnFlag && activeFlags.has(def.removeOnFlag)) continue;
      this.buildStatic(def);
    }

    for (const def of room.interactables) {
      this.buildInteractableVisual(def);
    }

    this.scene.add(this.group);
    return {
      group: this.group,
      dispose: () => this.dispose(),
    };
  }

  private buildLights(room: RoomDef): void {
    // Base fill so unlit corners stay readable. Authored intensities are
    // "game units" (RE-style), not candela — we scale them into three's
    // physical light model below.
    const ambient = new AmbientLight(
      room.ambientLight.color,
      // Floor so a mistyped 0.1 ambient cannot black out a room again.
      Math.max(0.35, room.ambientLight.intensity),
    );
    ambient.name = "ambient";
    this.group.add(ambient);

    for (const [index, light] of room.lights.entries()) {
      if (light.kind === "point") {
        // decay 1 is closer to classic console falloff than physical inverse-
        // square (decay 2), which snuffs out small rooms unless intensities
        // are cranked into the hundreds.
        const point = new PointLight(
          light.color,
          Math.max(8, light.intensity),
          light.distance ?? 0,
          1,
        );
        point.position.set(light.position.x, light.position.y, light.position.z);
        point.name = `light-${index}`;
        this.group.add(point);
      } else {
        const directional = new DirectionalLight(
          light.color,
          Math.max(0.35, light.intensity),
        );
        directional.position.set(light.position.x, light.position.y, light.position.z);
        directional.name = `light-${index}`;
        this.group.add(directional);
      }
    }
  }

  private buildStatic(def: StaticGeometryDef): void {
    const mesh = this.buildVisual(def);
    if (mesh) {
      mesh.name = def.id;
      mesh.position.set(def.position.x, def.position.y, def.position.z);
      if (def.yaw) mesh.rotation.y = def.yaw;
      this.group.add(mesh);
      this.meshesByGeometryId.set(def.id, mesh);
    }

    if (def.colliders.length > 0) {
      this.physics.createStatic({
        id: STATIC_PREFIX + def.id,
        position: def.position,
        colliders: def.colliders,
        material: def.material,
      });
    }
  }

  private buildVisual(def: StaticGeometryDef): Mesh | null {
    const { visual } = def;
    switch (visual.kind) {
      case "box":
        return new Mesh(
          new BoxGeometry(visual.size.x, visual.size.y, visual.size.z),
          this.makeMaterial(visual.color),
        );
      case "plane": {
        const mesh = new Mesh(
          new PlaneGeometry(visual.size[0], visual.size[1]),
          this.makeMaterial(visual.color),
        );
        mesh.rotation.x = -Math.PI / 2;
        return mesh;
      }
      case "hull": {
        const points = visual.points.map((p) => new Vector3(p.x, p.y, p.z));
        return new Mesh(new ConvexGeometry(points), this.makeMaterial(visual.color));
      }
      case "none":
        return null;
    }
  }

  private buildInteractableVisual(def: InteractableDef): void {
    if (!def.visual || def.visual.kind === "none") return;

    const mesh = new Mesh(
      new BoxGeometry(def.visual.size.x, def.visual.size.y, def.visual.size.z),
      this.makeMaterial(def.visual.color, true),
    );
    mesh.name = INTERACTABLE_PREFIX + def.id;
    mesh.position.set(def.position.x, def.position.y, def.position.z);
    this.group.add(mesh);
    this.interactableMeshes.set(def.id, mesh);
  }

  private makeMaterial(color: number, emissive = false): MeshLambertMaterial {
    const material = new MeshLambertMaterial({
      color,
      flatShading: true,
      ...(emissive ? { emissive: new Color(color).multiplyScalar(0.35) } : {}),
    });
    return applyPSXMaterial(material, { snapResolution: this.snapResolution });
  }

  /** Despawn a flag-gated piece of geometry, mesh and collider together. */
  removeGeometry(geometryId: string): void {
    const mesh = this.meshesByGeometryId.get(geometryId);
    if (mesh) {
      this.group.remove(mesh);
      mesh.geometry.dispose();
      (mesh.material as MeshLambertMaterial).dispose();
      this.meshesByGeometryId.delete(geometryId);
    }
    this.physics.remove(STATIC_PREFIX + geometryId);
  }

  removeInteractableVisual(interactableId: string): void {
    const mesh = this.interactableMeshes.get(interactableId);
    if (!mesh) return;
    this.group.remove(mesh);
    mesh.geometry.dispose();
    (mesh.material as MeshLambertMaterial).dispose();
    this.interactableMeshes.delete(interactableId);
  }

  getInteractableMesh(interactableId: string): Mesh | undefined {
    return this.interactableMeshes.get(interactableId);
  }

  private dispose(): void {
    this.group.traverse((object) => {
      if (!(object instanceof Mesh)) return;
      object.geometry.dispose();
      const material = object.material;
      if (Array.isArray(material)) material.forEach((m) => m.dispose());
      else material.dispose();
    });
    this.scene.remove(this.group);
    this.meshesByGeometryId.clear();
    this.interactableMeshes.clear();
  }
}
