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

const store = useSettingsStore();
const achievements = useAchievementsStore();
const confirmationModal = useConfirmationModal();
const { assets } = useAssets();
const { onRender } = useScreen();

const handleActivateB = () => {
  if (isAnimating.value) return;
  confirmationModal.open({
    text: $t("settings.options.2048.quitConfirmation"),
    bLabel: $t("common.no"),
    aLabel: $t("common.yes"),
    onClosed: async (choice) => {
      if (choice === "A") {
        await animateOutro();
        store.closeSubMenu();
      }
    },
    keepButtonsDown: (choice) => choice === "A",
  });
};

const handleActivateA = () => {
  if (isAnimating.value) return;
  if (isDead()) {
    resetBoard();
    return;
  }
  confirmationModal.open({
    text: $t("settings.options.2048.restartConfirmation"),
    bLabel: $t("common.no"),
    aLabel: $t("common.yes"),
    onClosed: (choice) => {
      if (choice === "A") {
        resetBoard();
      }
    },
  });
};

// TODO: one color scheme per app color
const TILE_COLORS: Record<number, { bg: string; fg: string }> = {
  [0]: { bg: "#f7f7f7", fg: "#776e65" },
  [2]: { bg: "#ebebf3", fg: "#292929" },
  [4]: { bg: "#d3dbe3", fg: "#292929" },
  [8]: { bg: "#bacbd3", fg: "#292929" },
  [16]: { bg: "#a2bac3", fg: "#f9f6f2" },
  [32]: { bg: "#8aa2b2", fg: "#f9f6f2" },
  [64]: { bg: "#7192a2", fg: "#f9f6f2" },
  [128]: { bg: "#698aa2", fg: "#f9f6f2" },
  [256]: { bg: "#61829a", fg: "#f9f6f2" },
  [512]: { bg: "#5c7b92", fg: "#f9f6f2" },
  [1024]: { bg: "#57758a", fg: "#f9f6f2" }, // -2.5L, -0.8C
  [2048]: { bg: "#476277", fg: "#f9f6f2" },
  [4046]: { bg: "#173446", fg: "#f9f6f2" },
};
const LAST_TILE_COLOR =
  Object.values(TILE_COLORS)[Object.values(TILE_COLORS).length - 1]!;
const TILE_SIZE = 28;
const ANIM_DURATION = 0.1;

const BORDER_COLOR = "#d7d7d7";
const BORDER_SIZE = 3;

const BOARD_X = 64;
const BOARD_Y = 32;
const BOARD_SIZE = 4;

const SLIDE_OFFSET = 96;
const SLIDE_DURATION = 0.25;
const SCORE_OFFSET = -20;
const SCORE_DURATION = 0.167;

const isAnimating = ref(true);

const intro = reactive({
  frameOffsetY: SLIDE_OFFSET,
  frameOpacity: 0,
  scoreOffsetY: SCORE_OFFSET,
  tilesVisible: false,
});

const emptyBoard = () =>
  Array.from({ length: BOARD_SIZE }, () =>
    Array.from({ length: BOARD_SIZE }, () => 0),
  );

const savedState = useLocalStorage("nds-2048", {
  board: emptyBoard(),
  score: 0,
  highScore: 0,
});
const board = savedState.value.board.map((r) => [...r]);
let score = savedState.value.score;

const saveState = () => {
  savedState.value.board = board.map((r) => [...r]);
  savedState.value.score = score;
  if (score > savedState.value.highScore) {
    savedState.value.highScore = score;
  }
};

type VisualTile = { value: number; x: number; y: number; scale: number };

const cellX = (col: number) => BORDER_SIZE + col * (TILE_SIZE + BORDER_SIZE);
const cellY = (row: number) => BORDER_SIZE + row * (TILE_SIZE + BORDER_SIZE);

let tiles: VisualTile[] = [];
let animating = false;

const showRestartModal = () => {
  confirmationModal.open({
    text: $t("settings.options.2048.gameOver"),
    onActivateA: () => {},
    onClosed: (choice) => {
      if (choice === "A") {
        resetBoard();
      }
    },
  });
};

