/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "chat_helpers/emoji_interactions.h"

#include "chat_helpers/stickers_emoji_pack.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_sticker.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "ui/emoji_config.h"
#include "base/random.h"
#include "apiwrap.h"

#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>

namespace ChatHelpers {
namespace {

constexpr auto kMinDelay = crl::time(200);
constexpr auto kAccumulateDelay = crl::time(1000);
constexpr auto kAccumulateSeenRequests = kAccumulateDelay;
constexpr auto kAcceptSeenSinceRequest = 3 * crl::time(1000);
constexpr auto kMaxDelay = 2 * crl::time(1000);
constexpr auto kTimeNever = std::numeric_limits<crl::time>::max();
constexpr auto kJsonVersion = 1;

} // namespace

auto EmojiInteractions::Combine(CheckResult a, CheckResult b) -> CheckResult {
	return {
		.nextCheckAt = std::min(a.nextCheckAt, b.nextCheckAt),
		.waitingForDownload = a.waitingForDownload || b.waitingForDownload,
	};
}

EmojiInteractions::EmojiInteractions(not_null<Main::Session*> session)
: _session(session)
, _checkTimer([=] { check(); }) {
	_session->changes().messageUpdates(
		Data::MessageUpdate::Flag::Destroyed
		| Data::MessageUpdate::Flag::Edited
	) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
		if (update.flags & Data::MessageUpdate::Flag::Destroyed) {
			_outgoing.remove(update.item);
			_incoming.remove(update.item);
		} else if (update.flags & Data::MessageUpdate::Flag::Edited) {
			checkEdition(update.item, _outgoing);
			checkEdition(update.item, _incoming);
		}
	}, _lifetime);
}

EmojiInteractions::~EmojiInteractions() = default;

void EmojiInteractions::checkEdition(
		not_null<HistoryItem*> item,
		base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map) {
	const auto &pack = _session->emojiStickersPack();
	const auto i = map.find(item);
	if (i != end(map)
		&& (i->second.front().emoji != pack.chooseInteractionEmoji(item))) {
		map.erase(i);
	}
}

void EmojiInteractions::startOutgoing(
		not_null<const HistoryView::Element*> view) {
	const auto item = view->data();
	if (!item->isRegular() || !item->history()->peer->isUser()) {
		return;
	}
	const auto &pack = _session->emojiStickersPack();
	const auto emoticon = item->originalText().text;
	const auto emoji = pack.chooseInteractionEmoji(emoticon);
	if (!emoji) {
		return;
	}
	const auto &list = pack.animationsForEmoji(emoji);
	if (list.empty()) {
		return;
	}
	auto &animations = _outgoing[item];
	if (!animations.empty() && animations.front().emoji != emoji) {
		// The message was edited, forget the old emoji.
		animations.clear();
	}
	const auto last = !animations.empty() ? &animations.back() : nullptr;
	const auto listSize = int(list.size());
	const auto chooseDifferent = (last && listSize > 1);
	const auto index = chooseDifferent
		? base::RandomIndex(listSize - 1)
		: base::RandomIndex(listSize);
	const auto selected = (begin(list) + index)->second;
	const auto document = (chooseDifferent && selected == last->document)
		? (begin(list) + index + 1)->second
		: selected;
	const auto media = document->createMediaView();
	media->checkStickerLarge();
	const auto now = crl::now();
	animations.push_back({
		.emoticon = emoticon,
		.emoji = emoji,
		.document = document,
		.media = media,
		.scheduledAt = now,
		.index = index,
	});
	check(now);
}

void EmojiInteractions::startIncoming(
		not_null<PeerData*> peer,
		MsgId messageId,
		const QString &emoticon,
		EmojiInteractionsBunch &&bunch) {
	if (!peer->isUser() || bunch.interactions.empty()) {
		return;
	}
	const auto item = _session->data().message(peer->id, messageId);
	if (!item || !item->isRegular()) {
		return;
	}
	const auto &pack = _session->emojiStickersPack();
	const auto emoji = pack.chooseInteractionEmoji(item);
	if (!emoji || emoji != pack.chooseInteractionEmoji(emoticon)) {
		return;
	}
	const auto &list = pack.animationsForEmoji(emoji);
	if (list.empty()) {
		return;
	}
	auto &animations = _incoming[item];
	if (!animations.empty() && animations.front().emoji != emoji) {
		// The message was edited, forget the old emoji.
		animations.clear();
	}
	const auto now = crl::now();
	for (const auto &single : bunch.interactions) {
		const auto at = now + crl::time(base::SafeRound(single.time * 1000));
		if (!animations.empty() && animations.back().scheduledAt >= at) {
			continue;
		}
		const auto listSize = int(list.size());
		const auto index = (single.index - 1);
		if (index < listSize) {
			const auto document = (begin(list) + index)->second;
			const auto media = document->createMediaView();
			media->checkStickerLarge();
			animations.push_back({
				.emoticon = emoticon,
				.emoji = emoji,
				.document = document,
				.media = media,
				.scheduledAt = at,
				.incoming = true,
				.index = index,
			});
		}
	}
	if (animations.empty()) {
		_incoming.remove(item);
	} else {
		check(now);
	}
}

