<script lang="ts" setup>
const canvas = useTemplateRef("canvas");

const renderCallbacks = new Map<RenderCallback, number>();
const screenClickCallbacks = new Set<ScreenClickCallback>();
const screenMouseDownCallbacks = new Set<ScreenClickCallback>();
const screenMouseWheelCallbacks = new Set<ScreenMouseWheelCallback>();

let ctx: CanvasRenderingContext2D | null = null;
let animationFrameId: number | null = null;
let lastFrameTime = 0;
let lastRealFrameTime = 0;

const registerRenderCallback = (callback: RenderCallback, zIndex = 0) => {
  renderCallbacks.set(callback, zIndex);
  return () => renderCallbacks.delete(callback);
};

const registerScreenClickCallback = (callback: ScreenClickCallback) => {
  screenClickCallbacks.add(callback);
  return () => screenClickCallbacks.delete(callback);
};

const registerScreenMouseDownCallback = (callback: ScreenClickCallback) => {
  screenMouseDownCallbacks.add(callback);
  return () => screenMouseDownCallbacks.delete(callback);
};

const registerScreenMouseWheelCallback = (
  callback: ScreenMouseWheelCallback,
) => {
  screenMouseWheelCallbacks.add(callback);
  return () => screenMouseWheelCallbacks.delete(callback);
};

const handleCanvasClick = (event: MouseEvent) => {
  if (!canvas.value) return;

  const rect = canvas.value.getBoundingClientRect();
  const scaleX = LOGICAL_WIDTH / rect.width;
  const scaleY = LOGICAL_HEIGHT / rect.height;

  const x = (event.clientX - rect.left) * scaleX;
  const y = (event.clientY - rect.top) * scaleY;

  for (const callback of screenClickCallbacks) {
    callback(x, y);
  }
};

const handleCanvasMouseDown = (event: MouseEvent) => {
  if (!canvas.value) return;

  const rect = canvas.value.getBoundingClientRect();
  const scaleX = LOGICAL_WIDTH / rect.width;
  const scaleY = LOGICAL_HEIGHT / rect.height;

  const x = (event.clientX - rect.left) * scaleX;
  const y = (event.clientY - rect.top) * scaleY;

  for (const callback of screenMouseDownCallbacks) {
    callback(x, y);
  }
};

const handleCanvasWheel = (event: WheelEvent) => {
  for (const callback of screenMouseWheelCallbacks) {
    callback(event.deltaY, event.deltaX);
  }
};

const SWIPE_THRESHOLD = 30;
let swipeStartX = 0;
let swipeStartY = 0;

const dispatchSwipe = (endX: number, endY: number) => {
  const deltaX = endX - swipeStartX;
  const deltaY = endY - swipeStartY;
  const absDeltaX = Math.abs(deltaX);
  const absDeltaY = Math.abs(deltaY);

  if (Math.max(absDeltaX, absDeltaY) < SWIPE_THRESHOLD) return;

  let direction: string;
  if (absDeltaX > absDeltaY) {
    direction = deltaX > 0 ? "RIGHT" : "LEFT";
  } else {
    direction = deltaY > 0 ? "DOWN" : "UP";
  }

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

const handleTouchStart = (event: TouchEvent) => {
  const touch = event.touches[0];
  if (!touch) return;

  swipeStartX = touch.clientX;
  swipeStartY = touch.clientY;

  canvas.value?.dispatchEvent(
    new MouseEvent("mousedown", {
      clientX: touch.clientX,
      clientY: touch.clientY,
    }),
  );
};

const handleTouchEnd = (event: TouchEvent) => {
  const touch = event.changedTouches[0];
  if (!touch) return;

  dispatchSwipe(touch.clientX, touch.clientY);
  document.dispatchEvent(
    new MouseEvent("mouseup", {
      clientX: touch.clientX,
      clientY: touch.clientY,
    }),
  );
};

let mouseSwiping = false;

const handleSwipeMouseDown = (event: MouseEvent) => {
  mouseSwiping = true;
  swipeStartX = event.clientX;
  swipeStartY = event.clientY;
};

const handleSwipeMouseUp = (event: MouseEvent) => {
  if (!mouseSwiping) return;
  mouseSwiping = false;
  dispatchSwipe(event.clientX, event.clientY);
};

const renderFrame = (timestamp: number) => {
  if (!ctx) return;

  const deltaTime = timestamp - lastFrameTime;
  lastFrameTime = timestamp;

  const start = Date.now();

  // render
  ctx.save();
  ctx.scale(SCREEN_SCALE, SCREEN_SCALE);

  ctx.clearRect(0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);

  const sortedCallbacks = Array.from(renderCallbacks.entries())
    .sort((a, b) => a[1] - b[1])
    .map(([callback]) => callback);

  for (const callback of sortedCallbacks) {
    ctx.save();

    try {
      callback(ctx, deltaTime, lastRealFrameTime);
    } catch (error: unknown) {
      console.error(error);
    }

    ctx.restore();
  }

  ctx.restore();

  lastRealFrameTime = Date.now() - start;

  animationFrameId = requestAnimationFrame(renderFrame);
};

provide("registerRenderCallback", registerRenderCallback);
provide("registerScreenClickCallback", registerScreenClickCallback);
provide("registerScreenMouseDownCallback", registerScreenMouseDownCallback);
provide("registerScreenMouseWheelCallback", registerScreenMouseWheelCallback);

onMounted(() => {
  if (!canvas.value) throw new Error("Missing canvas");

  ctx = canvas.value.getContext("2d");
  if (!ctx) throw new Error("Missing 2d context");

  ctx.imageSmoothingEnabled = false;

  canvas.value.addEventListener("click", handleCanvasClick);
  canvas.value.addEventListener("mousedown", handleCanvasMouseDown);
  canvas.value.addEventListener("wheel", handleCanvasWheel, { passive: true });
  canvas.value.addEventListener("touchstart", handleTouchStart, {
    passive: true,
  });
  canvas.value.addEventListener("touchend", handleTouchEnd, { passive: true });
  canvas.value.addEventListener("mousedown", handleSwipeMouseDown);
  canvas.value.addEventListener("mouseup", handleSwipeMouseUp);

  animationFrameId = requestAnimationFrame(renderFrame);
});

onUnmounted(() => {
  if (animationFrameId !== null) {
    cancelAnimationFrame(animationFrameId);
  }

  if (canvas.value) {
    canvas.value.removeEventListener("click", handleCanvasClick);
    canvas.value.removeEventListener("mousedown", handleCanvasMouseDown);
    canvas.value.removeEventListener("wheel", handleCanvasWheel);
    canvas.value.removeEventListener("touchstart", handleTouchStart);
    canvas.value.removeEventListener("touchend", handleTouchEnd);
    canvas.value.removeEventListener("mousedown", handleSwipeMouseDown);
    canvas.value.removeEventListener("mouseup", handleSwipeMouseUp);
  }
});

defineExpose({
  canvas,
});
</script>

<template>
  <canvas
    ref="canvas"
    :width="SCREEN_WIDTH"
    :height="SCREEN_HEIGHT"
    :style="{
      margin: '0',
      border: '1px solid red',
    }"
  />

  <slot v-if="canvas" />
</template>
