import { ThreeC } from "./ThreeC";
import { InputC } from "@hitplay/playable_template";
import { Box3, Mesh, Object3D, Vector3 } from "three";
import { Body, Box, Vec3 } from "cannon-es";
import { PhysicsBody, PhysicsC, PhysicsLayer } from "./PhysicsC";

export class TestSceneC {
  static mapObject: Object3D;
  static characterObject: Object3D;

  // Top-level groups the artist authored inside the "Map" node of the GLB.
  static environment: Object3D | null = null; // Ground_  — visual ground/road/borders, the only thing rendered
  static colliderGroup: Object3D | null = null; // Colliders — invisible BoxCollider proxies → cannon bodies
  static lootableGroup: Object3D | null = null; // Lootable  — interactive crates, hidden until activated per-crate
  static payZone: Object3D | null = null; // UI_Tool_Zone — the pay-zone pad
  static payZoneIcon: Object3D | null = null; // UI_Wood — wood icon shown on the pad
  static interactiveZone: Object3D | null = null; // UI_Interactive_Zone_02 — marks where the pad goes
  static healthBarPrototype: Object3D | null = null; // UI — 3-mesh health bar template, cloned per crate

  // World Y of the walkable sand surface (the floor proxy is aligned to it).
  static groundY = 0;

  // Static cannon bodies built from the map's collider proxies.
  static mapBodies: PhysicsBody[] = [];
  // Invisible perimeter walls keeping the player on the floor (the GLB has no wall colliders).
  static boundaryBodies: Body[] = [];

  static init() {
    this.loadScene();
    this.loadCharacter();

    InputC.onTouchDown.addDelegate((event) => {
      console.log("onMouseDown", event);
    });
  }

  private static loadScene() {
    this.mapObject = ThreeC.getObject("scene");

    // Resolve the named groups baked into the GLB.
    this.environment = this.mapObject.getObjectByName("Ground_") ?? null;
    this.colliderGroup = this.mapObject.getObjectByName("Colliders") ?? null;
    this.lootableGroup = this.mapObject.getObjectByName("Lootable") ?? null;
    this.payZone = this.mapObject.getObjectByName("UI_Tool_Zone") ?? null;
    this.payZoneIcon = this.mapObject.getObjectByName("UI_Wood") ?? null;
    this.interactiveZone =
      this.mapObject.getObjectByName("UI_Interactive_Zone_02") ?? null;
    // The "UI" group is the 3-mesh health-bar template; keep it as a hidden prototype.
    this.healthBarPrototype = this.mapObject.getObjectByName("UI") ?? null;

    // Add the whole graph so every world transform stays intact (the collider
    // proxies' world positions depend on the full parent chain), then hide
    // everything that is not the static environment.
    ThreeC.addToScene(this.mapObject);
    this.mapObject.updateMatrixWorld(true);

    // Build static physics from the invisible collider proxies first…
    this.buildMapPhysics();
    this.alignFloorToGround();
    this.buildBoundaryWalls();

    // …then hide the proxies and the groups we are not activating yet.
    if (this.colliderGroup) this.colliderGroup.visible = false; // physics-only, never rendered
    // Lootable crates are set up by LootableC (one state shown + colliders).
    if (this.healthBarPrototype) this.healthBarPrototype.visible = false; // template only; clones are shown per crate
    if (this.interactiveZone) this.interactiveZone.visible = false; // only its position is used
    // payZone + its icon are placed and shown by PayZoneC.
    if (this.payZone) this.payZone.visible = false;
    if (this.payZoneIcon) this.payZoneIcon.visible = false;
    // Hide the unused duplicate wood icon (three.js drops the dot → "UI_Wood001").
    const strayWoodIcon =
      this.mapObject.getObjectByName("UI_Wood001") ??
      this.mapObject.getObjectByName("UI_Wood.001");
    if (strayWoodIcon) strayWoodIcon.visible = false;

    // Only the environment is rendered with shadows.
    if (this.environment) {
      ThreeC.setShadowsStateForChildren(this.environment, true, true);
    } else {
      console.warn(
        "[Map] 'Ground_' group not found — nothing to render as environment",
      );
    }
  }

  // One static (mass 0) CANNON.Box per BoxCollider proxy under the Colliders group.
  // For this map that is a single floor slab; per-crate colliders live under
  // Lootable and are built separately when crates are activated.
  private static buildMapPhysics() {
    if (!this.colliderGroup) {
      console.warn(
        "[Map] No 'Colliders' group found in GLB — map has no physics",
      );
      return;
    }

    this.colliderGroup.traverse((child) => {
      if (!(child instanceof Mesh)) return;
      const body = new PhysicsBody(
        child,
        false, // not a trigger
        0, // mass 0 → static
        PhysicsLayer.Wall,
        PhysicsLayer.Player, // collides with the player (other masks added when needed)
      );
      this.mapBodies.push(body);
    });

    console.log(`[Map] Static collider bodies built: ${this.mapBodies.length}`);
  }

