<template>
	<MkStickyContainer>
		<template #header>
			<MkPageHeader :actions="headerActions" :tabs="[]" :display-back-button="true" />
		</template>
		<MkSpacer :content-max="980">
			<div class="media-service-page">
				<XAdServicePreroll v-if="!adDone" :service="kind" :tag="searchQuery" @done="adDone = true" />
				<section v-else class="viewer">
					<div class="viewerHead">
						<MkA :to="`/${kind}-service`" class="serviceHome">
							<i :class="serviceIcon"></i>
							<span>{{ title }}</span>
						</MkA>
					</div>
					<MkLoading v-if="loading" />
					<MkError v-else-if="error" @retry="fetchNote" />
					<template v-else-if="note && mediaFiles.length > 0">
						<img
							v-if="thumbnailImage && kind === 'audio'"
							class="mediaThumbnail"
							:src="thumbnailImage.url"
							alt=""
						/>
						<div v-if="kind === 'video'" class="mediaStack">
							<div v-for="item in mediaFiles" :key="item.id" class="playerItem">
								<video
									:ref="(el) => setMediaElement(item.id, el)"
									class="media"
									controls
									playsinline
									:src="item.url"
									:poster="thumbnailImage?.url ?? item.thumbnailUrl"
									:controlsList="item.allowDownload ? undefined : 'nodownload'"
									@loadedmetadata="applyPlaybackRate(item.id)"
									@play="onMediaPlay(item.id)"
									@pause="setPaused(item.id, true)"
									@ratechange="syncPlaybackRate(item.id)"
									@contextmenu="onMediaContextmenu($event, item)"
								></video>
								<div class="playerControls">
									<button class="_button transportButton" @click="seekMedia(item.id, -5)">
										<i class="ph-arrow-counter-clockwise ph-bold"></i>
										<span>5s</span>
									</button>
									<button class="_button playButton" @click="toggleMedia(item.id)">
										<i :class="isPaused(item.id) ? 'ph-play ph-bold' : 'ph-pause ph-bold'"></i>
										<span>{{ isPaused(item.id) ? "再生" : "一時停止" }}</span>
									</button>
									<button class="_button transportButton" @click="seekMedia(item.id, 5)">
										<span>5s</span>
										<i class="ph-arrow-clockwise ph-bold"></i>
									</button>
									<div class="speedControls">
										<button
											v-for="rate in playbackRatePresets"
											:key="`${item.id}-${rate}`"
											class="_button speedButton"
											:class="{ active: Math.abs(getPlaybackRate(item.id) - rate) < 0.001 }"
											@click="setPlaybackRate(item.id, rate)"
										>
											{{ rate }}x
										</button>
										<label class="speedInput">
											<span>速度</span>
											<input
												:value="getPlaybackRate(item.id)"
												type="number"
												min="0.25"
												max="4"
												step="0.05"
												@change="setPlaybackRateFromInput(item.id, $event)"
											/>
										</label>
									</div>
								</div>
							</div>
						</div>
						<div v-else-if="kind === 'audio'" class="audioList">
							<div v-for="item in mediaFiles" :key="item.id" class="playerItem audioItem">
								<audio
									:ref="(el) => setMediaElement(item.id, el)"
									class="audio"
									controls
									:src="item.url"
									:controlsList="item.allowDownload ? undefined : 'nodownload'"
									@loadedmetadata="applyPlaybackRate(item.id)"
									@play="onMediaPlay(item.id)"
									@pause="setPaused(item.id, true)"
									@ratechange="syncPlaybackRate(item.id)"
									@contextmenu="onMediaContextmenu($event, item)"
								></audio>
								<div class="playerControls">
									<button class="_button transportButton" @click="seekMedia(item.id, -5)">
										<i class="ph-arrow-counter-clockwise ph-bold"></i>
										<span>5s</span>
									</button>
									<button class="_button playButton" @click="toggleMedia(item.id)">
										<i :class="isPaused(item.id) ? 'ph-play ph-bold' : 'ph-pause ph-bold'"></i>
										<span>{{ isPaused(item.id) ? "再生" : "一時停止" }}</span>
									</button>
									<button class="_button transportButton" @click="seekMedia(item.id, 5)">
										<span>5s</span>
										<i class="ph-arrow-clockwise ph-bold"></i>
									</button>
									<div class="speedControls">
										<button
											v-for="rate in playbackRatePresets"
											:key="`${item.id}-${rate}`"
											class="_button speedButton"
											:class="{ active: Math.abs(getPlaybackRate(item.id) - rate) < 0.001 }"
											@click="setPlaybackRate(item.id, rate)"
										>
											{{ rate }}x
										</button>
										<label class="speedInput">
											<span>速度</span>
											<input
												:value="getPlaybackRate(item.id)"
												type="number"
												min="0.25"
												max="4"
												step="0.05"
												@change="setPlaybackRateFromInput(item.id, $event)"
											/>
										</label>
									</div>
								</div>
							</div>
						</div>
						<div v-else class="imageGallery">
							<img
								v-for="item in mediaFiles"
								:key="item.id"
								class="media imageItem"
								:src="item.url"
								alt=""
							/>
						</div>
						<div class="meta">
							<h1 class="mediaTitle">{{ noteTitle }}</h1>
							<Mfm
								v-if="noteDescription"
								class="mediaDescription"
								:text="noteDescription"
								:author="note.user"
							/>
							<div class="sub">
								<MkA :to="`/notes/${note.id}`">Open note</MkA>
								<span v-if="note.viewCount != null">
									<i class="ph-eye ph-bold"></i>
									{{ note.viewCount }}
								</span>
							</div>
						</div>
						<div class="rating">
							<button
								class="_button ratingButton"
								:class="{ active: note.myReaction === '👍' }"
								:disabled="!$i || ratingBusy"
								@click="rate('👍')"
							>
								<span>👍</span>
								<b>{{ thumbsUp }}</b>
							</button>
							<button
								class="_button ratingButton"
								:class="{ active: note.myReaction === '👎' }"
								:disabled="!$i || ratingBusy"
								@click="rate('👎')"
							>
								<span>👎</span>
								<b>{{ thumbsDown }}</b>
							</button>
							<button
								v-if="note.myReaction"
								class="_button ratingButton removeReaction"
								:disabled="!$i || ratingBusy"
								@click="removeReaction"
							>
								<span>{{ note.myReaction }}</span>
								<span>Remove</span>
							</button>
						</div>
					</template>
					<MkError v-else @retry="fetchNote" />
				</section>

				<section class="search">
					<div class="sectionHead">
						<h2>More from {{ title }}</h2>
						<MkA :to="`/${kind}-service`" class="topLink">Top</MkA>
					</div>
					<form class="searchForm" @submit.prevent="runSearch">
						<MkInput v-model="searchQuery">
							<template #prefix><i class="ph-magnifying-glass ph-bold"></i></template>
							<template #label>Search</template>
						</MkInput>
						<MkButton primary type="submit">Search</MkButton>
					</form>
					<div v-if="searching" class="searchState"><MkLoading /></div>
					<div v-else-if="results.length === 0" class="_fullinfo">
						<div>No results</div>
					</div>
					<div v-else class="results">
						<MkA
							v-for="item in results"
							:key="item.id"
							class="result"
							:to="`/${kind}-service/${item.id}`"
						>
							<img
								v-if="getThumbnailUrl(item)"
								:src="getThumbnailUrl(item)"
								alt=""
							/>
							<div>
								<div class="resultTitle">{{ getNoteTitle(item) }}</div>
								<div v-if="getNoteDescription(item)" class="resultText">
									{{ getNoteDescription(item) }}
								</div>
								<div class="resultMeta">
									<span>@{{ item.user.username }}</span>
									<span v-if="item.viewCount != null">
										<i class="ph-eye ph-bold"></i>
										{{ item.viewCount }}
									</span>
									<span>👍 {{ item.reactions?.["👍"] ?? 0 }}</span>
									<span>👎 {{ item.reactions?.["👎"] ?? 0 }}</span>
								</div>
							</div>
						</MkA>
					</div>
				</section>
			</div>
		</MkSpacer>
	</MkStickyContainer>
