import { Box3, Mesh, Object3D, Vector3 } from "three";
import { CameraC } from "../CameraC";
import { PropC } from "./PropC";

const BAR_Y_OFFSET = 0.8;
const BAR_X_OFFSET = -0.3;
const CATCHUP_SPEED = 2.5;

export class PropHpBar {
  private readonly prop: PropC;
  private readonly root: HTMLElement;
  private readonly currentEl: HTMLElement;
  private readonly delayedEl: HTMLElement;
  private readonly anchor = new Vector3();
  private readonly bounds = new Box3();

  private displayedHealth: number;
  private visible = false;

  constructor(prop: PropC, parent: HTMLElement) {
    this.prop = prop;
    this.displayedHealth = prop.health;

    this.root = document.createElement("div");
    this.root.className = "prop-hp-bar";

    const track = document.createElement("div");
    track.className = "prop-hp-bar__track";

    this.delayedEl = document.createElement("div");
    this.delayedEl.className = "prop-hp-bar__delayed";

    this.currentEl = document.createElement("div");
    this.currentEl.className = "prop-hp-bar__current";

    track.appendChild(this.delayedEl);
    track.appendChild(this.currentEl);
    this.root.appendChild(track);
    parent.appendChild(this.root);

    this.applyWidths();
  }

  onDamage() {
    if (!this.visible) {
      this.setVisible(true);
      return;
    }

    this.applyWidths();
  }

  update(delta: number) {
    if (this.prop.isBroken) return;

    const { health, maxHealth } = this.prop;
    if (this.displayedHealth > health) {
      this.displayedHealth = Math.max(
        health,
        this.displayedHealth - CATCHUP_SPEED * delta * maxHealth,
      );
    }

    this.delayedEl.style.width = `${this.getHealthRatio(this.displayedHealth) * 100}%`;

    if (!this.visible) return;

    this.updatePosition();
  }

  setVisible(value: boolean) {
    this.visible = value;
    this.root.classList.toggle("is-visible", value);

    if (value) {
      this.displayedHealth = this.prop.health;
      this.applyWidths();
      this.updatePosition();
    }
  }

  destroy() {
    this.root.remove();
  }

  private applyWidths() {
    const ratio = this.getHealthRatio(this.prop.health);
    this.currentEl.style.width = `${ratio * 100}%`;
    this.delayedEl.style.width = `${this.getHealthRatio(this.displayedHealth) * 100}%`;
  }

  private getHealthRatio(health: number) {
    if (this.prop.maxHealth <= 0) return 0;
    return Math.max(health, 0) / this.prop.maxHealth;
  }

  private updatePosition() {
    if (!this.computeVisualBounds()) {
      this.root.style.display = "none";
      return;
    }

    this.bounds.getCenter(this.anchor);
    this.anchor.x += BAR_X_OFFSET;
    this.anchor.y = this.bounds.max.y + BAR_Y_OFFSET;

    const camera = CameraC.camera;
    const projected = this.anchor.project(camera);

    if (projected.z > 1) {
      this.root.style.display = "none";
      return;
    }

    const screenPos = PropHpBar.worldToLayerPosition(projected, this.root.parentElement);
    if (!screenPos) {
      this.root.style.display = "none";
      return;
    }

    this.root.style.display = "";
    this.root.style.left = `${screenPos.x}px`;
    this.root.style.top = `${screenPos.y}px`;
  }

  private static worldToLayerPosition(
    projected: Vector3,
    layer: HTMLElement | null,
  ): { x: number; y: number } | null {
    const canvas = document.querySelector("canvas");
    if (!canvas || !layer) return null;

    const canvasRect = canvas.getBoundingClientRect();
    const layerRect = layer.getBoundingClientRect();

    return {
      x:
        (projected.x * 0.5 + 0.5) * canvasRect.width +
        canvasRect.left -
        layerRect.left,
      y:
        (-projected.y * 0.5 + 0.5) * canvasRect.height +
        canvasRect.top -
        layerRect.top,
    };
  }

  private computeVisualBounds() {
    this.prop.object.updateWorldMatrix(true, true);
    this.bounds.makeEmpty();

    const includeMesh = (node: Object3D) => {
      if (!(node as Mesh).isMesh) return;

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

      this.bounds.expandByObject(node);
    };

    includeMesh(this.prop.object);
    this.prop.object.traverse((child) => includeMesh(child));

    if (!this.bounds.isEmpty()) return true;

    this.bounds.setFromObject(this.prop.object);
    return !this.bounds.isEmpty();
  }
}
