<template>
	<MkSpacer :content-max="720">
		<div class="_panel root">
			<div class="dial">
				<MkInput v-model="target" placeholder="@user, @@group, group / 空欄で全体通話">
					<template #label>Call target</template>
				</MkInput>
				<div class="actions">
					<MkButton primary inline :disabled="busy" @click="start('video')">
						<i class="ph-video-camera ph-bold ph-lg"></i>
						Video
					</MkButton>
					<MkButton inline :disabled="busy" @click="start('audio')">
						<i class="ph-phone ph-bold ph-lg"></i>
						Audio
					</MkButton>
					<MkButton v-if="inCall" danger inline @click="hangup">
						<i class="ph-phone-disconnect ph-bold ph-lg"></i>
						Hang up
					</MkButton>
				</div>
			</div>
			<video ref="preview" class="preview" autoplay playsinline muted></video>
			<video ref="remotePreview" class="preview" autoplay playsinline></video>
			<div v-if="status" class="status">{{ status }}</div>
		</div>
	</MkSpacer>
</template>

<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from "vue";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue";
import { definePageMetadata } from "@/scripts/page-metadata";
import { stream as misskeyStream } from "@/stream";
import { $i } from "@/account";

const params = new URLSearchParams(location.search);
const target = ref(params.get("to") ?? "");
const incomingSession = params.get("session");
const preview = ref<HTMLVideoElement>();
const remotePreview = ref<HTMLVideoElement>();
const status = ref("");
const busy = ref(false);
const inCall = ref(false);
let stream: MediaStream | null = null;
let peer: RTCPeerConnection | null = null;
let sessionId = crypto.randomUUID();
let currentKind: "audio" | "video" = "video";
let directUserId: string | null = null;
let pendingIceCandidates: RTCIceCandidateInit[] = [];
const mainConnection = misskeyStream.useChannel("main", null, "Calls");

async function start(kind: "audio" | "video") {
	if (!$i) return;
	busy.value = true;
	try {
		status.value = "";
		currentKind = kind;
		directUserId = null;
		closeLocalPeer(false);
		stream?.getTracks().forEach((track) => track.stop());
		stream = null;
		await ensureLocalMedia(kind);
		sessionId = crypto.randomUUID();
		await createPeer();
		const offer = await peer!.createOffer();
		await peer!.setLocalDescription(offer);
		sendSignal({ type: "offer", sdp: offer });
		status.value = target.value.trim() ? `Calling ${target.value}` : "Calling everyone available";
	} catch (err: any) {
		status.value = err?.message ?? String(err);
	} finally {
		busy.value = false;
	}
}

onMounted(async () => {
	if (incomingSession) {
		const stored = sessionStorage.getItem(`ffy:pending-call:${incomingSession}`);
		if (stored) {
			sessionStorage.removeItem(`ffy:pending-call:${incomingSession}`);
			await handleCallSignal(JSON.parse(stored));
			return;
		}
		status.value = "Incoming call data was not found.";
		return;
	}

	if (target.value) start("video").catch((err) => (status.value = String(err)));
});

async function handleCallSignal(payload: any) {
	if (!$i || payload.fromUserId === $i.id) return;
	if (payload.sessionId !== sessionId && peer) return;
	sessionId = payload.sessionId;
	directUserId = payload.fromUserId;
	target.value = payload.to ?? "";
	currentKind = payload.kind ?? "video";
	const signal = payload.signal;
	try {
		if (signal.type === "offer") {
			closeLocalPeer(false);
			await ensureLocalMedia(currentKind);
			await createPeer();
			await peer!.setRemoteDescription(new RTCSessionDescription(signal.sdp));
			await flushPendingIceCandidates();
			const answer = await peer!.createAnswer();
			await peer!.setLocalDescription(answer);
			sendSignal({ type: "answer", sdp: answer });
			status.value = `Connected call from ${payload.fromUserId}`;
		} else if (signal.type === "answer") {
			await peer?.setRemoteDescription(new RTCSessionDescription(signal.sdp));
			await flushPendingIceCandidates();
			status.value = `Connected to ${target.value}`;
		} else if (signal.type === "ice" && signal.candidate) {
			if (peer?.remoteDescription) {
				await peer.addIceCandidate(new RTCIceCandidate(signal.candidate)).catch(() => {});
			} else {
				pendingIceCandidates.push(signal.candidate);
			}
		} else if (signal.type === "hangup") {
			closeCall();
		}
	} catch (err: any) {
		status.value = err?.message ?? String(err);
	}
}

const onCallSignal = (payload: any) => {
	handleCallSignal(payload);
};

mainConnection.on("callSignal", onCallSignal);

async function ensureLocalMedia(kind: "audio" | "video") {
	if (stream) return;
	if (!window.isSecureContext) {
		throw new Error("通話には HTTPS または localhost でのアクセスが必要です。");
	}
	stream = await navigator.mediaDevices.getUserMedia({
		audio: true,
		video: kind === "video",
	});
	inCall.value = true;
	if (preview.value) preview.value.srcObject = stream;
}

async function createPeer() {
	if (peer) return;
	peer = new RTCPeerConnection({
		iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
	});
	stream?.getTracks().forEach((track) => peer!.addTrack(track, stream!));
	peer.ontrack = (ev) => {
		if (remotePreview.value) remotePreview.value.srcObject = ev.streams[0];
	};
	peer.onicecandidate = (ev) => {
		if (ev.candidate) sendSignal({ type: "ice", candidate: ev.candidate });
	};
	peer.onconnectionstatechange = () => {
		status.value = `Call ${peer?.connectionState ?? "closed"}`;
	};
}

async function flushPendingIceCandidates() {
	if (!peer?.remoteDescription) return;
	const candidates = pendingIceCandidates;
	pendingIceCandidates = [];
	for (const candidate of candidates) {
		await peer.addIceCandidate(new RTCIceCandidate(candidate)).catch(() => {});
	}
}

function sendSignal(signal: any) {
	misskeyStream.send("callSignal", {
		to: target.value.trim(),
		toUserId: directUserId ?? undefined,
		sessionId,
		kind: currentKind,
		signal,
	});
}

function hangup() {
	sendSignal({ type: "hangup" });
	closeCall();
}

function closeCall() {
	closeLocalPeer(true);
	stream?.getTracks().forEach((track) => track.stop());
	stream = null;
	inCall.value = false;
	if (preview.value) preview.value.srcObject = null;
	status.value = "Call ended";
}

function closeLocalPeer(clearRemote: boolean) {
	peer?.getSenders().forEach((sender) => sender.track?.stop());
	peer?.close();
	peer = null;
	pendingIceCandidates = [];
	if (remotePreview.value) remotePreview.value.srcObject = null;
	if (clearRemote) directUserId = null;
}

onBeforeUnmount(() => {
	mainConnection.dispose();
	if (peer) sendSignal({ type: "hangup" });
	closeCall();
});

definePageMetadata({
	title: "Calls",
	icon: "ph-phone ph-bold ph-lg",
});
</script>

<style lang="scss" scoped>
.root {
	padding: 18px;
	border-radius: 8px;
}

.dial {
	display: grid;
	gap: 12px;
}

.actions {
	display: flex;
	gap: 8px;
	flex-wrap: wrap;
}

.preview {
	width: 100%;
	aspect-ratio: 16 / 9;
	margin-top: 16px;
	background: #000;
	border-radius: 8px;
	object-fit: cover;
}

.status {
	margin-top: 12px;
	color: var(--fgTransparentWeak);
}
</style>