</template>

<script lang="ts" setup>
import { computed, reactive, ref, watch } from "vue";
import * as misskey from "iceshrimp-sdk";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue";
import XAdServicePreroll from "@/components/AdServicePreroll.vue";
import * as os from "@/os";
import { $i } from "@/account";
import { definePageMetadata } from "@/scripts/page-metadata";
import { useRouter } from "@/router.js";

const props = defineProps<{
	kind: "video" | "audio" | "image";
	noteId: string;
}>();

const note = ref<misskey.entities.Note | null>(null);
const loading = ref(false);
const error = ref(false);
const ratingBusy = ref(false);
const searchQuery = ref("");
const searching = ref(false);
const results = ref<misskey.entities.Note[]>([]);
const adDone = ref(false);
const router = useRouter();
let playbackCountRequestedAt = 0;
const mediaElements = new Map<string, HTMLMediaElement>();
const playbackState = reactive<Record<string, { rate: number; paused: boolean }>>({});
const playbackRatePresets = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];

const title = computed(() => `${props.kind[0].toUpperCase()}${props.kind.slice(1)}Service`);
const serviceIcon = computed(() =>
	props.kind === "video"
		? "ph-video ph-bold ph-lg"
		: props.kind === "audio"
		? "ph-music-notes ph-bold ph-lg"
		: "ph-image ph-bold ph-lg",
);
const mediaFiles = computed(() => (note.value ? getPrimaryFiles(note.value) : []));
const thumbnailImage = computed(() =>
	note.value && (props.kind === "video" || props.kind === "audio")
		? getThumbnailImage(note.value)
		: null,
);
const noteTitle = computed(() => (note.value ? getNoteTitle(note.value) : ""));
const noteDescription = computed(() =>
	note.value ? getNoteDescription(note.value) : "",
);
const thumbsUp = computed(() => note.value?.reactions?.["👍"] ?? 0);
const thumbsDown = computed(() => note.value?.reactions?.["👎"] ?? 0);
const headerActions = computed(() => [
	{
		icon: "ph-note ph-bold ph-lg",
		text: "Open note",
		handler: () => {
			if (note.value) router.push(`/notes/${note.value.id}`);
		},
	},
]);

