import {
  BodyConfig,
  BodyType,
  ClientMessageType,
  MessageType,
  SerializedMesh,
  ShapeConfig,
  ShapeType,
  UUID,
} from "../../lib/types";
import { RigidBody } from "../wrappers/rigid-body";
import { BUFFER_CONFIG, IDENTITY_MATRIX } from "../../lib/constants";
import { notImplementedEventReceiver } from "../utils";
import { Matrix4 } from "three";
import {
  freeIndexArray,
  quatTmp1,
  sharedBuffers,
  vector3Tmp1,
  vector3Tmp2,
  world,
} from "./world-manager";
import { createCollisionShapes } from "../../../three-to-ammo";

export const bodies: Record<UUID, RigidBody> = {};
export const matrices: Record<UUID, Matrix4> = {};
export const indexes: Record<UUID, number> = {};
export const ptrToIndex: Record<number, number> = {};

export const ptrToRigidBody: Record<number, UUID> = {};

let freeIndex = 0;

export const uuids: UUID[] = [];

function addBody({
  uuid,
  matrix,
  serializedMesh,
  shapeConfig,
  options,
}: {
  uuid: UUID;
  matrix: number[];
  serializedMesh?: SerializedMesh;
  shapeConfig: ShapeConfig;
  options: BodyConfig;
}) {
  if (freeIndex !== -1) {
    const nextFreeIndex = freeIndexArray[freeIndex];
    freeIndexArray[freeIndex] = -1;

    indexes[uuid] = freeIndex;
    uuids.push(uuid);
    const transform = new Matrix4();
    transform.fromArray(matrix);
    matrices[uuid] = transform;

    // sharedBuffers.rigidBodies.objectMatricesFloatArray.set(
    //   transform.elements,
    //   freeIndex * BUFFER_CONFIG.BODY_DATA_SIZE
    // );

    const physicsShape = createCollisionShapes(
      serializedMesh?.vertices,
      serializedMesh?.matrices,
      serializedMesh?.indexes,
      serializedMesh?.matrixWorld ?? IDENTITY_MATRIX,
      shapeConfig || { type: ShapeType.BOX }
    );

    if (!physicsShape) {
      console.error(
        "could not create physicsShape",
        shapeConfig,
        serializedMesh
      );
      throw new Error("could not create physicsShape");
    }

    bodies[uuid] = new RigidBody(options || {}, transform, physicsShape, world);
    const ptr = Ammo.getPointer(bodies[uuid].physicsBody);
    ptrToIndex[ptr] = freeIndex;
    ptrToRigidBody[ptr] = uuid;

    postMessage({
      type: ClientMessageType.RIGIDBODY_READY,
      uuid,
      index: freeIndex,
    });
    freeIndex = nextFreeIndex;
  }
}

function updateBody({ uuid, options }) {
  if (bodies[uuid]) {
    bodies[uuid].update(options);
    bodies[uuid].physicsBody!.activate(true);
  }
}

function bodySetMotionState({ uuid, position, rotation }) {
  const body = bodies[uuid];
  if (body) {
    const transform = body.physicsBody!.getCenterOfMassTransform();

    if (position) {
      vector3Tmp1.setValue(position.x, position.y, position.z);
      transform.setOrigin(vector3Tmp1);
    }

    if (rotation) {
      quatTmp1.setValue(rotation._x, rotation._y, rotation._z, rotation._w);
      transform.setRotation(quatTmp1);
    }

    body.physicsBody!.setCenterOfMassTransform(transform);
    body.physicsBody!.activate(true);
  }
}

function bodySetLinearVelocity({ uuid, velocity }) {
  const body = bodies[uuid];
  if (body) {
    body
      .physicsBody!.getLinearVelocity()
      .setValue(velocity.x, velocity.y, velocity.z);
    body.physicsBody!.activate(true);
  }
}

function bodyApplyImpulse({ uuid, impulse, relativeOffset }) {
  const body = bodies[uuid];
  if (body) {
    vector3Tmp1.setValue(impulse.x, impulse.y, impulse.z);
    vector3Tmp2.setValue(relativeOffset.x, relativeOffset.y, relativeOffset.z);
    body.physicsBody!.applyImpulse(vector3Tmp1, vector3Tmp2);
    body.physicsBody!.activate(true);
  }
}

