import { Box3, BoxGeometry, Mesh, Object3D, Vector3 } from "three";
import { ThreeC } from "../ThreeC";
import { PhysicsBody, PhysicsLayer } from "../PhysicsC";
import { PhysicsTriggerC } from "./PhysicsTriggerC";
import { PropC } from "./PropC";
import { findDamageStateLayers } from "./PropDamageLayers";
import { resolvePropType } from "../Resources/PropDropTable";

import { InteractiveZoneC } from "./InteractiveZoneC";

const MAP_PHYSICS_LAYERS = ["Colliders", "Lootable"];
const GATHER_TRIGGER_PADDING = 0.5;

export class Map {
  static init() {
    const mapObject = ThreeC.getObject("map");
    if (!mapObject) {
      console.warn("Map model resource not found: map");
      return;
    }

    mapObject.position.set(0, 0, 0);
    ThreeC.setShadowsStateForChildren(mapObject, true, true);
    ThreeC.addToScene(mapObject);
    this.hideReferenceHpUi(mapObject);

    PhysicsTriggerC.init();
    this.buildPhysics(mapObject);
    InteractiveZoneC.init(mapObject);
  }

  private static hideReferenceHpUi(mapObject: Object3D) {
    const ui = mapObject.getObjectByName("UI");
    if (ui) ui.visible = false;
  }

  private static buildPhysics(mapObject: Object3D) {
    for (const layerName of MAP_PHYSICS_LAYERS) {
      const layer = mapObject.getObjectByName(layerName);
      if (!layer) {
        console.warn(`Map physics layer not found: ${layerName}`);
        continue;
      }

      if (layerName === "Colliders") {
        layer.visible = false;
        this.addWallPhysics(layer);
        continue;
      }

      layer.children.forEach((lootableObject) => {
        this.setupLootableObject(lootableObject);
      });
    }
  }

  private static addWallPhysics(root: Object3D) {
    root.traverse((child) => {
      if (child === root) return;
      if ((child as Mesh).isMesh) {
        new PhysicsBody(child, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player);
      }
    });
  }

  private static setupLootableObject(lootableObject: Object3D) {
    const propType = resolvePropType(lootableObject.name);
    const damageLayers = findDamageStateLayers(lootableObject);
    const maxHealth = damageLayers.length > 0 ? damageLayers.length : 1;
    const prop = new PropC(lootableObject, propType, maxHealth);
    prop.initDamageLayers(damageLayers);
    const colliderMesh = this.findMesh(lootableObject, (name) => name.includes("collider"));
    const visualMeshes = this.getVisualMeshes(lootableObject);

    if (colliderMesh) {
      colliderMesh.visible = false;
      prop.addWallBody(
        new PhysicsBody(colliderMesh, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player),
      );
    } else {
      visualMeshes.forEach((mesh) => {
        prop.addWallBody(
          new PhysicsBody(mesh, false, 0, PhysicsLayer.Wall, PhysicsLayer.Player),
        );
      });
    }

    PhysicsTriggerC.register(this.createGatherTrigger(lootableObject), lootableObject);
  }

  private static findMesh(
    root: Object3D,
    matcher: (name: string) => boolean,
  ): Mesh | null {
    let found: Mesh | null = null;

    root.traverse((child) => {
      if (found) return;
      if ((child as Mesh).isMesh && matcher(child.name.toLowerCase())) {
        found = child as Mesh;
      }
    });

    return found;
  }

  private static getVisualMeshes(root: Object3D): Mesh[] {
    const visualMeshes: Mesh[] = [];

    root.traverse((child) => {
      if (!(child as Mesh).isMesh) return;

      const name = child.name.toLowerCase();
      if (name.includes("collider") || name.includes("trigger") || name.includes("gathertrigger")) return;

      visualMeshes.push(child as Mesh);
    });

    return visualMeshes;
  }

  private static createGatherTrigger(lootableObject: Object3D): Mesh {
    lootableObject.updateWorldMatrix(true, true);

    const size = new Vector3();
    new Box3().setFromObject(lootableObject).getSize(size);

    const triggerMesh = new Mesh(
      new BoxGeometry(
        size.x + GATHER_TRIGGER_PADDING * 2,
        size.y + GATHER_TRIGGER_PADDING * 2,
        size.z + GATHER_TRIGGER_PADDING * 2,
      ),
    );
    triggerMesh.visible = false;
    triggerMesh.name = "GatherTrigger";
    lootableObject.add(triggerMesh);

    return triggerMesh;
  }
}
