<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<MkModalWindow
	ref="dialog"
	:width="1000"
	:height="600"
	:scroll="false"
	:withOkButton="true"
	@close="cancel()"
	@ok="save()"
	@closed="emit('closed')"
>
	<template #header><i class="ti ti-device-ipad-horizontal"></i> {{ i18n.ts._imageFrameEditor.title }}</template>

	<div :class="$style.root">
		<div :class="$style.container">
			<div :class="[$style.preview, prefer.s.animation ? $style.animatedBg : null]">
				<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
				<div :class="$style.previewContainer">
					<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
					<div v-if="props.image == null" class="_acrylic" :class="$style.previewControls">
						<button class="_button" :class="[$style.previewControlsButton, sampleImageType === '3_2' ? $style.active : null]" @click="sampleImageType = '3_2'"><i class="ti ti-crop-landscape"></i></button>
						<button class="_button" :class="[$style.previewControlsButton, sampleImageType === '2_3' ? $style.active : null]" @click="sampleImageType = '2_3'"><i class="ti ti-crop-portrait"></i></button>
						<button class="_button" :class="[$style.previewControlsButton]" @click="choiceImage"><i class="ti ti-upload"></i></button>
					</div>
				</div>
			</div>
			<div :class="$style.controls">
				<div class="_spacer _gaps">
					<MkRange v-model="params.borderThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true">
						<template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
					</MkRange>

					<MkInput :modelValue="getHex(params.bgColor)" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) params.bgColor = c; }">
						<template #label>{{ i18n.ts._imageFrameEditor.backgroundColor }}</template>
					</MkInput>

					<MkInput :modelValue="getHex(params.fgColor)" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) params.fgColor = c; }">
						<template #label>{{ i18n.ts._imageFrameEditor.textColor }}</template>
					</MkInput>

					<MkSelect
						v-model="params.font" :items="[
							{ label: i18n.ts._imageFrameEditor.fontSansSerif, value: 'sans-serif' },
							{ label: i18n.ts._imageFrameEditor.fontSerif, value: 'serif' },
						]"
					>
						<template #label>{{ i18n.ts._imageFrameEditor.font }}</template>
					</MkSelect>

					<MkFolder :defaultOpen="params.labelTop.enabled">
						<template #label>{{ i18n.ts._imageFrameEditor.header }}</template>

						<div class="_gaps">
							<MkSwitch v-model="params.labelTop.enabled">
								<template #label>{{ i18n.ts.show }}</template>
							</MkSwitch>

							<MkRange v-model="params.labelTop.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
								<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
							</MkRange>

							<MkRange v-model="params.labelTop.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
								<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
							</MkRange>

							<MkSwitch v-model="params.labelTop.centered">
								<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
							</MkSwitch>

							<MkInput v-model="params.labelTop.textBig">
								<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
							</MkInput>

							<MkTextarea v-model="params.labelTop.textSmall">
								<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
							</MkTextarea>

							<MkSwitch v-model="params.labelTop.withQrCode">
								<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
							</MkSwitch>
						</div>
					</MkFolder>

					<MkFolder :defaultOpen="params.labelBottom.enabled">
						<template #label>{{ i18n.ts._imageFrameEditor.footer }}</template>

						<div class="_gaps">
							<MkSwitch v-model="params.labelBottom.enabled">
								<template #label>{{ i18n.ts.show }}</template>
							</MkSwitch>

							<MkRange v-model="params.labelBottom.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
								<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
							</MkRange>

							<MkRange v-model="params.labelBottom.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
								<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
							</MkRange>

							<MkSwitch v-model="params.labelBottom.centered">
								<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
							</MkSwitch>

							<MkInput v-model="params.labelBottom.textBig">
								<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
							</MkInput>

							<MkTextarea v-model="params.labelBottom.textSmall">
								<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
							</MkTextarea>

							<MkSwitch v-model="params.labelBottom.withQrCode">
								<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
							</MkSwitch>
						</div>
					</MkFolder>

					<MkInfo>
						<div>{{ i18n.ts._imageFrameEditor.availableVariables }}:</div>
						<div><code class="_selectableAtomic">{filename}</code> - {{ i18n.ts._imageEditing._vars.filename }}</div>
						<div><code class="_selectableAtomic">{filename_without_ext}</code> - {{ i18n.ts._imageEditing._vars.filename_without_ext }}</div>
						<div><code class="_selectableAtomic">{caption}</code> - {{ i18n.ts._imageEditing._vars.caption }}</div>
						<div><code class="_selectableAtomic">{year}</code> - {{ i18n.ts._imageEditing._vars.year }}</div>
						<div><code class="_selectableAtomic">{month}</code> - {{ i18n.ts._imageEditing._vars.month }}</div>
						<div><code class="_selectableAtomic">{day}</code> - {{ i18n.ts._imageEditing._vars.day }}</div>
						<div><code class="_selectableAtomic">{hour}</code> - {{ i18n.ts._imageEditing._vars.hour }}</div>
						<div><code class="_selectableAtomic">{minute}</code> - {{ i18n.ts._imageEditing._vars.minute }}</div>
						<div><code class="_selectableAtomic">{second}</code> - {{ i18n.ts._imageEditing._vars.second }}</div>
						<div><code class="_selectableAtomic">{0month}</code> - {{ i18n.ts._imageEditing._vars.month }} ({{ i18n.ts.zeroPadding }})</div>
						<div><code class="_selectableAtomic">{0day}</code> - {{ i18n.ts._imageEditing._vars.day }} ({{ i18n.ts.zeroPadding }})</div>
						<div><code class="_selectableAtomic">{0hour}</code> - {{ i18n.ts._imageEditing._vars.hour }} ({{ i18n.ts.zeroPadding }})</div>
						<div><code class="_selectableAtomic">{0minute}</code> - {{ i18n.ts._imageEditing._vars.minute }} ({{ i18n.ts.zeroPadding }})</div>
						<div><code class="_selectableAtomic">{0second}</code> - {{ i18n.ts._imageEditing._vars.second }} ({{ i18n.ts.zeroPadding }})</div>
						<div><code class="_selectableAtomic">{camera_model}</code> - {{ i18n.ts._imageEditing._vars.camera_model }}</div>
						<div><code class="_selectableAtomic">{camera_lens_model}</code> - {{ i18n.ts._imageEditing._vars.camera_lens_model }}</div>
						<div><code class="_selectableAtomic">{camera_mm}</code> - {{ i18n.ts._imageEditing._vars.camera_mm }}</div>
						<div><code class="_selectableAtomic">{camera_mm_35}</code> - {{ i18n.ts._imageEditing._vars.camera_mm_35 }}</div>
						<div><code class="_selectableAtomic">{camera_f}</code> - {{ i18n.ts._imageEditing._vars.camera_f }}</div>
						<div><code class="_selectableAtomic">{camera_s}</code> - {{ i18n.ts._imageEditing._vars.camera_s }}</div>
						<div><code class="_selectableAtomic">{camera_iso}</code> - {{ i18n.ts._imageEditing._vars.camera_iso }}</div>
						<div><code class="_selectableAtomic">{gps_lat}</code> - {{ i18n.ts._imageEditing._vars.gps_lat }}</div>
						<div><code class="_selectableAtomic">{gps_long}</code> - {{ i18n.ts._imageEditing._vars.gps_long }}</div>
					</MkInfo>
				</div>
			</div>
		</div>
	</div>
