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

<template>
<div
	ref="rootEl"
	:class="$style.root"
	:style="{
		'--MI-QrReadViewHeight': 'calc(100cqh - var(--MI-stickyTop, 0px) - var(--MI-stickyBottom, 0px))',
		'--MI-QrReadVideoHeight': 'min(calc(var(--MI-QrReadViewHeight) * 0.3), 512px)',
	}"
>
	<MkStickyContainer>
		<template #header>
			<div :class="$style.view">
				<video ref="videoEl" :class="$style.video" autoplay muted playsinline></video>
				<div ref="overlayEl" :class="$style.overlay"></div>
				<div :class="$style.controls">
					<MkButton v-tooltip="i18n.ts._qr.scanFile" iconOnly @click="upload"><i class="ti ti-photo-plus"></i></MkButton>

					<MkButton v-if="qrStarted" v-tooltip="i18n.ts._qr.stopQr" iconOnly @click="stopQr"><i class="ti ti-player-play"></i></MkButton>
					<MkButton v-else v-tooltip="i18n.ts._qr.startQr" iconOnly danger @click="startQr"><i class="ti ti-player-pause"></i></MkButton>

					<MkButton v-tooltip="i18n.ts._qr.chooseCamera" iconOnly @click="chooseCamera"><i class="ti ti-camera-rotate"></i></MkButton>

					<MkButton v-if="!flashCanToggle" v-tooltip="i18n.ts._qr.cannotToggleFlash" iconOnly disabled><i class="ti ti-bolt"></i></MkButton>
					<MkButton v-else-if="!flash" v-tooltip="i18n.ts._qr.turnOnFlash" iconOnly @click="toggleFlash(true)"><i class="ti ti-bolt-off"></i></MkButton>
					<MkButton v-else v-tooltip="i18n.ts._qr.turnOffFlash" iconOnly @click="toggleFlash(false)"><i class="ti ti-bolt-filled"></i></MkButton>
				</div>
			</div>
		</template>
		<div
			:class="['_spacer', $style.contents]"
			:style="{
				'--MI_SPACER-w': '800px'
			}"
		>
			<MkStickyContainer>
				<template #header>
					<MkTab v-model="tab" :class="$style.tab">
						<option value="users">{{ i18n.ts.users }}</option>
						<option value="notes">{{ i18n.ts.notes }}</option>
						<option value="all">{{ i18n.ts.all }}</option>
					</MkTab>
				</template>
				<div v-if="tab === 'users'" :class="[$style.users, '_margin']" style="padding-bottom: var(--MI-margin);">
					<MkUserInfo v-for="user in users" :key="user.id" :user="user"/>
				</div>
				<div v-else-if="tab === 'notes'" class="_margin _gaps" style="padding-bottom: var(--MI-margin);">
					<MkNote v-for="note in notes" :key="note.id" :note="note" :class="$style.note"/>
				</div>
				<div v-else-if="tab === 'all'" class="_margin _gaps" style="padding-bottom: var(--MI-margin);">
					<MkQrReadRawViewer v-for="result in Array.from(results).reverse()" :key="result" :data="result"/>
				</div>
			</MkStickyContainer>
		</div>
	</MkStickyContainer>
</div>
</template>

<script lang="ts" setup>
import QrScanner from 'qr-scanner';
import { onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getScrollContainer } from '@@/js/scroll.js';
import type { ApShowResponse } from 'misskey-js/entities.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import MkUserInfo from '@/components/MkUserInfo.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkNote from '@/components/MkNote.vue';
import MkTab from '@/components/MkTab.vue';
import MkButton from '@/components/MkButton.vue';
import MkQrReadRawViewer from '@/pages/qr.read.raw-viewer.vue';

const LIST_RERENDER_INTERVAL = 1500;

const rootEl = ref<HTMLDivElement | null>(null);
const videoEl = ref<HTMLVideoElement | null>(null);
const overlayEl = ref<HTMLDivElement | null>(null);

