<template>
	<MkStickyContainer>
		<template #header>
			<MkPageHeader :actions="headerActions" :tabs="[]" />
		</template>
		<MkSpacer :content-max="980">
			<div class="memoriet">
				<section v-if="$i" class="composer">
					<div class="head">
						<i class="ph-sparkle ph-bold ph-lg"></i>
						<div>
							<h1>Memoriet</h1>
							<p>期限付きまたは無期限で公開できるストーリー投稿です。</p>
						</div>
					</div>
					<MkTextarea v-model="text">
						<template #label>本文</template>
					</MkTextarea>
					<div class="options">
						<label class="field">
							<span>公開範囲</span>
							<select v-model="visibility">
								<option value="home">ホーム</option>
								<option value="public">パブリック</option>
								<option value="followers">フォロワー</option>
							</select>
						</label>
						<label class="field">
							<span>公開期限</span>
							<select v-model="lifetime">
								<option value="3600000">1時間</option>
								<option value="21600000">6時間</option>
								<option value="86400000">24時間</option>
								<option value="604800000">7日</option>
								<option value="custom">日時を指定</option>
								<option value="forever">無期限</option>
							</select>
						</label>
						<label v-if="lifetime === 'custom'" class="field">
							<span>削除日時</span>
							<input v-model="customExpiresAt" type="datetime-local" />
						</label>
					</div>
					<div class="files">
						<div class="mediaActions">
							<MkButton @click="selectMedia">
								<i class="ph-images ph-bold"></i>
								メディアを追加
							</MkButton>
							<input ref="fileInput" type="file" multiple @change="onFilesSelected" />
						</div>
						<div v-if="files.length > 0" class="fileList">
							<div v-for="file in files" :key="file.id">
								<i class="ph-paperclip ph-bold"></i>
								<span>{{ file.name }}</span>
								<button class="_button" @click="removeFile(file.id)">
									<i class="ph-x ph-bold"></i>
								</button>
							</div>
						</div>
					</div>
					<div class="layerEditor">
						<div class="sectionHead">
							<h2>画像上のテキスト</h2>
							<button class="_button addLayer" @click="addTextLayer">
								<i class="ph-plus ph-bold"></i>
								追加
							</button>
						</div>
						<div class="storyPreview">
							<img v-if="previewImage" :src="previewImage.url" :alt="previewImage.name" />
							<div v-else class="previewEmpty">画像を添付するとここで配置を確認できます</div>
							<div
								v-for="layer in textLayers"
								:key="layer.id"
								class="storyText"
								:class="{ active: activeLayerId === layer.id }"
								contenteditable="true"
								spellcheck="false"
								:style="layerStyle(layer)"
								@input="updateLayerText(layer, $event)"
								@focus="activeLayerId = layer.id"
								@pointerdown.stop="startLayerDrag(layer, $event)"
							>
								{{ layer.text }}
							</div>
						</div>
						<div v-if="textLayers.length > 0" class="layerList">
							<div v-for="layer in textLayers" :key="layer.id" class="layerControl">
								<div class="layerTop">
									<input v-model="layer.text" class="layerTextInput" maxlength="200" @focus="activeLayerId = layer.id" />
									<button class="_button" @click="removeTextLayer(layer.id)">
										<i class="ph-trash ph-bold"></i>
									</button>
								</div>
								<div class="layerFields">
									<label>
										<span>文字色</span>
										<input v-model="layer.color" type="color" />
									</label>
									<label>
										<span>背景色</span>
										<input :value="layer.backgroundColor ?? '#000000'" :disabled="layer.backgroundColor == null" type="color" @input="setLayerBackground(layer, $event)" />
									</label>
									<label class="check">
										<input v-model="layer.transparentBackground" type="checkbox" />
										<span>背景透明</span>
									</label>
									<label>
										<span>文字サイズ</span>
										<input v-model.number="layer.fontSize" type="number" min="8" max="96" />
									</label>
									<label>
										<span>背景横幅(%)</span>
										<input :value="layer.backgroundWidth ?? ''" type="number" min="0" max="100" placeholder="自動" @input="setLayerBackgroundWidth(layer, $event)" />
									</label>
									<label>
										<span>背景縦幅(%)</span>
										<input :value="layer.backgroundHeight ?? ''" type="number" min="0" max="100" placeholder="自動" @input="setLayerBackgroundHeight(layer, $event)" />
									</label>
									<label>
										<span>横位置</span>
										<input v-model.number="layer.x" type="range" min="0" max="100" />
									</label>
									<label>
										<span>縦位置</span>
										<input v-model.number="layer.y" type="range" min="0" max="100" />
									</label>
									<label>
										<span>回転</span>
										<input v-model.number="layer.rotate" type="number" min="-180" max="180" />
									</label>
								</div>
							</div>
						</div>
					</div>
					<div class="actions">
						<MkButton primary :disabled="posting || !canPost" @click="post">
							<i class="ph-paper-plane-tilt ph-bold"></i>
							Memorietに投稿
						</MkButton>
					</div>
				</section>

				<section class="viewer">
					<div class="sectionHead">
						<h2>Memoriet</h2>
						<button class="_button refresh" :disabled="loading" @click="load">
							<i class="ph-arrow-clockwise ph-bold"></i>
						</button>
					</div>
					<MkLoading v-if="loading" />
					<div v-else-if="items.length === 0" class="_fullinfo">
						<div>表示できるMemorietはありません</div>
					</div>
					<div v-else class="items">
						<article v-for="item in items" :key="item.id" class="item">
							<div class="expires">
								<i class="ph-timer ph-bold"></i>
								<span>{{ item.expiresAt ? `${formatDate(item.expiresAt)}に削除` : "無期限公開" }}</span>
							</div>
							<div class="storyPreview published">
								<img v-if="itemImage(item)" :src="itemImage(item)?.url" :alt="itemImage(item)?.name" />
								<div v-else class="previewEmpty">画像なし</div>
								<div
									v-for="layer in item.textLayers"
									:key="layer.id"
									class="storyText"
									:style="layerStyle(layer)"
								>
									{{ layer.text }}
								</div>
							</div>
							<div v-if="item.viewerCount != null" class="footprints">
								<button class="_button footprintButton" @click="toggleViewers(item)">
									<i class="ph-footprints ph-bold"></i>
									足跡 {{ item.viewerCount }}
								</button>
								<div v-if="item.viewers" class="viewerList">
									<div v-if="item.viewers.length === 0">まだ閲覧者はいません</div>
									<template v-else>
										<div v-for="view in item.viewers" :key="`${item.id}-${view.user.id}`" class="viewerRow">
											<MkAvatar :user="view.user" class="avatar" />
											<span class="viewerName">{{ view.user.name || view.user.username }}</span>
											<span class="viewedAt">{{ formatDate(view.viewedAt) }}</span>
										</div>
									</template>
								</div>
							</div>
							<MkNote :note="item.note" />
						</article>
					</div>
				</section>

				<section v-if="$i" class="viewer">
					<div class="sectionHead">
						<h2>削除済みMemoriet</h2>
						<button class="_button refresh" :disabled="deletedLoading" @click="loadDeleted">
							<i class="ph-arrow-clockwise ph-bold"></i>
						</button>
					</div>
					<MkLoading v-if="deletedLoading" />
					<div v-else-if="deletedItems.length === 0" class="_fullinfo">
						<div>再投稿できる削除済みMemorietはありません</div>
					</div>
					<div v-else class="items">
						<article v-for="item in deletedItems" :key="item.id" class="item archived">
							<div class="expires">
								<i class="ph-trash ph-bold"></i>
								<span>{{ formatDate(item.deletedAt) }}に削除</span>
							</div>
							<div class="storyPreview published">
								<img v-if="archiveImage(item)" :src="archiveImage(item)?.url" :alt="archiveImage(item)?.name" />
								<div v-else class="previewEmpty">{{ item.text || "画像なし" }}</div>
								<div
									v-for="layer in item.textLayers"
									:key="layer.id"
									class="storyText"
									:style="layerStyle(layer)"
								>
									{{ layer.text }}
								</div>
							</div>
							<div class="archiveActions">
								<MkButton primary :disabled="repostingId === item.id" @click="repostArchive(item)">
									<i class="ph-repeat ph-bold"></i>
									再投稿
								</MkButton>
							</div>
						</article>
					</div>
				</section>
			</div>
		</MkSpacer>
	</MkStickyContainer>
