import { useEffect, useCallback, useRef } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { Euler } from "three";
import { useEnvironment } from "../layers/environment";
import * as THREE from "three";

const MIN_POLAR_ANGLE = 0; // radians
const MAX_POLAR_ANGLE = Math.PI; // radians
const SENSITIVITY = 0.8;
const PI_2 = Math.PI / 2;

/**
 * PointerLockCamera is a react port of PointerLockControls.js from THREE,
 * with some changes. Some parameters are listed above
 *
 * https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/PointerLockControls.js
 *
 * @param props = { onUnlock: function to run when the pointer lock controls are unlocked }
 * @constructor
 */
export default function PointerLockCamera() {
  const camera = useThree((state) => state.camera);
  const gl = useThree((state) => state.gl);
  const { domElement } = gl;
  const { paused, setPaused, addEvent } = useEnvironment();

  const { current: euler } = useRef(new Euler(0, 0, 0, "YXZ"));
  const isLocked = useRef(false);
  const lock = () => domElement.requestPointerLock();
  const unlock = () => domElement.ownerDocument.exitPointerLock();

  useFrame(() => {
    if (isLocked.current) {
      const lookAt = new THREE.Vector3(0, 0, -1);
      lookAt.applyQuaternion(camera.quaternion);
      lookAt.multiply(new THREE.Vector3(1, 0, 1)).normalize();
    }
  });

  // update camera while controls are locked
  const onMouseMove = useCallback(
    (event: MouseEvent) => {
      if (!isLocked.current) return;

      const movementX =
        // @ts-ignore
        event.movementX || event.mozMovementX || event.webkitMovementX || 0;
      const movementY =
        // @ts-ignore
        event.movementY || event.mozMovementY || event.webkitMovementY || 0;

      euler.setFromQuaternion(camera.quaternion);

      euler.y -= movementX * SENSITIVITY * 0.002;
      euler.x -= movementY * SENSITIVITY * 0.002;

      euler.x = Math.max(
        PI_2 - MAX_POLAR_ANGLE,
        Math.min(PI_2 - MIN_POLAR_ANGLE, euler.x)
      );

      camera.quaternion.setFromEuler(euler);
    },
    [isLocked]
  );

  // handle pointer lock change
  function onPointerlockChange() {
    if (domElement.ownerDocument.pointerLockElement === domElement) {
      isLocked.current = true;
      if (paused) {
        setPaused(false);
      }
    } else {
      isLocked.current = false;
      if (!paused) {
        setPaused(true);
      }
    }
  }

  // automatically unlock on pointer lock error
  function onPointerlockError() {
    console.error("PointerLockControls: Unable to use Pointer Lock API");
    isLocked.current = false;
    setPaused(true);
  }

  // events setup
  useEffect(() => {
    setTimeout(() => {
      if (!isLocked.current && !paused) {
        setPaused(true);
      }
    }, 250);

    const { ownerDocument } = domElement;

    ownerDocument.addEventListener("mousemove", onMouseMove, false);
    ownerDocument.addEventListener(
      "pointerlockchange",
      onPointerlockChange,
      false
    );
    ownerDocument.addEventListener(
      "pointerlockerror",
      onPointerlockError,
      false
    );

    return () => {
      ownerDocument.removeEventListener("mousemove", onMouseMove, false);
      ownerDocument.removeEventListener(
        "pointerlockchange",
        onPointerlockChange,
        false
      );
      ownerDocument.removeEventListener(
        "pointerlockerror",
        onPointerlockError,
        false
      );
    };
  }, [paused, onMouseMove, isLocked, onPointerlockChange]);

  useEffect(() => {
    // initial camera rotation
    if (camera) {
      camera?.lookAt(0, 2, 0);
    }

    // lock and unlock controls based on set paused value
    addEvent("paused", (pausedVal: boolean, overlayVal: string | undefined) => {
      if (pausedVal) {
        unlock();
      } else {
        lock();
      }
    });
  }, []);

  return null;
}