</MkModalWindow>
</template>

<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
import ExifReader from 'exifreader';
import { throttle } from 'throttle-debounce';
import type { ImageFrameParams, ImageFramePreset } from '@/utility/image-frame-renderer/ImageFrameRenderer.js';
import { ImageFrameRenderer } from '@/utility/image-frame-renderer/ImageFrameRenderer.js';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js';
import { ensureSignin } from '@/i.js';
import { genId } from '@/utility/id.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { prefer } from '@/preferences.js';

const $i = ensureSignin();

const props = defineProps<{
	presetEditMode?: boolean;
	preset?: ImageFramePreset | null;
	params?: ImageFrameParams | null;
	image?: File | null;
	imageCaption?: string | null;
	imageFilename?: string | null;
}>();

const preset = deepClone(props.preset) ?? {
	id: genId(),
	name: '',
};

const params = reactive<ImageFrameParams>(deepClone(props.params) ?? {
	borderThickness: 0.05,
	borderRadius: 0,
	labelTop: {
		enabled: false,
		scale: 1.0,
		padding: 0.2,
		textBig: '',
		textSmall: '',
		centered: false,
		withQrCode: false,
	},
	labelBottom: {
		enabled: true,
		scale: 1.0,
		padding: 0.2,
		textBig: '{year}/{0month}/{0day}',
		textSmall: '{camera_mm}mm   f/{camera_f}   {camera_s}s   ISO{camera_iso}',
		centered: false,
		withQrCode: true,
	},
	bgColor: [1, 1, 1],
	fgColor: [0, 0, 0],
	font: 'sans-serif',
});