</template>

<script lang="ts" setup>
import { computed, ref } from "vue";
import * as misskey from "iceshrimp-sdk";
import MkButton from "@/components/MkButton.vue";
import MkTextarea from "@/components/form/textarea.vue";
import MkNote from "@/components/MkNote.vue";
import MkAvatar from "@/components/global/MkAvatar.vue";
import * as os from "@/os";
import { $i } from "@/account";
import { definePageMetadata } from "@/scripts/page-metadata";
import { uploadFile } from "@/scripts/upload";
import { selectFiles } from "@/scripts/select-file";

type MemorietTextLayer = {
	id: string;
	text: string;
	color: string;
	backgroundColor: string | null;
	backgroundWidth: number | null;
	backgroundHeight: number | null;
	fontSize: number;
	x: number;
	y: number;
	rotate: number;
};

type EditableTextLayer = MemorietTextLayer & {
	transparentBackground: boolean;
};

type MemorietView = {
	viewedAt: string;
	user: misskey.entities.User;
};

type MemorietItem = {
	id: string;
	expiresAt: string | null;
	textLayers: MemorietTextLayer[];
	viewerCount: number | null;
	viewers?: MemorietView[];
	note: misskey.entities.Note;
};

type MemorietArchive = {
	id: string;
	createdAt: string;
	deletedAt: string;
	text: string | null;
	cw: string | null;
	fileIds: string[];
	files: misskey.entities.DriveFile[];
	textLayers: MemorietTextLayer[];
	visibility: "public" | "home" | "followers";
};

