<template>
  <el-main v-loading="firstLoading || placing" class="!flex bg-white">
    <el-container direction="vertical">
      <gobang-header class="justify-between">
        <div class="flex items-center">
          <template v-if="roomId">
            <h2 v-show="!smLess">蓝色基因对战平台</h2>
            <h3
              class="gobang-play-page__brief-id flex justify-center items-center absolute-self-center"
            >
              #{{ roomId.slice(0, 8) }}
            </h3>
          </template>
          <h2 v-else>单人游戏</h2>
        </div>
      </gobang-header>
      <el-container class="gobang-play-page-main-wrapper" direction="horizontal">
        <el-container direction="vertical">
          <el-header
            v-if="mdLess && otherUser"
            class="other-header flex justify-between items-center"
          >
            <gobang-user footer :user="otherUser" />
            <div class="absolute-center font-bold text-5">
              {{ otherPrompt }}
            </div>
            <gobang-side :white="!white" />
          </el-header>
          <el-main
            class="gobang-play-page-main !flex flex-col justify-center items-center realtive"
          >
            <div
              v-loading="matchState === MatchState.FINISHED"
              class="gobang-chessboard"
              element-loading-svg-view-box="0 0 0 0"
              :class="{
                'gobang-chessboard--enabled': enabled,
              }"
            >
              <canvas class="gobang-chessboard__background" ref="canvas"></canvas>
              <template v-for="(row, y) of grid">
                <template v-for="(cell, x) of row">
                  <!-- eslint-disable-next-line vue/require-v-for-key -->
                  <div
                    class="gobang-chessboard__cell absolute"
                    :class="[
                      cell
                        ? cell.isWhite
                          ? 'gobang-chessboard__cell--white'
                          : 'gobang-chessboard__cell--black'
                        : '',
                      ,
                      {
                        'gobang-chessboard__cell--win-blink': blink?.[y][x],
                        'gobang-chessboard__cell--last':
                          lastChess && lastChess[0] === x && lastChess[1] === y,
                      },
                    ]"
                    :style="getCellStyle(cell, x, y)"
                    @click="onCellClick(cell, x, y)"
                    @touchend="onCellClick(cell, x, y)"
                  ></div>
                </template>
              </template>
            </div>
            <el-button
              v-if="mdLess && matchState === MatchState.FINISHED"
              class="absolute bottom-10px"
              type="primary"
              :loading="restartState === RestartState.RESTARTING"
              :disabled="restartState === RestartState.WAITING"
              @click="restart"
            >
              重新开始
            </el-button>
          </el-main>
        </el-container>
        <el-aside
          v-if="!mdLess"
          class="gobang-play-page-aside !flex flex-col items-center justify-between"
        >
          <div class="flex flex-col items-center gap-5px">
            <template v-if="otherUser">
              <gobang-user :user="otherUser" />
              <gobang-side :white="!white" />
              <div class="font-bold">{{ otherPrompt }}</div>
            </template>
          </div>
          <div>
            <div
              v-if="otherUser"
              class="absolute-self-center font-bold text-12.5 flex flex-col items-center select-none"
            >
              VS
            </div>
            <div
              v-else
              class="absolute-self-center text-center text-5 font-bold w-100% select-none"
            >
              等待玩家加入.
            </div>
          </div>
          <div class="flex flex-col items-center gap-1">
            <gobang-user v-if="userStore.userInfo" :user="userStore.userInfo" />
            <gobang-side :white="white" />
            <div v-if="selfPrompt" class="font-bold">{{ selfPrompt }}</div>
            <el-button
              v-if="matchState === MatchState.FINISHED"
              type="primary"
              :loading="restartState === RestartState.RESTARTING"
              :disabled="restartState === RestartState.WAITING"
              @click="restart"
            >
              重新开始
            </el-button>
          </div>
        </el-aside>
      </el-container>
      <el-footer
        v-if="mdLess"
        class="gobang-play-page-footer flex justify-between items-center relative"
      >
        <gobang-user v-if="userStore.userInfo" footer :user="userStore.userInfo" />
        <div class="absolute-self-center font-bold text-5">
          <template v-if="otherUser">
            {{ selfPrompt }}
          </template>
          <template v-else>等待玩家加入.</template>
        </div>
        <gobang-side :white="white" />
      </el-footer>
    </el-container>
  </el-main>