function matchesService(target: misskey.entities.Note): boolean {
	if (!target.files || target.files.length < 1) return false;
	if (!["public", "home", "followers"].includes(target.visibility)) return false;
	const tags = (target.tags ?? []).map((tag) => tag.toLowerCase());
	return (
		tags.includes(`${props.kind}service`) &&
		hasValidMediaFiles(target.files, props.kind)
	);
}

function hasValidMediaFiles(
	files: misskey.entities.DriveFile[],
	kind: "video" | "audio" | "image",
): boolean {
	const mediaFiles = files.filter((target) => target.type.startsWith(`${kind}/`));
	if (kind === "image") {
		return mediaFiles.length >= 1 && files.every((target) => target.type.startsWith("image/"));
	}
	return (
		mediaFiles.length >= 1 &&
		files.every(
			(target) =>
				target.type.startsWith(`${kind}/`) || target.type.startsWith("image/"),
		)
	);
}

function getPrimaryFiles(target: misskey.entities.Note) {
	return target.files?.filter((item) => item.type.startsWith(`${props.kind}/`)) ?? [];
}

function getThumbnailImage(target: misskey.entities.Note) {
	return target.files?.find((item) => item.type.startsWith("image/")) ?? null;
}

function getThumbnailUrl(target: misskey.entities.Note): string | null {
	const thumbnail = props.kind === "video" || props.kind === "audio"
		? getThumbnailImage(target)
		: null;
	const primary = getPrimaryFiles(target)[0];
	return thumbnail?.thumbnailUrl ?? thumbnail?.url ?? primary?.thumbnailUrl ?? primary?.url ?? null;
}

function onMediaContextmenu(ev: MouseEvent, item: misskey.entities.DriveFile) {
	if (!item.allowDownload) ev.preventDefault();
}

function ensurePlaybackState(fileId: string) {
	if (!playbackState[fileId]) playbackState[fileId] = { rate: 1, paused: true };
	return playbackState[fileId];
}

function setMediaElement(fileId: string, el: Element | null) {
	if (el instanceof HTMLMediaElement) {
		mediaElements.set(fileId, el);
		ensurePlaybackState(fileId).paused = el.paused;
		applyPlaybackRate(fileId);
	} else {
		mediaElements.delete(fileId);
	}
}

function getPlaybackRate(fileId: string): number {
	return ensurePlaybackState(fileId).rate;
}

function isPaused(fileId: string): boolean {
	return ensurePlaybackState(fileId).paused;
}

function applyPlaybackRate(fileId: string) {
	const el = mediaElements.get(fileId);
	if (!el) return;
	el.playbackRate = getPlaybackRate(fileId);
}

function setPlaybackRate(fileId: string, rate: number) {
	const normalized = Math.max(0.25, Math.min(4, Number.isFinite(rate) ? rate : 1));
	ensurePlaybackState(fileId).rate = Math.round(normalized * 100) / 100;
	applyPlaybackRate(fileId);
}

