import {
  Box3,
  Color,
  DoubleSide,
  Material,
  Matrix4,
  Mesh,
  MeshBasicMaterial,
  MeshStandardMaterial,
  Object3D,
  Quaternion,
  Vector3,
} from "three";
import { ResourcesC } from "@24tools/playable_template";
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
import { ThreeC } from "../ThreeC";
import { ResourcesType } from "../Presets/Enums/ResourcesType";
import { PropC } from "./PropC";

const HP_BAR_MESH = "hpBar";
const NODE_UI = "UI";
const NODE_BACKGROUND = "UI_Background";
const NODE_FOREGROUND = "UI_Foreground";
const NODE_MIDDLEGROUND = "UI_Middleground";

const BAR_Y_OFFSET = 0.55;
const BAR_X_OFFSET = -0.2;
const CATCHUP_SPEED = 2.5;
const BAR_WORLD_SCALE = 1;

/** Transparent pass + renderOrder above all map decals. */
export const OVERLAY_RENDER_ORDER = 10000;
const RENDER_ORDER_BACKGROUND = OVERLAY_RENDER_ORDER;
const RENDER_ORDER_MIDDLEGROUND = OVERLAY_RENDER_ORDER + 1;
const RENDER_ORDER_FOREGROUND = OVERLAY_RENDER_ORDER + 2;

type FillPart = {
  node: Object3D;
  baseScaleX: number;
  basePosX: number;
  /** Local mesh min.x (negative); keeps left edge fixed when scaling. */
  anchorMinX: number;
};

export class PropHpBar {
  static readonly OVERLAY_RENDER_ORDER = OVERLAY_RENDER_ORDER;
  private static readonly identityMatrix = new Matrix4();
  private static readonly tempMatrix = new Matrix4();
  private static readonly tempPosition = new Vector3();
  private static readonly tempQuaternion = new Quaternion();
  private static readonly tempScale = new Vector3();
  private static readonly worldAnchor = new Vector3();

  private readonly prop: PropC;
  private readonly root: Object3D;
  private readonly foreground: FillPart | null;
  private readonly middleground: FillPart | null;
  private readonly bounds = new Box3();

  private displayedHealth: number;
  private visible = false;

  static getPrefab(): Object3D | null {
    const fromScene = ThreeC.getObject(HP_BAR_MESH);
    if (fromScene) return fromScene;

    const gltf = ResourcesC.getResource<GLTF>(ResourcesType.Mesh, HP_BAR_MESH);
    return gltf?.scene ?? null;
  }

