import { EasyEvent, UpdateController } from "@24tools/playable_template";
import { Euler, Material, Mesh, Object3D, Vector3 } from "three";
import { PhysicsBody } from "../PhysicsC";
import { PropType } from "./PropType";
import { PhysicsTriggerC } from "./PhysicsTriggerC";
import { PROP_DROPS } from "../Resources/PropDropTable";
import { createDropPlan, PropDropPlan } from "../Resources/PropDropPlanner";
import { ResourceSpawnC } from "../Resources/ResourceSpawnC";
import { PropHpUIC } from "./PropHpUIC";
import { PropHpBar } from "./PropHpBar";
import { PropVfxC } from "./PropVfxC";
import { VFXType } from "../Enums/VFXType";

/** Тривалість тряски після удару (секунди). */
const SHAKE_DURATION = 0.22;
/** Амплітуда зміщення моделі по X/Z під час тряски (world units). */
const SHAKE_POSITION_AMP = 0.045;
/** Амплітуда нахилу моделі під час тряски (радіани). */
const SHAKE_ROTATION_AMP = 0.1;
/** Частота коливань тряски (кількість «хитань» за секунду). */
const SHAKE_FREQUENCY = 2;

export class PropRegistry {
  private static props = new Map<Object3D, PropC>();

  static register(prop: PropC) {
    this.props.set(prop.object, prop);
  }

  static get(object: Object3D) {
    return this.props.get(object);
  }

  static unregister(object: Object3D) {
    this.props.delete(object);
  }
}

export class PropC {
  static readonly onBroken = new EasyEvent<PropC>();

  readonly object: Object3D;
  readonly propType: PropType;
  health: number;
  readonly maxHealth: number;
  isBroken = false;

  private wallBodies: PhysicsBody[] = [];
  private dropPlan: PropDropPlan | null = null;
  private hpBar: PropHpBar | null = null;
  private damageLayers: Object3D[] = [];

  private readonly restPosition = new Vector3();
  private readonly restRotation = new Euler();
  private shakeTimeLeft = 0;
  private shakeElapsed = 0;
  private onShakeComplete: (() => void) | null = null;

  private static shakingProps = new Set<PropC>();
  private static shakeSystemInited = false;

  constructor(object: Object3D, propType: PropType, maxHealth = 1) {
    this.object = object;
    this.propType = propType;
    this.maxHealth = maxHealth;
    this.health = maxHealth;
    this.restPosition.copy(object.position);
    this.restRotation.copy(object.rotation);
    PropRegistry.register(this);
    this.hpBar = PropHpUIC.createBar(this) ?? null;
    PropC.ensureShakeSystem();
  }

  private static ensureShakeSystem() {
    if (this.shakeSystemInited) return;
    this.shakeSystemInited = true;

    UpdateController.Instance.onUpdate.addDelegate((delta) => {
      for (const prop of [...this.shakingProps]) {
        prop.updateShake(delta);
      }
    });
  }

  addWallBody(physicsBody: PhysicsBody) {
    this.wallBodies.push(physicsBody);
  }

  initDamageLayers(layers: Object3D[]) {
    this.damageLayers = layers;
  }

  takeDamage(amount: number): boolean {
    if (this.isBroken) return false;

    const hitIndex = this.maxHealth - this.health;
    this.ensureDropPlan();

    this.applyDamageVisual(hitIndex);
    this.health -= amount;
    this.spawnForHit(hitIndex);
    this.hpBar?.onDamage();
    PropVfxC.Play(VFXType.LootableHit, null, this.object.getWorldPosition(new Vector3()));

    if (this.health > 0) {
      this.playHitShake();
      return false;
    }

    this.playHitShake(() => this.break());
    return true;
  }

  private applyDamageVisual(hitIndex: number) {
    const layer = this.damageLayers[hitIndex];
    if (!layer) return;

    layer.visible = false;
  }

  private playHitShake(onComplete?: () => void) {
    if (this.isBroken) return;

    if (onComplete) {
      this.onShakeComplete = onComplete;
    }

    this.shakeTimeLeft = SHAKE_DURATION;
    this.shakeElapsed = 0;
    PropC.shakingProps.add(this);
  }

  private updateShake(delta: number) {
    if (this.shakeTimeLeft <= 0) {
      this.finishShake();
      return;
    }

    this.shakeTimeLeft -= delta;
    this.shakeElapsed += delta;

    const progress = Math.max(this.shakeTimeLeft / SHAKE_DURATION, 0);
    const intensity = progress * progress;
    const phase = this.shakeElapsed * SHAKE_FREQUENCY * Math.PI * 2;
    const wobble = Math.sin(phase);

    this.object.position.set(
      this.restPosition.x + wobble * SHAKE_POSITION_AMP * intensity,
      this.restPosition.y,
      this.restPosition.z + Math.cos(phase) * SHAKE_POSITION_AMP * intensity * 0.75,
    );

    this.object.rotation.set(
      this.restRotation.x + wobble * SHAKE_ROTATION_AMP * intensity * 0.35,
      this.restRotation.y,
      this.restRotation.z + Math.cos(phase) * SHAKE_ROTATION_AMP * intensity,
    );

    if (this.shakeTimeLeft <= 0) {
      this.finishShake();
    }
  }

  private finishShake() {
    this.stopShake();

    const callback = this.onShakeComplete;
    this.onShakeComplete = null;
    callback?.();
  }

  private stopShake() {
    this.object.position.copy(this.restPosition);
    this.object.rotation.copy(this.restRotation);
    this.shakeTimeLeft = 0;
    PropC.shakingProps.delete(this);
  }

  break() {
    if (this.isBroken) return;

    this.isBroken = true;
    this.onShakeComplete = null;
    this.stopShake();

    PropHpUIC.removeBar(this);
    this.hpBar = null;

    this.wallBodies.forEach((body) => body.destroy());
    this.wallBodies = [];

    PhysicsTriggerC.unregister(this.object);
    PropC.onBroken.Invoke(this);
    PropRegistry.unregister(this.object);

    PropVfxC.Play(VFXType.LootableDestroy, null, this.object.getWorldPosition(new Vector3()));

    this.disposeProceduralResources();
    this.object.parent?.remove(this.object);
  }

  private disposeProceduralResources() {
    const trigger = this.object.getObjectByName("GatherTrigger");
    if (!trigger || !(trigger as Mesh).isMesh) return;

    const mesh = trigger as Mesh;
    mesh.geometry.dispose();

    const material = mesh.material;
    if (Array.isArray(material)) {
      material.forEach((entry) => entry.dispose());
    } else if (material) {
      (material as Material).dispose();
    }
  }

  private ensureDropPlan() {
    if (this.dropPlan) return;

    const rules = PROP_DROPS[this.propType];
    if (!rules.length) return;

    this.dropPlan = createDropPlan(this.maxHealth, rules[0]);
  }

  private spawnForHit(hitIndex: number) {
    if (!this.dropPlan) return;

    let spawnCount = this.dropPlan.hitAmounts[hitIndex] ?? 0;

    if (this.health <= 0) {
      const remaining = this.dropPlan.totalDrop - this.dropPlan.spawnedDrop - spawnCount;
      if (remaining > 0) {
        spawnCount += remaining;
      }
    }

    if (spawnCount <= 0) return;

    ResourceSpawnC.spawnFromProp(this, this.dropPlan.resource, spawnCount);
    this.dropPlan.spawnedDrop += spawnCount;
  }
}
