import { Euler, MathUtils, Object3D, Quaternion, Vector3 } from "three";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { useAmmoPhysicsContext } from "../physics-context";
import {
  BodyConfig,
  BodyType,
  ShapeConfig,
  ShapeType,
} from "../../three-ammo/lib/types";
import { createRigidBodyApi, RigidbodyApi } from "../api/rigidbody-api";
import {
  isEuler,
  isQuaternion,
  isVector3,
} from "../../three-ammo/worker/utils";

type UseRigidBodyOptions = Omit<BodyConfig, "type"> & {
  shapeType: ShapeType;
  bodyType?: BodyType;

  // Overrides the physics shape. If not defined the referenced object3Ds mesh will be used. Origins must match.
  mesh?: Object3D;

  // use for manual overrides with the physics shape.
  shapeConfig?: Omit<ShapeConfig, "type">;

  position?: Vector3 | [number, number, number];

  rotation?:
    | Euler
    | [number, number, number]
    | [number, number, number, string]
    | Quaternion;
};

export function useRigidBody(
  options: UseRigidBodyOptions | (() => UseRigidBodyOptions),
  object3D?: Object3D
): [MutableRefObject<Object3D | undefined>, RigidbodyApi] {
  const ref = useRef<Object3D>();

  const physicsContext = useAmmoPhysicsContext();
  const { addRigidBody, removeRigidBody } = physicsContext;

  const [bodyUUID] = useState(() => MathUtils.generateUUID());

  useEffect(() => {
    const objectToUse = object3D ? object3D : ref.current!;

    if (typeof options === "function") {
      options = options();
    }
    const {
      bodyType,
      shapeType,
      shapeConfig,
      position,
      rotation,
      mesh,
      ...rest
    } = options;

    if (position) {
      if (isVector3(position)) {
        objectToUse.position.set(position.x, position.y, position.z);
      } else if (position.length === 3) {
        objectToUse.position.set(position[0], position[1], position[2]);
      } else {
        throw new Error("invalid position: expected Vector3 or VectorTuple");
      }

      objectToUse.updateMatrixWorld();
    }

    if (rotation) {
      if (isEuler(rotation)) {
        objectToUse.rotation.copy(rotation);
      } else if (isQuaternion(rotation)) {
        objectToUse.rotation.setFromQuaternion(rotation);
      } else if (rotation.length === 3 || rotation.length === 4) {
        objectToUse.rotation.set(
          rotation[0],
          rotation[1],
          rotation[2],
          rotation[3]
        );
      } else {
        throw new Error("invalid rotation: expected Euler or EulerTuple");
      }

      objectToUse.updateMatrixWorld();
    }

    if (!objectToUse) {
      throw new Error("useRigidBody ref does not contain a object");
    }

    const meshToUse = mesh ? mesh : objectToUse;

    addRigidBody(
      bodyUUID,
      objectToUse,
      {
        meshToUse,
        shapeConfig: {
          type: shapeType,
          ...shapeConfig,
        },
      },
      {
        type: bodyType,
        ...rest,
      }
    );

    return () => {
      removeRigidBody(bodyUUID);
    };
  }, []);

  return [ref, createRigidBodyApi(physicsContext, bodyUUID)];
}

/**
 * @deprecated use useRigidBody instead
 */
export const usePhysics = useRigidBody;