import { Matrix4, Object3D } from "three";
import { iterateGeometries } from "../../three-to-ammo";
import AmmoWorker from "web-worker:../worker/ammo.worker";
import {
  BodyConfig,
  MessageType,
  SerializedMesh,
  SharedBuffers,
  SharedSoftBodyBuffers,
  SoftBodyConfig,
  UUID,
  WorkerRequestId,
  WorldConfig,
} from "./types";
import { isSharedArrayBufferSupported } from "../../utils/utils";
import { ShapeDescriptor } from "../../physics";

export function createAmmoWorker(): Worker {
  return new AmmoWorker();
}

export function WorkerHelpers(ammoWorker: Worker) {
  const transform = new Matrix4();
  const inverse = new Matrix4();

  let lastRequestId: number = 0;
  let requests: Record<WorkerRequestId, (data: any) => void> = {};

  return {
    initWorld(worldConfig: WorldConfig, sharedBuffers: SharedBuffers) {
      if (isSharedArrayBufferSupported) {
        ammoWorker.postMessage({
          type: MessageType.INIT,
          worldConfig,
          sharedBuffers,
          isSharedArrayBufferSupported,
        });
      } else {
        console.warn(
          "use-ammojs uses fallback to slower ArrayBuffers. To use the faster SharedArrayBuffers make sure that your environment is crossOriginIsolated. (see https://web.dev/coop-coep/)"
        );

        ammoWorker.postMessage(
          {
            type: MessageType.INIT,
            worldConfig,
            sharedBuffers,
            isSharedArrayBufferSupported,
          },
          [
            sharedBuffers.rigidBodies.headerIntArray.buffer,
            sharedBuffers.debug.vertexFloatArray.buffer,
            ...sharedBuffers.softBodies.map((sb) => sb.vertexFloatArray.buffer),
          ]
        );
      }
    },

    async makeAsyncRequest<T = any>(data): Promise<T> {
      return new Promise((resolve) => {
        const requestId = lastRequestId++;

        requests[requestId] = resolve;

        ammoWorker.postMessage({
          ...data,
          requestId,
        });
      });
    },

    resolveAsyncRequest(data) {
      if (requests[data.requestId]) {
        requests[data.requestId](data);
        delete requests[data.requestId];
      }
    },

    transferSharedBuffers(sharedBuffers: SharedBuffers) {
      ammoWorker.postMessage(
        { type: MessageType.TRANSFER_BUFFERS, sharedBuffers },
        [
          sharedBuffers.rigidBodies.headerIntArray.buffer,
          sharedBuffers.debug.vertexFloatArray.buffer,
          ...sharedBuffers.softBodies.map((sb) => sb.vertexFloatArray.buffer),
        ]
      );
    },

    addRigidBody(
      uuid: UUID,
      mesh: Object3D,
      shapeDescriptor: ShapeDescriptor,
      options: BodyConfig
    ) {
      let serializedMesh: SerializedMesh | undefined = undefined;

      if (shapeDescriptor.meshToUse) {
        inverse.copy(mesh.parent!.matrix).invert();
        transform.multiplyMatrices(inverse, mesh.parent!.matrix);
        const vertices: any[] = [];
        const matrices: any[] = [];
        const indexes: any[] = [];

        mesh.updateMatrixWorld(true);
        iterateGeometries(mesh, options, (vertexArray, matrix, index) => {
          vertices.push(vertexArray);
          matrices.push(matrix);
          indexes.push(index);
        });

        serializedMesh = {
          vertices,
          matrices,
          indexes,
          matrixWorld: mesh.matrixWorld.elements,
        };
      }

      inverse.copy(mesh.parent!.matrixWorld).invert();
      transform.multiplyMatrices(inverse, mesh.matrixWorld);
      ammoWorker.postMessage({
        type: MessageType.ADD_RIGIDBODY,
        uuid,
        matrix: transform.elements,
        serializedMesh,
        shapeConfig: shapeDescriptor.shapeConfig,
        options,
      });
    },

    updateRigidBody(uuid, options) {
      ammoWorker.postMessage({
        type: MessageType.UPDATE_RIGIDBODY,
        uuid,
        options,
      });
    },

    removeRigidBody(uuid) {
      ammoWorker.postMessage({
        type: MessageType.REMOVE_RIGIDBODY,
        uuid,
      });
    },

    addSoftBody(
      uuid: UUID,
      sharedSoftBodyBuffers: SharedSoftBodyBuffers,
      softBodyConfig: SoftBodyConfig
    ) {
      if (isSharedArrayBufferSupported) {
        ammoWorker.postMessage({
          type: MessageType.ADD_SOFTBODY,
          uuid,
          sharedSoftBodyBuffers,
          softBodyConfig,
        });
      } else {
        ammoWorker.postMessage(
          {
            type: MessageType.ADD_SOFTBODY,
            uuid,
            sharedSoftBodyBuffers,
            softBodyConfig,
          },
          [sharedSoftBodyBuffers.vertexFloatArray.buffer]
        );
      }
    },

    removeSoftBody(uuid: UUID) {
      ammoWorker.postMessage({
        type: MessageType.REMOVE_SOFTBODY,
        uuid,
      });
    },

    bodySetShapesOffset(bodyUuid, offset) {
      ammoWorker.postMessage({
        type: MessageType.SET_SHAPES_OFFSET,
        bodyUuid,
        offset,
      });
    },

    addConstraint(constraintId, bodyAUuid, bodyBUuid, options) {
      ammoWorker.postMessage({
        type: MessageType.ADD_CONSTRAINT,
        constraintId,
        bodyAUuid,
        bodyBUuid,
        options,
      });
    },

    updateConstraint(constraintId, options) {
      ammoWorker.postMessage({
        type: MessageType.UPDATE_CONSTRAINT,
        constraintId,
        options,
      });
    },

    removeConstraint(constraintId) {
      ammoWorker.postMessage({
        type: MessageType.REMOVE_CONSTRAINT,
        constraintId,
      });
    },

    enableDebug(enable, debugSharedArrayBuffer) {
      ammoWorker.postMessage({
        type: MessageType.ENABLE_DEBUG,
        enable,
        debugSharedArrayBuffer,
      });
    },

    resetDynamicBody(uuid) {
      ammoWorker.postMessage({
        type: MessageType.RESET_DYNAMIC_BODY,
        uuid,
      });
    },

    activateBody(uuid) {
      ammoWorker.postMessage({
        type: MessageType.ACTIVATE_BODY,
        uuid,
      });
    },

    bodySetMotionState(uuid, position, rotation) {
      ammoWorker.postMessage({
        type: MessageType.SET_MOTION_STATE,
        uuid,
        position,
        rotation,
      });
    },

    bodySetLinearVelocity(uuid, velocity) {
      ammoWorker.postMessage({
        type: MessageType.SET_LINEAR_VELOCITY,
        uuid,
        velocity,
      });
    },

    bodyApplyImpulse(uuid, impulse, relativeOffset) {
      if (!relativeOffset) {
        ammoWorker.postMessage({
          type: MessageType.APPLY_CENTRAL_IMPULSE,
          uuid,
          impulse,
        });
      } else {
        ammoWorker.postMessage({
          type: MessageType.APPLY_IMPULSE,
          uuid,
          impulse,
          relativeOffset,
        });
      }
    },

    bodyApplyForce(uuid, force, relativeOffset) {
      if (!relativeOffset) {
        ammoWorker.postMessage({
          type: MessageType.APPLY_CENTRAL_FORCE,
          uuid,
          force,
        });
      } else {
        ammoWorker.postMessage({
          type: MessageType.APPLY_FORCE,
          uuid,
          force,
          relativeOffset,
        });
      }
    },

    setSimulationSpeed(simulationSpeed: number) {
      ammoWorker.postMessage({
        type: MessageType.SET_SIMULATION_SPEED,
        simulationSpeed,
      });
    },
  };
}