<template>
	<MkSpacer :content-max="980">
		<div class="reversi">
			<section v-if="!gameId" class="panel">
				<div class="head">
					<div>
						<h1>Reversi</h1>
						<p>{{ $i ? "SNS上のユーザーと対局できます。" : "ログインすると対局できます。" }}</p>
					</div>
					<div class="actions">
						<MkButton v-if="$i" primary @click="inviteUser">ユーザーを招待</MkButton>
						<MkButton v-if="$i" :disabled="matching" @click="matchAny">ランダム対局</MkButton>
					</div>
				</div>

				<div v-if="invitations.length > 0" class="block">
					<h2>招待</h2>
					<div class="list">
						<button v-for="user in invitations" :key="user.id" class="row" @click="accept(user)">
							<MkAvatar :user="user" class="avatar"/>
							<span>{{ user.name || user.username }}</span>
							<i class="ph-caret-right ph-bold"></i>
						</button>
					</div>
				</div>

				<div class="block">
					<h2>対局</h2>
					<div v-if="games.length === 0" class="empty">対局はまだありません。</div>
					<div v-else class="list">
						<button v-for="game in games" :key="game.id" class="row" @click="router.push(`/reversi/${game.id}`)">
							<span class="players">
								{{ game.user1?.name || game.user1?.username }} vs {{ game.user2?.name || game.user2?.username }}
							</span>
							<span class="status">{{ game.isEnded ? "終了" : game.isStarted ? "対局中" : "準備中" }}</span>
						</button>
					</div>
				</div>
			</section>

			<section v-else-if="game" class="game">
				<div class="gameHeader">
					<MkButton @click="router.push('/reversi')"><i class="ph-arrow-left ph-bold"></i></MkButton>
					<div>
						<h1>{{ game.user1?.name || game.user1?.username }} vs {{ game.user2?.name || game.user2?.username }}</h1>
						<p>{{ gameStatus }}</p>
					</div>
					<MkButton v-if="canPlay && !game.isEnded" danger @click="surrenderGame">投了</MkButton>
				</div>

				<div class="playArea">
					<div class="board" :style="boardStyle">
						<button
							v-for="cell in cells"
							:key="cell.pos"
							class="cell"
							:class="{ null: cell.null, legal: cell.legal, last: cell.pos === lastPos }"
							:disabled="!cell.legal || busy"
							@click="put(cell.pos)"
						>
							<MkAvatar
								v-if="stoneUser(cell.stone)"
								:user="stoneUser(cell.stone)"
								class="stone avatarStone"
							/>
						</button>
					</div>

					<div class="side panel">
						<div class="score">
							<div>
								<MkAvatar v-if="blackUser" :user="blackUser" class="scoreAvatar"/>
								{{ blackUserName }}
								<b>{{ blackCount }}</b>
							</div>
							<div>
								<MkAvatar v-if="whiteUser" :user="whiteUser" class="scoreAvatar"/>
								{{ whiteUserName }}
								<b>{{ whiteCount }}</b>
							</div>
						</div>

						<div v-if="!game.isStarted && canPlay" class="ready">
							<MkButton :primary="!myReady" @click="setReady(!myReady)">
								{{ myReady ? "準備解除" : "準備完了" }}
							</MkButton>
							<div class="readyStates">
								<span :class="{ on: game.user1Ready }">{{ game.user1?.username }}</span>
								<span :class="{ on: game.user2Ready }">{{ game.user2?.username }}</span>
							</div>
						</div>

						<div v-if="!game.isStarted && canPlay && !myReady" class="settings">
							<label>
								<span>先手</span>
								<select v-model="bw" @change="updateSetting('bw', bw)">
									<option value="random">ランダム</option>
									<option value="1">{{ game.user1?.username }}</option>
									<option value="2">{{ game.user2?.username }}</option>
								</select>
							</label>
							<label>
								<span>1手の制限時間</span>
								<input v-model.number="timeLimit" type="number" min="5" max="300" @change="updateSetting('timeLimitForEachTurn', timeLimit)">
							</label>
						</div>
					</div>
				</div>
			</section>
		</div>
	</MkSpacer>
</template>

<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { v4 as uuid } from "uuid";
import MkButton from "@/components/MkButton.vue";
import * as os from "@/os";
import { $i } from "@/account";
import { stream } from "@/stream";
import { useRouter } from "@/router";
import { definePageMetadata } from "@/scripts/page-metadata";

const props = defineProps<{ gameId?: string }>();
const router = useRouter();

const games = ref<any[]>([]);
const invitations = ref<any[]>([]);
const game = ref<any | null>(null);
const matching = ref(false);
const busy = ref(false);
const conn = ref<any | null>(null);
const lobbyConn = ref<any | null>(null);
const bw = ref("random");
const timeLimit = ref(90);

type Cell = { pos: number; null: boolean; stone: boolean | null | undefined; legal: boolean };

class Engine {
	map: ("empty" | "null")[];
	board: (boolean | null | undefined)[];
	turn: boolean | null = true;
	width: number;
	height: number;

