import { Box3, DoubleSide, Group, Material, Mesh, Object3D, Vector3 } from "three";
import * as TWEEN from "@tweenjs/tween.js";
import { CameraC_internal } from "@hitplay/playable_template";
import { ThreeC } from "./ThreeC";
import { GameEvents } from "../core/GameEvents";
import { GameLoop } from "../core/GameLoop";
import type { Crate } from "./LootableC";

// --- Placement ---
const HEIGHT_ABOVE = 1.4;    // world Y offset of the bar above the crate origin
const BAR_SCALE    = 1.2;    // world scale of the cloned bar (prototype is ~1m wide)

// --- Fill animation ---
const FOREGROUND_MS       = 120; // Foreground (current health) drops fast — animates first
const MIDDLEGROUND_DELAY  = 120; // Middleground starts a touch later…
const MIDDLEGROUND_MS     = 600; // …and catches up slowly behind it (the "lost chunk" sliver)

// --- Show / hide ---
const FADE_IN_MS  = 150;
const FADE_OUT_MS = 700; // slow, soft fade-out
const HOLD_S      = 3.0; // stay visible this long after the last hit, then fade out

// One bar instance bound to a crate. The two fills slide their right edge while
// keeping the left edge pinned: foreground leads (fast), middleground trails.
interface Bar {
  group: Group;                  // billboarded container, positioned above the crate
  foreground: Mesh;              // lead fill (snaps to the new health quickly)
  middleground: Mesh;            // catch-up fill (trails behind the foreground)
  foregroundBaseX: number;       // foreground resting local X
  foregroundMinX: number;        // foreground geometry left edge (anchor for the shrink)
  middlegroundBaseX: number;
  middlegroundMinX: number;
  materials: Material[];         // all 3 cloned materials (driven together by the fade)
  alpha: number;                 // current fade level (0 hidden … 1 shown)
  idleSeconds: number;           // time since the last hit (drives the auto fade-out)
  visible: boolean;
  fadeTween: TWEEN.Tween<{ alpha: number }> | null;        // live fade (stopped before re-firing)
  foregroundTween: TWEEN.Tween<{ scaleX: number }> | null; // live fill tweens (stopped before re-firing)
  middlegroundTween: TWEEN.Tween<{ scaleX: number }> | null;
}

/**
 * In-world health bars above crates, cloned from the GLB "UI" prototype
 * (Background track + Foreground + Middleground). A bar appears on the first hit
 * and fades out when the crate is left alone. On each hit the Foreground snaps to
 * the new health quickly and the Middleground trails behind it.
 */
export class HealthBarC {
  private static protoNode: Object3D | null = null;
  private static bars = new Map<Crate, Bar>();
  private static tweens = new TWEEN.Group();

  static init(prototype: Object3D | null) {
    if (!prototype) {
      console.warn("[HealthBar] no prototype (UI node) found — bars disabled");
      return;
    }
    this.protoNode = prototype;

    // Listen for health display events
    GameEvents.onHealthDisplay.addDelegate(({ crate, healthFraction }) => this.showDamage(crate, healthFraction));
    GameEvents.onHealthHide.addDelegate((crate) => this.hide(crate));

    GameLoop.register((delta: number) => this.update(delta));
  }

  /** Show/refresh the bar for a crate at the given health fraction (0..1). */
  static showDamage(crate: Crate, fraction: number) {
    if (!this.protoNode) return;
    let bar = this.bars.get(crate);
    if (!bar) {
      const built = this.build();
      if (!built) return;
      bar = built;
      this.bars.set(crate, bar);
    }

    bar.idleSeconds = 0;
    if (!bar.visible) { bar.visible = true; this.fade(bar, 1, FADE_IN_MS); }

    const health = Math.max(0, Math.min(1, fraction));
    // Foreground first (fast), Middleground catching up behind it (slower, delayed).
    bar.foregroundTween = this.tweenFill(
      bar.foregroundTween, bar.foreground, bar.foregroundBaseX, bar.foregroundMinX, health, FOREGROUND_MS, 0);
    bar.middlegroundTween = this.tweenFill(
      bar.middlegroundTween, bar.middleground, bar.middlegroundBaseX, bar.middlegroundMinX, health, MIDDLEGROUND_MS, MIDDLEGROUND_DELAY);
  }

  /** Crate gone (broken): fade the bar out and drop it. */
  static hide(crate: Crate) {
    const bar = this.bars.get(crate);
    if (!bar) return;
    this.bars.delete(crate);
    this.fade(bar, 0, FADE_OUT_MS, () => {
      ThreeC.removeFromScene(bar.group);
      bar.materials.forEach(material => material.dispose());
    });
  }