  constructor(prop: PropC, parent: Object3D) {
    const template = PropHpBar.getPrefab();
    if (!template) {
      throw new Error(`HP bar mesh resource not found: ${HP_BAR_MESH}`);
    }

    const sourceUi = template.getObjectByName(NODE_UI);
    if (!sourceUi) {
      throw new Error(`HP bar node not found: ${NODE_UI}`);
    }

    this.prop = prop;
    this.displayedHealth = prop.health;

    this.root = new Object3D();
    this.root.visible = false;
    this.root.scale.setScalar(BAR_WORLD_SCALE);
    this.root.frustumCulled = false;
    this.root.renderOrder = RENDER_ORDER_FOREGROUND;

    const ui = sourceUi.clone(true);
    this.bakeMatrices(ui);
    this.centerGroup(ui);
    this.prepareMeshes(ui);

    this.root.add(ui);
    parent.add(this.root);

    const fgNode = ui.getObjectByName(NODE_FOREGROUND);
    const mgNode = ui.getObjectByName(NODE_MIDDLEGROUND);
    this.foreground = fgNode ? this.createFillPart(fgNode) : null;
    this.middleground = mgNode ? this.createFillPart(mgNode) : null;

    const background = ui.getObjectByName(NODE_BACKGROUND);
    if (background) background.visible = true;

    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,
      );
    }

    if (this.middleground) {
      this.applyFill(this.middleground, this.getHealthRatio(this.displayedHealth));
    }

    if (!this.visible) return;

    this.updateTransform();
  }

  setVisible(value: boolean) {
    this.visible = value;
    this.root.visible = value;

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

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

  private applyWidths() {
    if (this.foreground) {
      this.applyFill(this.foreground, this.getHealthRatio(this.prop.health));
    }
    if (this.middleground) {
      this.applyFill(this.middleground, this.getHealthRatio(this.displayedHealth));
    }
  }

  private applyFill(part: FillPart, ratio: number) {
    const clamped = Math.max(0, Math.min(1, ratio));
    part.node.scale.x = part.baseScaleX * clamped;
    part.node.position.x =
      part.basePosX + part.anchorMinX * part.baseScaleX * (1 - clamped);
  }

  private createFillPart(node: Object3D): FillPart {
    const meshMinX = this.getMeshMinX(node);
    const isForeground = node.name.includes(NODE_FOREGROUND);

    node.renderOrder = isForeground
      ? RENDER_ORDER_FOREGROUND
      : RENDER_ORDER_MIDDLEGROUND;

    return {
      node,
      baseScaleX: node.scale.x,
      basePosX: node.position.x,
      anchorMinX: meshMinX,
    };
  }

  private getMeshMinX(node: Object3D) {
    const mesh = node as Mesh;
    if (!mesh.isMesh || !mesh.geometry) return -0.935;

    mesh.geometry.computeBoundingBox();
    return mesh.geometry.boundingBox?.min.x ?? -0.935;
  }

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

  private updateTransform() {
    if (!this.computeVisualBounds()) {
      this.root.visible = false;
      return;
    }

    PropHpBar.worldAnchor.set(
      (this.bounds.min.x + this.bounds.max.x) * 0.5 + BAR_X_OFFSET,
      this.bounds.max.y + BAR_Y_OFFSET,
      (this.bounds.min.z + this.bounds.max.z) * 0.5,
    );

    this.root.position.copy(PropHpBar.worldAnchor);
    this.root.quaternion.set(0, 0, 0, 1);
    this.root.visible = this.visible;
  }

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

    this.prop.object.traverse((child) => {
      if (!(child as Mesh).isMesh) return;

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

      this.bounds.expandByObject(child);
    });

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

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

  private bakeMatrices(ui: Object3D) {
    this.decomposeNodeMatrix(ui);
    ui.traverse((child) => {
      if (child === ui) return;
      this.decomposeNodeMatrix(child);
    });
  }

  private decomposeNodeMatrix(node: Object3D) {
    node.updateMatrix();
    PropHpBar.tempMatrix.copy(node.matrix);
    if (PropHpBar.tempMatrix.equals(PropHpBar.identityMatrix)) return;

    PropHpBar.tempMatrix.decompose(
      PropHpBar.tempPosition,
      PropHpBar.tempQuaternion,
      PropHpBar.tempScale,
    );

    node.matrix.identity();
    node.position.copy(PropHpBar.tempPosition);
    node.quaternion.copy(PropHpBar.tempQuaternion);
    node.scale.copy(PropHpBar.tempScale);
    node.matrixAutoUpdate = true;
  }

  private centerGroup(group: Object3D) {
    const box = new Box3().setFromObject(group);
    const center = new Vector3();
    box.getCenter(center);
    group.position.sub(center);
  }

  private prepareMeshes(root: Object3D) {
    root.traverse((child) => {
      child.frustumCulled = false;

      if (!(child as Mesh).isMesh) return;

      const mesh = child as Mesh;
      mesh.castShadow = false;
      mesh.receiveShadow = false;

      if (child.name.includes(NODE_BACKGROUND)) {
        child.renderOrder = RENDER_ORDER_BACKGROUND;
      } else if (child.name.includes(NODE_MIDDLEGROUND)) {
        child.renderOrder = RENDER_ORDER_MIDDLEGROUND;
      } else if (child.name.includes(NODE_FOREGROUND)) {
        child.renderOrder = RENDER_ORDER_FOREGROUND;
      }

      if (Array.isArray(mesh.material)) {
        mesh.material = mesh.material.map((material) => this.toVisibleMaterial(material));
      } else {
        mesh.material = this.toVisibleMaterial(mesh.material);
      }
    });
  }

  /** Keeps GLB map/color; unlit basic material renders reliably in playable. */
  private toVisibleMaterial(source: Material) {
    if (source instanceof MeshBasicMaterial) {
      const cloned = source.clone();
      cloned.side = DoubleSide;
      cloned.depthTest = false;
      cloned.depthWrite = false;
      cloned.toneMapped = false;
      cloned.transparent = true;
      return cloned;
    }

    const src = source as MeshStandardMaterial;
    const material = new MeshBasicMaterial({
      side: DoubleSide,
      depthTest: false,
      depthWrite: false,
      toneMapped: false,
      transparent: true,
      opacity: src.opacity ?? 1,
    });

    if (src.map) {
      material.map = src.map;
    }
    if (src.color) {
      material.color.copy(src.color);
    } else {
      material.color = new Color(0xffffff);
    }
    if (src.alphaTest) {
      material.alphaTest = src.alphaTest;
      material.transparent = true;
    }

    return material;
  }
}
