<script setup lang="ts">
import { useLocalStorage } from "@vueuse/core";
import gsap from "gsap";

const store = useSettingsStore();
const achievements = useAchievementsStore();
const confirmationModal = useConfirmationModal();

const { onRender, onClick } = useScreen();
const { assets } = useAssets();

const BAR_HEIGHT = 24;
const MAX_RADIUS = 27;
const MIN_RADIUS = 3;
const BASE_SHRINK_SPEED = 0.09;
const MAX_SHRINK_SPEED = 0.14;
const BASE_SPAWN_INTERVAL = 90;
const MIN_SPAWN_INTERVAL = 45;
const DIFFICULTY_SCORE_CAP = 100;
const RING_STROKE_WIDTH = 5;
const RING_EXPAND_SPEED = 0.3;
const RING_FADE_SPEED = 0.05;
const MAX_LIVES = 3;
const CROSSHAIR_MOVE_SPEED = 5;

const getDifficulty = () => {
  const progress = Math.min(score / DIFFICULTY_SCORE_CAP, 1);
  return {
    shrinkSpeed:
      BASE_SHRINK_SPEED + (MAX_SHRINK_SPEED - BASE_SHRINK_SPEED) * progress,
    spawnInterval: Math.floor(
      BASE_SPAWN_INTERVAL -
        (BASE_SPAWN_INTERVAL - MIN_SPAWN_INTERVAL) * progress,
    ),
  };
};

type Circle = {
  x: number;
  y: number;
  radius: number;
};

type Ring = {
  x: number;
  y: number;
  radius: number;
  alpha: number;
};

const state = ref<"waiting" | "playing" | "paused" | "ended">("waiting");
let lives = MAX_LIVES;

let circles: Circle[] = [];
let rings: Ring[] = [];
let spawnTimer = 0;

// crosshair
let x = 0;
let y = LOGICAL_HEIGHT * 2 - 20;
let targetX = 31;
let targetY = 31;
let horizontalFirst = false;

const highScore = useLocalStorage("taptap_high_score", 0);
let score = 0;
let isNewBest = false;

const AREA_FADE_DURATION = 0.2;
const SCORE_OFFSET = -20;
const SCORE_DURATION = 0.15;

const animation = reactive({
  areaOpacity: 0,
  circlesOpacity: 1,
  scoreOffsetY: SCORE_OFFSET,
});

const animateIntro = async () => {
  await gsap
    .timeline()
    .to(
      animation,
      { areaOpacity: 1, duration: AREA_FADE_DURATION, ease: "none" },
      0,
    )
    .to(
      animation,
      { scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
      AREA_FADE_DURATION,
    );
};

const animateOutro = async () => {
  targetX = 0;
  targetY = LOGICAL_HEIGHT * 2 - 20;

  await gsap
    .timeline()
    .to(
      animation,
      { areaOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
      0,
    )
    .to(
      animation,
      { circlesOpacity: 0, duration: AREA_FADE_DURATION, ease: "none" },
      0,
    )
    .to(
      animation,
      { scoreOffsetY: SCORE_OFFSET, duration: SCORE_DURATION, ease: "none" },
      0,
    );
};

onMounted(() => {
  animateIntro();
});

const handleActivateB = () => {
  if (state.value === "playing") {
    state.value = "paused";
    confirmationModal.open({
      text: $t("settings.touchScreen.tapTap.quitConfirmation"),
      bLabel: $t("common.no"),
      aLabel: $t("common.yes"),
      onClosed: async (choice) => {
        if (choice === "confirm") {
          await animateOutro();
          store.closeSubMenu();
        } else {
          state.value = "playing";
        }
      },
    });
  } else {
    animateOutro().then(() => store.closeSubMenu());
  }
};

const handleActivateA = async () => {
  if (state.value === "playing") {
    state.value = "paused";
    confirmationModal.open({
      text: $t("settings.touchScreen.tapTap.restartConfirmation"),
      bLabel: $t("common.no"),
      aLabel: $t("common.yes"),
      onClosed: (choice) => {
        if (choice === "confirm") {
          resetGame();
        } else {
          state.value = "playing";
        }
      },
    });
  } else if (state.value === "waiting") {
    await gsap.to(animation, {
      areaOpacity: 0,
      duration: AREA_FADE_DURATION,
      ease: "none",
    });
    resetGame();
  } else {
    resetGame();
  }
};

const moveTowards = (current: number, target: number) => {
  if (current === target) return current;
  const direction = Math.sign(target - current);
  return (
    current +
    direction * Math.min(CROSSHAIR_MOVE_SPEED, Math.abs(target - current))
  );
};

const spawnCircle = () => {
  const PADDING_X = MAX_RADIUS;
  const PADDING_TOP = MAX_RADIUS + BAR_HEIGHT;
  const PADDING_BOT = MAX_RADIUS + BAR_HEIGHT;
  const MAX_ATTEMPTS = 25;

  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
    const newX = Math.floor(
      PADDING_X + Math.random() * (LOGICAL_WIDTH - PADDING_X * 2),
    );
    const newY = Math.floor(
      PADDING_TOP +
        Math.random() * (LOGICAL_HEIGHT - PADDING_TOP - PADDING_BOT),
    );

    const overlaps = circles.some((circle) => {
      const dx = newX - circle.x;
      const dy = newY - circle.y;
      const distance = Math.sqrt(dx * dx + dy * dy);
      return distance < MAX_RADIUS + circle.radius;
    });

    if (!overlaps) {
      circles.push({ x: newX, y: newY, radius: MAX_RADIUS });
      return;
    }
  }
};

const resetGame = () => {
  state.value = "playing";
  circles = [];
  rings = [];
  spawnTimer = 0;
  score = 0;
  lives = MAX_LIVES;
  isNewBest = false;
};

const showDeathScreen = () => {
  const title = isNewBest
    ? $t("settings.touchScreen.tapTap.newRecord")
    : $t("settings.touchScreen.tapTap.gameOver");
  const text = `${title}\n${$t("settings.touchScreen.tapTap.finalScore", { score })}`;

  confirmationModal.open({
    text,
    bLabel: $t("common.quit"),
    aLabel: $t("common.restart"),
    onClosed: async (choice) => {
      if (choice === "confirm") {
        resetGame();
      } else {
        await animateOutro();
        store.closeSubMenu();
      }
    },
  });
};

onClick((mx, my) => {
  if (state.value !== "playing") return;
  if (my <= BAR_HEIGHT || my >= LOGICAL_HEIGHT - BAR_HEIGHT - 1) return;

  targetX = mx;
  targetY = my;
  horizontalFirst = Math.abs(targetX - x) > Math.abs(targetY - y);

  for (let i = circles.length - 1; i >= 0; i--) {
    const circle = circles[i]!;
    const dx = mx - circle.x;
    const dy = my - circle.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance <= circle.radius) {
      rings.push({
        x: circle.x,
        y: circle.y,
        radius: circle.radius,
        alpha: 1,
      });
      circles.splice(i, 1);
      score++;
      if (score > highScore.value) {
        highScore.value = score;
        isNewBest = true;
      }
      if (score === 20) achievements.unlock("taptap_score_20");
      break;
    }
  }
});