const emit = defineEmits<{
	(ev: 'ok', frame: ImageFrameParams): void;
	(ev: 'presetOk', preset: ImageFramePreset): void;
	(ev: 'cancel'): void;
	(ev: 'closed'): void;
}>();

const dialog = useTemplateRef('dialog');

async function cancel() {
	if (props.presetEditMode) {
		const { canceled } = await os.confirm({
			type: 'question',
			text: i18n.ts._imageFrameEditor.quitWithoutSaveConfirm,
		});
		if (canceled) return;
	}

	dialog.value?.close();
}

const updateThrottled = throttle(50, () => {
	if (renderer != null) {
		renderer.render(params);
	}
});

watch(params, async (newValue, oldValue) => {
	updateThrottled();
}, { deep: true });

const canvasEl = useTemplateRef('canvasEl');

const sampleImage_3_2 = new Image();
sampleImage_3_2.src = '/client-assets/sample/3-2.jpg';
const sampleImage_3_2_loading = new Promise<void>(resolve => {
	sampleImage_3_2.onload = () => resolve();
});

const sampleImage_2_3 = new Image();
sampleImage_2_3.src = '/client-assets/sample/2-3.jpg';
const sampleImage_2_3_loading = new Promise<void>(resolve => {
	sampleImage_2_3.onload = () => resolve();
});

const sampleImageType = ref(props.image != null ? 'provided' : '3_2');
watch(sampleImageType, async () => {
	if (sampleImageType.value === 'provided') return;
	if (renderer != null) {
		renderer.destroy(false);
		renderer = null;
		initRenderer();
	}
});

let imageFile = props.image;

async function choiceImage() {
	const files = await os.chooseFileFromPc({ multiple: false });
	if (files.length === 0) return;
	imageFile = files[0];
	sampleImageType.value = 'provided';
	if (renderer != null) {
		renderer.destroy(false);
		renderer = null;
		initRenderer();
	}
}

let renderer: ImageFrameRenderer | null = null;
let imageBitmap: ImageBitmap | null = null;