	constructor(map: string[]) {
		this.width = map[0]?.length ?? 8;
		this.height = map.length;
		const data = map.join("");
		this.board = data.split("").map((d) => d === "-" ? null : d === "b" ? true : d === "w" ? false : undefined);
		this.map = data.split("").map((d) => d === "-" || d === "b" || d === "w" ? "empty" : "null");
		if (!this.canPutSomewhere(true)) this.turn = this.canPutSomewhere(false) ? false : null;
	}

	xy(pos: number) { return [pos % this.width, Math.floor(pos / this.width)]; }
	pos(x: number, y: number) { return x + y * this.width; }
	canPutSomewhere(color: boolean) { return this.board.some((_v, i) => this.canPut(color, i)); }
	canPut(color: boolean, pos: number) { return this.board[pos] === null && this.effects(color, pos).length > 0; }
	effects(color: boolean, init: number) {
		const dirs = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]];
		return dirs.flatMap(([dx, dy]) => {
			const found: number[] = [];
			let [x, y] = this.xy(init);
			while (true) {
				x += dx; y += dy;
				if (x < 0 || y < 0 || x >= this.width || y >= this.height) return [];
				const p = this.pos(x, y);
				if (this.map[p] === "null" || this.board[p] == null) return [];
				if (this.board[p] === !color) found.push(p);
				if (this.board[p] === color) return found;
			}
		});
	}
	put(pos: number) {
		const color = this.turn;
		if (color == null) return;
		this.board[pos] = color;
		for (const p of this.effects(color, pos)) this.board[p] = color;
		this.turn = this.canPutSomewhere(!color) ? !color : this.canPutSomewhere(color) ? color : null;
	}
}

const engine = computed(() => {
	if (!game.value) return null;
	const e = new Engine(game.value.map);
	let t = 0;
	for (const log of game.value.logs ?? []) {
		t += log[0];
		if (log[2] === 0) e.put(log[3]);
	}
	return e;
});

const myColor = computed(() => {
	if (!$i || !game.value || game.value.black == null) return null;
	return ((game.value.user1Id === $i.id && game.value.black === 1) || (game.value.user2Id === $i.id && game.value.black === 2));
});
const canPlay = computed(() => $i && game.value && (game.value.user1Id === $i.id || game.value.user2Id === $i.id));
const myReady = computed(() => $i && game.value && (game.value.user1Id === $i.id ? game.value.user1Ready : game.value.user2Ready));
const cells = computed<Cell[]>(() => {
	if (!engine.value || !game.value) return [];
	return engine.value.board.map((stone, pos) => ({
		pos,
		null: engine.value!.map[pos] === "null",
		stone,
		legal: game.value.isStarted && !game.value.isEnded && myColor.value === engine.value!.turn && engine.value!.canPut(myColor.value, pos),
	}));
});
const boardStyle = computed(() => ({
	gridTemplateColumns: `repeat(${engine.value?.width ?? 8}, 1fr)`,
}));
const blackCount = computed(() => engine.value?.board.filter((x) => x === true).length ?? 0);
const whiteCount = computed(() => engine.value?.board.filter((x) => x === false).length ?? 0);
const blackUser = computed(() => game.value?.black === 2 ? game.value?.user2 : game.value?.user1);
const whiteUser = computed(() => game.value?.black === 2 ? game.value?.user1 : game.value?.user2);
const blackUserName = computed(() => blackUser.value?.name || blackUser.value?.username);
const whiteUserName = computed(() => whiteUser.value?.name || whiteUser.value?.username);
const lastPos = computed(() => {
	const logs = game.value?.logs ?? [];
	return logs.length ? logs[logs.length - 1][3] : -1;
});
const gameStatus = computed(() => {
	if (!game.value) return "";
	if (game.value.isEnded) return game.value.winnerId ? `${game.value.winner?.username ?? "勝者"} の勝ち` : "引き分け";
	if (!game.value.isStarted) return "準備中";
	if (engine.value?.turn == null) return "終了";
	return engine.value.turn === myColor.value ? "あなたの番です" : "相手の番です";
});

async function loadLobby() {
	if (!$i) return;
	invitations.value = await os.api("reversi/invitations");
	games.value = await os.api("reversi/games", { my: true, limit: 30 });
}

async function loadGame() {
	if (!props.gameId) return;
	game.value = await os.api("reversi/show-game", { gameId: props.gameId });
	bw.value = game.value.bw ?? "random";
	timeLimit.value = game.value.timeLimitForEachTurn ?? 90;
	connectGame();
}

function connectGame() {
	conn.value?.dispose();
	if (!props.gameId) return;
	conn.value = stream.useChannel("reversiGame", { gameId: props.gameId } as any);
	conn.value.on("changeReadyStates", ({ user1, user2 }) => {
		if (!game.value) return;
		game.value.user1Ready = user1;
		game.value.user2Ready = user2;
	});
	conn.value.on("updateSettings", ({ key, value }) => {
		if (game.value) game.value[key] = value;
	});
	conn.value.on("started", ({ game: g }) => game.value = g);
	conn.value.on("log", (log) => {
		if (!game.value) return;
		const prev = game.value.logs?.at(-1)?.[0] ?? Date.now();
		game.value.logs.push([Math.max(0, log.time - prev), log.player ? 1 : 0, 0, log.pos]);
	});
	conn.value.on("ended", ({ game: g }) => game.value = g);
	conn.value.on("canceled", () => router.push("/reversi"));
}