void EmojiInteractions::seenOutgoing(
		not_null<PeerData*> peer,
		const QString &emoticon) {
	const auto &pack = _session->emojiStickersPack();
	if (const auto i = _playsSent.find(peer); i != end(_playsSent)) {
		if (const auto emoji = pack.chooseInteractionEmoji(emoticon)) {
			if (const auto j = i->second.find(emoji); j != end(i->second)) {
				const auto last = j->second.lastDoneReceivedAt;
				if (!last || last + kAcceptSeenSinceRequest > crl::now()) {
					_seen.fire({ peer, emoticon });
				}
			}
		}
	}
}

auto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult {
	return Combine(
		checkAnimations(now, _outgoing),
		checkAnimations(now, _incoming));
}

auto EmojiInteractions::checkAnimations(
		crl::time now,
		base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map
) -> CheckResult {
	auto nearest = kTimeNever;
	auto waitingForDownload = false;
	for (auto i = begin(map); i != end(map);) {
		auto lastStartedAt = crl::time();

		auto &animations = i->second;
		// Erase too old requests.
		const auto j = ranges::find_if(animations, [&](const Animation &a) {
			return !a.startedAt && (a.scheduledAt + kMaxDelay <= now);
		});
		if (j == begin(animations)) {
			i = map.erase(i);
			continue;
		} else if (j != end(animations)) {
			animations.erase(j, end(animations));
		}
		const auto item = i->first;
		for (auto &animation : animations) {
			if (animation.startedAt) {
				lastStartedAt = animation.startedAt;
			} else if (!animation.media->loaded()) {
				animation.media->checkStickerLarge();
				waitingForDownload = true;
				break;
			} else if (!lastStartedAt || lastStartedAt + kMinDelay <= now) {
				animation.startedAt = now;
				_playRequests.fire({
					animation.emoticon,
					item,
					animation.media,
					animation.scheduledAt,
					animation.incoming,
				});
				break;
			} else {
				nearest = std::min(nearest, lastStartedAt + kMinDelay);
				break;
			}
		}
		++i;
	}
	return {
		.nextCheckAt = nearest,
		.waitingForDownload = waitingForDownload,
	};
}

void EmojiInteractions::sendAccumulatedOutgoing(
		crl::time now,
		not_null<HistoryItem*> item,
		std::vector<Animation> &animations) {
	Expects(!animations.empty());

	const auto firstStartedAt = animations.front().startedAt;
	const auto intervalEnd = firstStartedAt + kAccumulateDelay;
	if (intervalEnd > now) {
		return;
	}
	const auto from = begin(animations);
	const auto till = ranges::find_if(animations, [&](const auto &animation) {
		return !animation.startedAt || (animation.startedAt >= intervalEnd);
	});
	auto bunch = EmojiInteractionsBunch();
	bunch.interactions.reserve(till - from);
	for (const auto &animation : ranges::make_subrange(from, till)) {
		bunch.interactions.push_back({
			.index = animation.index + 1,
			.time = (animation.startedAt - firstStartedAt) / 1000.,
		});
	}
	if (bunch.interactions.empty()) {
		return;
	}
	const auto peer = item->history()->peer;
	const auto emoji = from->emoji;
	const auto requestId = _session->api().request(MTPmessages_SetTyping(
		MTP_flags(0),
		peer->input,
		MTPint(), // top_msg_id
		MTP_sendMessageEmojiInteraction(
			MTP_string(from->emoticon),
			MTP_int(item->id),
			MTP_dataJSON(MTP_bytes(ToJson(bunch))))
	)).done([=](const MTPBool &result, mtpRequestId requestId) {
		auto &sent = _playsSent[peer][emoji];
		if (sent.lastRequestId == requestId) {
			sent.lastDoneReceivedAt = crl::now();
			if (!_checkTimer.isActive()) {
				_checkTimer.callOnce(kAcceptSeenSinceRequest);
			}
		}
	}).send();
	_playsSent[peer][emoji] = PlaySent{ .lastRequestId = requestId };
	animations.erase(from, till);
}

void EmojiInteractions::clearAccumulatedIncoming(
		crl::time now,
		std::vector<Animation> &animations) {
	Expects(!animations.empty());

	const auto from = begin(animations);
	const auto till = ranges::find_if(animations, [&](const auto &animation) {
		return !animation.startedAt
			|| (animation.startedAt + kMinDelay) > now;
	});
	animations.erase(from, till);
}

auto EmojiInteractions::checkAccumulated(crl::time now) -> CheckResult {
	auto nearest = kTimeNever;
	for (auto i = begin(_outgoing); i != end(_outgoing);) {
		auto &[item, animations] = *i;
		sendAccumulatedOutgoing(now, item, animations);
		if (animations.empty()) {
			i = _outgoing.erase(i);
			continue;
		} else if (const auto firstStartedAt = animations.front().startedAt) {
			nearest = std::min(nearest, firstStartedAt + kAccumulateDelay);
			Assert(nearest > now);
		}
		++i;
	}
	for (auto i = begin(_incoming); i != end(_incoming);) {
		auto &animations = i->second;
		clearAccumulatedIncoming(now, animations);
		if (animations.empty()) {
			i = _incoming.erase(i);
			continue;
		} else {
			// Doesn't really matter when, just clear them finally.
			nearest = std::min(nearest, now + kAccumulateDelay);
		}
		++i;
	}
	return {
		.nextCheckAt = nearest,
	};
}

