<script setup lang="ts">
import { useLoop, useTresContext } from "@tresjs/core";
import * as THREE from "three";
import gsap from "gsap";
import { LOGICAL_WIDTH, LOGICAL_HEIGHT } from "~/utils/screen";

const INTRO_ANIMATION = {
  MODEL_SPIN_DURATION: 3,
  LID_OPEN_DURATION: 2,
  LID_OPEN_OVERLAP: 1.5,

  CAMERA_START_POSITION: new THREE.Vector3(0, 15, 2.6),
  CAMERA_START_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-90), 0, 0),
  CAMERA_END_POSITION: new THREE.Vector3(0, 14, 10),
  CAMERA_END_ROTATION: new THREE.Euler(THREE.MathUtils.degToRad(-55), 0, 0),
  CAMERA_DURATION: 3,

  LID_CLOSED_ROTATION: THREE.MathUtils.degToRad(120),
  LID_OPENED_ROTATION: THREE.MathUtils.degToRad(-30),
};

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

const { assets } = useAssets();
const app = useAppStore();

const model = assets.nitendoDs.model.clone(true);

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

/// meshes ///
// screens
const TOP_SCREEN = "top_screen";
const BOTTOM_SCREEN = "bottom_screen";
const LID = "lid";

// buttons
const CROSS_BUTTON = "button_pad";
const X_BUTTON = "button_x";
const A_BUTTON = "button_a";
const Y_BUTTON = "button_y";
const B_BUTTON = "button_b";
const SELECT_BUTTON = "button_select";
const START_BUTTON = "button_start";

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();

model.scale.set(100, 100, 100);

watch(
  () => camera.activeCamera.value,
  (cam) => {
    if (cam) {
      app.setCamera(cam);

      if (app.booted) {
        cam.position.copy(INTRO_ANIMATION.CAMERA_END_POSITION);
        cam.rotation.copy(INTRO_ANIMATION.CAMERA_END_ROTATION);
      } else {
        cam.position.copy(INTRO_ANIMATION.CAMERA_START_POSITION);
        cam.rotation.copy(INTRO_ANIMATION.CAMERA_START_ROTATION);
      }
    }
  },
  { immediate: true },
);

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

const lidMesh = requireMesh(LID);
const topScreenMesh = requireMesh(TOP_SCREEN);

if (app.booted) {
  lidMesh.rotation.x = INTRO_ANIMATION.LID_OPENED_ROTATION;
  topScreenMesh.rotation.x = INTRO_ANIMATION.LID_OPENED_ROTATION;
} else {
  lidMesh.rotation.x = INTRO_ANIMATION.LID_CLOSED_ROTATION;
  topScreenMesh.rotation.x = INTRO_ANIMATION.LID_CLOSED_ROTATION;
}

const hasAnimated = ref(app.booted);

const animateIntro = () => {
  if (hasAnimated.value) return;
  hasAnimated.value = true;

  const introTimeline = gsap.timeline();

  introTimeline.to(
    camera.activeCamera.value.position,
    {
      x: INTRO_ANIMATION.CAMERA_END_POSITION.x,
      y: INTRO_ANIMATION.CAMERA_END_POSITION.y,
      z: INTRO_ANIMATION.CAMERA_END_POSITION.z,
      duration: INTRO_ANIMATION.CAMERA_DURATION,
      ease: "power2.inOut",
    },
    0,
  );

  introTimeline.to(
    camera.activeCamera.value.rotation,
    {
      x: INTRO_ANIMATION.CAMERA_END_ROTATION.x,
      y: INTRO_ANIMATION.CAMERA_END_ROTATION.y,
      z: INTRO_ANIMATION.CAMERA_END_ROTATION.z,
      duration: INTRO_ANIMATION.CAMERA_DURATION,
      ease: "power2.inOut",
    },
    0,
  );

  introTimeline.to(
    model.rotation,
    {
      y: Math.PI * 2,
      duration: INTRO_ANIMATION.MODEL_SPIN_DURATION,
      ease: "power2.inOut",
    },
    0,
  );

  introTimeline.to(
    lidMesh.rotation,
    {
      x: INTRO_ANIMATION.LID_OPENED_ROTATION,
      duration: INTRO_ANIMATION.LID_OPEN_DURATION,
      ease: "power2.out",
    },
    INTRO_ANIMATION.MODEL_SPIN_DURATION - INTRO_ANIMATION.LID_OPEN_OVERLAP,
  );

  introTimeline.to(
    topScreenMesh.rotation,
    {
      x: INTRO_ANIMATION.LID_OPENED_ROTATION,
      duration: INTRO_ANIMATION.LID_OPEN_DURATION,
      ease: "power2.out",
    },
    "<",
  );
};

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

    const webglRenderer = renderer.instance;
    if (!(webglRenderer instanceof THREE.WebGLRenderer)) return;

    const createScreenTexture = (
      canvas: HTMLCanvasElement,
    ): THREE.CanvasTexture => {
      const texture = new THREE.CanvasTexture(canvas);
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      texture.colorSpace = THREE.SRGBColorSpace;
      texture.wrapS = THREE.RepeatWrapping;
      texture.repeat.x = -1;
      texture.anisotropy = webglRenderer.capabilities.getMaxAnisotropy();
      texture.generateMipmaps = false;
      return texture;
    };

    topScreenTexture = createScreenTexture(props.topScreenCanvas);
    bottomScreenTexture = createScreenTexture(props.bottomScreenCanvas);

    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,
    });
  },
  { immediate: true },
);