const text = ref("");
const visibility = ref<"public" | "home" | "followers">("home");
const lifetime = ref("86400000");
const customExpiresAt = ref("");
const files = ref<misskey.entities.DriveFile[]>([]);
const textLayers = ref<EditableTextLayer[]>([]);
const fileInput = ref<HTMLInputElement | null>(null);
const loading = ref(false);
const deletedLoading = ref(false);
const posting = ref(false);
const repostingId = ref<string | null>(null);
const items = ref<MemorietItem[]>([]);
const deletedItems = ref<MemorietArchive[]>([]);
const activeLayerId = ref<string | null>(null);

const canPost = computed(() => text.value.trim().length > 0 || files.value.length > 0);
const previewImage = computed(() => files.value.find((file) => file.type?.startsWith("image/")) ?? null);
const headerActions = computed(() => [
	{
		icon: "ph-list-bullets ph-bold ph-lg",
		text: "タイムライン",
		to: "/timeline/memoriet",
	},
	{
		icon: "ph-arrow-clockwise ph-bold ph-lg",
		text: "更新",
		handler: load,
	},
]);

function getExpiresAt(): number | null {
	if (lifetime.value === "forever") return null;
	if (lifetime.value === "custom") {
		const time = new Date(customExpiresAt.value).getTime();
		return Number.isFinite(time) ? time : null;
	}
	return Date.now() + Number(lifetime.value);
}

function createLayerId(): string {
	return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}

function addTextLayer() {
	textLayers.value.push({
		id: createLayerId(),
		text: "テキスト",
		color: "#ffffff",
		backgroundColor: "#000000",
		backgroundWidth: null,
		backgroundHeight: null,
		transparentBackground: false,
		fontSize: 28,
		x: 50,
		y: 50,
		rotate: 0,
	});
}