function bodyApplyCentralImpulse({ uuid, impulse }) {
  const body = bodies[uuid];
  if (body) {
    vector3Tmp1.setValue(impulse.x, impulse.y, impulse.z);
    body.physicsBody!.applyCentralImpulse(vector3Tmp1);
    body.physicsBody!.activate(true);
  }
}

function bodyApplyForce({ uuid, force, relativeOffset }) {
  const body = bodies[uuid];
  if (body) {
    vector3Tmp1.setValue(force.x, force.y, force.z);
    vector3Tmp2.setValue(relativeOffset.x, relativeOffset.y, relativeOffset.z);
    body.physicsBody!.applyImpulse(vector3Tmp1, vector3Tmp2);
    body.physicsBody!.activate(true);
  }
}

function bodyApplyCentralForce({ uuid, force }) {
  const body = bodies[uuid];
  if (body) {
    vector3Tmp1.setValue(force.x, force.y, force.z);
    body.physicsBody!.applyCentralForce(vector3Tmp1);
    body.physicsBody!.activate(true);
  }
}

function removeBody({ uuid }) {
  const ptr = Ammo.getPointer(bodies[uuid].physicsBody);
  delete ptrToIndex[ptr];
  delete ptrToRigidBody[ptr];
  bodies[uuid].destroy();
  delete bodies[uuid];
  delete matrices[uuid];
  const index = indexes[uuid];
  freeIndexArray[index] = freeIndex;
  freeIndex = index;
  delete indexes[uuid];
  uuids.splice(uuids.indexOf(uuid), 1);
}

function setShapesOffset({ bodyUuid, offset }) {
  if (!bodies[bodyUuid]) return;

  bodies[bodyUuid].setShapesOffset(offset);
}

function resetDynamicBody({ uuid }) {
  if (bodies[uuid]) {
    const body = bodies[uuid];
    const index = indexes[uuid];
    // matrices[uuid].fromArray(
    //   sharedBuffers.rigidBodies.objectMatricesFloatArray,
    //   index * BUFFER_CONFIG.BODY_DATA_SIZE
    // );
    body.syncToPhysics(true);
    body.physicsBody!.getLinearVelocity().setValue(0, 0, 0);
    body.physicsBody!.getAngularVelocity().setValue(0, 0, 0);
  }
}

function activateBody({ uuid }) {
  if (bodies[uuid]) {
    bodies[uuid].physicsBody!.activate();
  }
}

// export function initSharedArrayBuffer(sharedArrayBuffer, maxBodies) {
//   /** BUFFER HEADER
//    * When using SAB, the first 4 bytes (1 int) are reserved for signaling BUFFER_STATE
//    * This is used to determine which thread is currently allowed to modify the SAB.
//    * The second 4 bytes (1 float) is used for storing stepDuration for stats.
//    */
//   headerIntArray = new Int32Array(
//     sharedArrayBuffer,
//     0,
//     BUFFER_CONFIG.HEADER_LENGTH
//   );
//   headerFloatArray = new Float32Array(
//     sharedArrayBuffer,
//     0,
//     BUFFER_CONFIG.HEADER_LENGTH
//   );
//   objectMatricesFloatArray = new Float32Array(
//     sharedArrayBuffer,
//     BUFFER_CONFIG.HEADER_LENGTH * 4,
//     BUFFER_CONFIG.BODY_DATA_SIZE * BUFFER_CONFIG.MAX_BODIES
//   );
//   objectMatricesIntArray = new Int32Array(
//     sharedArrayBuffer,
//     BUFFER_CONFIG.HEADER_LENGTH * 4,
//     BUFFER_CONFIG.BODY_DATA_SIZE * BUFFER_CONFIG.MAX_BODIES
//   );
// }
//
// export function initTransferrables(arrayBuffer) {
//   objectMatricesFloatArray = new Float32Array(arrayBuffer);
//   objectMatricesIntArray = new Int32Array(arrayBuffer);
// }

