import { Matrix4, Quaternion, Vector3 } from "three";
import {
  BodyActivationState,
  BodyConfig,
  BodyType,
  CollisionFlag,
  SerializedVector3,
  ShapeType,
  UpdateBodyOptions,
} from "../../lib/types";
import { World } from "./world";
import { almostEqualsBtVector3, almostEqualsQuaternion, almostEqualsVector3, } from "../utils";
import { FinalizedShape } from "../../../three-to-ammo";

enum RigidBodyFlags {
  NONE = 0,
  DISABLE_WORLD_GRAVITY = 1,
}

const pos = new Vector3();
const quat = new Quaternion();
const scale = new Vector3();
const v = new Vector3();
const q = new Quaternion();

const needsPolyhedralInitialization = [
  ShapeType.HULL,
  ShapeType.HACD,
  ShapeType.VHACD,
];

/**
 * Initializes a body component, assigning it to the physics system and binding listeners for
 * parsing the elements geometry.
 */
export class RigidBody {
  loadedEvent: string;
  mass: number;
  gravity: Ammo.btVector3;
  linearDamping: number;
  angularDamping: number;
  linearSleepingThreshold: number;
  angularSleepingThreshold: number;
  angularFactor: Vector3;
  activationState: BodyActivationState;
  type: BodyType;
  emitCollisionEvents: boolean;
  collisionFilterGroup: number;
  collisionFilterMask: number;
  scaleAutoUpdate: boolean;
  matrix: Matrix4;
  // shapes: Ammo.btCollisionShape[];
  world: World;
  disableCollision: boolean;
  physicsBody: Ammo.btRigidBody;
  localScaling?: Ammo.btVector3;
  prevScale: any;
  prevNumChildShapes?: number;
  msTransform?: Ammo.btTransform;
  rotation?: Ammo.btQuaternion;
  motionState?: Ammo.btDefaultMotionState;
  localInertia?: Ammo.btVector3;
  physicsShape: FinalizedShape;
  // compoundShape?: Ammo.btCompoundShape;
  rbInfo?: Ammo.btRigidBodyConstructionInfo;
  shapesChanged?: boolean;
  polyHedralFeaturesInitialized?: boolean;
  triMesh?: Ammo.btTriangleMesh;
  enableCCD: boolean;
  ccdMotionThreshold: number;
  ccdSweptSphereRadius: number;

  tmpVec: Ammo.btVector3;
  tmpTransform1: Ammo.btTransform;
  tmpTransform2: Ammo.btTransform;

  constructor(
    bodyConfig: BodyConfig,
    matrix: Matrix4,
    physicsShape: FinalizedShape,
    world: World
  ) {
    this.loadedEvent = bodyConfig.loadedEvent ? bodyConfig.loadedEvent : "";
    this.mass = bodyConfig.mass ?? 1;
    const worldGravity = world.physicsWorld.getGravity();
    this.gravity = new Ammo.btVector3(
      worldGravity.x(),
      worldGravity.y(),
      worldGravity.z()
    );
    if (bodyConfig.gravity) {
      this.gravity.setValue(
        bodyConfig.gravity.x,
        bodyConfig.gravity.y,
        bodyConfig.gravity.z
      );
    }
    this.linearDamping = bodyConfig.linearDamping ?? 0.01;
    this.angularDamping = bodyConfig.angularDamping ?? 0.01;
    this.linearSleepingThreshold = bodyConfig.linearSleepingThreshold ?? 1.6;
    this.angularSleepingThreshold = bodyConfig.angularSleepingThreshold ?? 2.5;
    this.angularFactor = new Vector3(1, 1, 1);
    if (bodyConfig.angularFactor) {
      this.angularFactor.copy(bodyConfig.angularFactor);
    }
    this.activationState =
      bodyConfig.activationState ?? BodyActivationState.ACTIVE_TAG;
    this.type = bodyConfig.type ? bodyConfig.type : BodyType.DYNAMIC;
    this.emitCollisionEvents = bodyConfig.emitCollisionEvents ?? false;
    this.disableCollision = bodyConfig.disableCollision ?? false;
    this.collisionFilterGroup = bodyConfig.collisionFilterGroup ?? 1; //32-bit mask
    this.collisionFilterMask = bodyConfig.collisionFilterMask ?? 1; //32-bit mask
    this.scaleAutoUpdate = bodyConfig.scaleAutoUpdate ?? true;

    this.enableCCD = bodyConfig.enableCCD ?? false;
    this.ccdMotionThreshold = bodyConfig.ccdMotionThreshold ?? 1e-7;
    this.ccdSweptSphereRadius = bodyConfig.ccdSweptSphereRadius ?? 0.5;

    this.matrix = matrix;
    this.world = world;
    // this.shapes = [];

    this.tmpVec = new Ammo.btVector3();
    this.tmpTransform1 = new Ammo.btTransform();
    this.tmpTransform2 = new Ammo.btTransform();

    this.physicsShape = physicsShape;
    if (physicsShape.type === ShapeType.MESH && this.type !== BodyType.STATIC) {
      throw new Error("non-static mesh colliders not supported");
    }

    this.physicsBody = this._initBody();

    this.shapesChanged = true;
    this.updateShapes();
  }