function removeTextLayer(id: string) {
	textLayers.value = textLayers.value.filter((layer) => layer.id !== id);
	if (activeLayerId.value === id) activeLayerId.value = null;
}

function updateLayerText(layer: EditableTextLayer, ev: Event) {
	layer.text = ((ev.currentTarget as HTMLElement).textContent ?? "").slice(0, 200);
}

function startLayerDrag(layer: EditableTextLayer, ev: PointerEvent) {
	activeLayerId.value = layer.id;
	const preview = (ev.currentTarget as HTMLElement).closest(".storyPreview") as HTMLElement | null;
	if (!preview) return;
	const target = ev.currentTarget as HTMLElement;
	target.setPointerCapture(ev.pointerId);
	const move = (moveEv: PointerEvent) => {
		const rect = preview.getBoundingClientRect();
		layer.x = Math.max(0, Math.min(100, ((moveEv.clientX - rect.left) / rect.width) * 100));
		layer.y = Math.max(0, Math.min(100, ((moveEv.clientY - rect.top) / rect.height) * 100));
	};
	const stop = () => {
		target.removeEventListener("pointermove", move);
		target.removeEventListener("pointerup", stop);
		target.removeEventListener("pointercancel", stop);
	};
	target.addEventListener("pointermove", move);
	target.addEventListener("pointerup", stop, { once: true });
	target.addEventListener("pointercancel", stop, { once: true });
	move(ev);
}

function setLayerBackground(layer: EditableTextLayer, ev: Event) {
	layer.backgroundColor = (ev.target as HTMLInputElement).value;
	layer.transparentBackground = false;
}

function getOptionalPercent(ev: Event): number | null {
	const value = (ev.target as HTMLInputElement).value;
	if (value === "") return null;
	const num = Number(value);
	if (!Number.isFinite(num) || num <= 0) return null;
	return Math.min(100, Math.max(0, num));
}

function setLayerBackgroundWidth(layer: EditableTextLayer, ev: Event) {
	layer.backgroundWidth = getOptionalPercent(ev);
}

function setLayerBackgroundHeight(layer: EditableTextLayer, ev: Event) {
	layer.backgroundHeight = getOptionalPercent(ev);
}

function serializeTextLayers(): MemorietTextLayer[] {
	return textLayers.value
		.filter((layer) => layer.text.trim().length > 0)
		.map((layer) => ({
			id: layer.id,
			text: layer.text.trim(),
			color: layer.color,
			backgroundColor: layer.transparentBackground ? null : layer.backgroundColor,
			backgroundWidth: layer.backgroundWidth,
			backgroundHeight: layer.backgroundHeight,
			fontSize: layer.fontSize,
			x: layer.x,
			y: layer.y,
			rotate: layer.rotate,
		}));
}

function layerStyle(layer: MemorietTextLayer | EditableTextLayer) {
	const backgroundColor = "transparentBackground" in layer && layer.transparentBackground ? null : layer.backgroundColor;
	return {
		color: layer.color,
		backgroundColor: backgroundColor ?? "transparent",
		fontSize: `${layer.fontSize}px`,
		left: `${layer.x}%`,
		top: `${layer.y}%`,
		transform: `translate(-50%, -50%) rotate(${layer.rotate}deg)`,
		padding: backgroundColor == null ? "0" : "0.25em 0.45em",
		width: layer.backgroundWidth != null && layer.backgroundWidth > 0 ? `${layer.backgroundWidth}%` : "auto",
		height: layer.backgroundHeight != null && layer.backgroundHeight > 0 ? `${layer.backgroundHeight}%` : "auto",
	};
}

function itemImage(item: MemorietItem): misskey.entities.DriveFile | null {
	return item.note.files?.find((file) => file.type?.startsWith("image/")) ?? null;
}