</template>
<style lang="scss" scoped>
@use '@/assets/mixins' as m;
.gobang-play-page {
  &-main-wrapper {
    margin-top: var(--header-height);
  }
  &-main {
    --el-main-padding: 0;
    margin-top: v-bind(mainMarginTopCSS);
    position: relative;
  }
  &-aside {
    position: relative;
    border-left: 1px solid var(--el-border-color);
    width: 200px;
    padding: 80px 0;
  }
  &-footer {
    border-top: 1px solid var(--el-border-color);
    --el-footer-height: var(--header-height);
  }
}
.other-header {
  @include m.header(2);
}
.gobang-chessboard {
  position: relative;
  zoom: v-bind(boardZoom);
  --el-mask-color: rgb(255, 255, 255, 0.7);
  .gobang-chessboard__cell {
    transform: translate(-50%, -50%);
  }
  &--enabled {
    .gobang-chessboard__cell {
      &:not(&--white, &--black):hover {
        background-color: rgb(red, 0.5);
        cursor: pointer;
      }
    }
  }
}
:deep(.gobang-chessboard__cell) {
  @apply border border-black rounded-full;
  --size: 24px;
  width: var(--size);
  height: var(--size);
  user-select: none;
}
:deep(.gobang-chessboard__cell--white) {
  @apply bg-white border-inset;
}
:deep(.gobang-chessboard__cell--black) {
  @apply bg-black;
}
:deep(.gobang-chessboard__cell--last::after) {
  @apply absolute inset--1;
  content: '';
  border: 2px solid red;
  animation: blink 1s ease-in-out infinite;
  @keyframes blink {
    0% {
      opacity: 1;
    }
    50% {
      opacity: 0;
    }
    100% {
      opacity: 1;
    }
  }
}
:deep(.gobang-chessboard__cell--win-blink) {
  // 超过遮罩
  z-index: 2001;
}
</style>
<script lang="ts">
const boardBackground = '#E2CFA9';
const boardLength = 16;
const gridSize = 30;
const lineSize = 2;
/** 棋盘外边框大小 */
const boardOuterBorderSize = 2;
/** 棋盘外边框+内边框总大小 */
const boardBorderSize = 5;
const boardPadding = 15;
const deltaMap: Record<WinFace, [number, number]> = {
  [WinFace.UP]: [0, -1],
  [WinFace.UP_RIGHT]: [1, -1],
  [WinFace.RIGHT]: [1, 0],
  [WinFace.DOWN_RIGHT]: [1, 1],
  [WinFace.DOWN]: [0, 1],
  [WinFace.DOWN_LEFT]: [-1, 1],
  [WinFace.LEFT]: [-1, 0],
  [WinFace.UP_LEFT]: [-1, -1],
};
</script>
<script setup lang="ts">
import GobangHeader from '@/components/gobang/GobangHeader.vue';
import GobangSide from '@/components/gobang/GobangSide.vue';
import GobangUser from '@/components/gobang/GobangUser.vue';
import router from '@/router';
import { useMediaStore } from '@/stores/media';
import { useUserStore } from '@/stores/user';
import { create2DArray, iter2DArray, zip } from '@/utils/array';
import { useLogoutInterceptor } from '@/utils/user';
import { useGobangSocket, WinFace, type RoomDetail, type RoomId } from '@/views/GobangListPage.vue';
import { ElMessage, ElMessageBox, ElNotification, type Action } from 'element-plus';
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref, watch, watchEffect, type CSSProperties } from 'vue';
import { onBeforeRouteLeave, useRoute } from 'vue-router';
const userStore = useUserStore();
const { smLess, mdLess } = storeToRefs(useMediaStore());
let canExit = false;
let confirming = false;
function confirm(description: string) {
  confirming = true;
  return ElMessageBox.confirm(description, '警告', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then((action: Action) => {
      if (action !== 'confirm') return;
      canExit = true;
    })
    .catch(() => {})
    .finally(() => {
      confirming = false;
    });
}
useLogoutInterceptor(async () => {
  if (confirming) {
    return;
  }
  if (canExit || matchState.value !== MatchState.GAMING) return true;
  await confirm('你正在游戏中, 是否退出登录?');
  return canExit;
});
onBeforeRouteLeave(async (_, __, next) => {
  if (confirming) {
    next(false);
    return;
  }
  if (canExit || matchState.value !== MatchState.GAMING) {
    next(true);
    return;
  }
  confirming = true;
  await confirm('你正在游戏中, 是否退出房间?');
  next(canExit);
});
const roomId = useRoute().params.id as RoomId | undefined;
const canvas = ref<HTMLCanvasElement>();
const mainMarginTopCSS = computed(() =>
  mdLess.value && otherUser.value ? 'var(--header-height)' : 0,
);
/** 棋盘大小 */
const boardSize = computed(
  () => gridSize * (boardLength - 1) + boardBorderSize * 2 + boardPadding * 2,
);
const boardZoom = computed(() => (smLess.value ? 0.75 : mdLess.value ? 0.9 : 1));
watchEffect(() => {
  if (!canvas.value) return;
  const ctx = canvas.value.getContext('2d');
  if (!ctx) return;
  const _boardBorderSize = boardBorderSize;
  const _boardSize = boardSize.value;
  canvas.value.width = _boardSize;
  canvas.value.height = _boardSize;
  ctx.clearRect(0, 0, _boardSize, _boardSize);
  ctx.fillStyle = boardBackground;
  ctx.fillRect(0, 0, _boardSize, _boardSize);
  ctx.strokeStyle = 'black';
  const drawBorder = (borderSize: number, offset: number) => {
    ctx.lineWidth = borderSize;
    ctx.strokeRect(
      borderSize / 2 + offset,
      borderSize / 2 + offset,
      _boardSize - borderSize - offset * 2,
      _boardSize - borderSize - offset * 2,
    );
  };
  drawBorder(2, 0);
  drawBorder(boardOuterBorderSize, boardPadding);
  drawBorder(lineSize, _boardBorderSize - lineSize + boardPadding);
  ctx.lineWidth = lineSize;
  const _gridSize = gridSize;
  for (let i = 1; i < boardLength - 1; i++) {
    ctx.beginPath();
    ctx.moveTo(boardPadding + _boardBorderSize + _gridSize * i, _boardBorderSize + boardPadding);
    ctx.lineTo(
      boardPadding + _boardBorderSize + _gridSize * i,
      _boardSize - _boardBorderSize - boardPadding,
    );
    ctx.stroke();
  }
  for (let i = 1; i < boardLength - 1; i++) {
    ctx.beginPath();
    ctx.moveTo(_boardBorderSize + boardPadding, boardPadding + _boardBorderSize + _gridSize * i);
    ctx.lineTo(
      _boardSize - _boardBorderSize - boardPadding,
      boardPadding + _boardBorderSize + _gridSize * i,
    );
    ctx.stroke();
  }
});
interface Chess {
  isWhite: boolean;
}
type Grid = (Chess | undefined)[][];
function getCellStyle(chess: Chess | undefined, x: number, y: number) {
  const res: CSSProperties = {
    left: `${boardPadding + boardBorderSize + gridSize * x}px`,
    top: `${boardPadding + boardBorderSize + gridSize * y}px`,
  };
  return res;
}
const selfPrompt = computed(() => {
  switch (matchState.value) {
    case MatchState.GAMING:
      if (selfRound.value) return '请落子.';
      else return '等待对方落子.';
    case MatchState.FINISHED:
      if (restartState.value === RestartState.WAITING) return '等待对方重新开始.';
      return '等待重新开始.';
  }
  return undefined;
});
const otherPrompt = computed(() => {
  switch (matchState.value) {
    case MatchState.GAMING:
      if (!selfRound.value) return '对方思考中.';
      return undefined;
    case MatchState.FINISHED:
      if (otherRequiresRestart.value) return '对方已请求重新开始.';
  }
  return undefined;
});
const firstLoading = ref(true);
enum MatchState {
  WAITING = 'WAITING',
  GAMING = 'GAMING',
  FINISHED = 'FINISHED',
}
const matchState = ref<MatchState>(MatchState.WAITING);
function updateMatchState(finished?: boolean) {
  if (finished) {
    matchState.value = MatchState.FINISHED;
    return;
  }
  matchState.value = otherUser.value ? MatchState.GAMING : MatchState.WAITING;
}
watch(matchState, (v) => {
  console.log(v);
  switch (v) {
    case MatchState.GAMING:
      ElMessage('对局开始');
      break;
  }
});
const placing = ref(false);
const selfRound = ref(false);
function resetSelfRound() {
  selfRound.value = !white.value;
}
const enabled = computed(
  () => matchState.value === MatchState.GAMING && selfRound.value && !placing.value,
);
const room = ref<RoomDetail>();
const otherUser = computed(() => {
  if (!room.value) return;
  if (!userStore.userInfo || userStore.userInfo.id === -1) return;
  const { whiteUser, blackUser } = room.value;
  return [whiteUser, blackUser].find((v) => v && v.id !== userStore.userInfo!.id);
});
watch(
  () => otherUser.value?.id,
  () => updateMatchState(),
);
const white = ref(false);
const grid = computed<Grid | undefined>(() =>
  room.value?.pieces.map((row) => row.map((v) => (v !== -1 ? { isWhite: !!v } : undefined))),
);
watch(
  grid,
  (v, old) => {
    if (!v || !old) return;
    let changed = false;
    for (const [[x, y, newItem], [, , oldItem]] of zip(iter2DArray(v), iter2DArray(old))) {
      if (newItem && !oldItem && !selfRound.value) {
        lastChess.value = [x, y];
        changed = true;
        break;
      }
    }
    if (!changed) {
      lastChess.value = undefined;
    }
  },
  { flush: 'sync' },
);
const blink = ref<boolean[][]>();
const lastChess = ref<[number, number]>();
const win = ref(false);
enum RestartState {
  NO = 'NO',
  RESTARTING = 'RESTARTING',
  WAITING = 'WAITING',
}

const restartState = ref<RestartState>(RestartState.NO);
const otherRequiresRestart = ref(false);
function resetNecessaryStates() {
  resetSelfRound();
  blink.value = undefined;
  restartState.value = RestartState.NO;
  lastChess.value = undefined;
}
function onCellClick(cell: Chess | undefined, x: number, y: number) {
  if (cell || !enabled.value) return;
  placing.value = true;
  send({
    name: 'PlaceChessPiece',
    payload: { x, y },
  });
}
const { send } = useGobangSocket({
  succeed: {
    UserInfo() {
      if (!roomId) return;
      send({
        name: 'PlayerJoin',
        payload: {
          roomId,
        },
      });
    },
    PlayerSideAllocation(p) {
      white.value = p.isWhite;
      resetSelfRound();
    },
    RoomInfo(p) {
      room.value = p;
      const { whiteRequestRestart, blackRequestRestart } = room.value;
      const selfRequiresRestart = white.value ? whiteRequestRestart : blackRequestRestart;
      otherRequiresRestart.value =
        (white.value ? blackRequestRestart : whiteRequestRestart) ?? false;
      if (matchState.value === MatchState.GAMING) {
        selfRound.value = white.value === p.canWhiteDown;
      }
      if (
        restartState.value === RestartState.WAITING &&
        !selfRequiresRestart &&
        !otherRequiresRestart.value
      ) {
        otherRequiresRestart.value = false;
        resetNecessaryStates();
        updateMatchState();
      }
    },
    PlayerLeave() {
      if (matchState.value === MatchState.FINISHED) {
        ElMessage.warning('对方已离开房间.');
      } else {
        ElMessage.warning('对方已离开房间, 对局自动结束.');
      }
      room.value = undefined;
      sendResetRoom();
      resetNecessaryStates();
    },
    HasPlayerWin({ winInfo }) {
      const [deltaX, deltaY] = deltaMap[winInfo.face];
      const { originalX, originalY } = winInfo;
      blink.value = create2DArray(boardLength, boardLength, false);
      for (let i = 0; i < 5; i++) {
        blink.value[originalY + deltaY * i][originalX + deltaX * i] = true;
      }
      updateMatchState(true);
    },
    PlayerWin() {
      win.value = true;
      ElNotification({
        title: '你赢了！',
        message: '🎉🥳🎊',
        type: 'success',
        position: 'bottom-left',
      });
    },
    PlayerLose() {
      win.value = false;
      ElNotification({
        title: '你输了！',
        message: '🤣👉🤡',
        type: 'error',
        position: 'bottom-left',
      });
    },
  },
  error: {
    PlayerJoin() {
      canExit = true;
      router.back();
      return false;
    },
  },
  before: {
    ResetRoom() {
      restartState.value = RestartState.WAITING;
    },
  },
  finally: {
    PlayerJoin(error) {
      if (!error) {
        firstLoading.value = false;
        updateMatchState();
      }
    },
    PlaceChessPiece() {
      placing.value = false;
    },
  },
});
function sendResetRoom() {
  send({
    name: 'ResetRoom',
  });
}
function restart() {
  sendResetRoom();
  restartState.value = RestartState.RESTARTING;
}
onMounted(() => {
  if (!roomId) {
    matchState.value = MatchState.GAMING;
  }
});
</script>