async function initRenderer() {
	if (canvasEl.value == null) return;

	if (sampleImageType.value === '3_2') {
		renderer = new ImageFrameRenderer({
			canvas: canvasEl.value,
			image: sampleImage_3_2,
			exif: null,
			caption: 'Example caption',
			filename: 'example_file_name.jpg',
			renderAsPreview: true,
		});
	} else if (sampleImageType.value === '2_3') {
		renderer = new ImageFrameRenderer({
			canvas: canvasEl.value,
			image: sampleImage_2_3,
			exif: null,
			caption: 'Example caption',
			filename: 'example_file_name.jpg',
			renderAsPreview: true,
		});
	} else if (imageFile != null) {
		imageBitmap = await window.createImageBitmap(imageFile);

		const exif = ExifReader.load(await imageFile.arrayBuffer());

		renderer = new ImageFrameRenderer({
			canvas: canvasEl.value,
			image: imageBitmap,
			exif: exif,
			caption: props.imageCaption ?? null,
			filename: props.imageFilename ?? null,
			renderAsPreview: true,
		});
	}

	await renderer!.render(params);
}

onMounted(async () => {
	const closeWaiting = os.waiting();

	await nextTick(); // waitingがレンダリングされるまで待つ

	await sampleImage_3_2_loading;
	await sampleImage_2_3_loading;

	try {
		await initRenderer();
	} catch (err) {
		console.error(err);
		os.alert({
			type: 'error',
			text: i18n.ts._imageFrameEditor.failedToLoadImage,
		});
	}

	closeWaiting();
});

onUnmounted(() => {
	if (renderer != null) {
		renderer.destroy();
		renderer = null;
	}
	if (imageBitmap != null) {
		imageBitmap.close();
		imageBitmap = null;
	}
});

async function save() {
	if (props.presetEditMode) {
		const { canceled, result: name } = await os.inputText({
			title: i18n.ts.name,
			default: preset.name,
		});
		if (canceled) return;

		preset.name = name || '';

		dialog.value?.close();
		if (renderer != null) {
			renderer.destroy();
			renderer = null;
		}

		emit('presetOk', {
			...preset,
			params: deepClone(params),
		});
	} else {
		dialog.value?.close();
		if (renderer != null) {
			renderer.destroy();
			renderer = null;
		}

		emit('ok', params);
	}
}

function getHex(c: [number, number, number]) {
	return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
}

function getRgb(hex: string | number): [number, number, number] | null {
	if (
		typeof hex === 'number' ||
		typeof hex !== 'string' ||
		!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
	) {
		return null;
	}

	const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
	if (m == null) return [0, 0, 0];
	return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
</script>

<style module>
.root {
	container-type: inline-size;
	height: 100%;
}

.container {
	height: 100%;
	display: grid;
	grid-template-columns: 1fr 400px;
}

.preview {
	position: relative;
	background-color: var(--MI_THEME-bg);
	background-image: linear-gradient(135deg, transparent 30%, var(--MI_THEME-panel) 30%, var(--MI_THEME-panel) 50%, transparent 50%, transparent 80%, var(--MI_THEME-panel) 80%, var(--MI_THEME-panel) 100%);
	background-size: 20px 20px;
}

.animatedBg {
	animation: bg 1.2s linear infinite;
}

@keyframes bg {
	0% { background-position: 0 0; }
	100% { background-position: -20px -20px; }
}

.previewContainer {
	display: flex;
	flex-direction: column;
	height: 100%;
	user-select: none;
	-webkit-user-drag: none;
}

.previewTitle {
	position: absolute;
	z-index: 100;
	top: 8px;
	left: 8px;
	padding: 6px 10px;
	border-radius: 6px;
	font-size: 85%;
}

.previewControls {
	position: absolute;
	z-index: 100;
	bottom: 8px;
	right: 8px;
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 10px;
	border-radius: 6px;
}

.previewControlsButton {
	&.active {
		color: var(--MI_THEME-accent);
	}
}

.previewSpinner {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	pointer-events: none;
	user-select: none;
	-webkit-user-drag: none;
}

.previewCanvas {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	padding: 20px;
	box-sizing: border-box;
	object-fit: contain;
}

.controls {
	overflow-y: scroll;
}

@container (max-width: 800px) {
	.container {
		grid-template-columns: 1fr;
		grid-template-rows: 1fr 1fr;
	}
}
</style>