  /**
   * Parses an element's geometry and component metadata to create an Ammo body instance for the component.
   */
  _initBody() {
    this.localScaling = new Ammo.btVector3();

    this.matrix.decompose(pos, quat, scale);

    this.localScaling.setValue(scale.x, scale.y, scale.z);
    this.prevScale = new Vector3(1, 1, 1);
    this.prevNumChildShapes = 0;

    this.msTransform = new Ammo.btTransform();
    this.msTransform.setIdentity();
    this.rotation = new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w);

    this.msTransform.getOrigin().setValue(pos.x, pos.y, pos.z);
    this.msTransform.setRotation(this.rotation);

    this.motionState = new Ammo.btDefaultMotionState(
      this.msTransform,
      this.physicsShape.localTransform
    );

    this.localInertia = new Ammo.btVector3(0, 0, 0);

    this.physicsShape.setLocalScaling(this.localScaling);

    this.rbInfo = new Ammo.btRigidBodyConstructionInfo(
      this.mass,
      this.motionState,
      this.physicsShape,
      this.localInertia
    );

    this.physicsBody = new Ammo.btRigidBody(this.rbInfo);
    this.physicsBody.setActivationState(this.activationState);
    this.physicsBody.setSleepingThresholds(
      this.linearSleepingThreshold,
      this.angularSleepingThreshold
    );

    this.physicsBody.setDamping(this.linearDamping, this.angularDamping);

    const angularFactor = new Ammo.btVector3(
      this.angularFactor.x,
      this.angularFactor.y,
      this.angularFactor.z
    );
    this.physicsBody.setAngularFactor(angularFactor);
    Ammo.destroy(angularFactor);

    if (
      !almostEqualsBtVector3(
        0.001,
        this.gravity,
        this.world.physicsWorld.getGravity()
      )
    ) {
      this.physicsBody.setGravity(this.gravity);
      this.physicsBody.setFlags(RigidBodyFlags.DISABLE_WORLD_GRAVITY);
    }

    this.updateCollisionFlags();

    this.world.addRigidBody(
      this.physicsBody,
      this.matrix,
      this.collisionFilterGroup,
      this.collisionFilterMask
    );

    if (this.emitCollisionEvents) {
      // @ts-ignore
      this.world.addEventListener(this.physicsBody);
    }