onRender((ctx) => {
  assets.images.home.topScreen.background.draw(ctx, 0, 0);

  // update crosshair position in all modes except paused
  if (state.value !== "paused") {
    if (horizontalFirst) {
      if (x !== targetX) x = moveTowards(x, targetX);
      else y = moveTowards(y, targetY);
    } else {
      if (y !== targetY) y = moveTowards(y, targetY);
      else x = moveTowards(x, targetX);
    }
  }

  // game logic
  if (state.value === "playing") {
    const { shrinkSpeed, spawnInterval } = getDifficulty();

    // spawn circles
    spawnTimer++;
    if (spawnTimer >= spawnInterval) {
      spawnCircle();
      spawnTimer = 0;
    }

    // update circles and rings
    circles = circles.filter((circle) => {
      circle.radius -= shrinkSpeed;
      if (circle.radius < MIN_RADIUS) {
        lives--;
        if (lives <= 0) {
          state.value = "ended";
          showDeathScreen();
        }
        return false;
      }
      return true;
    });

    rings = rings.filter((ring) => {
      ring.radius += RING_EXPAND_SPEED;
      ring.alpha -= RING_FADE_SPEED;
      return ring.alpha > 0;
    });
  }

  // draw start instructions
  if (state.value === "waiting") {
    ctx.save();
    ctx.globalAlpha = animation.areaOpacity;
    ctx.fillStyle = "#fbfbfb";
    ctx.textBaseline = "top";
    ctx.fillRect(32, 112, 191, 31);

    ctx.fillStyle = "#797979";
    ctx.font = "10px NDS10";
    fillTextHCentered(
      ctx,
      $t("settings.touchScreen.tapTap.startPrompt"),
      32,
      123,
      191,
    );
    ctx.restore();
  }

  if (state.value !== "waiting") {
    // draw circles and rings
    for (const circle of circles) {
      ctx.globalAlpha = animation.circlesOpacity;
      ctx.fillStyle = "#fb0000";
      fillCirclePixelated(ctx, circle.x, circle.y, circle.radius);

      ctx.fillStyle = "#fbfafa";
      fillCirclePixelated(ctx, circle.x, circle.y, circle.radius * 0.6);
    }

    for (const ring of rings) {
      ctx.globalAlpha = ring.alpha * animation.circlesOpacity;
      ctx.fillStyle = "#fb0000";
      strokeCirclePixelated(
        ctx,
        ring.x,
        ring.y,
        ring.radius,
        RING_STROKE_WIDTH,
      );
    }
    ctx.globalAlpha = 1;
  }

  // draw crosshair
  const cx = Math.floor(x);
  const cy = Math.floor(y);

  ctx.fillStyle = "#494949";
  ctx.fillRect(cx, 0, 1, LOGICAL_HEIGHT);
  ctx.fillRect(0, cy, LOGICAL_WIDTH, 1);

  ctx.fillStyle = "#fb0000";
  ctx.fillRect(cx - 4, cy - 4, 9, 9);

  ctx.fillStyle = "#fbfafa";
  ctx.fillRect(cx - 2, cy - 2, 5, 5);

  ctx.fillStyle = "#fb0000";
  ctx.fillRect(cx, cy - 1, 1, 3);
  ctx.fillRect(cx - 1, cy, 3, 1);
}, 0);

onRender((ctx) => {
  ctx.translate(0, animation.scoreOffsetY);
  drawButton(
    ctx,
    $t("settings.touchScreen.tapTap.score", { score }),
    10,
    2,
    88,
  );
  drawButton(
    ctx,
    $t("settings.touchScreen.tapTap.best", { best: highScore.value }),
    108,
    2,
    88,
  );
  drawButton(
    ctx,
    lives > 0 ? ICONS.HEART.repeat(lives) : ICONS.SAD,
    206,
    2,
    40,
  );
}, 110);

defineOptions({ render: () => null });
</script>

<template>
  <CommonButtons
    :y-offset="confirmationModal.buttonsYOffset"
    :b-label="$t('common.quit')"
    :a-label="state === 'waiting' ? $t('common.start') : $t('common.restart')"
    @activate-b="handleActivateB"
    @activate-a="handleActivateA"
  />
</template>