function setPlaybackRateFromInput(fileId: string, ev: Event) {
	const input = ev.currentTarget as HTMLInputElement;
	setPlaybackRate(fileId, Number(input.value));
	input.value = String(getPlaybackRate(fileId));
}

function syncPlaybackRate(fileId: string) {
	const el = mediaElements.get(fileId);
	if (!el) return;
	ensurePlaybackState(fileId).rate = Math.round(el.playbackRate * 100) / 100;
}

function seekMedia(fileId: string, seconds: number) {
	const el = mediaElements.get(fileId);
	if (!el) return;
	const duration = Number.isFinite(el.duration) ? el.duration : Number.POSITIVE_INFINITY;
	el.currentTime = Math.max(0, Math.min(duration, el.currentTime + seconds));
}

async function toggleMedia(fileId: string) {
	const el = mediaElements.get(fileId);
	if (!el) return;
	if (el.paused) {
		await el.play().catch(() => undefined);
	} else {
		el.pause();
	}
	ensurePlaybackState(fileId).paused = el.paused;
}

function setPaused(fileId: string, paused: boolean) {
	ensurePlaybackState(fileId).paused = paused;
}

function onMediaPlay(fileId: string) {
	setPaused(fileId, false);
	countPlayback();
}

function countPlayback() {
	if (!note.value || (props.kind !== "video" && props.kind !== "audio")) return;
	const now = Date.now();
	if (now - playbackCountRequestedAt < 1000) return;
	playbackCountRequestedAt = now;
	os.api("notes/increment-service-view" as any, {
		noteId: note.value.id,
		service: props.kind,
	}).then((res: any) => {
		if (res?.counted && note.value) note.value.viewCount = (note.value.viewCount ?? 0) + 1;
	}).catch(() => undefined);
}

function splitNoteText(target: misskey.entities.Note): string[] {
	return (target.text ?? "").replace(/\r\n/g, "\n").split("\n");
}

function getNoteTitle(target: misskey.entities.Note): string {
	return splitNoteText(target)[0]?.trim() || "(no title)";
}

function getNoteDescription(target: misskey.entities.Note): string {
	return splitNoteText(target).slice(1).join("\n").trim();
}

async function fetchNote() {
	loading.value = true;
	error.value = false;
	try {
		const res = await os.api("notes/show", { noteId: props.noteId });
		if (!matchesService(res)) throw new Error("Invalid media service note");
		note.value = res;
	} catch {
		error.value = true;
		note.value = null;
	} finally {
		loading.value = false;
	}
}

async function rate(reaction: "👍" | "👎") {
	if (!$i || !note.value || ratingBusy.value) return;
	ratingBusy.value = true;
	try {
		const oldReaction = note.value.myReaction;
		if (oldReaction) {
			await os.api("notes/reactions/delete", { noteId: note.value.id });
		}
		if (oldReaction !== reaction) {
			await os.api("notes/reactions/create", {
				noteId: note.value.id,
				reaction,
			});
		}
		await fetchNote();
	} finally {
		ratingBusy.value = false;
	}
}

async function removeReaction() {
	if (!$i || !note.value || !note.value.myReaction || ratingBusy.value) return;
	ratingBusy.value = true;
	try {
		await os.api("notes/reactions/delete", { noteId: note.value.id });
		await fetchNote();
	} finally {
		ratingBusy.value = false;
	}
}

async function runSearch() {
	searching.value = true;
	try {
		results.value = (await (os.api as any)("notes/media-service-search", {
			kind: props.kind,
			query: searchQuery.value,
			limit: 30,
		})) as misskey.entities.Note[];
	} finally {
		searching.value = false;
	}
}

watch(
	() => [props.kind, props.noteId],
	() => {
		adDone.value = false;
		playbackCountRequestedAt = 0;
		mediaElements.clear();
		fetchNote();
		runSearch();
	},
	{ immediate: true },
);

definePageMetadata(
	computed(() => ({
		title: title.value,
		icon:
			props.kind === "video"
				? "ph-video ph-bold ph-lg"
				: props.kind === "audio"
				? "ph-music-notes ph-bold ph-lg"
				: "ph-image ph-bold ph-lg",
	})),
);
</script>

<style lang="scss" scoped>
.media-service-page {
	display: grid;
	gap: 24px;
}

.viewer {
	background: var(--panel);
	border-radius: 8px;
	border: solid 1px var(--divider);
	overflow: hidden;
}

