import { OrbitControls } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useEffect, useMemo, useRef } from "react";
import { MathUtils, Vector3 } from "three";
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";

type KeyboardOrbitControlsProps = {
  terrainWidth: number;
  terrainDepth: number;
};

export function KeyboardOrbitControls({
  terrainWidth,
  terrainDepth,
}: KeyboardOrbitControlsProps) {
  const controlsRef = useRef<OrbitControlsImpl>(null);
  const pressedKeysRef = useRef<Set<string>>(new Set());
  const forward = useMemo(() => new Vector3(), []);
  const right = useMemo(() => new Vector3(), []);
  const movement = useMemo(() => new Vector3(), []);
  const previousTarget = useMemo(() => new Vector3(), []);
  const appliedMovement = useMemo(() => new Vector3(), []);

  useEffect(() => {
    const trackedKeys = new Set([
      "arrowup",
      "arrowdown",
      "arrowleft",
      "arrowright",
      "w",
      "a",
      "s",
      "d",
    ]);
    const keyAliases = new Map([
      ["ArrowUp", "arrowup"],
      ["ArrowDown", "arrowdown"],
      ["ArrowLeft", "arrowleft"],
      ["ArrowRight", "arrowright"],
      ["KeyW", "w"],
      ["KeyA", "a"],
      ["KeyS", "s"],
      ["KeyD", "d"],
    ]);

    const updateKey = (event: KeyboardEvent, isPressed: boolean) => {
      const key = keyAliases.get(event.code) ?? event.key.toLowerCase();
      if (!trackedKeys.has(key) || isTypingTarget(event.target)) {
        return;
      }
      event.preventDefault();
      if (isPressed) {
        pressedKeysRef.current.add(key);
      } else {
        pressedKeysRef.current.delete(key);
      }
    };

    const handleKeyDown = (event: KeyboardEvent) => updateKey(event, true);
    const handleKeyUp = (event: KeyboardEvent) => updateKey(event, false);
    const clearKeys = () => pressedKeysRef.current.clear();
    window.addEventListener("keydown", handleKeyDown);
    window.addEventListener("keyup", handleKeyUp);
    window.addEventListener("blur", clearKeys);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
      window.removeEventListener("keyup", handleKeyUp);
      window.removeEventListener("blur", clearKeys);
    };
  }, []);

  useFrame(({ camera }, delta) => {
    const keys = pressedKeysRef.current;
    const zInput =
      Number(keys.has("w") || keys.has("arrowup")) -
      Number(keys.has("s") || keys.has("arrowdown"));
    const xInput =
      Number(keys.has("d") || keys.has("arrowright")) -
      Number(keys.has("a") || keys.has("arrowleft"));

    if (!zInput && !xInput) {
      return;
    }

    camera.getWorldDirection(forward);
    forward.y = 0;
    if (forward.lengthSq() < 0.001) {
      forward.set(0, 0, -1);
    }
    forward.normalize();
    right.crossVectors(forward, camera.up).normalize();
    movement.set(0, 0, 0).addScaledVector(forward, zInput).addScaledVector(right, xInput);
    if (movement.lengthSq() === 0) {
      return;
    }

    movement.normalize().multiplyScalar(54 * delta);
    moveCamera(
      camera.position,
      movement,
      controlsRef.current,
      terrainWidth,
      terrainDepth,
      previousTarget,
      appliedMovement,
    );
  });

  return (
    <OrbitControls
      ref={controlsRef}
      makeDefault
      enableDamping
      dampingFactor={0.08}
      maxPolarAngle={Math.PI / 2.2}
      minDistance={16}
      maxDistance={105}
      target={[0, 0, 0]}
    />
  );
}

function moveCamera(
  cameraPosition: Vector3,
  moveDelta: Vector3,
  controls: OrbitControlsImpl | null,
  width: number,
  depth: number,
  previousTarget: Vector3,
  appliedMovement: Vector3,
) {
  if (!controls) {
    cameraPosition.add(moveDelta);
    return;
  }
  previousTarget.copy(controls.target);
  controls.target.add(moveDelta);
  controls.target.x = MathUtils.clamp(controls.target.x, -width / 2, width / 2);
  controls.target.z = MathUtils.clamp(controls.target.z, -depth / 2, depth / 2);
  appliedMovement.copy(controls.target).sub(previousTarget);
  cameraPosition.add(appliedMovement);
  controls.update();
}

function isTypingTarget(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  const tagName = target.tagName.toLowerCase();
  return tagName === "input" || tagName === "textarea" || target.isContentEditable;
}
