<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<MkSpacer :contentMax="600">
	<div :class="$style.root" class="_gaps">
		<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
			<span>({{ i18n.ts._reversi.black }})</span>
			<MkAvatar style="width: 32px; height: 32px;" :user="blackUser" :showIndicator="true"/>
			<span> vs </span>
			<MkAvatar style="width: 32px; height: 32px;" :user="whiteUser" :showIndicator="true"/>
			<span>({{ i18n.ts._reversi.white }})</span>
		</div>

		<div style="overflow: clip; line-height: 28px;">
			<div v-if="!iAmPlayer && !game.isEnded && turnUser" class="turn">
				<Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
				<MkEllipsis/>
			</div>
			<div v-if="(logPos !== game.logs.length) && turnUser" class="turn">
				<Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
			</div>
			<div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div>
			<div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div>
			<div v-if="game.isEnded && logPos == game.logs.length" class="result">
				<template v-if="game.winner">
					<Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
					<span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span>
				</template>
				<template v-else>{{ i18n.ts._reversi.drawn }}</template>
			</div>
		</div>

		<div :class="$style.board">
			<div v-if="showBoardLabels" :class="$style.labelsX">
				<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
			</div>
			<div style="display: flex;">
				<div v-if="showBoardLabels" :class="$style.labelsY">
					<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
				</div>
				<div :class="$style.boardCells" :style="cellsStyle">
					<div
						v-for="(stone, i) in engine.board"
						:key="i"
						v-tooltip="`${String.fromCharCode(65 + engine.posToXy(i)[0])}${engine.posToXy(i)[1] + 1}`"
						:class="[$style.boardCell, {
							[$style.boardCell_empty]: stone == null,
							[$style.boardCell_none]: engine.map[i] === 'null',
							[$style.boardCell_isEnded]: game.isEnded,
							[$style.boardCell_myTurn]: !game.isEnded && isMyTurn,
							[$style.boardCell_can]: turnUser ? engine.canPut(turnUser.id === blackUser.id, i) : null,
							[$style.boardCell_prev]: engine.prevPos === i
						}]"
						@click="putStone(i)"
					>
						<Transition
							:enterActiveClass="$style.transition_flip_enterActive"
							:leaveActiveClass="$style.transition_flip_leaveActive"
							:enterFromClass="$style.transition_flip_enterFrom"
							:leaveToClass="$style.transition_flip_leaveTo"
							mode="default"
						>
							<template v-if="useAvatarAsStone">
								<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl"/>
								<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl"/>
							</template>
							<template v-else>
								<img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
								<img v-else-if="stone === false" :class="$style.boardCellStone" src="/client-assets/reversi/stone_w.png"/>
							</template>
						</Transition>
					</div>
				</div>
				<div v-if="showBoardLabels" :class="$style.labelsY">
					<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
				</div>
			</div>
			<div v-if="showBoardLabels" :class="$style.labelsX">
				<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
			</div>
		</div>

		<div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
			<div>{{ logPos }} / {{ game.logs.length }}</div>
			<div v-if="!autoplaying" class="_buttonsCenter">
				<MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton>
				<MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton>
				<MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton>
				<MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ti ti-chevrons-right"></i></MkButton>
			</div>
			<MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton>
		</div>

		<div class="_panel" style="padding: 16px;">
			<div>
				<b>{{ i18n.tsx._reversi.turnCount({ count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}
			</div>
			<div>
				<div style="display: flex; align-items: center;">
					<span style="margin-right: 8px;">({{ i18n.ts._reversi.black }})</span>
					<MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="blackUser" :showIndicator="true"/>
					<MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA>
				</div>
				<div> vs </div>
				<div style="display: flex; align-items: center;">
					<span style="margin-right: 8px;">({{ i18n.ts._reversi.white }})</span>
					<MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="whiteUser" :showIndicator="true"/>
					<MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA>
				</div>
			</div>
			<div>
				<p v-if="game.isLlotheo">{{ i18n.ts._reversi.isLlotheo }}</p>
				<p v-if="game.loopedBoard">{{ i18n.ts._reversi.loopedMap }}</p>
				<p v-if="game.canPutEverywhere">{{ i18n.ts._reversi.canPutEverywhere }}</p>
			</div>
		</div>

		<MkFolder>
			<template #label>{{ i18n.ts.options }}</template>
			<div class="_gaps_s" style="text-align: left;">
				<MkSwitch v-model="showBoardLabels">Show labels</MkSwitch>
				<MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch>
			</div>
		</MkFolder>

		<div class="_buttonsCenter">
			<MkButton v-if="!game.isEnded && iAmPlayer" danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton>
			<MkButton @click="share">{{ i18n.ts.share }}</MkButton>
		</div>

		<MkA v-if="game.isEnded" :to="`/reversi`">
			<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; width: 200px; margin: auto;"/>
		</MkA>
	</div>
</MkSpacer>
</template>

<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
import * as CRC32 from 'crc-32';
import * as Misskey from 'misskey-js';
import * as Reversi from 'misskey-reversi';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { deepClone } from '@/scripts/clone.js';
import { useInterval } from '@/scripts/use-interval.js';
import { signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { userPage } from '@/filters/user.js';
import * as sound from '@/scripts/sound.js';
import * as os from '@/os.js';

const $i = signinRequired();

const props = defineProps<{
	game: Misskey.entities.ReversiGameDetailed;
	connection: Misskey.ChannelConnection;
}>();

const showBoardLabels = ref<boolean>(false);
const useAvatarAsStone = ref<boolean>(true);
const autoplaying = ref<boolean>(false);
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
const logPos = ref<number>(game.value.logs.length);
const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
	map: game.value.map,
	isLlotheo: game.value.isLlotheo,
	canPutEverywhere: game.value.canPutEverywhere,
	loopedBoard: game.value.loopedBoard,
	logs: game.value.logs,
}));

const iAmPlayer = computed(() => {
	return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
});

const myColor = computed(() => {
	if (!iAmPlayer.value) return null;
	if (game.value.user1Id === $i.id && game.value.black === 1) return true;
	if (game.value.user2Id === $i.id && game.value.black === 2) return true;
	return false;
});

const opColor = computed(() => {
	if (!iAmPlayer.value) return null;
	return !myColor.value;
});

const blackUser = computed(() => {
	return game.value.black === 1 ? game.value.user1 : game.value.user2;
});

const whiteUser = computed(() => {
	return game.value.black === 1 ? game.value.user2 : game.value.user1;
});

const turnUser = computed(() => {
	if (engine.value.turn === true) {
		return game.value.black === 1 ? game.value.user1 : game.value.user2;
	} else if (engine.value.turn === false) {
		return game.value.black === 1 ? game.value.user2 : game.value.user1;
	} else {
		return null;
	}
});

const isMyTurn = computed(() => {
	if (!iAmPlayer.value) return false;
	const u = turnUser.value;
	if (u == null) return false;
	return u.id === $i.id;
});

const cellsStyle = computed(() => {
	return {
		'grid-template-rows': `repeat(${game.value.map.length}, 1fr)`,
		'grid-template-columns': `repeat(${game.value.map[0].length}, 1fr)`,
	};
});

watch(logPos, (v) => {
	if (!game.value.isEnded) return;
	engine.value = Reversi.Serializer.restoreGame({
		map: game.value.map,
		isLlotheo: game.value.isLlotheo,
		canPutEverywhere: game.value.canPutEverywhere,
		loopedBoard: game.value.loopedBoard,
		logs: game.value.logs.slice(0, v),
	});
});

if (game.value.isStarted && !game.value.isEnded) {
	useInterval(() => {
		if (game.value.isEnded) return;
		const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
		if (_DEV_) console.log('crc32', crc32);
		props.connection.send('syncState', {
			crc32: crc32,
		});
	}, 10000, { immediate: false, afterMounted: true });
}

const appliedOps: string[] = [];

function putStone(pos) {
	if (game.value.isEnded) return;
	if (!iAmPlayer.value) return;
	if (!isMyTurn.value) return;
	if (!engine.value.canPut(myColor.value!, pos)) return;

	engine.value.putStone(pos);

	triggerRef(engine);

	sound.playUrl('/client-assets/reversi/put.mp3', {
		volume: 1,
		playbackRate: 1,
	});

	const id = Math.random().toString(36).slice(2);
	props.connection.send('putStone', {
		pos: pos,
		id,
	});
	appliedOps.push(id);

	checkEnd();
}

function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
	game.value.logs = Reversi.Serializer.serializeLogs([
		...Reversi.Serializer.deserializeLogs(game.value.logs),
		log,
	]);

	logPos.value++;

	if (log.id == null || !appliedOps.includes(log.id)) {
		switch (log.operation) {
			case 'put': {
				engine.value.putStone(log.pos);
				triggerRef(engine);

				sound.playUrl('/client-assets/reversi/put.mp3', {
					volume: 1,
					playbackRate: 1,
				});

				checkEnd();
				break;
			}

			default:
				break;
		}
	}
}