function archiveImage(item: MemorietArchive): misskey.entities.DriveFile | null {
	return item.files?.find((file) => file.type?.startsWith("image/")) ?? null;
}

async function onFilesSelected(ev: Event) {
	const selected = Array.from((ev.target as HTMLInputElement).files ?? []);
	if (selected.length === 0) return;
	try {
		const uploaded = [];
		for (const file of selected) {
			uploaded.push(await uploadFile(file, undefined, file.name, true));
		}
		appendFiles(uploaded);
	} finally {
		if (fileInput.value) fileInput.value.value = "";
	}
}

function appendFiles(selected: misskey.entities.DriveFile[]) {
	const known = new Set(files.value.map((file) => file.id));
	const next = selected.filter((file) => !known.has(file.id));
	files.value = [...files.value, ...next].slice(0, 16);
}

async function selectMedia(ev: MouseEvent) {
	const selected = await selectFiles(ev.currentTarget ?? ev.target, "Memorietに追加するメディア");
	appendFiles(selected);
}

function removeFile(id: string) {
	files.value = files.value.filter((file) => file.id !== id);
}

async function post() {
	if (!$i || !canPost.value) return;
	const expiresAt = getExpiresAt();
	if (lifetime.value === "custom" && (expiresAt == null || expiresAt <= Date.now())) {
		await os.alert({ type: "error", title: "削除日時が不正です", text: "現在より後の日時を指定してください。" });
		return;
	}

	posting.value = true;
	try {
		await (os.api as any)("memoriet/create", {
			text: text.value.trim() || null,
			fileIds: files.value.map((file) => file.id),
			textLayers: serializeTextLayers(),
			visibility: visibility.value,
			expiresAt,
		});
		text.value = "";
		files.value = [];
		textLayers.value = [];
		await load();
	} finally {
		posting.value = false;
	}
}

async function load() {
	loading.value = true;
	try {
		items.value = (await (os.api as any)("memoriet/list", { limit: 50 })) as MemorietItem[];
	} finally {
		loading.value = false;
	}
}

async function loadDeleted() {
	if (!$i) return;
	deletedLoading.value = true;
	try {
		deletedItems.value = (await (os.api as any)("memoriet/deleted-list", { limit: 50 })) as MemorietArchive[];
	} finally {
		deletedLoading.value = false;
	}
}

async function repostArchive(item: MemorietArchive) {
	repostingId.value = item.id;
	try {
		await (os.api as any)("memoriet/repost", {
			archiveId: item.id,
			visibility: item.visibility,
			expiresAt: getExpiresAt(),
		});
		await load();
	} finally {
		repostingId.value = null;
	}
}

async function toggleViewers(item: MemorietItem) {
	if (item.viewers) {
		item.viewers = undefined;
		return;
	}
	item.viewers = (await (os.api as any)("memoriet/viewers", { memorietId: item.id, limit: 100 })) as MemorietView[];
}

function formatDate(value: string): string {
	return new Date(value).toLocaleString();
}

load();
loadDeleted();

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

<style lang="scss" scoped>
.memoriet {
	display: grid;
	gap: 20px;
}

.composer,
.viewer {
	display: grid;
	gap: 16px;
	padding: 16px;
	border-radius: 8px;
	background: var(--panel);
	border: solid 1px var(--divider);
}

.head {
	display: flex;
	gap: 14px;
	align-items: center;
}

.head > i {
	display: grid;
	place-items: center;
	width: 48px;
	height: 48px;
	border-radius: 8px;
	color: var(--accent);
	background: var(--accentedBg);
}

.head h1,
.sectionHead h2 {
	margin: 0;
}

.head p {
	margin: 4px 0 0;
	color: var(--fgTransparentWeak);
}