const scannerInstance = shallowRef<QrScanner | null>(null);

const tab = ref<'users' | 'notes' | 'all'>('users');

// higher is recent
const results = ref(new Set<string>());
// lower is recent
const uris = ref<string[]>([]);
const sources = new Map<string, ApShowResponse | null>();
const users = ref<(misskey.entities.UserDetailed)[]>([]);
const usersCount = ref(0);
const notes = ref<misskey.entities.Note[]>([]);
const notesCount = ref(0);

const timer = ref<number | null>(null);

function updateLists() {
	const responses = uris.value.map(uri => sources.get(uri)).filter((r): r is ApShowResponse => !!r);
	users.value = responses.filter(r => r.type === 'User').map(r => r.object).filter((u): u is misskey.entities.UserDetailed => !!u);
	usersCount.value = users.value.length;
	notes.value = responses.filter(r => r.type === 'Note').map(r => r.object).filter((n): n is misskey.entities.Note => !!n);
	notesCount.value = notes.value.length;
	updateRequired.value = false;
}

const updateRequired = ref(false);

watch(uris, () => {
	if (timer.value) {
		updateRequired.value = true;
		return;
	}

	updateLists();

	timer.value = window.setTimeout(() => {
		timer.value = null;
		if (updateRequired.value) {
			updateLists();
		}
	}, LIST_RERENDER_INTERVAL) as number;
});

watch(tab, () => {
	if (timer.value) {
		window.clearTimeout(timer.value);
		timer.value = null;
	}
	updateLists();
});

async function processResult(result: QrScanner.ScanResult) {
	if (!result) return;
	const trimmed = result.data.trim();

	if (!trimmed) return;

	const haveExisted = results.value.has(trimmed);
	results.value.add(trimmed);

	try {
		new URL(trimmed);
	} catch {
		if (!haveExisted) {
			tab.value = 'all';
		}
		return;
	}

	if (uris.value[0] !== trimmed) {
		// 並べ替え
		uris.value = [trimmed, ...uris.value.slice(0, 29).filter(u => u !== trimmed)];
	}

	if (sources.has(trimmed)) return;
	// Start fetching user info
	sources.set(trimmed, null);

	await misskeyApi('ap/show', { uri: trimmed })
		.then(data => {
			if (data.type === 'User') {
				sources.set(trimmed, data);
				tab.value = 'users';
			} else if (data.type === 'Note') {
				sources.set(trimmed, data);
				tab.value = 'notes';
			}
			updateLists();
		})
		.catch(err => {
			tab.value = 'all';
			throw err;
		});
}

const qrStarted = ref(true);
const flashCanToggle = ref(false);
const flash = ref(false);

async function upload() {
	os.chooseFileFromPc({ multiple: true }).then(files => {
		if (files.length === 0) return;
		for (const file of files) {
			QrScanner.scanImage(file, { returnDetailedScanResult: true })
				.then(result => {
					processResult(result);
				})
				.catch(err => {
					if (err.toString().includes('No QR code found')) {
						os.alert({
							type: 'info',
							text: i18n.ts._qr.noQrCodeFound,
						});
					} else {
						os.alert({
							type: 'error',
							text: err.toString(),
						});
						console.error(err);
					}
				});
		}
	});
}

async function chooseCamera() {
	if (!scannerInstance.value) return;
	const cameras = await QrScanner.listCameras(true);
	if (cameras.length === 0) {
		os.alert({
			type: 'error',
		});
		return;
	}

	const select = await os.select({
		title: i18n.ts._qr.chooseCamera,
		items: cameras.map(camera => ({
			label: camera.label,
			value: camera.id,
		})),
	});
	if (select.canceled) return;
	if (select.result == null) return;

	await scannerInstance.value.setCamera(select.result);
	flashCanToggle.value = await scannerInstance.value.hasFlash();
	flash.value = scannerInstance.value.isFlashOn();
}

