import { Mesh, Object3D, Vector3 } from "three";
import * as TWEEN from "@tweenjs/tween.js";
import { PhysicsBody, PhysicsLayer } from "./PhysicsC";
import { Trigger } from "./TriggerC";
import { GameEvents } from "../core/GameEvents";
import { GameLoop } from "../core/GameLoop";
import { VfxManager } from "../resources/vfx/VfxManager";

// Crate health, split evenly across 3 damage states (S1/S2/S3), broken at 0.
const CRATE_MAX_HEALTH = 100;
const LEVELS = 3;

// --- Hit/break animation tunables ---
const HIT_PUNCH      = 0.30;  // how hard a hit squashes the crate
const HIT_PUNCH_MS   = 280;   // squash → spring-back time
const FLASH_MS       = 200;   // white blink time
const BREAK_MS       = 300;   // shrink-to-nothing time on break
const BREAK_SPIN     = Math.PI * 0.66; // how far it spins while vanishing
const VFX_CENTER_Y   = 0.5;   // VFX height (crate middle, not its base)

/** One breakable crate. Damage states are indexed by level: 0=S1, 1=S2, 2=S3. */
export interface Crate {
  root: Object3D;
  statesByLevel: (Object3D | null)[]; // the 3 damage meshes; null where not authored
  startLevel: number;                 // how damaged it starts
  level: number;                      // currently shown level
  collider: PhysicsBody;
  trigger: Trigger | null;
  health: number;
  maxHealth: number;
  broken: boolean;
  baseScale: Vector3;                 // resting scale (tweens multiply this)
  baseRotY: number;                   // resting Y rotation
  hitTween: TWEEN.Tween<{ progress: number }> | null; // live hit-punch tween (killed before re-firing)
}

/**
 * Builds the breakable crates from the map's "Lootable" group: shows one damage
 * state per crate, sets its health, and gives it a static collider.
 */
export class LootableC {
  static crates: Crate[] = [];
  private static tweens = new TWEEN.Group();          // our tween group, pumped each frame
  private static flashedStates = new WeakSet<Object3D>(); // states whose materials we've cloned for flashing

  static init(lootableGroup: Object3D | null) {
    if (!lootableGroup) {
      console.warn("[Lootable] group not found");
      return;
    }

    // Listen for crate hits from combat system
    GameEvents.onCrateHit.addDelegate(({ crate, damage }) => this.damageCrate(crate, damage));

    // Drive our juice tweens (hit punch, flash, break) every frame.
    GameLoop.register(() => this.tweens.update());

    lootableGroup.visible = true;
    lootableGroup.updateWorldMatrix(true, true); // collider world positions must be current

    for (const crate of lootableGroup.children) {
      // Map each authored damage state to its level via the _S<n> suffix.
      const statesGroup = crate.children.find(child => child.name.includes("_States"));
      const statesByLevel: (Object3D | null)[] = [null, null, null];
      if (statesGroup) {
        for (const stateNode of statesGroup.children) {
          const match = stateNode.name.match(/_S(\d)$/);
          if (match) {
            const level = parseInt(match[1], 10) - 1; // S1→0, S2→1, S3→2
            if (level >= 0 && level < LEVELS) statesByLevel[level] = stateNode;
          }
        }
      }

      // Start at the lowest authored state (most intact one present).
      let startLevel = statesByLevel.findIndex(state => state !== null);
      if (startLevel < 0) startLevel = 0;
      statesByLevel.forEach((state, level) => { if (state) state.visible = level === startLevel; });

      // Health for that starting level (full crate = 100, S2 ≈ 67, S3 ≈ 33).
      const health = CRATE_MAX_HEALTH * (LEVELS - startLevel) / LEVELS;

      // The per-crate collider proxy → static box, then hide it (physics only).
      const proxy = crate.children.find(child => child.name.startsWith("BoxCollider"));
      if (!proxy) continue;

      const collider = new PhysicsBody(
        proxy,
        false,               // not a trigger — it's solid
        0,                   // mass 0 → static
        PhysicsLayer.Wall,   // same layer as walls, so the player collides with it
        PhysicsLayer.Player
      );
      proxy.visible = false;

      this.crates.push({
        root: crate, statesByLevel, startLevel, level: startLevel,
        collider, trigger: null, health, maxHealth: CRATE_MAX_HEALTH, broken: false,
        baseScale: crate.scale.clone(), baseRotY: crate.rotation.y, hitTween: null,
      });
    }

    console.log(`[Lootable] crates built: ${this.crates.length}`);
  }