const { onRender } = useLoop();

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

const keyToButton: Record<string, string> = {
  ArrowUp: "UP",
  ArrowDown: "DOWN",
  ArrowLeft: "LEFT",
  ArrowRight: "RIGHT",
  d: "A",
  s: "B",
  w: "X",
  a: "Y",
  " ": "SELECT",
  Enter: "START",
};

useKeyDown((key) => {
  const button = keyToButton[key];
  if (button) {
    physicalButtonsDown.add(button);
    window.dispatchEvent(
      new KeyboardEvent("keydown", { key: `NDS_${button}` }),
    );
  }
});

useKeyUp((key) => {
  const button = keyToButton[key];
  if (button) {
    physicalButtonsDown.delete(button);
    window.dispatchEvent(new KeyboardEvent("keyup", { key: `NDS_${button}` }));
  }
});

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

  // cross
  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);
  }

  // action buttons
  const PRESS_SPEED = 100;

  const ACTION_BUTTONS = [
    { mesh: meshes.get(X_BUTTON), name: "X", offset: -0.0012 },
    { mesh: meshes.get(A_BUTTON), name: "A", offset: -0.0012 },
    { mesh: meshes.get(Y_BUTTON), name: "Y", offset: -0.0012 },
    { mesh: meshes.get(B_BUTTON), name: "B", offset: -0.0012 },
    { mesh: meshes.get(SELECT_BUTTON), name: "SELECT", offset: -0.0007 },
    { mesh: meshes.get(START_BUTTON), name: "START", offset: -0.0007 },
  ] as const;

  for (const { mesh, name, offset } of ACTION_BUTTONS) {
    if (!mesh) continue;

    const targetY = physicalButtonsDown.has(name) ? offset : 0;
    const lerpFactor = Math.min(delta * PRESS_SPEED, 1);
    mesh.position.y += (targetY - mesh.position.y) * lerpFactor;
  }
});

const pressButton = (button: string) => {
  if (mousePressedButton) {
    physicalButtonsDown.delete(mousePressedButton);
    window.dispatchEvent(
      new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
    );
  }

  physicalButtonsDown.add(button);
  mousePressedButton = button;

  window.dispatchEvent(new KeyboardEvent("keydown", { key: `NDS_${button}` }));
};

const handleClick = (event: MouseEvent) => {
  if (!hasAnimated.value) {
    animateIntro();
    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(model.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 logicalX = (1 - intersection.uv.x) * LOGICAL_WIDTH;
      const logicalY = (1 - intersection.uv.y) * LOGICAL_HEIGHT;

      const rect = canvas.getBoundingClientRect();
      canvas.dispatchEvent(
        new MouseEvent("click", {
          bubbles: true,
          cancelable: true,
          clientX: (logicalX / LOGICAL_WIDTH) * rect.width + rect.left,
          clientY: (logicalY / LOGICAL_HEIGHT) * 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) pressButton(button);
      break;
    }

    case X_BUTTON:
    case A_BUTTON:
    case Y_BUTTON:
    case B_BUTTON:
    case SELECT_BUTTON:
    case START_BUTTON: {
      const BUTTON_MAP = {
        [X_BUTTON]: "X",
        [A_BUTTON]: "A",
        [Y_BUTTON]: "Y",
        [B_BUTTON]: "B",
        [SELECT_BUTTON]: "SELECT",
        [START_BUTTON]: "START",
      } as const;

      const button = BUTTON_MAP[intersection.object.name];
      if (button) pressButton(button);
      break;
    }
  }
};

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

    window.dispatchEvent(
      new KeyboardEvent("keyup", { key: `NDS_${mousePressedButton}` }),
    );

    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 :object="model" />
</template>
