<template>
  <transition name="game-container" @after-enter="onContainerTransitionEnd">
    <div v-show="showContainer" class="game-container" :style="containerSizeStyle">
      <transition name="failed-mask">
        <div v-show="gameStatus !== 'playing'" class="failed-mask">
          <div v-if="gameStatus === 'succeed'">
            你赢了!
            <br />
            🎉🥳🎊
          </div>
          <div v-else>
            你输了
            <br />
            🤣👉🤡
          </div>
        </div>
      </transition>
      <template v-for="y in game2048Store.height">
        <!-- eslint-disable-next-line vue/require-v-for-key -->
        <div
          v-for="x in game2048Store.width"
          class="background-tile"
          :style="getTilePosition(y - 1, x - 1)"
        ></div>
      </template>
      <div v-show="showTiles">
        <transition-group name="tile" type="animation" @after-enter="onTileAddCompleted">
          <template v-for="(row, y) in tileGrid">
            <template v-for="(tiles, x) in row">
              <template v-for="tile in tiles" :key="tile.id">
                <div
                  :class="[
                    'tile',
                    {
                      'tile-appear-from-others': tile.fromOthers
                    }
                  ]"
                  :style="[getTilePosition(y, x), getTileStyle(tile)]"
                  :data-tile-id="tile.id"
                  @transitionend="onTileTransitionCompleted"
                >
                  {{ tile.number }}
                </div>
              </template>
            </template>
          </template>
        </transition-group>
      </div>
    </div>
  </transition>
</template>
<style scoped lang="scss">
@keyframes container-appear {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes tile-common-appear {
  from {
    transform: scale(0);
  }
  to {
    transform: scale(1);
  }
}
@keyframes tile-bounce-appear {
  from {
    transform: scale(0.5);
  }
  80% {
    transform: scale(1.1);
  }
  to {
    transform: scale(1);
  }
}
.game-container {
  position: relative;
  background-color: rgb(187, 174, 158);
  border-radius: 6px;
}
.game-container-enter-active {
  animation: container-appear 0.5s ease;
}
.failed-mask {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 50px;
  font-weight: bold;
  text-align: center;
  background-color: rgb(white, 0.3);
  color: rgb(119, 110, 101);
  z-index: 20;
  user-select: none;
}
.failed-mask-enter-active {
  transition: opacity 1s ease;
}
.failed-mask-enter-from {
  opacity: 0;
}
.failed-mask-enter-to {
  opacity: 1;
}
.tile,
.background-tile {
  position: absolute;
  width: 100px;
  height: 100px;
  border-radius: 3px;
  user-select: none;
}
.tile {
  text-align: center;
  font-size: 20px;
  font-weight: bold;
  line-height: 100px;
  transition:
    all 0.1s ease-in,
    z-index 0s linear;
}
.tile-enter-active.tile-appear-from-others {
  animation: tile-bounce-appear 0.15s ease;
}
.tile-enter-active:not(.tile-appear-from-others) {
  animation: tile-common-appear 0.15s ease;
}
.background-tile {
  background-color: rgb(205, 193, 180);
}
</style>
<script setup lang="ts">
import { useGame2048Store, type Tile } from '@/stores';
import { IdDispenser, get2DArrayItem } from '@/utils';
import { sample, shuffle, pick } from 'lodash-es';
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { useEventListener } from '@vueuse/core';
import { ElNotification } from 'element-plus';
const game2048Store = useGame2048Store();
/**
 * 方向键代码.
 */
enum Directions {
  UP = 'ArrowUp',
  DOWN = 'ArrowDown',
  LEFT = 'ArrowLeft',
  RIGHT = 'ArrowRight'
}
type SingleTileLine = (Tile | undefined)[];
type OverlappedTileLine = Tile[][];
/**
 * 存储合并操作所有被合并的块.
 */
type OriginalTilesMap = Map<Tile, Tile[]>;
/**
 * 行合并结果.
 */
interface LineMergeResult {
  maxNumber: number;
  originalLine: SingleTileLine;
  transitionLine: OverlappedTileLine;
  resultLine: OverlappedTileLine;
}
/**
 * 界面过渡是否完成?
 */
type ContainerTransitionFuture = PromiseWithResolvers<void>;
/**
 * 数块是否添加完成?
 */
type TileAddFuture = PromiseWithResolvers<void>;
/**
 * 数块过渡是否完成?
 */
type TileTransitionFuture = PromiseWithResolvers<void>;
/**
 * 合并结果网格.
 */
type MergedGrid = (LineMergeResult | undefined)[];
const tileWidth = 100;
const borderWidth = 15;
/**
 * 成功目标数字.
 */
const successNumber = 2048;
const containerSizeStyle = computed(
  () => (
    console.log(game2048Store.width, typeof game2048Store.width),
    {
      minWidth: `${game2048Store.width * tileWidth + (game2048Store.width + 1) * borderWidth}px`,
      minHeight: `${game2048Store.height * tileWidth + (game2048Store.height + 1) * borderWidth}px`
    }
  )
);
/**
 * 是否显示容器.
 */
const showContainer = ref(false);
const showTiles = ref(false);
/**
 * 游戏状态.
 */
const gameStatus = ref<'playing' | 'succeed' | 'failed'>('playing');
watch(gameStatus, (value) => {
  if (value === 'succeed') {
    ElNotification({
      title: '你赢了!',
      message: `单个块分数达到${successNumber}`,
      type: 'success',
      duration: 3500
    });
  } else if (value === 'failed') {
    ElNotification({
      title: '你输了',
      message: '你无路可走',
      type: 'error',
      duration: 3500
    });
  }
});
const locked = ref(true);
let containerTransitionFuture: ContainerTransitionFuture | undefined;
/**
 * 数块id分配器, 便于重用id.
 */
const tileIdDispenser = new IdDispenser();
const tileGrid = reactive<OverlappedTileLine[]>([]);
const tileAddFutureMap = new Map<number, TileAddFuture>();
const tileTransitionFutureMap = new Map<number, TileTransitionFuture>();
const tileBackgroundColorMapping: Record<number, string | undefined> = {
  2: 'rgb(238, 228, 218)',
  4: 'rgb(237, 224, 200)',
  8: 'rgb(242, 177, 121)',
  16: 'rgb(242, 177, 121)',
  32: 'rgb(246, 124, 95)',
  64: 'rgb(246, 94, 59)',
  128: 'rgb(237, 207, 114)',
  256: 'rgb(237, 204, 97)',
  512: 'rgb(237, 200, 80)',
  1024: 'rgb(237, 197, 63)',
  2048: 'rgb(237, 194, 46)'
};
/**
 * 持久化存储数块网格.
 */
function storeTileGrid() {
  game2048Store.rawTileGrid = tileGrid.map((row) =>
    row.map((tiles) => tiles.map((tile) => pick(tile, 'number', 'removed')))
  );
}
function addFuture<T = void>(
  tileId: number,
  futureMap: Map<number, PromiseWithResolvers<T>>
): Promise<T> {
  const future = Promise.withResolvers<T>();
  futureMap.set(tileId, future);
  return future.promise;
}
function getTilePosition(y: number, x: number) {
  return {
    left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
    top: `${borderWidth + y * (tileWidth + borderWidth)}px`
  };
}
/**
 * 获取数块的样式.
 */
function getTileStyle(tile: Tile) {
  const { number } = tile;
  return {
    fontSize: `${number < 128 ? 55 : number < 1024 ? 45 : 35}px`,
    color: number < 8 ? 'rgb(119, 110, 101)' : 'white',
    backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)'
  };
}
/**
 * 添加随机数块, 空间不够则跳过.
 * @param randomCount 随机数量.
 * @param randomNumbers 随机的数字列表, 默认为2或4.
 */