void EmojiInteractions::check(crl::time now) {
	if (!now) {
		now = crl::now();
	}
	checkSeenRequests(now);
	checkSentRequests(now);
	const auto result1 = checkAnimations(now);
	const auto result2 = checkAccumulated(now);
	const auto result = Combine(result1, result2);
	if (result.nextCheckAt < kTimeNever) {
		Assert(result.nextCheckAt > now);
		_checkTimer.callOnce(result.nextCheckAt - now);
	} else if (!_playStarted.empty()) {
		_checkTimer.callOnce(kAccumulateSeenRequests);
	} else if (!_playsSent.empty()) {
		_checkTimer.callOnce(kAcceptSeenSinceRequest);
	}
	setWaitingForDownload(result.waitingForDownload);
}

void EmojiInteractions::checkSeenRequests(crl::time now) {
	for (auto i = begin(_playStarted); i != end(_playStarted);) {
		auto &animations = i->second;
		for (auto j = begin(animations); j != end(animations);) {
			if (j->second + kAccumulateSeenRequests <= now) {
				j = animations.erase(j);
			} else {
				++j;
			}
		}
		if (animations.empty()) {
			i = _playStarted.erase(i);
		} else {
			++i;
		}
	}
}

void EmojiInteractions::checkSentRequests(crl::time now) {
	for (auto i = begin(_playsSent); i != end(_playsSent);) {
		auto &animations = i->second;
		for (auto j = begin(animations); j != end(animations);) {
			const auto last = j->second.lastDoneReceivedAt;
			if (last && last + kAcceptSeenSinceRequest <= now) {
				j = animations.erase(j);
			} else {
				++j;
			}
		}
		if (animations.empty()) {
			i = _playsSent.erase(i);
		} else {
			++i;
		}
	}
}

void EmojiInteractions::setWaitingForDownload(bool waiting) {
	if (_waitingForDownload == waiting) {
		return;
	}
	_waitingForDownload = waiting;
	if (_waitingForDownload) {
		_session->downloaderTaskFinished(
		) | rpl::start_with_next([=] {
			check();
		}, _downloadCheckLifetime);
	} else {
		_downloadCheckLifetime.destroy();
		_downloadCheckLifetime.destroy();
	}
}

void EmojiInteractions::playStarted(not_null<PeerData*> peer, QString emoji) {
	auto &map = _playStarted[peer];
	const auto i = map.find(emoji);
	const auto now = crl::now();
	if (i != end(map) && now - i->second < kAccumulateSeenRequests) {
		return;
	}
	_session->api().request(MTPmessages_SetTyping(
		MTP_flags(0),
		peer->input,
		MTPint(), // top_msg_id
		MTP_sendMessageEmojiInteractionSeen(MTP_string(emoji))
	)).send();
	map[emoji] = now;
	if (!_checkTimer.isActive()) {
		_checkTimer.callOnce(kAccumulateSeenRequests);
	}
}

EmojiInteractionsBunch EmojiInteractions::Parse(const QByteArray &json) {
	auto error = QJsonParseError{ 0, QJsonParseError::NoError };
	const auto document = QJsonDocument::fromJson(json, &error);
	if (error.error != QJsonParseError::NoError || !document.isObject()) {
		LOG(("API Error: Bad interactions json received."));
		return {};
	}
	const auto root = document.object();
	const auto version = root.value("v").toInt();
	if (version != kJsonVersion) {
		LOG(("API Error: Bad interactions version: %1").arg(version));
		return {};
	}
	const auto actions = root.value("a").toArray();
	if (actions.empty()) {
		LOG(("API Error: Empty interactions list."));
		return {};
	}
	auto result = EmojiInteractionsBunch();
	for (const auto interaction : actions) {
		const auto object = interaction.toObject();
		const auto index = object.value("i").toInt();
		if (index < 0 || index > 10) {
			LOG(("API Error: Bad interaction index: %1").arg(index));
			return {};
		}
		const auto time = object.value("t").toDouble();
		if (time < 0.
			|| time > 1.
			|| (!result.interactions.empty()
				&& time <= result.interactions.back().time)) {
			LOG(("API Error: Bad interaction time: %1").arg(time));
			continue;
		}
		result.interactions.push_back({ .index = index, .time = time });
	}

	return result;
}

QByteArray EmojiInteractions::ToJson(const EmojiInteractionsBunch &bunch) {
	auto list = QJsonArray();
	for (const auto &single : bunch.interactions) {
		list.push_back(QJsonObject{
			{ "i", single.index },
			{ "t", single.time },
		});
	}
	return QJsonDocument(QJsonObject{
		{ "v", kJsonVersion },
		{ "a", std::move(list) },
	}).toJson(QJsonDocument::Compact);
}

} // namespace ChatHelpers
