<script setup lang="ts">
import * as THREE from "three";
import { useIntervalFn, useLocalStorage } from "@vueuse/core";
import gsap from "gsap";

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

const { onRender } = useScreen();
const { assets: atlas } = useAssets();
const assets = atlas.images.settings.bottomScreen;

const highScore = useLocalStorage("snake_high_score", 0);

const BOARD_SLIDE_OFFSET = 96;
const BOARD_SLIDE_DURATION = 0.25;
const TEXT_FADE_DURATION = 0.15;
const SCORE_OFFSET = -20;
const SCORE_DURATION = 0.15;

const intro = reactive({
  boardOffsetY: BOARD_SLIDE_OFFSET,
  boardOpacity: 0,
  textOpacity: 0,
  scoreOffsetY: SCORE_OFFSET,
});

const animateIntro = async () => {
  await gsap
    .timeline()
    .to(
      intro,
      { boardOffsetY: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
      0,
    )
    .to(
      intro,
      { boardOpacity: 1, duration: BOARD_SLIDE_DURATION, ease: "none" },
      0,
    )
    .to(
      intro,
      { textOpacity: 1, duration: TEXT_FADE_DURATION, ease: "none" },
      BOARD_SLIDE_DURATION,
    )
    .to(
      intro,
      { scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
      BOARD_SLIDE_DURATION + TEXT_FADE_DURATION,
    );
};

const animateOutro = async () => {
  await gsap
    .timeline()
    .to(
      intro,
      {
        boardOffsetY: BOARD_SLIDE_OFFSET,
        duration: BOARD_SLIDE_DURATION,
        ease: "none",
      },
      0,
    )
    .to(
      intro,
      { boardOpacity: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
      0,
    )
    .to(
      intro,
      { scoreOffsetY: SCORE_OFFSET, duration: SCORE_DURATION, ease: "none" },
      0,
    );
};

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

const handleCancel = async () => {
  switch (state.value) {
    case "alive": {
      state.value = "pause";
      confirmationModal.open({
        text: $t("settings.user.snake.quitConfirmation"),
        onConfirm: () => {},
        onClosed: async (choice) => {
          if (choice === "confirm") {
            await animateOutro();
            store.closeSubMenu();
          }
        },
        onCancel: () => {
          state.value = "alive";
        },
      });
      break;
    }

    case "dead":
    case "waiting": {
      await animateOutro();
      store.closeSubMenu();
      break;
    }
  }
};

const handleConfirm = () => {
  switch (state.value) {
    case "alive": {
      state.value = "pause";
      confirmationModal.open({
        text: $t("settings.user.snake.restartConfirmation"),
        onConfirm: () => {
          spawn();
        },
        onCancel: () => {
          state.value = "alive";
        },
      });
      break;
    }

    case "waiting": {
      spawn();
      break;
    }
    case "dead": {
      spawn();
      break;
    }
  }
};

const BOARD_X = 15;
const BOARD_Y = 33;
const BOARD_WIDTH = 13;
const BOARD_HEIGHT = 8;
const CELL_SIZE = 15;
const CELL_PADDING = 1;

const position = new THREE.Vector2();
const direction = new THREE.Vector2();
const nextDirection = new THREE.Vector2();
const tail: THREE.Vector2[] = [];
const food = new THREE.Vector2();

let score = 0;
const state = ref<"waiting" | "alive" | "pause" | "dead">("waiting");

const randomFoodPos = (): THREE.Vector2 => {
  // can't spawn on the head or tail
  const occupiedPositions = [position, ...tail];
  const emptyPositions: THREE.Vector2[] = [];

  for (let x = 0; x < BOARD_WIDTH; x++) {
    for (let y = 0; y < BOARD_HEIGHT; y++) {
      const candidate = new THREE.Vector2(x, y);
      if (!occupiedPositions.find((part) => part.equals(candidate))) {
        emptyPositions.push(candidate);
      }
    }
  }

  return emptyPositions[THREE.MathUtils.randInt(0, emptyPositions.length - 1)]!;
};

const eat = () => {
  highScore.value = Math.max(highScore.value, score);
  food.copy(randomFoodPos());
  score += 1;

  if (score === 40) {
    achievements.unlock("snake_score_40");
  }
};

const die = () => {
  state.value = "dead";
};

const spawn = () => {
  state.value = "alive";
  score = 0;
  tail.length = 0;

  position.set(0, 0);
  direction.set(1, 0);
  nextDirection.set(1, 0);
  food.copy(randomFoodPos());
};

useIntervalFn(() => {
  if (state.value !== "alive") return;

  direction.copy(nextDirection);

  const previousPosition = position.clone();
  position.set(
    (position.x + direction.x + BOARD_WIDTH) % BOARD_WIDTH,
    (position.y + direction.y + BOARD_HEIGHT) % BOARD_HEIGHT,
  );

  if (!position.equals(previousPosition)) {
    if (tail.find((part) => part.equals(position))) {
      die();
    }
    tail.push(previousPosition);
    if (food.equals(position)) {
      eat();
    } else {
      tail.shift();
    }
  }
}, 200);

const cellToBoardPos = (pos: THREE.Vector2) => ({
  x: BOARD_X + CELL_SIZE + 1 + pos.x * (CELL_SIZE + CELL_PADDING),
  y: BOARD_Y + pos.y * (CELL_SIZE + CELL_PADDING),
});

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

  ctx.save();
  ctx.globalAlpha = intro.boardOpacity;
  ctx.translate(0, intro.boardOffsetY);

  ctx.textBaseline = "top";
  ctx.font = "10px NDS10";

  // board
  assets.user.snakeBoard.draw(ctx, BOARD_X, BOARD_Y);

  if (state.value === "waiting") {
    ctx.globalAlpha = intro.textOpacity;
    ctx.fillStyle = "#fbfbfb";
    const text = $t("settings.user.snake.startPrompt");
    let x = 15;
    let y = 37;
    for (let i = 0; i < text.length; i += 1, x += 16) {
      while (text[i] === "\n") {
        x = 15;
        y += 16;
        i += 1;
      }
      ctx.fillText(text[i]!, x + 20, y);
    }
  } else {
    // food
    ctx.fillStyle = "#ff2020";
    ctx.fillRect(
      cellToBoardPos(food).x + 1,
      cellToBoardPos(food).y + 1,
      CELL_SIZE,
      CELL_SIZE,
    );

    // snake
    for (const part of tail) {
      ctx.fillStyle = state.value === "dead" ? "#991010" : "#20ff20";
      ctx.fillRect(
        cellToBoardPos(part).x + 1,
        cellToBoardPos(part).y + 1,
        CELL_SIZE,
        CELL_SIZE,
      );
    }
    assets.user.snakeHead.draw(
      ctx,
      cellToBoardPos(position).x + 1,
      cellToBoardPos(position).y,
    );
  }

  ctx.restore();
});

onRender((ctx) => {
  ctx.translate(0, intro.scoreOffsetY);
  drawButton(ctx, $t("settings.user.snake.score", { score }), 10, 2, 118);
  drawButton(
    ctx,
    $t("settings.user.snake.best", { best: highScore.value }),
    138,
    2,
    108,
  );
}, 110);

useKeyDown((key) => {
  if (state.value !== "alive") return;

  const newDirection = direction.clone();
  switch (key) {
    case "NDS_UP":
      newDirection.set(0, -1);
      break;
    case "NDS_RIGHT":
      newDirection.set(1, 0);
      break;
    case "NDS_DOWN":
      newDirection.set(0, 1);
      break;
    case "NDS_LEFT":
      newDirection.set(-1, 0);
      break;
  }

  if (newDirection.clone().dot(direction) === 0) {
    nextDirection.copy(newDirection);
  }
});

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="handleCancel"
    @activate-a="handleConfirm"
  />
</template>
