/*
 * SPDX-FileCopyrightText: syuilo and misskey-project
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import { onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import { EventEmitter } from 'eventemitter3';
import type { Ref, ShallowRef } from 'vue';
import { useStream } from '@/stream.js';
import { $i } from '@/i.js';
import { store } from '@/store.js';
import { misskeyApi } from '@/utility/misskey-api.js';

export const noteEvents = new EventEmitter<{
	[`reacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void;
	[`unreacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void;
	[`pollVoted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; choice: string; }) => void;
	[`deleted:${string}`]: () => void;
}>();

const fetchEvent = new EventEmitter<{
	[id: string]: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>;
}>();

const capturedNoteIdMapForPolling = new Map<string, number>();

const CAPTURE_MAX = 30;
const POLLING_INTERVAL = 1000 * 15;

window.setInterval(() => {
	// TODO: IDごとにpriorityを付け、CAPTURE_MAXを超えた場合は優先度の低いものから削除する
	// priorityは 自分のノート > リノートされているノート > その他のノート > 投稿から3分以上経過しているノート の順で高くするとよさそう
	const ids = [...capturedNoteIdMapForPolling.keys()].sort((a, b) => (a > b ? -1 : 1)).slice(0, CAPTURE_MAX); // 新しいものを優先するためにIDで降順ソート
	if (ids.length === 0) return;
	if (window.document.hidden) return;

	// まとめてリクエストするのではなく、個別にHTTPリクエスト投げてCDNにキャッシュさせた方がサーバーの負荷低減には良いかもしれない?
	misskeyApi('notes/show-partial-bulk', {
		noteIds: ids,
	}).then((items) => {
		for (const item of items) {
			fetchEvent.emit(item.id, {
				reactions: item.reactions,
				reactionEmojis: item.reactionEmojis,
			});
		}
	});
}, POLLING_INTERVAL);

function pollingSubscribe(props: {
	note: Ref<Misskey.entities.Note>;
	isDeletedRef: Ref<boolean>;
}) {
	const note = props.note;

	function onFetched(data: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>): void {
		note.value.reactions = data.reactions;
		note.value.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0);
		note.value.reactionEmojis = data.reactionEmojis;
	}

	if (capturedNoteIdMapForPolling.has(note.value.id)) {
		capturedNoteIdMapForPolling.set(note.value.id, capturedNoteIdMapForPolling.get(note.value.id)! + 1);
	} else {
		capturedNoteIdMapForPolling.set(note.value.id, 1);
	}

	fetchEvent.on(note.value.id, onFetched);

	onUnmounted(() => {
		capturedNoteIdMapForPolling.set(note.value.id, capturedNoteIdMapForPolling.get(note.value.id)! - 1);
		if (capturedNoteIdMapForPolling.get(note.value.id) === 0) {
			capturedNoteIdMapForPolling.delete(note.value.id);
		}

		fetchEvent.off(note.value.id, onFetched);
	});
}

function realtimeSubscribe(props: {
	note: Ref<Misskey.entities.Note>;
	isDeletedRef: Ref<boolean>;
}): void {
	const note = props.note;
	const connection = useStream();

	function onStreamNoteUpdated(noteData): void {
		const { type, id, body } = noteData;

		if (id !== note.value.id) return;

		switch (type) {
			case 'reacted': {
				noteEvents.emit(`reacted:${id}`, {
					userId: body.userId,
					reaction: body.reaction,
					emoji: body.emoji,
				});
				break;
			}

			case 'unreacted': {
				noteEvents.emit(`unreacted:${id}`, {
					userId: body.userId,
					reaction: body.reaction,
					emoji: body.emoji,
				});
				break;
			}

			case 'pollVoted': {
				noteEvents.emit(`pollVoted:${id}`, {
					userId: body.userId,
					choice: body.choice,
				});
				break;
			}

			case 'deleted': {
				noteEvents.emit(`deleted:${id}`);
				break;
			}
		}
	}

	function capture(withHandler = false): void {
		connection.send('sr', { id: note.value.id });
		if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
	}

	function decapture(withHandler = false): void {
		connection.send('un', { id: note.value.id });
		if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
	}

	function onStreamConnected() {
		capture(false);
	}

	capture(true);
	connection.on('_connected_', onStreamConnected);

	onUnmounted(() => {
		decapture(true);
		connection.off('_connected_', onStreamConnected);
	});
}

export function useNoteCapture(props: {
	note: Ref<Misskey.entities.Note>;
	parentNote: Ref<Misskey.entities.Note> | null;
	isDeletedRef: Ref<boolean>;
}) {
	const note = props.note;
	const parentNote = props.parentNote;

	noteEvents.on(`reacted:${note.value.id}`, onReacted);
	noteEvents.on(`unreacted:${note.value.id}`, onUnreacted);
	noteEvents.on(`pollVoted:${note.value.id}`, onPollVoted);
	noteEvents.on(`deleted:${note.value.id}`, onDeleted);

	let latestReactedKey: string | null = null;
	let latestUnreactedKey: string | null = null;
	let latestPollVotedKey: string | null = null;

	function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
		console.log('reacted', ctx);
		const newReactedKey = `${ctx.userId}:${ctx.reaction}`;
		if (newReactedKey === latestReactedKey) return;
		latestReactedKey = newReactedKey;

		if (ctx.emoji && !(ctx.emoji.name in note.value.reactionEmojis)) {
			note.value.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
		}

		const currentCount = note.value.reactions[ctx.reaction] || 0;

		note.value.reactions[ctx.reaction] = currentCount + 1;
		note.value.reactionCount += 1;

		if ($i && (ctx.userId === $i.id)) {
			note.value.myReaction = ctx.reaction;
		}
	}

	function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
		const newUnreactedKey = `${ctx.userId}:${ctx.reaction}`;
		if (newUnreactedKey === latestUnreactedKey) return;
		latestUnreactedKey = newUnreactedKey;

		const currentCount = note.value.reactions[ctx.reaction] || 0;

		note.value.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
		note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
		if (note.value.reactions[ctx.reaction] === 0) delete note.value.reactions[ctx.reaction];

		if ($i && (ctx.userId === $i.id)) {
			note.value.myReaction = null;
		}
	}

	function onPollVoted(ctx: { userId: Misskey.entities.User['id']; choice: string; }): void {
		const newPollVotedKey = `${ctx.userId}:${ctx.choice}`;
		if (newPollVotedKey === latestPollVotedKey) return;
		latestPollVotedKey = newPollVotedKey;

		const choices = [...note.value.poll.choices];
		choices[ctx.choice] = {
			...choices[ctx.choice],
			votes: choices[ctx.choice].votes + 1,
			...($i && (ctx.userId === $i.id) ? {
				isVoted: true,
			} : {}),
		};

		note.value.poll.choices = choices;
	}

	function onDeleted(): void {
		props.isDeletedRef.value = true;
	}

	onUnmounted(() => {
		noteEvents.off(`reacted:${note.value.id}`, onReacted);
		noteEvents.off(`unreacted:${note.value.id}`, onUnreacted);
		noteEvents.off(`pollVoted:${note.value.id}`, onPollVoted);
		noteEvents.off(`deleted:${note.value.id}`, onDeleted);
	});

	// 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない
	// ただし「リノートされたばかりの過去のノート」(= parentNoteが存在し、かつparentNoteの投稿日時が最近)はイベント発生が考えられるため購読する
	// TODO: デバイスとサーバーの時計がズレていると不具合の元になるため、ズレを検知して警告を表示するなどのケアが必要かもしれない
	if (parentNote == null) {
		if ((Date.now() - new Date(note.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
			// リノートで表示されているノートでもないし、投稿からある程度経過しているので購読しない
			return;
		}
	} else {
		if ((Date.now() - new Date(parentNote.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
			// リノートで表示されているノートだが、リノートされてからある程度経過しているので購読しない
			return;
		}
	}

	if ($i && store.s.realtimeMode) {
		realtimeSubscribe(props);
	} else {
		pollingSubscribe(props);
	}
}