import { useRef, useEffect, ReactNode, useMemo } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { Camera, Quaternion, Raycaster, Vector3 } from "three";
import NippleMovement from "../controls/NippleMovement";
import KeyboardMovement from "../controls/KeyboardMovement";
import PointerLockControls from "../controls/PointerLockControls";
import TouchFPSCamera from "../controls/TouchFPSCamera";
import {
  useCapsuleCollider,
  VisibleCapsuleCollider,
} from "./colliders/CapsuleCollider";
import { GyroControls } from "../controls/GyroControls";
import { useSpringVelocity } from "./utils/velocity";
import { useLimiter } from "../../../utils/limiter";
import { useSimulation } from "../layers/simulation";
import { PlayerContext } from "../layers/player";
import { createPlayerState } from "../utils/player";
import { useEnvironment } from "../layers/environment";
import VRControllerMovement from "../controls/VRControllerMovement";

const SPEED = 3.6; // (m/s) 1.4 walking, 2.6 jogging, 4.1 running
const SHOW_PLAYER_HITBOX = false;

export type PlayerProps = {
  pos?: number[];
  rot?: number;
  speed?: number;
  controls?: {
    disableGyro?: boolean;
  };
};

/**
 * Player represents a physics-enabled player in the environment, complete with a
 * control scheme and a physical representation that interacts with other physics-
 * enabled objects.
 *
 * There should only be one player per environment.
 *
 * @constructor
 */
export default function Player(
  props: { children: ReactNode[] | ReactNode } & PlayerProps
) {
  const { children, pos = [0, 1, 0], rot = 0, speed = SPEED, controls } = props;

  const camera = useThree((state) => state.camera);
  const gl = useThree((state) => state.gl);
  const defaultRaycaster = useThree((state) => state.raycaster);

  const { device } = useEnvironment();

  // physical body
  const [bodyRef, bodyApi] = useCapsuleCollider(pos);
  const { direction, updateVelocity } = useSpringVelocity(bodyApi, speed);

  // local state
  const position = useRef(new Vector3());
  const velocity = useRef(new Vector3());
  const lockControls = useRef(false);
  const raycaster = useMemo(
    () => new Raycaster(new Vector3(), new Vector3(), 0, 1.5),
    []
  );
  const { connected, frequency, sendEvent } = useSimulation();
  const simulationLimiter = useLimiter(frequency);

  // setup player
  useEffect(() => {
    // store position and velocity
    bodyApi.position.subscribe((p) => position.current.fromArray(p));
    bodyApi.velocity.subscribe((v) => velocity.current.fromArray(v));

    // rotation happens before position move
    camera.rotation.setFromQuaternion(
      new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), rot)
    );
  }, []);

  useFrame(({ clock }) => {
    const cam: Camera = device.xr ? gl.xr.getCamera(camera) : camera;

    // update raycaster
    if (device.desktop) {
      raycaster.ray.origin.copy(position.current);
      const lookAt = new Vector3(0, 0, -1);
      lookAt.applyQuaternion(cam.quaternion);
      raycaster.ray.direction.copy(lookAt);
    }

    // update camera position
    camera.position.copy(position.current);

    // update velocity
    if (!lockControls.current) {
      updateVelocity(cam, velocity.current);
    }

    // p2p stream position and rotation
    if (connected && simulationLimiter.isReady(clock)) {
      sendEvent(
        "player",
        JSON.stringify({
          position: camera.position
            .toArray()
            .map((p) => parseFloat(p.toPrecision(3))),
          rotation: camera.rotation
            .toArray()
            .slice(0, 3)
            .map((r) => parseFloat(r.toPrecision(3))),
        })
      );
    }
  });

  const state = createPlayerState(
    bodyApi,
    position,
    velocity,
    lockControls,
    device.mobile ? defaultRaycaster : raycaster
  );

  return (
    <PlayerContext.Provider value={state}>
      {device.mobile && (
        <>
          {controls?.disableGyro ? (
            <TouchFPSCamera />
          ) : (
            <GyroControls fallback={<TouchFPSCamera />} />
          )}
          <NippleMovement direction={direction} />
        </>
      )}
      {device.desktop && (
        <>
          <KeyboardMovement direction={direction} />
          <PointerLockControls />
        </>
      )}
      {device.xr && (
        <>
          <VRControllerMovement position={position} direction={direction} />
        </>
      )}
      <group name="player" ref={bodyRef}>
        {SHOW_PLAYER_HITBOX && <VisibleCapsuleCollider />}
      </group>
      {children}
    </PlayerContext.Provider>
  );
}