async function addRandomTiles(randomCount: number, randomNumbers: number[] = [2, 4]) {
  let emptyPositions: [y: number, x: number][] = [];
  for (const y of tileGrid.keys()) {
    for (const [x, tiles] of tileGrid[y].entries()) {
      if (tiles.length) {
        continue;
      }
      emptyPositions.push([y, x]);
    }
  }
  emptyPositions = shuffle(emptyPositions);
  const count = Math.min(emptyPositions.length, randomCount);

  const tileAddPromises: TileAddFuture['promise'][] = [];
  for (let i = 0; i < count; i++) {
    const [y, x] = emptyPositions[i];
    const tile: Tile = {
      id: tileIdDispenser.getId(),
      number: sample(randomNumbers) ?? 2,
      removed: false,
      fromOthers: false
    };
    tileGrid[y][x].push(tile);
    tileAddPromises.push(addFuture(tile.id, tileAddFutureMap));
  }
  await Promise.all(tileAddPromises);
}
/**
 * 获取数块Div的对应Id.
 * @param element 数块Div.
 */
function getElementTileId(element: HTMLDivElement) {
  return parseInt(element.dataset['tileId']!);
}

/**
 * 合并算法.
 * @param once 相邻块仅合并一次?
 */
function mergeLineImpl(originalTilesMap: OriginalTilesMap, line: SingleTileLine, once: boolean) {
  let changed = false;
  let finished;
  do {
    let prev: [number, Tile] | undefined;
    finished = true;
    for (const [i, tile] of Array.from(line.entries()).reverse()) {
      if (tile === undefined) {
        continue;
      }
      if (prev === undefined) {
        prev = [i, tile];
      } else {
        const [prevIndex, prevTile] = prev;
        if (prevTile.number === tile.number) {
          finished = false;
          changed = true;
          prevTile.removed = true;
          tile.removed = true;
          const newTile: Tile = {
            id: tileIdDispenser.getId(),
            number: tile.number * 2,
            removed: false,
            fromOthers: true
          };
          originalTilesMap.set(
            newTile,
            (originalTilesMap.get(prevTile) ?? [prevTile]).concat(
              originalTilesMap.get(tile) ?? [tile]
            )
          );
          prev = once ? undefined : [i, newTile];
          line[i] = newTile;
          line[prevIndex] = undefined;
        } else {
          prev = [i, tile];
        }
      }
    }
  } while (!once && !finished);
  return changed;
}
/**
 * 合并一行.
 * @return 若该行未更改返回合并结果, 否则返回undefined.
 */