    return this.physicsBody;
  }

  /**
   * Updates the body when shapes have changed. Should be called whenever shapes are added/removed or scale is changed.
   */
  updateShapes() {
    let updated = false;
    this.matrix.decompose(pos, quat, scale);
    if (
      this.scaleAutoUpdate &&
      this.prevScale &&
      !almostEqualsVector3(0.001, scale, this.prevScale)
    ) {
      this.prevScale.copy(scale);
      updated = true;

      this.localScaling!.setValue(
        this.prevScale.x,
        this.prevScale.y,
        this.prevScale.z
      );
      this.physicsShape.setLocalScaling(this.localScaling!);
    }

    if (this.shapesChanged) {
      this.shapesChanged = false;
      updated = true;
      if (this.type === BodyType.DYNAMIC) {
        this.updateMass();
      }

      this.world.updateRigidBody(this.physicsBody);
    }

    //call initializePolyhedralFeatures for hull shapes if debug is turned on and/or scale changes
    if (
      this.world.isDebugEnabled() &&
      (updated || !this.polyHedralFeaturesInitialized)
    ) {
      const shapes = this.physicsShape.shapes || [this.physicsShape];

      for (let i = 0; i < shapes.length; i++) {
        const collisionShape = shapes[i];
        if (needsPolyhedralInitialization.indexOf(collisionShape.type) !== -1) {
          ((collisionShape as unknown) as Ammo.btConvexHullShape).initializePolyhedralFeatures(
            0
          );
        }
      }
      this.polyHedralFeaturesInitialized = true;
    }
  }

  /**
   * Update the configuration of the body.
   */
  update(bodyConfig: UpdateBodyOptions) {
    if (
      (bodyConfig.type && bodyConfig.type !== this.type) ||
      (bodyConfig.disableCollision &&
        bodyConfig.disableCollision !== this.disableCollision)
    ) {
      if (bodyConfig.type) this.type = bodyConfig.type;
      if (bodyConfig.disableCollision)
        this.disableCollision = bodyConfig.disableCollision;
      this.updateCollisionFlags();
    }

    if (
      bodyConfig.activationState &&
      bodyConfig.activationState !== this.activationState
    ) {
      this.activationState = bodyConfig.activationState;
      this.physicsBody!.forceActivationState(this.activationState);
      if (this.activationState === BodyActivationState.ACTIVE_TAG) {
        this.physicsBody!.activate(true);
      }
    }

    if (
      (bodyConfig.collisionFilterGroup &&
        bodyConfig.collisionFilterGroup !== this.collisionFilterGroup) ||
      (bodyConfig.collisionFilterMask &&
        bodyConfig.collisionFilterMask !== this.collisionFilterMask)
    ) {
      if (bodyConfig.collisionFilterGroup)
        this.collisionFilterGroup = bodyConfig.collisionFilterGroup;
      if (bodyConfig.collisionFilterMask)
        this.collisionFilterMask = bodyConfig.collisionFilterMask;
      const broadphaseProxy = this.physicsBody!.getBroadphaseProxy();
      broadphaseProxy.set_m_collisionFilterGroup(this.collisionFilterGroup);
      broadphaseProxy.set_m_collisionFilterMask(this.collisionFilterMask);
      this.world.broadphase
        .getOverlappingPairCache()
        // @ts-ignore
        .removeOverlappingPairsContainingProxy(
          broadphaseProxy,
          this.world.dispatcher
        );
    }

    if (
      (bodyConfig.linearDamping &&
        bodyConfig.linearDamping != this.linearDamping) ||
      (bodyConfig.angularDamping &&
        bodyConfig.angularDamping != this.angularDamping)
    ) {
      if (bodyConfig.linearDamping)
        this.linearDamping = bodyConfig.linearDamping;
      if (bodyConfig.angularDamping)
        this.angularDamping = bodyConfig.angularDamping;
      this.physicsBody!.setDamping(this.linearDamping, this.angularDamping);
    }

    if (bodyConfig.gravity) {
      this.gravity.setValue(
        bodyConfig.gravity.x,
        bodyConfig.gravity.y,
        bodyConfig.gravity.z
      );
      if (
        !almostEqualsBtVector3(
          0.001,
          this.gravity,
          this.physicsBody!.getGravity()
        )
      ) {
        if (
          !almostEqualsBtVector3(
            0.001,
            this.gravity,
            this.world.physicsWorld.getGravity()
          )
        ) {
          this.physicsBody.setFlags(RigidBodyFlags.DISABLE_WORLD_GRAVITY);
        } else {
          this.physicsBody.setFlags(RigidBodyFlags.NONE);
        }
        this.physicsBody!.setGravity(this.gravity);
      }
    }

    if (
      (bodyConfig.linearSleepingThreshold &&
        bodyConfig.linearSleepingThreshold != this.linearSleepingThreshold) ||
      (bodyConfig.angularSleepingThreshold &&
        bodyConfig.angularSleepingThreshold != this.angularSleepingThreshold)
    ) {
      if (bodyConfig.linearSleepingThreshold)
        this.linearSleepingThreshold = bodyConfig.linearSleepingThreshold;
      if (bodyConfig.angularSleepingThreshold)
        this.angularSleepingThreshold = bodyConfig.angularSleepingThreshold;
      this.physicsBody.setSleepingThresholds(
        this.linearSleepingThreshold,
        this.angularSleepingThreshold
      );
    }

    if (
      bodyConfig.angularFactor &&
      !almostEqualsVector3(0.001, bodyConfig.angularFactor, this.angularFactor)
    ) {
      this.angularFactor.copy(bodyConfig.angularFactor);
      const angularFactor = new Ammo.btVector3(
        this.angularFactor.x,
        this.angularFactor.y,
        this.angularFactor.z
      );
      this.physicsBody.setAngularFactor(angularFactor);
      Ammo.destroy(angularFactor);
    }

    //TODO: support dynamic update for other properties
  }

  /**
   * Removes the component and all physics and scene side effects.
   */
  destroy() {
    if (this.triMesh) Ammo.destroy(this.triMesh);
    if (this.localScaling) Ammo.destroy(this.localScaling);

    this.physicsShape.destroy();

    this.world.removeRigidBody(this.physicsBody);
    Ammo.destroy(this.physicsBody);
    delete (this as any).physicsBody;
    Ammo.destroy(this.rbInfo);
    Ammo.destroy(this.msTransform);
    Ammo.destroy(this.motionState);
    Ammo.destroy(this.localInertia);
    Ammo.destroy(this.rotation);
    Ammo.destroy(this.gravity);
    Ammo.destroy(this.tmpVec);
    Ammo.destroy(this.tmpTransform1);
    Ammo.destroy(this.tmpTransform2);
  }

  /**
   * Updates the rigid body's position, velocity, and rotation, based on the scene.
   */
  syncToPhysics(setCenterOfMassTransform: boolean = false) {
    const body = this.physicsBody;
    if (!body) return;

    this.motionState!.getWorldTransform(this.msTransform!);

    this.matrix.decompose(pos, quat, scale);

    const position = this.msTransform!.getOrigin();
    v.set(position.x(), position.y(), position.z());

    const quaternion = this.msTransform!.getRotation();
    q.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w());

    console.log(v, pos);

    if (
      !almostEqualsVector3(0.001, pos, v) ||
      !almostEqualsQuaternion(0.001, quat, q)
    ) {
      if (!this.physicsBody!.isActive()) {
        this.physicsBody!.activate(true);
      }
      this.msTransform!.getOrigin().setValue(pos.x, pos.y, pos.z);
      this.rotation!.setValue(quat.x, quat.y, quat.z, quat.w);
      this.msTransform!.setRotation(this.rotation!);
      this.motionState!.setWorldTransform(this.msTransform!);

      if (this.type === BodyType.STATIC || setCenterOfMassTransform) {
        this.physicsBody!.setCenterOfMassTransform(this.msTransform!);
      }
    }
  }

  /**
   * Updates the scene object's position and rotation, based on the physics simulation.
   */
  syncFromPhysics() {
    const graphicsTransform = this.motionState!.get_m_graphicsWorldTrans();
    const position = graphicsTransform.getOrigin();
    const quaternion = graphicsTransform.getRotation();

    if (!this.physicsBody) return;

    this.matrix.decompose(pos, quat, scale);
    pos.set(position.x(), position.y(), position.z());
    quat.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w());
    this.matrix.compose(pos, quat, scale);
  }

  setShapesOffset(offset: SerializedVector3) {
    this.tmpVec.setValue(-offset.x, -offset.y, -offset.z);

    this.physicsShape.localTransform.setOrigin(this.tmpVec);
  }

  updateMass() {
    const mass = this.type === BodyType.STATIC ? 0 : this.mass;
    this.physicsShape.calculateLocalInertia(mass, this.localInertia!);
    this.physicsBody.setMassProps(mass, this.localInertia!);
    this.physicsBody.updateInertiaTensor();
  }

  updateCollisionFlags() {
    let flags = this.disableCollision ? 4 : 0;
    switch (this.type) {
      case BodyType.STATIC:
        flags |= CollisionFlag.STATIC_OBJECT;
        break;
      case BodyType.KINEMATIC:
        flags |= CollisionFlag.KINEMATIC_OBJECT;
        break;
      default:
        this.physicsBody!.applyGravity();
        break;
    }

    if (this.physicsShape.type === ShapeType.MESH) {
      // Enables callback to improve internal-edge collisions
      flags |= CollisionFlag.CUSTOM_MATERIAL_CALLBACK;
    }

    this.physicsBody!.setCollisionFlags(flags);

    this.updateMass();

    if (this.enableCCD) {
      this.physicsBody!.setCcdMotionThreshold(this.ccdMotionThreshold);
      this.physicsBody!.setCcdSweptSphereRadius(this.ccdSweptSphereRadius);
    }

    this.world.updateRigidBody(this.physicsBody);
  }

  getVelocity() {
    return this.physicsBody!.getLinearVelocity();
  }
}