<script setup lang="ts">
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { useLoop, useTresContext } from "@tresjs/core";
import * as THREE from "three";

const props = defineProps<{
  topScreenCanvas: HTMLCanvasElement | null;
  bottomScreenCanvas: HTMLCanvasElement | null;
}>();

const { state: model } = useLoader(
  GLTFLoader,
  "/models/nintendo-ds/scene.gltf",
);

const scene = computed(() => model.value?.scene);

let topScreenTexture: THREE.CanvasTexture | null = null;
let bottomScreenTexture: THREE.CanvasTexture | null = null;

/// meshes ///
// screens
const TOP_SCREEN = "Object_9";
const BOTTOM_SCREEN = "Object_28";

// buttons
const CROSS_BUTTON = "Object_21";

const meshes = new Map<string, THREE.Mesh>();

const requireMesh = (key: string): THREE.Mesh => {
  const mesh = meshes.get(key);
  if (!mesh) throw new Error(`Missing mesh '${key}'`);

  return mesh;
};

const { camera, renderer } = useTresContext();

watch(
  () => [props.topScreenCanvas, props.bottomScreenCanvas],
  () => {
    if (!props.topScreenCanvas || !props.bottomScreenCanvas) return;

    topScreenTexture = new THREE.CanvasTexture(props.topScreenCanvas);
    topScreenTexture.minFilter = THREE.NearestFilter;
    topScreenTexture.magFilter = THREE.NearestFilter;
    topScreenTexture.flipY = false;
    topScreenTexture.repeat.set(1, 1024 / 404);
    topScreenTexture.offset.set(0, -4 / 1024);

    bottomScreenTexture = new THREE.CanvasTexture(props.bottomScreenCanvas);
    bottomScreenTexture.minFilter = THREE.NearestFilter;
    bottomScreenTexture.magFilter = THREE.NearestFilter;
    bottomScreenTexture.flipY = false;
    bottomScreenTexture.repeat.set(1, 1024 / 532);
    bottomScreenTexture.offset.set(0, -1024 / 532 + 1);
  },
  { immediate: true },
);

watch(scene, () => {
  if (!scene.value) return;

  meshes.clear();

  scene.value.scale.set(100, 100, 100);

  scene.value.traverse((child) => {
    if (child instanceof THREE.Mesh) {
      meshes.set(child.name, child);
    }
  });

  if (!topScreenTexture || !bottomScreenTexture)
    throw new Error(
      "topScreenTexture and bottomScreenTexture should be initialized",
    );

  requireMesh(TOP_SCREEN).material = new THREE.MeshStandardMaterial({
    map: topScreenTexture,
    emissive: new THREE.Color(0x222222),
    emissiveIntensity: 0.5,
  });

  requireMesh(BOTTOM_SCREEN).material = new THREE.MeshStandardMaterial({
    map: bottomScreenTexture,
    emissive: new THREE.Color(0x222222),
    emissiveIntensity: 0.5,
  });
});

const { onRender } = useLoop();

const physicalButtonsDown = new Set<string>();
let mousePressedButton: string | null = null;

const keyButtonMappings: [key: string, physicalButton: string][] = [
  ["ArrowUp", "UP"],
  ["ArrowDown", "DOWN"],
  ["ArrowLeft", "LEFT"],
  ["ArrowRight", "RIGHT"],
];

const keyToButton = new Map(keyButtonMappings);
const buttonToKey = new Map(keyButtonMappings.map(([k, b]) => [b, k]));

useKeyDown((key) => {
  const button = keyToButton.get(key);
  if (button) physicalButtonsDown.add(button);
});

useKeyUp((key) => {
  const button = keyToButton.get(key);
  if (button) physicalButtonsDown.delete(button);
});