function mergeLine(line: SingleTileLine, test: boolean): LineMergeResult | undefined {
  const originalLine = Array.from(line);
  const transitionLine: OverlappedTileLine = line.map(() => []);
  const originalTilesMap: OriginalTilesMap = new Map();
  let changed = mergeLineImpl(originalTilesMap, line, true);
  // 将所有块移到一侧.
  let dumpIndex = line.length - 1;
  let maxNumber = 0;
  for (const [i, tile] of [...line.entries()].reverse()) {
    if (tile === undefined) {
      continue;
    }
    if (i !== dumpIndex) {
      changed = true;
    }
    if (tile.number > maxNumber) {
      maxNumber = tile.number;
    }
    if (!originalTilesMap.has(tile)) {
      transitionLine[dumpIndex].push(tile);
    }
    line[i] = undefined;
    line[dumpIndex--] = tile;
  }
  // 回收所有临时块的id.
  tileIdDispenser.removeId(
    ...Array.from(originalTilesMap.keys())
      .filter((tile) => test || !line.includes(tile)) // 没有出现在结果行的块为临时块.
      .map((tile) => tile.id)
  );
  if (!changed) {
    return;
  }
  for (const [i, tile] of line.entries()) {
    if (tile === undefined) {
      continue;
    }
    transitionLine[i].push(...(originalTilesMap.get(tile) ?? []));
  }
  return {
    maxNumber,
    originalLine,
    transitionLine,
    resultLine: line.map((tile) => (tile !== undefined ? [tile] : []))
  };
}

/**
 * 合并所有块.
 * @param direction 合并方向
 * @return 是否有活动空间
 */
