/*
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 "data/data_message_reactions.h"

#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "lottie/lottie_icon.h"
#include "base/timer_rpl.h"
#include "apiwrap.h"
#include "styles/style_chat.h"

namespace Data {
namespace {

constexpr auto kRefreshFullListEach = 60 * 60 * crl::time(1000);
constexpr auto kPollEach = 20 * crl::time(1000);
constexpr auto kSizeForDownscale = 64;

} // namespace

Reactions::Reactions(not_null<Session*> owner)
: _owner(owner)
, _repaintTimer([=] { repaintCollected(); }) {
	refresh();

	base::timer_each(
		kRefreshFullListEach
	) | rpl::start_with_next([=] {
		refresh();
	}, _lifetime);

	_owner->session().changes().messageUpdates(
		MessageUpdate::Flag::Destroyed
	) | rpl::start_with_next([=](const MessageUpdate &update) {
		const auto item = update.item;
		_pollingItems.remove(item);
		_pollItems.remove(item);
		_repaintItems.remove(item);
	}, _lifetime);

	const auto appConfig = &_owner->session().account().appConfig();
	appConfig->value(
	) | rpl::start_with_next([=] {
		const auto favorite = appConfig->get<QString>(
			u"reactions_default"_q,
			QString::fromUtf8("\xf0\x9f\x91\x8d"));
		if (_favorite != favorite) {
			_favorite = favorite;
			_updated.fire({});
		}
	}, _lifetime);
}

Reactions::~Reactions() = default;

void Reactions::refresh() {
	request();
}

const std::vector<Reaction> &Reactions::list(Type type) const {
	switch (type) {
	case Type::Active: return _active;
	case Type::All: return _available;
	}
	Unexpected("Type in Reactions::list.");
}

QString Reactions::favorite() const {
	return _favorite;
}

rpl::producer<> Reactions::updates() const {
	return _updated.events();
}

void Reactions::preloadImageFor(const QString &emoji) {
	if (_images.contains(emoji)) {
		return;
	}
	auto &set = _images.emplace(emoji).first->second;
	const auto i = ranges::find(_available, emoji, &Reaction::emoji);
	const auto document = (i == end(_available))
		? nullptr
		: i->centerIcon
		? i->centerIcon
		: i->appearAnimation.get();
	if (document) {
		loadImage(set, document, !i->centerIcon);
	} else if (!_waitingForList) {
		_waitingForList = true;
		refresh();
	}
}

void Reactions::preloadAnimationsFor(const QString &emoji) {
	const auto i = ranges::find(_available, emoji, &Reaction::emoji);
	if (i == end(_available)) {
		return;
	}

	const auto preload = [&](DocumentData *document) {
		const auto view = document
			? document->activeMediaView()
			: nullptr;
		if (view) {
			view->checkStickerLarge();
		}
	};
	preload(i->centerIcon);
	preload(i->aroundAnimation);
}

QImage Reactions::resolveImageFor(
		const QString &emoji,
		ImageSize size) {
	const auto i = _images.find(emoji);
	if (i == end(_images)) {
		preloadImageFor(emoji);
	}
	auto &set = (i != end(_images)) ? i->second : _images[emoji];
	const auto resolve = [&](QImage &image, int size) {
		const auto factor = style::DevicePixelRatio();
		const auto frameSize = set.fromAppearAnimation
			? (size / 2)
			: size;
		image = set.icon->frame().scaled(
			frameSize * factor,
			frameSize * factor,
			Qt::IgnoreAspectRatio,
			Qt::SmoothTransformation);
		if (set.fromAppearAnimation) {
			auto result = QImage(
				size * factor,
				size * factor,
				QImage::Format_ARGB32_Premultiplied);
			result.fill(Qt::transparent);

			auto p = QPainter(&result);
			p.drawImage(
				(size - frameSize) * factor / 2,
				(size - frameSize) * factor / 2,
				image);
			p.end();

			std::swap(result, image);
		}
		image.setDevicePixelRatio(factor);
	};
	if (set.bottomInfo.isNull() && set.icon) {
		resolve(set.bottomInfo, st::reactionInfoImage);
		resolve(set.inlineList, st::reactionInlineImage);
		crl::async([icon = std::move(set.icon)]{});
	}
	switch (size) {
	case ImageSize::BottomInfo: return set.bottomInfo;
	case ImageSize::InlineList: return set.inlineList;
	}
	Unexpected("ImageSize in Reactions::resolveImageFor.");
}

void Reactions::resolveImages() {
	for (auto &[emoji, set] : _images) {
		if (!set.bottomInfo.isNull() || set.icon || set.media) {
			continue;
		}
		const auto i = ranges::find(_available, emoji, &Reaction::emoji);
		const auto document = (i == end(_available))
			? nullptr
			: i->centerIcon
			? i->centerIcon
			: i->appearAnimation.get();
		if (document) {
			loadImage(set, document, !i->centerIcon);
		} else {
			LOG(("API Error: Reaction for emoji '%1' not found!"
				).arg(emoji));
		}
	}
}

void Reactions::loadImage(
		ImageSet &set,
		not_null<DocumentData*> document,
		bool fromAppearAnimation) {
	if (!set.bottomInfo.isNull() || set.icon) {
		return;
	} else if (!set.media) {
		set.fromAppearAnimation = fromAppearAnimation;
		set.media = document->createMediaView();
		set.media->checkStickerLarge();
	}
	if (set.media->loaded()) {
		setLottie(set);
	} else if (!_imagesLoadLifetime) {
		document->session().downloaderTaskFinished(
		) | rpl::start_with_next([=] {
			downloadTaskFinished();
		}, _imagesLoadLifetime);
	}
}

void Reactions::setLottie(ImageSet &set) {
	const auto size = style::ConvertScale(kSizeForDownscale);
	set.icon = std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{
		.path = set.media->owner()->filepath(true),
		.json = set.media->bytes(),
		.sizeOverride = QSize(size, size),
		.frame = -1,
	});
	set.media = nullptr;
}

void Reactions::downloadTaskFinished() {
	auto hasOne = false;
	for (auto &[emoji, set] : _images) {
		if (!set.media) {
			continue;
		} else if (set.media->loaded()) {
			setLottie(set);
		} else {
			hasOne = true;
		}
	}
	if (!hasOne) {
		_imagesLoadLifetime.destroy();
	}
}

base::flat_set<QString> Reactions::ParseAllowed(
		const MTPVector<MTPstring> *list) {
	if (!list) {
		return {};
	}
	const auto parsed = ranges::views::all(
		list->v
	) | ranges::views::transform([](const MTPstring &string) {
		return qs(string);
	}) | ranges::to_vector;
	return { begin(parsed), end(parsed) };
}

void Reactions::request() {
	auto &api = _owner->session().api();
	if (_requestId) {
		return;
	}
	_requestId = api.request(MTPmessages_GetAvailableReactions(
		MTP_int(_hash)
	)).done([=](const MTPmessages_AvailableReactions &result) {
		_requestId = 0;
		const auto oldCache = base::take(_iconsCache);
		const auto toCache = [&](DocumentData *document) {
			if (document) {
				_iconsCache.emplace(document, document->createMediaView());
			}
		};
		result.match([&](const MTPDmessages_availableReactions &data) {
			_hash = data.vhash().v;

			const auto &list = data.vreactions().v;
			_active.clear();
			_available.clear();
			_active.reserve(list.size());
			_available.reserve(list.size());
			_iconsCache.reserve(list.size() * 2);
			for (const auto &reaction : list) {
				if (const auto parsed = parse(reaction)) {
					_available.push_back(*parsed);
					if (parsed->active) {
						_active.push_back(*parsed);
						toCache(parsed->appearAnimation);
						toCache(parsed->selectAnimation);
						toCache(parsed->centerIcon);
						toCache(parsed->aroundAnimation);
					}
				}
			}
			if (_waitingForList) {
				_waitingForList = false;
				resolveImages();
			}
			_updated.fire({});
		}, [&](const MTPDmessages_availableReactionsNotModified &) {
		});
	}).fail([=] {
		_requestId = 0;
		_hash = 0;
	}).send();
}

std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
	return entry.match([&](const MTPDavailableReaction &data) {
		const auto emoji = qs(data.vreaction());
		const auto known = (Ui::Emoji::Find(emoji) != nullptr);
		if (!known) {
			LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji));
		}
		const auto selectAnimation = _owner->processDocument(
			data.vselect_animation());
		return known
			? std::make_optional(Reaction{
				.emoji = emoji,
				.title = qs(data.vtitle()),
				.staticIcon = _owner->processDocument(data.vstatic_icon()),
				.appearAnimation = _owner->processDocument(
					data.vappear_animation()),
				.selectAnimation = selectAnimation,
				//.activateAnimation = _owner->processDocument(
				//	data.vactivate_animation()),
				//.activateEffects = _owner->processDocument(
				//	data.veffect_animation()),
				.centerIcon = (data.vcenter_icon()
					? _owner->processDocument(*data.vcenter_icon()).get()
					: nullptr),
				.aroundAnimation = (data.varound_animation()
					? _owner->processDocument(
						*data.varound_animation()).get()
					: nullptr),
				.active = !data.is_inactive(),
			})
			: std::nullopt;
	});
}

void Reactions::send(not_null<HistoryItem*> item, const QString &chosen) {
	const auto id = item->fullId();
	auto &api = _owner->session().api();
	auto i = _sentRequests.find(id);
	if (i != end(_sentRequests)) {
		api.request(i->second).cancel();
	} else {
		i = _sentRequests.emplace(id).first;
	}
	const auto flags = chosen.isEmpty()
		? MTPmessages_SendReaction::Flag(0)
		: MTPmessages_SendReaction::Flag::f_reaction;
	i->second = api.request(MTPmessages_SendReaction(
		MTP_flags(flags),
		item->history()->peer->input,
		MTP_int(id.msg),
		MTP_string(chosen)
	)).done([=](const MTPUpdates &result) {
		_sentRequests.remove(id);
		_owner->session().api().applyUpdates(result);
	}).fail([=](const MTP::Error &error) {
		_sentRequests.remove(id);
	}).send();
}

void Reactions::poll(not_null<HistoryItem*> item, crl::time now) {
	// Group them by one second.
	const auto last = item->lastReactionsRefreshTime();
	const auto grouped = ((last + 999) / 1000) * 1000;
	if (!grouped || item->history()->peer->isUser()) {
		// First reaction always edits message.
		return;
	} else if (const auto left = grouped + kPollEach - now; left > 0) {
		if (!_repaintItems.contains(item)) {
			_repaintItems.emplace(item, grouped + kPollEach);
			if (!_repaintTimer.isActive()
				|| _repaintTimer.remainingTime() > left) {
				_repaintTimer.callOnce(left);
			}
		}
	} else if (!_pollingItems.contains(item)) {
		if (_pollItems.empty() && !_pollRequestId) {
			crl::on_main(&_owner->session(), [=] {
				pollCollected();
			});
		}
		_pollItems.emplace(item);
	}
}

void Reactions::updateAllInHistory(not_null<PeerData*> peer, bool enabled) {
	if (const auto history = _owner->historyLoaded(peer)) {
		history->reactionsEnabledChanged(enabled);
	}
}

void Reactions::repaintCollected() {
	const auto now = crl::now();
	auto closest = 0;
	for (auto i = begin(_repaintItems); i != end(_repaintItems);) {
		if (i->second <= now) {
			_owner->requestItemRepaint(i->first);
			i = _repaintItems.erase(i);
		} else {
			if (!closest || i->second < closest) {
				closest = i->second;
			}
			++i;
		}
	}
	if (closest) {
		_repaintTimer.callOnce(closest - now);
	}
}

void Reactions::pollCollected() {
	auto toRequest = base::flat_map<not_null<PeerData*>, QVector<MTPint>>();
	_pollingItems = std::move(_pollItems);
	for (const auto &item : _pollingItems) {
		toRequest[item->history()->peer].push_back(MTP_int(item->id));
	}
	auto &api = _owner->session().api();
	for (const auto &[peer, ids] : toRequest) {
		const auto finalize = [=] {
			const auto now = crl::now();
			for (const auto &item : base::take(_pollingItems)) {
				const auto last = item->lastReactionsRefreshTime();
				if (last && last + kPollEach <= now) {
					item->updateReactions(nullptr);
				}
			}
			_pollRequestId = 0;
			if (!_pollItems.empty()) {
				crl::on_main(&_owner->session(), [=] {
					pollCollected();
				});
			}
		};
		_pollRequestId = api.request(MTPmessages_GetMessagesReactions(
			peer->input,
			MTP_vector<MTPint>(ids)
		)).done([=](const MTPUpdates &result) {
			_owner->session().api().applyUpdates(result);
			finalize();
		}).fail([=] {
			finalize();
		}).send();
	}
}

bool Reactions::sending(not_null<HistoryItem*> item) const {
	return _sentRequests.contains(item->fullId());
}

MessageReactions::MessageReactions(not_null<HistoryItem*> item)
: _item(item) {
}

void MessageReactions::add(const QString &reaction) {
	if (_chosen == reaction) {
		return;
	}
	const auto history = _item->history();
	const auto self = history->session().user();
	if (!_chosen.isEmpty()) {
		const auto i = _list.find(_chosen);
		Assert(i != end(_list));
		--i->second;
		const auto removed = !i->second;
		if (removed) {
			_list.erase(i);
		}
		const auto j = _recent.find(_chosen);
		if (j != end(_recent)) {
			j->second.erase(ranges::remove(j->second, self), end(j->second));
			if (j->second.empty() || removed) {
				_recent.erase(j);
			}
		}
	}
	_chosen = reaction;
	if (!reaction.isEmpty()) {
		if (_item->canViewReactions()) {
			_recent[reaction].push_back(self);
		}
		++_list[reaction];
	}
	auto &owner = history->owner();
	owner.reactions().send(_item, _chosen);
	owner.notifyItemDataChange(_item);
}

void MessageReactions::remove() {
	add(QString());
}

void MessageReactions::set(
		const QVector<MTPReactionCount> &list,
		const QVector<MTPMessageUserReaction> &recent,
		bool ignoreChosen) {
	auto &owner = _item->history()->owner();
	if (owner.reactions().sending(_item)) {
		// We'll apply non-stale data from the request response.
		return;
	}
	auto changed = false;
	auto existing = base::flat_set<QString>();
	for (const auto &count : list) {
		count.match([&](const MTPDreactionCount &data) {
			const auto reaction = qs(data.vreaction());
			if (data.is_chosen() && !ignoreChosen) {
				if (_chosen != reaction) {
					_chosen = reaction;
					changed = true;
				}
			}
			const auto nowCount = data.vcount().v;
			auto &wasCount = _list[reaction];
			if (wasCount != nowCount) {
				wasCount = nowCount;
				changed = true;
			}
			existing.emplace(reaction);
		});
	}
	if (_list.size() != existing.size()) {
		changed = true;
		for (auto i = begin(_list); i != end(_list);) {
			if (!existing.contains(i->first)) {
				i = _list.erase(i);
			} else {
				++i;
			}
		}
		if (!_chosen.isEmpty() && !_list.contains(_chosen)) {
			_chosen = QString();
		}
	}
	auto parsed = base::flat_map<
		QString,
		std::vector<not_null<UserData*>>>();
	for (const auto &reaction : recent) {
		reaction.match([&](const MTPDmessageUserReaction &data) {
			const auto emoji = qs(data.vreaction());
			if (_list.contains(emoji)) {
				parsed[emoji].push_back(owner.user(data.vuser_id()));
			}
		});
	}
	if (_recent != parsed) {
		_recent = std::move(parsed);
		changed = true;
	}

	if (changed) {
		owner.notifyItemDataChange(_item);
	}
}

const base::flat_map<QString, int> &MessageReactions::list() const {
	return _list;
}

auto MessageReactions::recent() const
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
	return _recent;
}

bool MessageReactions::empty() const {
	return _list.empty();
}

QString MessageReactions::chosen() const {
	return _chosen;
}

} // namespace Data