  /** Subtract health; switch to the matching damage state, or break at 0. */
  static damageCrate(crate: Crate, amount: number) {
    if (crate.broken) return;

    crate.health -= amount;
    if (crate.health <= 0) {
      this.breakCrate(crate);
      return;
    }

    // Per-hit juice: a quick squash-punch + a white blink.
    this.punchCrate(crate);
    this.flashCrate(crate);

    // Show/refresh the floating health bar above the crate.
    GameEvents.onHealthDisplay.Invoke({ crate, healthFraction: crate.health / crate.maxHealth });

    // Map remaining health to a level, never below where this crate started.
    let level = Math.floor((1 - crate.health / crate.maxHealth) * LEVELS);
    level = Math.max(crate.startLevel, Math.min(LEVELS - 1, level));
    if (level !== crate.level) {
      crate.level = level;
      crate.statesByLevel.forEach((state, lvl) => { if (state) state.visible = lvl === level; });
      // Loot drops on every state change, not only on destruction.
      const position = crate.root.getWorldPosition(new Vector3());
      GameEvents.onLootSpawn.Invoke({ position });
    }
  }

  /** Crate destroyed: drop loot, then shrink-and-spin away. */
  static breakCrate(crate: Crate) {
    if (crate.broken) return;
    crate.broken = true;

    // Drop the floating health bar.
    GameEvents.onHealthHide.Invoke(crate);

    // Stop the hit-punch so it doesn't fight the break animation.
    crate.hitTween?.stop();
    crate.hitTween = null;

    // Remove physics + trigger now so the player walks through right away.
    crate.collider.destroy();
    crate.trigger?.destroy();
    crate.trigger = null;

    // Drop loot at the crate's spot.
    const position = crate.root.getWorldPosition(new Vector3());
    GameEvents.onLootSpawn.Invoke({ position });

    // Shrink to nothing while spinning, then hide the meshes.
    const anim = { progress: 0 };
    new TWEEN.Tween(anim, this.tweens)
      .to({ progress: 1 }, BREAK_MS)
      .easing(TWEEN.Easing.Back.In)
      .onUpdate(({ progress }) => {
        const scale = Math.max(0, 1 - progress);
        crate.root.scale.set(crate.baseScale.x * scale, crate.baseScale.y * scale, crate.baseScale.z * scale);
        crate.root.rotation.y = crate.baseRotY + progress * BREAK_SPIN;
      })
      .onComplete(() => {
        crate.statesByLevel.forEach(state => { if (state) state.visible = false; });
      })
      .start();

    const breakFxPos = crate.root.getWorldPosition(new Vector3());
    breakFxPos.y += VFX_CENTER_Y;
    VfxManager.Play("DestroyEffect", null, breakFxPos);
  }

  // Squash the crate, then spring back (with a small overshoot pop).
  private static punchCrate(crate: Crate) {
    crate.hitTween?.stop(); // drop the previous punch so rapid hits don't stack
    const anim = { progress: 0 };
    crate.hitTween = new TWEEN.Tween(anim, this.tweens)
      .to({ progress: 1 }, HIT_PUNCH_MS)
      .easing(TWEEN.Easing.Back.Out)
      .onUpdate(({ progress }) => {
        const squash = (1 - progress) * HIT_PUNCH; // HIT_PUNCH (squashed) → 0 (rest)
        crate.root.scale.set(
          crate.baseScale.x * (1 + squash),
          crate.baseScale.y * (1 - squash),
          crate.baseScale.z * (1 + squash),
        );
      })
      .onComplete(() => {
        crate.root.scale.copy(crate.baseScale); // snap back exactly
        crate.hitTween = null;
      })
      .start();

    // Hit spark at the crate's middle (world position, no parent).
    const hitPos = crate.root.getWorldPosition(new Vector3());
    hitPos.y += VFX_CENTER_Y;
    VfxManager.Play("HitEffect", null, hitPos);
  }

  // Flash the crate white, then fade out. Materials are cloned per state so the
  // flash doesn't bleed onto other crates sharing the same material.
  private static flashCrate(crate: Crate) {
    const state = crate.statesByLevel[crate.level];
    if (!state) return;

    if (!this.flashedStates.has(state)) {
      state.traverse(node => {
        const mesh = node as Mesh;
        if (!mesh.isMesh) return;
        mesh.material = Array.isArray(mesh.material)
          ? mesh.material.map(material => material.clone())
          : (mesh.material as any).clone();
      });
      this.flashedStates.add(state);
    }

    const materials: any[] = [];
    state.traverse(node => {
      const mesh = node as Mesh;
      if (!mesh.isMesh) return;
      (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).forEach(material => materials.push(material));
    });
    materials.forEach(material => { if (material.emissive) material.emissive.setRGB(1, 1, 1); });

    const anim = { intensity: 1 };
    new TWEEN.Tween(anim, this.tweens)
      .to({ intensity: 0 }, FLASH_MS)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(({ intensity }) => { materials.forEach(material => { if (material.emissive) material.emissiveIntensity = intensity; }); })
      .onComplete(() => { materials.forEach(material => { if (material.emissive) material.emissiveIntensity = 0; }); })
      .start();
  }
}