  // The authored floor collider sits a few cm above the visual sand, so the
  // player would rest slightly floating. Measure the sand surface and shift the
  // floor body so its top matches it — feet then sit exactly on the ground,
  // independent of the collision sphere radius.
  private static alignFloorToGround() {
    if (!this.environment || this.mapBodies.length === 0) return;

    const sand =
      this.environment.getObjectByName("M_Floor_Sand") ?? this.environment;
    this.groundY = new Box3().setFromObject(sand).max.y;

    for (const physicsBody of this.mapBodies) {
      const body = physicsBody.getPhysicsBody();
      const halfHeight = (body.shapes[0] as Box).halfExtents.y;
      const top = body.position.y + halfHeight;
      body.position.y += this.groundY - top;
    }
    console.log(
      `[Map] Floor aligned to sand surface Y=${this.groundY.toFixed(3)}`,
    );
  }

  // The GLB only authors a floor collider, so the play area would be unbounded.
  // Build four thin static walls around the floor's footprint to keep the
  // player in. Bounds are read from the floor proxy so the walls always match
  // the authored map, even if it changes.
  private static buildBoundaryWalls() {
    if (!this.colliderGroup || !PhysicsC.world) return;

    const bounds = new Box3().setFromObject(this.colliderGroup);
    if (bounds.isEmpty()) return;

    const min = bounds.min;
    const max = bounds.max;
    const sizeX = max.x - min.x;
    const sizeZ = max.z - min.z;
    const floorCenterX = (min.x + max.x) / 2;
    const floorCenterZ = (min.z + max.z) / 2;

    const thickness = 0.5;
    const height = 3;
    const midY = max.y + height / 2; // sits on top of the floor

    // Each spec is [centerX, centerZ, halfX, halfZ]; height is shared. The +thickness
    // makes the walls overlap at the corners so there are no gaps.
    const wallSpecs: [number, number, number, number][] = [
      [floorCenterX, max.z + thickness / 2, sizeX / 2 + thickness, thickness / 2], // +Z
      [floorCenterX, min.z - thickness / 2, sizeX / 2 + thickness, thickness / 2], // -Z
      [max.x + thickness / 2, floorCenterZ, thickness / 2, sizeZ / 2 + thickness], // +X
      [min.x - thickness / 2, floorCenterZ, thickness / 2, sizeZ / 2 + thickness], // -X
    ];

    for (const [wallX, wallZ, halfX, halfZ] of wallSpecs) {
      const body = new Body({
        mass: 0,
        shape: new Box(new Vec3(halfX, height / 2, halfZ)),
        collisionFilterGroup: PhysicsLayer.Wall,
        collisionFilterMask: PhysicsLayer.Player,
      });
      body.position.set(wallX, midY, wallZ);
      PhysicsC.addBody(body);
      this.boundaryBodies.push(body);
    }

    console.log(
      `[Map] Boundary walls built: ${this.boundaryBodies.length} (floor ${sizeX.toFixed(1)}×${sizeZ.toFixed(1)})`,
    );
  }

  private static loadCharacter() {
    this.characterObject = ThreeC.getObject("character");
    this.configureCharacterLoadout();
    ThreeC.setShadowsStateForChildren(this.characterObject, true, true);
    this.characterObject.position.set(0, 0.1, -8);
    this.characterObject.scale.setScalar(1);
    ThreeC.addToScene(this.characterObject);
    this.liftBlobShadow();
  }

  // The baked blob shadow sits at exactly the sand height, which z-fights with
  // the ground (the depth test flips per-pixel as the view moves → flicker),
  // even though the blob doesn't write depth. Raise it a few cm so it is
  // unambiguously in front of the sand. The rig is heavily scaled internally,
  // so convert the world-space lift through the parent's world scale.
  private static liftBlobShadow() {
    const blob = this.characterObject.getObjectByName("Shadow");
    if (!blob || !blob.parent) return;
    blob.parent.updateWorldMatrix(true, false);
    const worldScaleY = blob.parent.getWorldScale(new Vector3()).y || 1;
    blob.position.y += 0.04 / worldScaleY; // ~4 cm above the sand
  }

  // The character GLB ships its full loadout visible at once. Here we only hide
  // the pistol setup: the skinned pistol (node "Character_Pistol" — that's the
  // gun, not a second body) and the separate "Bullet" VFX root (muzzle flash /
  // tracer). Kept in the graph (not removed) so a future "draw pistol" state
  // can re-enable them. The bat (Tool_1 in hand / Tool_2 on back) is managed by
  // PlayerC, since which one is shown depends on the melee state.
  private static configureCharacterLoadout() {
    for (const name of ["Character_Pistol", "Bullet"]) {
      const obj = this.characterObject.getObjectByName(name);
      if (obj) obj.visible = false;
      else
        console.warn(
          `[Character] node '${name}' not found while hiding loadout`,
        );
    }
  }
}