async function mergeTiles(direction: Directions) {
  function getMergedGrid(
    test: boolean,
    d: Directions = direction
  ): [MergedGrid, changed: boolean, maxNumber: number] {
    let changed = false;
    let maxNumber = 0;
    const mergedGrid: MergedGrid = Array(firstIndices.length).fill(undefined);
    for (const first of firstIndices(d)) {
      const line = Array.from(secondIndices(d), (second) =>
        get2DArrayItem(tileGrid, first, second, isColFirst(d)).at(0)
      );
      const lineMergeResult = mergeLine(line, test);
      if (lineMergeResult === undefined) {
        continue;
      }
      changed = true;
      if (lineMergeResult.maxNumber > maxNumber) {
        maxNumber = lineMergeResult.maxNumber;
      }
      mergedGrid[first] = lineMergeResult;
    }
    return [mergedGrid, changed, maxNumber];
  }
  function forEachMergedGrid(
    callback: (data: { tiles: Tile[]; lineMergeResult: LineMergeResult; lineIndex: number }) => void
  ) {
    for (const first of firstIndices()) {
      const mergeResult = mergedGrid[first];
      if (mergeResult === undefined) {
        continue;
      }
      for (const [lineIndex, second] of secondIndices().entries()) {
        const tiles = get2DArrayItem(tileGrid, first, second, isColFirst());
        callback({ tiles, lineMergeResult: mergeResult, lineIndex });
      }
    }
  }
  function firstIndices(d: Directions = direction) {
    return Array.from(
      (function* () {
        switch (d) {
          case 'ArrowUp':
          case 'ArrowDown':
            for (let x = 0; x < game2048Store.width; x++) {
              yield x;
            }
            break;
          case 'ArrowLeft':
          case 'ArrowRight':
            for (let y = 0; y < game2048Store.height; y++) {
              yield y;
            }
            break;
        }
      })()
    );
  }
  function secondIndices(d: Directions = direction) {
    return Array.from(
      (function* () {
        switch (d) {
          case 'ArrowUp':
            for (let y = game2048Store.height - 1; y >= 0; y--) {
              yield y;
            }
            break;
          case 'ArrowDown':
            for (let y = 0; y < game2048Store.height; y++) {
              yield y;
            }
            break;

          case 'ArrowLeft':
            for (let x = game2048Store.width - 1; x >= 0; x--) {
              yield x;
            }
            break;
          case 'ArrowRight':
            for (let x = 0; x < game2048Store.width; x++) {
              yield x;
            }
            break;
        }
      })()
    );
  }
  function isColFirst(d: Directions = direction) {
    return d == Directions.LEFT || d == Directions.RIGHT;
  }
  const [mergedGrid, changed, maxNumber] = getMergedGrid(false);
  const transitionTilesAndPromises: TileTransitionFuture['promise'][] = [];
  const tilesToRemoveId: Tile[] = [];
  forEachMergedGrid(({ tiles, lineMergeResult: { originalLine, transitionLine }, lineIndex }) => {
    const transitionTiles = transitionLine[lineIndex];
    transitionTilesAndPromises.push(
      ...transitionTiles
        .filter((tile) => tile !== originalLine[lineIndex]) // 过滤没有移动位置的过渡块.
        .map((tile) => addFuture(tile.id, tileTransitionFutureMap))
    );

    tiles.splice(0, tiles.length, ...transitionTiles);
  });
  await Promise.all(transitionTilesAndPromises);
  const tileAddPromises: TileAddFuture['promise'][] = [];
  forEachMergedGrid(
    ({ tiles, lineMergeResult: { originalLine, transitionLine, resultLine }, lineIndex }) => {
      const transitionTiles = transitionLine[lineIndex];
      const resultTiles = resultLine[lineIndex];
      tiles.splice(0, tiles.length, ...resultTiles);
      tileAddPromises.push(
        ...resultTiles
          .filter((tile) => !originalLine.includes(tile)) // 过滤仅移动位置的结果块.
          .map((tile) => addFuture(tile.id, tileAddFutureMap))
      );
      tilesToRemoveId.push(...transitionTiles.filter((tile) => !resultTiles.includes(tile)));
    }
  );
  await Promise.all(tileAddPromises.concat(addRandomTiles(changed ? 1 : 0)));
  tileIdDispenser.removeId(...tilesToRemoveId.map((tile) => tile.id)); // 回收所有已删除块的id.
  if (maxNumber === successNumber) {
    gameStatus.value = 'succeed';
    game2048Store.$reset();
  }
  return Object.values(Directions).some((d) => getMergedGrid(true, d)[1]);
}
/**
 * 界面过渡完成.
 */
function onContainerTransitionEnd() {
  containerTransitionFuture?.resolve();
}
/**
 * 数块添加完成.
 * @param element 过渡完成的数块.
 */
function onTileAddCompleted(element: Element) {
  const tileId = getElementTileId(element as HTMLDivElement);
  tileAddFutureMap.get(tileId)?.resolve();
  tileAddFutureMap.delete(tileId);
}
/**
 * 数块过渡完成.
 */
function onTileTransitionCompleted(event: TransitionEvent) {
  const tileId = getElementTileId(event.currentTarget as HTMLDivElement);
  tileTransitionFutureMap.get(tileId)?.resolve();
  tileTransitionFutureMap.delete(tileId);
}
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
  if (locked.value) {
    return;
  }
  switch (e.key) {
    case Directions.UP:
    case Directions.DOWN:
    case Directions.LEFT:
    case Directions.RIGHT:
      locked.value = true;
      if ((await mergeTiles(e.key)) && gameStatus.value === 'playing') {
        locked.value = false;
        storeTileGrid();
      } else if (gameStatus.value === 'playing') {
        gameStatus.value = 'failed';
        game2048Store.$reset();
      }
      break;
  }
});
onMounted(async () => {
  containerTransitionFuture = Promise.withResolvers();
  showContainer.value = true;
  await containerTransitionFuture.promise;
  const tileAddPromises: TileAddFuture['promise'][] = [];
  tileGrid.splice(
    0,
    0,
    ...game2048Store.rawTileGrid.map((row) =>
      row.map((rawTiles) =>
        rawTiles.map((rawTile) => {
          const id = tileIdDispenser.getId();
          tileAddPromises.push(addFuture(id, tileAddFutureMap));
          return {
            ...rawTile,
            id,
            fromOthers: false
          };
        })
      )
    )
  );
  if (game2048Store.isInitial) {
    tileAddPromises.push(addRandomTiles(2));
  }
  showTiles.value = true;
  await Promise.all(tileAddPromises);
  storeTileGrid();
  locked.value = false;
});
onUnmounted(() => {
  locked.value = true;
});
</script>