.viewerHead {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 12px 16px;
	border-bottom: solid 1px var(--divider);
}

.serviceHome {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	color: var(--accent);
	font-weight: 700;
}

.media {
	display: block;
	width: 100%;
	max-height: 72vh;
	object-fit: contain;
	background: var(--acrylicBg);
}

.mediaStack,
.audioList,
.imageGallery {
	display: grid;
	gap: 12px;
	padding: 12px;
}

.imageGallery {
	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
	align-items: start;
}

.imageItem {
	max-height: 62vh;
	border-radius: 8px;
}

.mediaThumbnail {
	display: block;
	width: min(100%, 720px);
	max-height: 44vh;
	margin: 18px auto 0;
	object-fit: contain;
	border-radius: 8px;
	background: var(--acrylicBg);
}

.audio {
	display: block;
	width: 100%;
}

.playerItem {
	display: grid;
	gap: 10px;
}

.audioItem {
	padding: 12px;
	border-radius: 8px;
	background: var(--bg);
	border: solid 1px var(--divider);
}

.playerControls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
}

.transportButton,
.playButton,
.speedButton {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 6px;
	min-height: 36px;
	border-radius: 8px;
	background: var(--buttonBg);
	color: var(--fg);
}

.transportButton {
	min-width: 68px;
	padding: 0 10px;
}

.playButton {
	min-width: 112px;
	padding: 0 12px;
	color: var(--fgOnAccent);
	background: var(--accent);
	font-weight: 700;
}

.speedControls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
	padding: 6px;
	border-radius: 8px;
	background: var(--bg);
	border: solid 1px var(--divider);
}

.audioItem .speedControls {
	background: var(--panel);
}

.speedButton {
	min-width: 46px;
	padding: 0 8px;
	font-size: 0.9em;
}

.speedButton.active {
	color: var(--fgOnAccent);
	background: var(--accent);
}

.speedInput {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	margin-left: 4px;
	color: var(--fgTransparentWeak);
	font-size: 0.9em;
}

.speedInput input {
	width: 76px;
	min-height: 32px;
	padding: 0 8px;
	border-radius: 8px;
	color: var(--fg);
	background: var(--panel);
	border: solid 1px var(--divider);
}

.audioItem .speedInput input {
	background: var(--bg);
}

.meta {
	padding: 16px;
	border-top: solid 1px var(--divider);
}

.mediaTitle {
	margin: 0;
	font-size: 1.25rem;
	line-height: 1.35;
}

.mediaDescription {
	display: block;
	margin-top: 10px;
	color: var(--fg);
}

.sub,
.resultMeta {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
	margin-top: 8px;
	color: var(--fgTransparentWeak);
	font-size: 0.9em;
}

.rating {
	display: flex;
	gap: 8px;
	padding: 0 16px 16px;
}

.ratingButton {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	padding: 8px 12px;
	border-radius: 8px;
	background: var(--buttonBg);
}

.ratingButton.active {
	color: var(--accent);
	background: var(--accentedBg);
}

.removeReaction {
	margin-left: auto;
	color: var(--fgTransparentWeak);
}

.searchForm {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: 12px;
	align-items: end;
}

.search {
	padding: 16px;
	border-radius: 8px;
	background: var(--panel);
	border: solid 1px var(--divider);
}

.sectionHead {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 12px;
}

.sectionHead h2 {
	margin: 0;
	font-size: 1rem;
}

.topLink {
	color: var(--accent);
}

.searchState {
	padding: 24px;
}

.results {
	display: grid;
	gap: 8px;
	margin-top: 16px;
}

.result {
	display: grid;
	grid-template-columns: 112px 1fr;
	gap: 12px;
	padding: 12px;
	border-radius: 8px;
	background: var(--bg);
	border: solid 1px var(--divider);
	color: var(--fg);
}

.result > img {
	width: 112px;
	height: 64px;
	object-fit: cover;
	border-radius: 6px;
	background: var(--acrylicBg);
}

.resultTitle {
	font-weight: 700;
	overflow: hidden;
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
}

.resultText {
	margin-top: 4px;
	color: var(--fgTransparentWeak);
	overflow: hidden;
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
}

@media (max-width: 600px) {
	.searchForm {
		grid-template-columns: 1fr;
	}

	.result {
		grid-template-columns: 84px 1fr;
	}

	.result > img {
		width: 84px;
		height: 56px;
	}
}
</style>
