import {
  Delegate,
  Physics_internal,
  UpdateController,
} from "@hitplay/playable_template";
import { Box3, Mesh, Object3D, Quaternion, Vector3 } from "three";
import { Body, Box, Sphere, Vec3 } from "cannon-es";

export enum PhysicsLayer {
  Player = 1,
  Wall = 2,
  Trigger = 4,
  Enemy = 8,
}

export class PhysicsBody {
  private body: Body;
  private pair: PhysicsObjPair | null = null;

  constructor(
    threeObj: Object3D,
    trigger: boolean,
    mass: number,
    col_group: PhysicsLayer,
    col_mask: PhysicsLayer,
    player_sphere: number = 0.3
  ) {
    let isPlayer = col_group === PhysicsLayer.Player;

    // Local (unrotated) bounding size scaled by world scale, combined with
    // the world quaternion below, instead of a world-space Box3 with just
    // this object's own rotation zeroed out — colliders here are nested
    // under rotated parents (e.g. a Lootable crate's root), and a Box3
    // still bakes in every ancestor's rotation, so it comes out skewed/
    // oversized for anything not axis-aligned all the way up the chain.
    let size = isPlayer ? new Vector3() : PhysicsBody.getLocalSize(threeObj);
    let worldScale = threeObj.getWorldScale(new Vector3());
    size.multiply(worldScale);

    this.body = new Body({
      isTrigger: trigger,
      mass: mass,
      shape: isPlayer
        ? new Sphere(player_sphere)
        : new Box(new Vec3(size.x / 2, size.y / 2, size.z / 2)),
      collisionFilterGroup: col_group,
      collisionFilterMask: col_mask,
    });

    let worldPos = threeObj.getWorldPosition(new Vector3());
    this.body.position.set(worldPos.x, worldPos.y, worldPos.z);

    let worldQuat = threeObj.getWorldQuaternion(new Quaternion());
    this.body.quaternion.set(worldQuat.x, worldQuat.y, worldQuat.z, worldQuat.w);

    // if you need sync three obj and physics body
    // if (isEnemy) {
    //   let pair = new PhysicsObjPair(threeObj, this.body);
    //   PhysicsC_Instance.addPhysicsPair(pair);

    //   this.pair = pair;
    // }

    Physics_internal.physicsWorld &&
      Physics_internal.physicsWorld.addBody(this.body);

    return this;
  }

  // Mesh geometry's own bounding box lives in local, unrotated mesh space —
  // exactly what an oriented Box shape needs before the world quaternion
  // above places and rotates it. Falls back to a Box3 for non-mesh objects
  // (e.g. an empty group), which is only correct if nothing up its parent
  // chain is rotated.
  private static getLocalSize(threeObj: Object3D): Vector3 {
    const mesh = threeObj as Mesh;
    if (mesh.geometry) {
      if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
      return mesh.geometry.boundingBox!.getSize(new Vector3());
    }
    return new Box3().setFromObject(threeObj).getSize(new Vector3());
  }

  disablePhysicsPair() {
    if (this.pair) {
      this.pair.destroyed = true;
    }
  }

  getPhysicsBody() {
    return this.body;
  }

  destroy() {
    if (!Physics_internal.physicsWorld) return;

    Physics_internal.physicsWorld.removeBody(this.body);

    (this.body as any) = null;
  }
}

export class PhysicsObjPair {
  threeObj: Object3D;
  physicsObj: Body;
  destroyed: boolean;
  delegateId: null | Delegate<number>;

  constructor(threeObj: Object3D, physicsObj: Body) {
    this.threeObj = threeObj;
    this.physicsObj = physicsObj;
    this.destroyed = false;

    this.delegateId = UpdateController.Instance.onUpdate.addDelegate(() => {
      this.update();
    });
  }

  update() {
    if (this.destroyed) return;

    if (this.threeObj && this.physicsObj) {
      // @ts-ignore
      this.threeObj.position.copy(this.physicsObj.position);
      // @ts-ignore
      this.threeObj.quaternion.copy(this.physicsObj.quaternion);
    }
  }
}
