<template>
	<MkModalWindow
		ref="windowEl"
		:width="980"
		:height="760"
		:scroll="false"
		@close="close"
		@closed="$emit('closed')"
	>
		<template #header>
			<span class="title">{{ file.name }}</span>
		</template>
		<div class="epubViewer">
			<div class="toolbar">
				<button class="_button control" type="button" @click="go('prev')">
					<i class="ph-caret-left ph-bold ph-lg"></i>
				</button>
				<div class="status">
					<span>{{ status }}</span>
					<progress v-if="progress != null" :value="progress" max="100"></progress>
				</div>
				<button class="_button control" type="button" @click="go('next')">
					<i class="ph-caret-right ph-bold ph-lg"></i>
				</button>
				<a
					v-if="file.allowDownload"
					class="_button control"
					:href="file.url"
					:download="file.name"
					target="_blank"
					rel="noopener"
				>
					<i class="ph-download-simple ph-bold ph-lg"></i>
				</a>
				<a
					class="_button control"
					:href="file.url"
					target="_blank"
					rel="noopener"
				>
					<i class="ph-arrow-square-out ph-bold ph-lg"></i>
				</a>
			</div>
			<iframe
				ref="frameEl"
				class="frame"
				:title="file.name"
				:sandbox="sandbox"
				:srcdoc="frameHtml"
			></iframe>
		</div>
	</MkModalWindow>
</template>

<script lang="ts" setup>
import { computed, onBeforeUnmount, ref, shallowRef } from "vue";
import type * as misskey from "iceshrimp-sdk";
import MkModalWindow from "@/components/MkModalWindow.vue";

const EPUBJS_CDN =
	"https://cdn.jsdelivr.net/npm/epubjs@0.3.93/dist/epub.min.js";

const props = defineProps<{
	file: misskey.entities.DriveFile;
}>();

defineEmits<{
	(event: "closed"): void;
}>();

const windowEl = shallowRef<InstanceType<typeof MkModalWindow>>();
const frameEl = ref<HTMLIFrameElement | null>(null);
const status = ref("読み込み中");
const progress = ref<number | null>(null);
const loading = ref(false);
const controller = new AbortController();
const sandbox = [
	"allow-scripts",
	"allow-forms",
	"allow-popups",
	"allow-modals",
	"allow-downloads",
].join(" ");

const frameHtml = computed(() => `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html, body, #viewer {
	margin: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
	background: #fff;
	color: #111;
}
#viewer {
	box-sizing: border-box;
	padding: 0 12px;
}
#message {
	position: fixed;
	inset: 0;
	display: grid;
	place-items: center;
	padding: 24px;
	box-sizing: border-box;
	font: 14px/1.6 system-ui, sans-serif;
	color: #555;
	background: #fff;
	z-index: 1;
}
#message[hidden] {
	display: none;
}
</style>
</head>
<body>
<div id="message">EPUBビューアを読み込み中</div>
<div id="viewer"></div>
${"<scr" + "ipt"} src="${EPUBJS_CDN}">${"</scr" + "ipt"}
${"<scr" + "ipt"}
(() => {
	const message = document.getElementById("message");
	const viewer = document.getElementById("viewer");
	let rendition;
	let book;

	function post(type, payload = {}) {
		parent.postMessage({ source: "mk-epub-viewer", type, ...payload }, "*");
	}

	function show(text) {
		message.hidden = false;
		message.textContent = text;
	}

	function hide() {
		message.hidden = true;
	}

	function reportProgress(location) {
		const cfi = location?.start?.cfi;
		if (!book?.locations || !cfi) return;
		const percentage = book.locations.percentageFromCfi(cfi);
		if (Number.isFinite(percentage)) {
			post("progress", { progress: Math.round(percentage * 1000) / 10 });
		}
	}

	async function load(buffer) {
		try {
			if (!window.ePub) throw new Error("epub.js の読み込みに失敗しました");
			show("EPUBを開いています");
			book = window.ePub();
			await book.open(buffer, "binary");
			rendition = book.renderTo(viewer, {
				width: "100%",
				height: "100%",
				spread: "auto",
				flow: "paginated",
				allowScriptedContent: true,
			});
			rendition.on("relocated", reportProgress);
			await rendition.display();
			hide();
			post("loaded");
			book.ready
				.then(() => book.locations.generate(1200))
				.then(() => reportProgress(rendition.currentLocation()))
				.catch(() => {});
		} catch (err) {
			show(err?.message || String(err));
			post("error", { message: err?.message || String(err) });
		}
	}

	window.addEventListener("message", (event) => {
		const data = event.data || {};
		if (data.source !== "mk-epub-viewer-host") return;
		if (data.type === "load") load(data.buffer);
		if (data.type === "next") rendition?.next();
		if (data.type === "prev") rendition?.prev();
	});

	if (document.readyState === "complete") {
		post("ready");
	} else {
		window.addEventListener("load", () => post("ready"), { once: true });
	}
})();
${"</scr" + "ipt"}
</body>
</html>`);

async function loadBook() {
	if (loading.value) return;
	const frame = frameEl.value?.contentWindow;
	if (!frame) return;
	loading.value = true;
	status.value = "EPUBを取得中";
	try {
		const res = await fetch(props.file.url, {
			credentials: "omit",
			cache: "default",
			signal: controller.signal,
		});
		if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
		const buffer = await res.arrayBuffer();
		status.value = "EPUBを開いています";
		frame.postMessage(
			{
				source: "mk-epub-viewer-host",
				type: "load",
				buffer,
			},
			"*",
			[buffer],
		);
	} catch (err) {
		if (controller.signal.aborted) return;
		status.value = `読み込みに失敗しました: ${
			err instanceof Error ? err.message : String(err)
		}`;
	} finally {
		loading.value = false;
	}
}

function onMessage(event: MessageEvent) {
	const data = event.data || {};
	if (data.source !== "mk-epub-viewer") return;
	if (data.type === "ready") loadBook();
	if (data.type === "loaded") status.value = "EPUB";
	if (data.type === "progress") {
		progress.value = data.progress;
		status.value = `EPUB ${data.progress}%`;
	}
	if (data.type === "error") status.value = `表示に失敗しました: ${data.message}`;
}

function go(direction: "prev" | "next") {
	frameEl.value?.contentWindow?.postMessage(
		{
			source: "mk-epub-viewer-host",
			type: direction,
		},
		"*",
	);
}

function close() {
	windowEl.value?.close();
}

window.addEventListener("message", onMessage);

onBeforeUnmount(() => {
	controller.abort();
	window.removeEventListener("message", onMessage);
});
</script>

<style lang="scss" scoped>
.title {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.epubViewer {
	display: flex;
	flex-direction: column;
	height: 100%;
	background: var(--panel);
}

.toolbar {
	display: grid;
	grid-template-columns: 38px minmax(0, 1fr) 38px 38px 38px;
	gap: 8px;
	align-items: center;
	padding: 8px;
	border-bottom: 1px solid var(--divider);
}

.control {
	display: grid;
	place-items: center;
	width: 38px;
	height: 38px;
	border-radius: 6px;
	background: var(--buttonBg);
	color: var(--fg);

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

.status {
	display: grid;
	gap: 4px;
	min-width: 0;
	font-size: 0.9em;

	> span {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	> progress {
		width: 100%;
		height: 5px;
		accent-color: var(--accent);
	}
}

.frame {
	display: block;
	flex: 1;
	width: 100%;
	min-height: 0;
	border: 0;
	background: #fff;
}
</style>