async function inviteUser() {
	const user = await os.selectUser();
	if (!user) return;
	matching.value = true;
	const matched = await os.api("reversi/match", { userId: user.id });
	matching.value = false;
	if (matched) router.push(`/reversi/${matched.id}`);
}

async function matchAny() {
	matching.value = true;
	const matched = await os.api("reversi/match", {});
	matching.value = false;
	if (matched) router.push(`/reversi/${matched.id}`);
}

async function accept(user: any) {
	const matched = await os.api("reversi/match", { userId: user.id });
	if (matched) router.push(`/reversi/${matched.id}`);
}

function setReady(value: boolean) {
	conn.value?.send("ready", value);
}

function updateSetting(key: string, value: any) {
	conn.value?.send("updateSettings", { key, value });
}

function stoneUser(stone: boolean | null | undefined) {
	if (stone == null) return null;
	return stone ? blackUser.value : whiteUser.value;
}

function put(pos: number) {
	busy.value = true;
	conn.value?.send("putStone", { pos, id: uuid() });
	window.setTimeout(() => busy.value = false, 250);
}

async function surrenderGame() {
	await os.api("reversi/surrender", { gameId: props.gameId });
}

onMounted(() => {
	if (props.gameId) {
		loadGame();
	} else {
		loadLobby();
		if ($i) {
			lobbyConn.value = stream.useChannel("reversi" as any);
			lobbyConn.value.on("invited", loadLobby);
			lobbyConn.value.on("matched", ({ game }) => router.push(`/reversi/${game.id}`));
		}
	}
});

watch(() => props.gameId, () => {
	game.value = null;
	conn.value?.dispose();
	if (props.gameId) loadGame();
});

onUnmounted(() => {
	conn.value?.dispose();
	lobbyConn.value?.dispose();
});

definePageMetadata({
	title: "Reversi",
	icon: "ph-circle-half-tilt ph-bold ph-lg",
});
</script>

<style lang="scss" scoped>
.reversi { display: flex; flex-direction: column; gap: 16px; }
.panel { background: var(--panel); border-radius: 8px; padding: 18px; }
.head, .gameHeader { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
h1 { margin: 0; font-size: 1.35em; }
h2 { margin: 0 0 10px; font-size: 1.05em; }
p { margin: 6px 0 0; color: var(--fgTransparentWeak); }
.actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.block { margin-top: 18px; }
.list { display: grid; gap: 8px; }
.row {
	display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px;
	border: 1px solid var(--divider); border-radius: 8px; background: var(--buttonBg);
	color: var(--fg); text-align: left;
}
.row:hover { background: var(--buttonHoverBg); }
.avatar { width: 34px; height: 34px; }
.players { flex: 1; }
.status { color: var(--fgTransparentWeak); }
.empty { color: var(--fgTransparentWeak); padding: 12px 0; }
.playArea { display: grid; grid-template-columns: minmax(260px, 1fr) 280px; gap: 16px; align-items: start; }
.board {
	display: grid; gap: 4px; width: min(100%, 680px); aspect-ratio: 1 / 1;
	background: #1f6b4b; padding: 8px; border-radius: 8px;
}
.cell {
	position: relative; border: 1px solid rgba(0, 0, 0, .32); background: #269662;
	aspect-ratio: 1 / 1; border-radius: 4px;
}
.cell.null { visibility: hidden; }
.cell.legal::after {
	content: ""; position: absolute; inset: 38%; border-radius: 999px; background: rgba(255,255,255,.55);
}
.cell.last { box-shadow: inset 0 0 0 2px #f5c542; }
.stone {
	position: absolute;
	inset: 9%;
	border-radius: 999px;
	box-shadow: 0 4px 10px rgba(0,0,0,.28);
}
.avatarStone {
	width: 82%;
	height: 82%;
	left: 9%;
	top: 9%;
}
.side { display: grid; gap: 16px; }
.score { display: grid; gap: 10px; }
.score div { display: flex; align-items: center; gap: 8px; }
.score b { margin-left: auto; font-size: 1.4em; }
.scoreAvatar { width: 28px; height: 28px; }
.ready, .settings { display: grid; gap: 10px; }
.readyStates { display: flex; gap: 8px; flex-wrap: wrap; }
.readyStates span { color: var(--fgTransparentWeak); }
.readyStates span.on { color: var(--accent); font-weight: 700; }
label { display: grid; gap: 6px; }
select, input {
	width: 100%; border: 1px solid var(--divider); border-radius: 6px;
	background: var(--panel); color: var(--fg); padding: 8px;
}
@media (max-width: 760px) {
	.head, .gameHeader { align-items: stretch; flex-direction: column; }
	.playArea { grid-template-columns: 1fr; }
}
</style>