  // Clone the prototype, make its materials fade-able, center it, and add it to the
  // scene. The bar is positioned over its crate every frame in update(), so build
  // itself needs nothing from the crate.
  private static build(): Bar | null {
    const group = new Group();
    const ui = this.protoNode!.clone(true);
    ui.quaternion.identity(); // drop the prototype's authored tilt — we billboard instead
    ui.visible = true;
    ui.traverse(o => { o.visible = true; });

    const foreground = ui.getObjectByName("UI_Foreground") as Mesh | undefined;
    const middleground = ui.getObjectByName("UI_Middleground") as Mesh | undefined;
    const background = ui.getObjectByName("UI_Background") as Mesh | undefined;
    if (!foreground || !middleground || !background) {
      console.warn("[HealthBar] prototype missing UI_Foreground/Middleground/Background");
      return null;
    }

    // Keep the GLB's original look: clone each mesh's authored (textured, unlit)
    // material and only make it fade-able + force the layer order (Foreground on
    // top of Middleground on top of Background). depthTest off so the bar always
    // draws over the scene; DoubleSide so it shows no matter how it's billboarded.
    const materials: Material[] = [];
    const prepMaterial = (mesh: Mesh, renderOrder: number) => {
      const material = (mesh.material as Material).clone();
      material.transparent = true;
      material.opacity = 0; // start hidden; the fade-in brings it up
      material.depthTest = false;
      material.depthWrite = false;
      material.side = DoubleSide;
      mesh.material = material;
      mesh.renderOrder = renderOrder;
      materials.push(material);
    };
    prepMaterial(background, 0);
    prepMaterial(middleground, 1);
    prepMaterial(foreground, 2);

    // Anchor data so the fills shrink from the right (left edge stays put).
    const leftEdgeX = (mesh: Mesh) => { mesh.geometry.computeBoundingBox(); return mesh.geometry.boundingBox!.min.x; };
    const foregroundMinX = leftEdgeX(foreground), middlegroundMinX = leftEdgeX(middleground);
    const foregroundBaseX = foreground.position.x, middlegroundBaseX = middleground.position.x;

    // Center the bar content on the group origin (the prototype meshes sit offset).
    ui.updateMatrixWorld(true);
    const center = new Box3().setFromObject(ui).getCenter(new Vector3());
    ui.position.sub(center);

    group.add(ui);
    group.scale.setScalar(BAR_SCALE);
    ThreeC.addToScene(group);

    return {
      group, foreground, middleground,
      foregroundBaseX, foregroundMinX, middlegroundBaseX, middlegroundMinX,
      materials, alpha: 0, idleSeconds: 0, visible: false,
      fadeTween: null, foregroundTween: null, middlegroundTween: null,
    };
  }

  // Animate one fill to a fraction, keeping its left edge fixed (right edge moves).
  private static tweenFill(
    prev: TWEEN.Tween<{ scaleX: number }> | null,
    mesh: Mesh, baseX: number, minX: number, targetFraction: number, ms: number, delay: number,
  ) {
    prev?.stop();
    const state = { scaleX: mesh.scale.x };
    return new TWEEN.Tween(state, this.tweens)
      .to({ scaleX: targetFraction }, ms)
      .delay(delay)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(({ scaleX }) => {
        mesh.scale.x = scaleX;
        mesh.position.x = baseX + minX * (1 - scaleX); // pin the left edge as it shrinks
      })
      .start();
  }

  // Fade all of a bar's materials to a target alpha. Stops any in-flight fade
  // first so a re-show mid-fade-out doesn't fight it (fixes the flicker on the
  // next hit right after the bar started disappearing).
  private static fade(bar: Bar, to: number, ms: number, onDone?: () => void) {
    bar.fadeTween?.stop();
    const state = { alpha: bar.alpha };
    bar.fadeTween = new TWEEN.Tween(state, this.tweens)
      .to({ alpha: to }, ms)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(({ alpha }) => {
        bar.alpha = alpha;
        bar.materials.forEach(material => { material.opacity = alpha; });
      })
      .onComplete(() => { bar.fadeTween = null; onDone?.(); })
      .start();
  }

  // Each frame: billboard active bars to the camera, keep them above their crate,
  // fade out idle bars, and advance the tweens.
  private static update(delta: number) {
    this.tweens.update();
    const camera = CameraC_internal.camera;

    for (const [crate, bar] of this.bars) {
      crate.root.getWorldPosition(bar.group.position);
      bar.group.position.y += HEIGHT_ABOVE;
      if (camera) bar.group.quaternion.copy(camera.quaternion);

      if (bar.visible) {
        bar.idleSeconds += delta;
        if (bar.idleSeconds >= HOLD_S) { bar.visible = false; this.fade(bar, 0, FADE_OUT_MS); }
      }
    }
  }
}