function onStreamEnded(x) {
	game.value = deepClone(x.game);
}

function checkEnd() {
	game.value.isEnded = engine.value.isEnded;
	if (game.value.isEnded) {
		if (engine.value.winner === true) {
			game.value.winnerId = game.value.black === 1 ? game.value.user1Id : game.value.user2Id;
			game.value.winner = game.value.black === 1 ? game.value.user1 : game.value.user2;
		} else if (engine.value.winner === false) {
			game.value.winnerId = game.value.black === 1 ? game.value.user2Id : game.value.user1Id;
			game.value.winner = game.value.black === 1 ? game.value.user2 : game.value.user1;
		} else {
			game.value.winnerId = null;
			game.value.winner = null;
		}
	}
}

function onStreamRescue(_game) {
	console.log('rescue');

	game.value = deepClone(_game);

	engine.value = Reversi.Serializer.restoreGame({
		map: game.value.map,
		isLlotheo: game.value.isLlotheo,
		canPutEverywhere: game.value.canPutEverywhere,
		loopedBoard: game.value.loopedBoard,
		logs: game.value.logs,
	});

	logPos.value = game.value.logs.length;

	checkEnd();
}

async function surrender() {
	const { canceled } = await os.confirm({
		type: 'warning',
		text: i18n.ts.areYouSure,
	});
	if (canceled) return;

	misskeyApi('reversi/surrender', {
		gameId: game.value.id,
	});
}