const buildTilesFromBoard = () => {
  tiles = [];
  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      const value = board[row]![col]!;
      if (value === 0) continue;
      tiles.push({ value, x: cellX(col), y: cellY(row), scale: 1 });
    }
  }
};

const animateSpawnAll = () => {
  for (const tile of tiles) {
    tile.scale = 0;
    gsap.to(tile, { scale: 1, duration: ANIM_DURATION, ease: "back.out(2)" });
  }
};

const animateIntro = async () => {
  isAnimating.value = true;
  buildTilesFromBoard();

  await gsap
    .timeline()
    .to(intro, { frameOffsetY: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
    .to(intro, { frameOpacity: 1, duration: SLIDE_DURATION, ease: "none" }, 0)
    .call(
      () => {
        intro.tilesVisible = true;
        animateSpawnAll();
      },
      [],
      SLIDE_DURATION,
    )
    .to(
      intro,
      { scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
      SLIDE_DURATION,
    );
  isAnimating.value = false;
};

const animateOutro = async () => {
  isAnimating.value = true;
  await gsap
    .timeline()
    .to(
      intro,
      { frameOffsetY: SLIDE_OFFSET, duration: SLIDE_DURATION, ease: "none" },
      0,
    )
    .to(intro, { frameOpacity: 0, duration: SLIDE_DURATION, ease: "none" }, 0)
    .to(
      intro,
      { scoreOffsetY: SCORE_OFFSET, duration: SCORE_DURATION, ease: "none" },
      0,
    );
};

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

onRender((ctx) => {
  ctx.textBaseline = "top";

  ctx.save();
  ctx.globalAlpha = intro.frameOpacity;
  ctx.translate(BOARD_X, BOARD_Y + intro.frameOffsetY);

  assets.images.settings.bottomScreen.options._2048.frame.draw(ctx, -3, -3);

  ctx.fillStyle = BORDER_COLOR;
  ctx.fillRect(
    0,
    0,
    BORDER_SIZE * (BOARD_SIZE + 1) + TILE_SIZE * BOARD_SIZE,
    BORDER_SIZE * (BOARD_SIZE + 1) + TILE_SIZE * BOARD_SIZE,
  );

  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      ctx.fillStyle = TILE_COLORS[0]!.bg;
      ctx.fillRect(cellX(col), cellY(row), TILE_SIZE, TILE_SIZE);
    }
  }

  if (!intro.tilesVisible) {
    ctx.restore();
    return;
  }

  for (const tile of tiles) {
    const color = TILE_COLORS[tile.value] ?? LAST_TILE_COLOR;

    ctx.save();
    const cx = tile.x + TILE_SIZE / 2;
    const cy = tile.y + TILE_SIZE / 2;
    ctx.translate(cx, cy);
    ctx.scale(tile.scale, tile.scale);
    ctx.translate(-cx, -cy);

    ctx.fillStyle = color.bg;
    ctx.fillRect(tile.x, tile.y, TILE_SIZE, TILE_SIZE);

    ctx.fillStyle = color.fg;
    ctx.font = tile.value <= 2048 ? "10px NDS10" : "7px NDS7";
    fillTextHCentered(
      ctx,
      tile.value.toString(),
      tile.x + 1,
      tile.y + (tile.value <= 2048 ? 9 : 10),
      TILE_SIZE,
    );
    ctx.restore();
  }
  ctx.restore();
});

onRender((ctx) => {
  ctx.translate(0, intro.scoreOffsetY);
  drawButton(
    ctx,
    `${$t("settings.options.2048.score")}: ${score}`,
    10,
    2,
    118,
    true,
  );
  drawButton(
    ctx,
    `${$t("settings.options.2048.highScore")}: ${savedState.value.highScore}`,
    138,
    2,
    108,
    true,
  );
}, 110);

const getCell = (row: number, col: number) => board[row]![col]!;
const setCell = (row: number, col: number, val: number) => {
  board[row]![col] = val;
};

const compact = (rowDir: number, colDir: number) => {
  const reversed = rowDir > 0 || colDir > 0;
  const start = reversed ? BOARD_SIZE - 1 : 0;
  const step = reversed ? -1 : 1;

  for (let major = 0; major < BOARD_SIZE; major += 1) {
    for (let times = 0; times < BOARD_SIZE - 1; times += 1) {
      for (let minor = start; minor >= 0 && minor < BOARD_SIZE; minor += step) {
        const row = rowDir !== 0 ? minor : major;
        const col = colDir !== 0 ? minor : major;
        const nextRow = row - rowDir;
        const nextCol = col - colDir;

        if (nextRow < 0 || nextRow >= BOARD_SIZE) continue;
        if (nextCol < 0 || nextCol >= BOARD_SIZE) continue;

        if (getCell(row, col) === 0 && getCell(nextRow, nextCol) !== 0) {
          setCell(row, col, getCell(nextRow, nextCol));
          setCell(nextRow, nextCol, 0);
        }
      }
    }
  }
};

const merge = (rowDir: number, colDir: number) => {
  const reversed = rowDir > 0 || colDir > 0;
  const start = reversed ? BOARD_SIZE - 1 : 0;
  const step = reversed ? -1 : 1;

  for (let major = 0; major < BOARD_SIZE; major += 1) {
    for (let minor = start; minor >= 0 && minor < BOARD_SIZE; minor += step) {
      const row = rowDir !== 0 ? minor : major;
      const col = colDir !== 0 ? minor : major;
      const nextRow = row - rowDir;
      const nextCol = col - colDir;

      if (nextRow < 0 || nextRow >= BOARD_SIZE) continue;
      if (nextCol < 0 || nextCol >= BOARD_SIZE) continue;

      if (
        getCell(row, col) !== 0 &&
        getCell(row, col) === getCell(nextRow, nextCol)
      ) {
        const merged = getCell(row, col) * 2;
        setCell(row, col, merged);
        setCell(nextRow, nextCol, 0);
        score += merged;
      }
    }
  }
};

const spawnTile = () => {
  const empty: [number, number][] = [];
  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      if (getCell(row, col) === 0) empty.push([row, col]);
    }
  }
  if (empty.length === 0) return null;

  const [row, col] = empty[Math.floor(Math.random() * empty.length)]!;
  setCell(row, col, Math.random() < 0.9 ? 2 : 4);
  return { row, col };
};