onRender(({ delta }) => {
  if (topScreenTexture) topScreenTexture.needsUpdate = true;
  if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true;

  const crossButton = meshes.get(CROSS_BUTTON);
  if (crossButton) {
    const PRESS_ANGLE = Math.PI / 28;
    const PRESS_SPEED = 150;

    const targetRotation = new THREE.Vector3(0);
    if (physicalButtonsDown.has("UP")) targetRotation.setX(-PRESS_ANGLE);
    if (physicalButtonsDown.has("RIGHT")) targetRotation.setZ(-PRESS_ANGLE);
    if (physicalButtonsDown.has("DOWN")) targetRotation.setX(PRESS_ANGLE);
    if (physicalButtonsDown.has("LEFT")) targetRotation.setZ(PRESS_ANGLE);

    const lerpFactor = Math.min(delta * PRESS_SPEED, 1);
    const currentRotation = new THREE.Vector3().setFromEuler(
      crossButton.rotation,
    );
    currentRotation.lerp(targetRotation, lerpFactor);
    crossButton.rotation.setFromVector3(currentRotation);
  }
});

const handleClick = (event: MouseEvent) => {
  if (!scene.value) return;

  const domElement = renderer.instance.domElement;
  const rect = domElement.getBoundingClientRect();

  const raycaster = new THREE.Raycaster();
  raycaster.setFromCamera(
    new THREE.Vector2(
      ((event.clientX - rect.left) / rect.width) * 2 - 1,
      -((event.clientY - rect.top) / rect.height) * 2 + 1,
    ),
    camera.activeCamera.value,
  );

  const intersects = raycaster.intersectObjects(scene.value.children, true);
  const intersection = intersects[0];
  if (!intersection?.uv) return;

  switch (intersection.object.name) {
    case TOP_SCREEN:
    case BOTTOM_SCREEN: {
      const canvas =
        intersection.object.name === TOP_SCREEN
          ? props.topScreenCanvas
          : props.bottomScreenCanvas;

      if (!canvas) break;

      const x = Math.floor(intersection.uv.x * 256);
      let y: number;

      if (intersection.object.name === TOP_SCREEN) {
        y = Math.floor(intersection.uv.y * (1024 / 404) * 192);
      } else {
        y = Math.floor(192 - (1 - intersection.uv.y) * (1024 / 532) * 192);
      }

      const rect = canvas.getBoundingClientRect();
      canvas.dispatchEvent(
        new MouseEvent("click", {
          bubbles: true,
          cancelable: true,
          clientX: (x / 256) * rect.width + rect.left,
          clientY: (y / 192) * rect.height + rect.top,
        }),
      );

      break;
    }

    case CROSS_BUTTON: {
      const localPos = intersection.point
        .clone()
        .sub(intersection.object.getWorldPosition(new THREE.Vector3()));

      const MIN_DIST = 0.45;
      let button: string | null = null;

      if (localPos.z <= -MIN_DIST) button = "UP";
      else if (localPos.x >= MIN_DIST) button = "RIGHT";
      else if (localPos.z >= MIN_DIST) button = "DOWN";
      else if (localPos.x <= -MIN_DIST) button = "LEFT";

      if (button) {
        if (mousePressedButton) {
          physicalButtonsDown.delete(mousePressedButton);
          const prevKey = buttonToKey.get(mousePressedButton);
          if (prevKey) {
            window.dispatchEvent(new KeyboardEvent("keyup", { key: prevKey }));
          }
        }

        physicalButtonsDown.add(button);
        mousePressedButton = button;

        const key = buttonToKey.get(button);
        if (key) {
          window.dispatchEvent(new KeyboardEvent("keydown", { key }));
        }
      }

      break;
    }
  }
};

const handleMouseUp = () => {
  if (mousePressedButton) {
    physicalButtonsDown.delete(mousePressedButton);

    const key = buttonToKey.get(mousePressedButton);
    if (key) {
      window.dispatchEvent(new KeyboardEvent("keyup", { key }));
    }

    mousePressedButton = null;
  }
};

onMounted(() => {
  if (renderer) {
    renderer.instance.domElement.addEventListener("mousedown", handleClick);
    renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
  }
});

onUnmounted(() => {
  if (renderer) {
    renderer.instance.domElement.removeEventListener("mousedown", handleClick);
    renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp);
  }
  topScreenTexture?.dispose();
  bottomScreenTexture?.dispose();
});
</script>

<template>
  <primitive v-if="scene" :object="scene" />
</template>