function autoplay() {
	autoplaying.value = true;
	logPos.value = 0;
	const logs = Reversi.Serializer.deserializeLogs(game.value.logs);

	window.setTimeout(() => {
		logPos.value = 1;

		let i = 1;
		let previousLog = logs[0];
		const tick = () => {
			const log = logs[i];
			const time = log.time - previousLog.time;
			setTimeout(() => {
				i++;
				logPos.value++;
				previousLog = log;

				if (i < logs.length) {
					tick();
				} else {
					autoplaying.value = false;
				}
			}, time);
		};

		tick();
	}, 1000);
}

function share() {
	os.post({
		initialText: `#MisskeyReversi ${location.href}`,
		instant: true,
	});
}

onMounted(() => {
	props.connection.on('log', onStreamLog);
	props.connection.on('rescue', onStreamRescue);
	props.connection.on('ended', onStreamEnded);
});

onUnmounted(() => {
	props.connection.off('log', onStreamLog);
	props.connection.off('rescue', onStreamRescue);
	props.connection.off('ended', onStreamEnded);
});
</script>

<style lang="scss" module>
@use "sass:math";

.transition_flip_enterActive,
.transition_flip_leaveActive {
	backface-visibility: hidden;
	transition: opacity 0.5s ease, transform 0.5s ease;
}
.transition_flip_enterFrom {
	transform: rotateY(-180deg);
	opacity: 0;
}
.transition_flip_leaveTo {
	transform: rotateY(180deg);
	opacity: 0;
}

$label-size: 16px;
$gap: 4px;

.root {
	text-align: center;
}

.board {
	width: 100%;
	max-width: 500px;
	margin: 0 auto;
}

.labelsX {
	height: $label-size;
	padding: 0 $label-size;
	display: flex;
}

.labelsXLabel {
	flex: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 0.8em;

	&:first-child {
		margin-left: -(math.div($gap, 2));
	}

	&:last-child {
		margin-right: -(math.div($gap, 2));
	}
}

.labelsY {
	width: $label-size;
	display: flex;
	flex-direction: column;
}

.labelsYLabel {
	flex: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 12px;

	&:first-child {
		margin-top: -(math.div($gap, 2));
	}

	&:last-child {
		margin-bottom: -(math.div($gap, 2));
	}
}

.boardCells {
	flex: 1;
	display: grid;
	grid-gap: $gap;
}

.boardCell {
	background: transparent;
	border-radius: 6px;
	aspect-ratio: 1;
	transform-style: preserve-3d;
	perspective: 150px;

	&.boardCell_empty {
		border: solid 2px var(--divider);
	}

	&.boardCell_empty.boardCell_can {
		border-color: var(--accent);
		opacity: 0.5;
	}

	&.boardCell_empty.boardCell_myTurn {
		border-color: var(--divider);
		opacity: 1;

		&.boardCell_can {
			border-color: var(--accent);
			cursor: pointer;

			&:hover {
				background: var(--accent);
			}
		}
	}

	&.boardCell_prev {
		box-shadow: 0 0 0 4px var(--accent);
	}

	&.boardCell_isEnded {
		border-color: var(--divider);
	}

	&.boardCell_none {
		border-color: transparent !important;
	}
}

.boardCellStone {
	position: absolute;
	top: 0;
	left: 0;
	pointer-events: none;
	user-select: none;
	display: block;
	width: 100%;
	height: 100%;
	border-radius: 6px;
}
</style>