async function toggleFlash(to = false) {
	if (!scannerInstance.value) return;

	flash.value = to;
	if (flash.value) {
		await scannerInstance.value.turnFlashOn();
	} else {
		await scannerInstance.value.turnFlashOff();
	}
}

async function startQr() {
	if (!scannerInstance.value) return;
	await scannerInstance.value.start();
	qrStarted.value = true;
}

function stopQr() {
	if (!scannerInstance.value) return;
	scannerInstance.value.stop();
	qrStarted.value = false;
}

onActivated(() => {
	startQr;
});

onDeactivated(() => {
	stopQr;
});

const alertLock = ref(false);

onMounted(() => {
	if (!videoEl.value || !overlayEl.value) {
		os.alert({
			type: 'error',
			text: i18n.ts.somethingHappened,
		});
		return;
	}

	scannerInstance.value = new QrScanner(
		videoEl.value,
		processResult,
		{
			highlightScanRegion: true,
			highlightCodeOutline: true,
			overlay: overlayEl.value,
			calculateScanRegion(video: HTMLVideoElement): QrScanner.ScanRegion {
				const aspectRatio = video.videoWidth / video.videoHeight;
				const SHORT_SIDE_SIZE_DOWNSCALED = 360;
				return {
					x: 0,
					y: 0,
					width: video.videoWidth,
					height: video.videoHeight,
					downScaledWidth: aspectRatio > 1 ? Math.round(SHORT_SIDE_SIZE_DOWNSCALED * aspectRatio) : SHORT_SIDE_SIZE_DOWNSCALED,
					downScaledHeight: aspectRatio > 1 ? SHORT_SIDE_SIZE_DOWNSCALED : Math.round(SHORT_SIDE_SIZE_DOWNSCALED / aspectRatio),
				};
			},
			onDecodeError(err) {
				if (err.toString().includes('No QR code found')) return;
				if (alertLock.value) return;
				alertLock.value = true;
				os.alert({
					type: 'error',
					text: err.toString(),
				}).finally(() => {
					alertLock.value = false;
				});
			},
		},
	);

	scannerInstance.value.start()
		.then(async () => {
			qrStarted.value = true;
			if (!scannerInstance.value) return;
			flashCanToggle.value = await scannerInstance.value.hasFlash();
			flash.value = scannerInstance.value.isFlashOn();
		})
		.catch(err => {
			qrStarted.value = false;
			os.alert({
				type: 'error',
				text: err.toString(),
			});
			console.error(err);
		});
});

onUnmounted(() => {
	if (timer.value) {
		window.clearTimeout(timer.value);
		timer.value = null;
	}

	scannerInstance.value?.destroy();
});
</script>

<style lang="scss" module>
.root {
	position: relative;
}

.view {
	position: sticky;
	top: var(--MI-stickyTop, 0);
	z-index: 1;
	background: var(--MI_THEME-bg);
	background-size: 16px 16px;
	width: 100%;
	height: var(--MI-QrReadVideoHeight);
}

.video {
	width: 100%;
	height: 100%;
	object-fit: contain;
}

.controls {
	width: 100%;
	position: absolute;
	left: 0;
	bottom: 10px;
	display: flex;
	justify-content: center;
	align-items: center;
	gap: 10px;
}

html[data-color-scheme=dark] .view {
	--c: rgb(255 255 255 / 2%);
	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}

html[data-color-scheme=light] .view {
	--c: rgb(0 0 0 / 2%);
	background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}

.contents {
	padding-top: calc(var(--MI-margin) / 2);
}

.tab {
	padding: calc(var(--MI-margin) / 2) 0;
	background: var(--MI_THEME-bg);
}

.users {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
	grid-gap: var(--MI-margin);
}

.note {
	background: var(--MI_THEME-panel);
	border-radius: var(--MI-radius);
}
</style>