.options {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

.field {
	display: grid;
	gap: 6px;
	min-width: 180px;
}

.field span {
	font-size: 0.85em;
	color: var(--fgTransparentWeak);
}

.field select,
.field input {
	min-height: 40px;
	border-radius: 8px;
	padding: 0 10px;
	background: var(--bg);
	color: var(--fg);
	border: solid 1px var(--divider);
}

.fileList {
	display: grid;
	gap: 8px;
	margin-top: 10px;
}

.mediaActions {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 10px;
}

.mediaActions input {
	max-width: 100%;
}

.fileList > div {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 10px;
	border-radius: 8px;
	background: var(--bg);
}

.fileList span {
	flex: 1;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

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

.addLayer,
.footprintButton {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	min-height: 36px;
	padding: 0 12px;
	border-radius: 8px;
	background: var(--buttonBg);
}

.storyPreview {
	position: relative;
	overflow: hidden;
	aspect-ratio: 9 / 16;
	max-height: 560px;
	width: min(100%, 360px);
	border-radius: 8px;
	background: var(--bg);
	border: solid 1px var(--divider);
}

.storyPreview.published {
	width: min(100%, 420px);
}

.storyPreview img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.previewEmpty {
	display: grid;
	place-items: center;
	width: 100%;
	height: 100%;
	padding: 16px;
	text-align: center;
	color: var(--fgTransparentWeak);
}

.storyText {
	position: absolute;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	max-width: 92%;
	overflow-wrap: anywhere;
	text-align: center;
	font-weight: 700;
	line-height: 1.2;
	white-space: pre-wrap;
	text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
	border-radius: 4px;
	pointer-events: auto;
	cursor: move;
	user-select: text;
	outline: none;
}

.storyText.active,
.storyText:focus {
	box-shadow: 0 0 0 2px var(--accent);
}

.published .storyText {
	pointer-events: none;
	cursor: default;
}

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

.layerControl {
	display: grid;
	gap: 10px;
	padding: 12px;
	border-radius: 8px;
	background: var(--bg);
	border: solid 1px var(--divider);
}

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

.layerTextInput {
	flex: 1;
	min-width: 0;
	min-height: 38px;
	border-radius: 8px;
	padding: 0 10px;
	background: var(--panel);
	color: var(--fg);
	border: solid 1px var(--divider);
}

.layerFields {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
	gap: 10px;
}

.layerFields label {
	display: grid;
	gap: 6px;
	min-width: 0;
}

.layerFields label > span {
	color: var(--fgTransparentWeak);
	font-size: 0.85em;
}

.layerFields input[type="number"] {
	min-height: 36px;
	border-radius: 8px;
	padding: 0 10px;
	background: var(--panel);
	color: var(--fg);
	border: solid 1px var(--divider);
}

.layerFields input[type="color"] {
	width: 100%;
	min-height: 36px;
	border: 0;
	border-radius: 8px;
	background: var(--panel);
}

.layerFields .check {
	display: flex;
	align-items: center;
	gap: 8px;
	padding-top: 20px;
}

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

.refresh {
	width: 36px;
	height: 36px;
	border-radius: 8px;
	background: var(--buttonBg);
}

.items {
	display: grid;
	gap: 14px;
}

.item {
	display: grid;
	gap: 8px;
}

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

.archiveActions {
	display: flex;
	gap: 8px;
}

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

.footprints {
	display: grid;
	gap: 8px;
}

.viewerList {
	display: grid;
	gap: 8px;
	padding: 10px;
	border-radius: 8px;
	background: var(--bg);
	border: solid 1px var(--divider);
}

.viewerRow {
	display: grid;
	grid-template-columns: 32px minmax(0, 1fr) auto;
	align-items: center;
	gap: 8px;
}

.avatar {
	width: 32px;
	height: 32px;
}

.viewerName {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.viewedAt {
	color: var(--fgTransparentWeak);
	font-size: 0.85em;
}

@media (max-width: 500px) {
	.viewerRow {
		grid-template-columns: 32px minmax(0, 1fr);
	}

	.viewedAt {
		grid-column: 2;
	}
}
</style>