const resetBoard = () => {
  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      setCell(row, col, 0);
    }
  }

  score = 0;
  spawnTile();
  spawnTile();
  saveState();
  buildTilesFromBoard();
  animateSpawnAll();
};

const isDead = () => {
  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      if (
        getCell(row, col) === 0 ||
        (col < BOARD_SIZE - 1 && getCell(row, col) === getCell(row, col + 1)) ||
        (row < BOARD_SIZE - 1 && getCell(row, col) === getCell(row + 1, col))
      )
        return false;
    }
  }
  return true;
};

if (board.every((r) => r.every((c) => c === 0))) {
  spawnTile();
  spawnTile();
  saveState();
}

const slide = (rowDir: number, colDir: number) => {
  if (isDead() && !confirmationModal.isOpen) {
    showRestartModal();
    return;
  }

  if (animating) {
    gsap.globalTimeline.clear();
    animating = false;
    buildTilesFromBoard();
  }

  const beforeTiles: { row: number; col: number; value: number }[] = [];
  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      if (getCell(row, col) !== 0) {
        beforeTiles.push({ row, col, value: getCell(row, col) });
      }
    }
  }

  compact(rowDir, colDir);
  merge(rowDir, colDir);
  compact(rowDir, colDir);

  const afterTiles: { row: number; col: number; value: number }[] = [];
  for (let row = 0; row < BOARD_SIZE; row += 1) {
    for (let col = 0; col < BOARD_SIZE; col += 1) {
      if (getCell(row, col) !== 0) {
        afterTiles.push({ row, col, value: getCell(row, col) });
      }
    }
  }

  const changed =
    beforeTiles.length !== afterTiles.length ||
    afterTiles.some(
      (t, i) =>
        t.row !== beforeTiles[i]?.row ||
        t.col !== beforeTiles[i]?.col ||
        t.value !== beforeTiles[i]?.value,
    );
  if (!changed) return;

  if (board.some((r) => r.some((c) => c >= 512))) {
    achievements.unlock("2048_score_512");
  }

  const horizontal = colDir !== 0;
  const reversed = rowDir > 0 || colDir > 0;

  const animTiles: VisualTile[] = [];
  const tweens: { tile: VisualTile; toX: number; toY: number }[] = [];
  const mergePairs: {
    keep: VisualTile;
    remove: VisualTile;
    mergedValue: number;
  }[] = [];

  for (let major = 0; major < BOARD_SIZE; major += 1) {
    const lineBefore = beforeTiles
      .filter((t) => (horizontal ? t.row : t.col) === major)
      .sort((a, b) => {
        const aPos = horizontal ? a.col : a.row;
        const bPos = horizontal ? b.col : b.row;
        return reversed ? bPos - aPos : aPos - bPos;
      });

    const lineAfter = afterTiles
      .filter((t) => (horizontal ? t.row : t.col) === major)
      .sort((a, b) => {
        const aPos = horizontal ? a.col : a.row;
        const bPos = horizontal ? b.col : b.row;
        return reversed ? bPos - aPos : aPos - bPos;
      });

    let afterIdx = 0;
    let i = 0;
    while (i < lineBefore.length) {
      const cur = lineBefore[i]!;
      const next = lineBefore[i + 1];
      const target = lineAfter[afterIdx]!;

      const tile: VisualTile = {
        value: cur.value,
        x: cellX(cur.col),
        y: cellY(cur.row),
        scale: 1,
      };
      animTiles.push(tile);
      tweens.push({ tile, toX: cellX(target.col), toY: cellY(target.row) });

      if (next && next.value === cur.value && target.value === cur.value * 2) {
        const mergeTile: VisualTile = {
          value: next.value,
          x: cellX(next.col),
          y: cellY(next.row),
          scale: 1,
        };
        animTiles.push(mergeTile);
        tweens.push({
          tile: mergeTile,
          toX: cellX(target.col),
          toY: cellY(target.row),
        });
        mergePairs.push({
          keep: tile,
          remove: mergeTile,
          mergedValue: target.value,
        });
        i += 2;
      } else {
        i += 1;
      }
      afterIdx += 1;
    }
  }

  const spawned = spawnTile();
  saveState();

  if (isDead()) {
    buildTilesFromBoard();
    showRestartModal();
    return;
  }

  tiles = animTiles;
  animating = true;

  const tl = gsap.timeline({
    onComplete: () => {
      animating = false;
      buildTilesFromBoard();
    },
  });

  for (const { tile, toX, toY } of tweens) {
    tl.to(tile, { x: toX, y: toY, duration: ANIM_DURATION, ease: "none" }, 0);
  }

  if (mergePairs.length > 0) {
    tl.call(() => {
      for (const { keep, remove, mergedValue } of mergePairs) {
        keep.value = mergedValue;
        tiles = tiles.filter((t) => t !== remove);
      }
    });
    for (const { keep } of mergePairs) {
      tl.fromTo(
        keep,
        { scale: 1.1 },
        { scale: 1, duration: ANIM_DURATION, ease: "none" },
      );
    }
  }

  if (spawned) {
    const spawnedTile: VisualTile = {
      value: getCell(spawned.row, spawned.col),
      x: cellX(spawned.col),
      y: cellY(spawned.row),
      scale: 0,
    };
    tl.call(() => {
      tiles.push(spawnedTile);
    });
    tl.to(spawnedTile, {
      scale: 1,
      duration: ANIM_DURATION,
      ease: "back.out(2)",
    });
  }
};

useKeyDown(({ key }) => {
  if (isAnimating.value) return;
  switch (key) {
    // TODO: remove this, testing only
    case "n":
      savedState.value.board = [
        [0, 0, 2, 4],
        [8, 16, 32, 64],
        [128, 256, 512, 1024],
        [2048, 4096, 8192, 16384],
      ];
      break;
    case "NDS_UP":
    case "NDS_SWIPE_UP":
      slide(-1, 0);
      break;
    case "NDS_DOWN":
    case "NDS_SWIPE_DOWN":
      slide(1, 0);
      break;
    case "NDS_LEFT":
    case "NDS_SWIPE_LEFT":
      slide(0, -1);
      break;
    case "NDS_RIGHT":
    case "NDS_SWIPE_RIGHT":
      slide(0, 1);
      break;
  }
});
</script>

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