export function copyToRigidBodyBuffer() {
  /** Buffer Schema
   * Every physics body has 26 * 4 bytes (64bit float/int) assigned in the buffer
   * 0-15:  Matrix4 elements (floats)
   * 16:    Linear Velocity (float)
   * 17:    Angular Velocity (float)
   * 18-25: first 8 Collisions (ints)
   */

  for (let i = 0; i < uuids.length; i++) {
    const uuid = uuids[i];
    const body = bodies[uuid];
    const index = indexes[uuid];
    const matrix = matrices[uuid];

    body.updateShapes();

    if (body.type === BodyType.DYNAMIC) {
      body.syncFromPhysics();

      sharedBuffers.rigidBodies.objectMatricesFloatArray.set(
        matrix.elements,
        index * BUFFER_CONFIG.BODY_DATA_SIZE + BUFFER_CONFIG.MATRIX_OFFSET
      );

      sharedBuffers.rigidBodies.objectMatricesFloatArray[
        index * BUFFER_CONFIG.BODY_DATA_SIZE +
          BUFFER_CONFIG.LINEAR_VELOCITY_OFFSET
      ] = body.physicsBody!.getLinearVelocity().length();

      sharedBuffers.rigidBodies.objectMatricesFloatArray[
        index * BUFFER_CONFIG.BODY_DATA_SIZE +
          BUFFER_CONFIG.ANGULAR_VELOCITY_OFFSET
      ] = body.physicsBody!.getAngularVelocity().length();
    } else {
      // matrix.fromArray(
      //   sharedBuffers.rigidBodies.objectMatricesFloatArray,
      //   index * BUFFER_CONFIG.BODY_DATA_SIZE + BUFFER_CONFIG.MATRIX_OFFSET
      // );
      // body.syncToPhysics(false);
    }

    const ptr = Ammo.getPointer(body.physicsBody);
    const collisions = world.collisions.get(ptr);
    for (
      let j = 0;
      j < BUFFER_CONFIG.BODY_DATA_SIZE - BUFFER_CONFIG.COLLISIONS_OFFSET;
      j++
    ) {
      if (!collisions || j >= collisions.length) {
        sharedBuffers.rigidBodies.objectMatricesIntArray[
          index * BUFFER_CONFIG.BODY_DATA_SIZE +
            BUFFER_CONFIG.COLLISIONS_OFFSET +
            j
        ] = -1;
      } else {
        const collidingPtr = collisions[j];
        if (ptrToIndex[collidingPtr]) {
          sharedBuffers.rigidBodies.objectMatricesIntArray[
            index * BUFFER_CONFIG.BODY_DATA_SIZE +
              BUFFER_CONFIG.COLLISIONS_OFFSET +
              j
          ] = ptrToIndex[collidingPtr];
        }
      }
    }
  }
}

export const rigidBodyEventReceivers = {
  [MessageType.ADD_RIGIDBODY]: addBody,
  [MessageType.UPDATE_RIGIDBODY]: updateBody,
  [MessageType.REMOVE_RIGIDBODY]: removeBody,
  [MessageType.SET_SHAPES_OFFSET]: setShapesOffset,
  [MessageType.RESET_DYNAMIC_BODY]: resetDynamicBody,
  [MessageType.ACTIVATE_BODY]: activateBody,
  [MessageType.SET_MOTION_STATE]: bodySetMotionState,
  [MessageType.SET_LINEAR_VELOCITY]: bodySetLinearVelocity,
  [MessageType.SET_ANGULAR_VELOCITY]: notImplementedEventReceiver,
  [MessageType.APPLY_IMPULSE]: bodyApplyImpulse,
  [MessageType.APPLY_CENTRAL_IMPULSE]: bodyApplyCentralImpulse,
  [MessageType.APPLY_FORCE]: bodyApplyForce,
  [MessageType.APPLY_CENTRAL_FORCE]: bodyApplyCentralForce,

  // TODO implement
  [MessageType.APPLY_TORQUE_IMPULSE]: notImplementedEventReceiver,
  [MessageType.CLEAR_FORCES]: notImplementedEventReceiver,
  [MessageType.SET_RESTITUTION]: notImplementedEventReceiver,
  [MessageType.SET_ROLLING_FRICTION]: notImplementedEventReceiver,
  [MessageType.SET_FRICTION]: notImplementedEventReceiver,
  [MessageType.SET_SPINNING_FRICTION]: notImplementedEventReceiver,
};