/*
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 "ui/controls/who_reacted_context_action.h"

#include "base/call_delayed.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/popup_menu.h"
#include "ui/effects/ripple_animation.h"
#include "ui/chat/group_call_userpics.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"

namespace Lang {
namespace {

struct StringWithReacted {
	QString text;
	int seen = 0;
};

} // namespace

template <typename ResultString>
struct StartReplacements;

template <>
struct StartReplacements<StringWithReacted> {
	static inline StringWithReacted Call(QString &&langString) {
		return { std::move(langString) };
	}
};

template <typename ResultString>
struct ReplaceTag;

template <>
struct ReplaceTag<StringWithReacted> {
	static StringWithReacted Call(
		StringWithReacted &&original,
		ushort tag,
		const StringWithReacted &replacement);
};

StringWithReacted ReplaceTag<StringWithReacted>::Call(
		StringWithReacted &&original,
		ushort tag,
		const StringWithReacted &replacement) {
	const auto offset = FindTagReplacementPosition(original.text, tag);
	if (offset < 0) {
		return std::move(original);
	}
	original.text = ReplaceTag<QString>::Call(
		std::move(original.text),
		tag,
		replacement.text + '/' + QString::number(original.seen));
	return std::move(original);
}

} // namespace Lang

namespace Ui {
namespace {

constexpr auto kPreloaderAlpha = 0.2;

using Text::CustomEmojiFactory;

class Action final : public Menu::ItemBase {
public:
	Action(
		not_null<PopupMenu*> parentMenu,
		rpl::producer<WhoReadContent> content,
		CustomEmojiFactory factory,
		Fn<void(uint64)> participantChosen,
		Fn<void()> showAllChosen);

	bool isEnabled() const override;
	not_null<QAction*> action() const override;

	void handleKeyPress(not_null<QKeyEvent*> e) override;

protected:
	QPoint prepareRippleStartPosition() const override;
	QImage prepareRippleMask() const override;

	int contentHeight() const override;

private:
	void paint(Painter &p);

	void updateUserpicsFromContent();
	void resolveMinWidth();
	void refreshText();
	void refreshDimensions();
	void populateSubmenu();

	const not_null<PopupMenu*> _parentMenu;
	const not_null<QAction*> _dummyAction;
	const Fn<void(uint64)> _participantChosen;
	const Fn<void()> _showAllChosen;
	const std::unique_ptr<GroupCallUserpics> _userpics;
	const style::Menu &_st;
	const CustomEmojiFactory _customEmojiFactory;

	WhoReactedListMenu _submenu;

	Text::String _text;
	std::unique_ptr<Ui::Text::CustomEmoji> _custom;
	int _textWidth = 0;
	const int _height = 0;
	int _userpicsWidth = 0;
	bool _appeared = false;

	WhoReadContent _content;

};

TextParseOptions MenuTextOptions = {
	TextParseLinks, // flags
	0, // maxw
	0, // maxh
	Qt::LayoutDirectionAuto, // dir
};

[[nodiscard]] QString FormatReactedString(int reacted, int seen) {
	const auto projection = [&](const QString &text) {
		return Lang::StringWithReacted{ text, seen };
	};
	return tr::lng_context_seen_reacted(
		tr::now,
		lt_count_short,
		reacted,
		projection
	).text;
}

Action::Action(
	not_null<PopupMenu*> parentMenu,
	rpl::producer<WhoReadContent> content,
	Text::CustomEmojiFactory factory,
	Fn<void(uint64)> participantChosen,
	Fn<void()> showAllChosen)
: ItemBase(parentMenu->menu(), parentMenu->menu()->st())
, _parentMenu(parentMenu)
, _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
, _participantChosen(std::move(participantChosen))
, _showAllChosen(std::move(showAllChosen))
, _userpics(std::make_unique<GroupCallUserpics>(
	st::defaultWhoRead.userpics,
	rpl::never<bool>(),
	[=] { update(); }))
, _st(parentMenu->menu()->st())
, _customEmojiFactory(std::move(factory))
, _submenu(_customEmojiFactory, _participantChosen, _showAllChosen)
, _height(st::defaultWhoRead.itemPadding.top()
		+ _st.itemStyle.font->height
		+ st::defaultWhoRead.itemPadding.bottom()) {
	const auto parent = parentMenu->menu();
	const auto delay = anim::Disabled() ? 0 : parentMenu->st().duration;
	const auto checkAppeared = [=, now = crl::now()](bool force = false) {
		_appeared = force || ((crl::now() - now) >= delay);
	};

	setAcceptBoth(true);
	initResizeHook(parent->sizeValue());

	std::move(
		content
	) | rpl::start_with_next([=](WhoReadContent &&content) {
		checkAppeared();
		const auto changed = (_content.participants != content.participants)
			|| (_content.unknown != content.unknown);
		_content = content;
		if (changed) {
			PostponeCall(this, [=] { populateSubmenu(); });
		}
		updateUserpicsFromContent();
		refreshText();
		refreshDimensions();
		setPointerCursor(isEnabled());
		_dummyAction->setEnabled(isEnabled());
		if (!isEnabled()) {
			setSelected(false);
		}
		update();
	}, lifetime());

	resolveMinWidth();

	_userpics->widthValue(
	) | rpl::start_with_next([=](int width) {
		_userpicsWidth = width;
		refreshDimensions();
		update();
	}, lifetime());

	paintRequest(
	) | rpl::start_with_next([=] {
		Painter p(this);
		paint(p);
	}, lifetime());

	clicks(
	) | rpl::start_with_next([=] {
		if (_content.participants.size() == 1) {
			if (const auto onstack = _participantChosen) {
				onstack(_content.participants.front().id);
			}
		} else if (_content.fullReactionsCount > 0) {
			if (const auto onstack = _showAllChosen) {
				onstack();
			}
		}
	}, lifetime());

	enableMouseSelecting();

	base::call_delayed(parentMenu->st().duration, this, [=] {
		if (!_appeared) {
			checkAppeared(true);
			updateUserpicsFromContent();
		}
	});
}

void Action::resolveMinWidth() {
	const auto maxIconWidth = 0;
	const auto width = [&](const QString &text) {
		return _st.itemStyle.font->width(text);
	};
	const auto maxText = (_content.type == WhoReadType::Listened)
		? tr::lng_context_seen_listened(tr::now, lt_count, 999)
		: (_content.type == WhoReadType::Watched)
		? tr::lng_context_seen_watched(tr::now, lt_count, 999)
		: (_content.type == WhoReadType::Seen)
		? tr::lng_context_seen_text(tr::now, lt_count, 999)
		: QString();
	const auto maxReacted = (_content.fullReactionsCount > 0)
		? (!maxText.isEmpty()
			? FormatReactedString(_content.fullReactionsCount, 999)
			: tr::lng_context_seen_reacted(
				tr::now,
				lt_count_short,
				_content.fullReactionsCount))
		: QString();
	const auto maxTextWidth = std::max(width(maxText), width(maxReacted));
	const auto maxWidth = st::defaultWhoRead.itemPadding.left()
		+ maxIconWidth
		+ maxTextWidth
		+ _userpics->maxWidth()
		+ st::defaultWhoRead.itemPadding.right();
	setMinWidth(maxWidth);
}

void Action::updateUserpicsFromContent() {
	if (!_appeared) {
		return;
	}
	auto users = std::vector<GroupCallUser>();
	if (!_content.participants.empty()) {
		const auto count = std::min(
			int(_content.participants.size()),
			WhoReadParticipant::kMaxSmallUserpics);
		const auto factor = style::DevicePixelRatio();
		users.reserve(count);
		for (auto i = 0; i != count; ++i) {
			auto &participant = _content.participants[i];
			participant.userpicSmall.setDevicePixelRatio(factor);
			users.push_back({
				.userpic = participant.userpicSmall,
				.userpicKey = participant.userpicKey,
				.id = participant.id,
			});
		}
	}
	_userpics->update(users, true);
}

void Action::populateSubmenu() {
	if (_content.participants.size() < 1) {
		_submenu.clear();
		_parentMenu->removeSubmenu(action());
		if (!isEnabled()) {
			setSelected(false);
		}
		return;
	}

	const auto submenu = _parentMenu->ensureSubmenu(
		action(),
		st::whoReadMenu);
	_submenu.populate(submenu, _content);
	_parentMenu->checkSubmenuShow();
}

void Action::paint(Painter &p) {
	const auto enabled = isEnabled();
	const auto selected = isSelected();
	if (selected && _st.itemBgOver->c.alpha() < 255) {
		p.fillRect(0, 0, width(), _height, _st.itemBg);
	}
	const auto &bg = selected ? _st.itemBgOver : _st.itemBg;
	p.fillRect(0, 0, width(), _height, bg);
	if (enabled) {
		paintRipple(p, 0, 0);
	}
	if (!_custom && !_content.singleCustomEntityData.isEmpty()) {
		_custom = _customEmojiFactory(
			_content.singleCustomEntityData,
			[=] { update(); });
	}
	if (_custom) {
		const auto ratio = style::DevicePixelRatio();
		const auto size = Emoji::GetSizeNormal() / ratio;
		const auto adjusted = Text::AdjustCustomEmojiSize(size);
		const auto x = st::defaultWhoRead.iconPosition.x()
			+ (st::whoReadChecks.width() - adjusted) / 2;
		const auto y = (_height - adjusted) / 2;
		_custom->paint(p, {
			.textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,
			.now = crl::now(),
			.position = { x, y },
		});
	} else {
		const auto &icon = (_content.fullReactionsCount)
			? (!enabled
				? st::whoReadReactionsDisabled
				: selected
				? st::whoReadReactionsOver
				: st::whoReadReactions)
			: (_content.type == WhoReadType::Seen)
			? (!enabled
				? st::whoReadChecksDisabled
				: selected
				? st::whoReadChecksOver
				: st::whoReadChecks)
			: (!enabled
				? st::whoReadPlayedDisabled
				: selected
				? st::whoReadPlayedOver
				: st::whoReadPlayed);
		icon.paint(p, st::defaultWhoRead.iconPosition, width());
	}
	p.setPen(!enabled
		? _st.itemFgDisabled
		: selected
		? _st.itemFgOver
		: _st.itemFg);
	_text.drawLeftElided(
		p,
		st::defaultWhoRead.itemPadding.left(),
		st::defaultWhoRead.itemPadding.top(),
		_textWidth,
		width());
	if (_appeared) {
		_userpics->paint(
			p,
			width() - st::defaultWhoRead.itemPadding.right(),
			(height() - st::defaultWhoRead.userpics.size) / 2,
			st::defaultWhoRead.userpics.size);
	}
}

void Action::refreshText() {
	const auto usersCount = int(_content.participants.size());
	const auto onlySeenCount = ranges::count(
		_content.participants,
		QString(),
		&WhoReadParticipant::customEntityData);
	const auto count = std::max(_content.fullReactionsCount, usersCount);
	_text.setMarkedText(
		_st.itemStyle,
		{ (_content.unknown
			? tr::lng_context_seen_loading(tr::now)
			: (usersCount == 1)
			? _content.participants.front().name
			: (_content.fullReactionsCount > 0
				&& _content.fullReactionsCount <= _content.fullReadCount)
			? FormatReactedString(
				_content.fullReactionsCount,
				_content.fullReadCount)
			: (_content.type == WhoReadType::Reacted
				|| (count > 0 && _content.fullReactionsCount > usersCount)
				|| (count > 0 && onlySeenCount == 0))
			? (count
				? tr::lng_context_seen_reacted(
					tr::now,
					lt_count_short,
					count)
				: tr::lng_context_seen_reacted_none(tr::now))
			: (_content.type == WhoReadType::Watched)
			? (count
				? tr::lng_context_seen_watched(tr::now, lt_count, count)
				: tr::lng_context_seen_watched_none(tr::now))
			: (_content.type == WhoReadType::Listened)
			? (count
				? tr::lng_context_seen_listened(tr::now, lt_count, count)
				: tr::lng_context_seen_listened_none(tr::now))
			: (count
				? tr::lng_context_seen_text(tr::now, lt_count, count)
				: tr::lng_context_seen_text_none(tr::now))) },
		MenuTextOptions);
}

void Action::refreshDimensions() {
	if (!minWidth()) {
		return;
	}
	const auto textWidth = _text.maxWidth();
	const auto &padding = st::defaultWhoRead.itemPadding;

	const auto goodWidth = padding.left()
		+ textWidth
		+ (_userpicsWidth ? (_st.itemStyle.font->spacew + _userpicsWidth) : 0)
		+ padding.right();

	const auto w = std::clamp(
		goodWidth,
		_st.widthMin,
		std::max(minWidth(), _st.widthMin));
	_textWidth = w - (goodWidth - textWidth);
}

bool Action::isEnabled() const {
	return !_content.participants.empty();
}

not_null<QAction*> Action::action() const {
	return _dummyAction;
}

QPoint Action::prepareRippleStartPosition() const {
	return mapFromGlobal(QCursor::pos());
}

QImage Action::prepareRippleMask() const {
	return Ui::RippleAnimation::RectMask(size());
}

int Action::contentHeight() const {
	return _height;
}

void Action::handleKeyPress(not_null<QKeyEvent*> e) {
	if (!isSelected()) {
		return;
	}
	const auto key = e->key();
	if (key == Qt::Key_Enter || key == Qt::Key_Return) {
		setClicked(Menu::TriggeredSource::Keyboard);
	}
}

} // namespace

WhoReactedEntryAction::WhoReactedEntryAction(
	not_null<RpWidget*> parent,
	CustomEmojiFactory customEmojiFactory,
	const style::Menu &st,
	Data &&data)
: ItemBase(parent, st)
, _dummyAction(CreateChild<QAction>(parent.get()))
, _customEmojiFactory(std::move(customEmojiFactory))
, _st(st)
, _height(st::defaultWhoRead.photoSkip * 2 + st::defaultWhoRead.photoSize) {
	setAcceptBoth(true);

	initResizeHook(parent->sizeValue());
	setData(std::move(data));

	paintRequest(
	) | rpl::start_with_next([=] {
		paint(Painter(this));
	}, lifetime());

	enableMouseSelecting();
}

not_null<QAction*> WhoReactedEntryAction::action() const {
	return _dummyAction.get();
}

bool WhoReactedEntryAction::isEnabled() const {
	return true;
}

int WhoReactedEntryAction::contentHeight() const {
	return _height;
}

void WhoReactedEntryAction::setData(Data &&data) {
	setClickedCallback(std::move(data.callback));
	_userpic = std::move(data.userpic);
	_text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);
	if (data.date.isEmpty()) {
		_date = Text::String();
	} else {
		_date.setMarkedText(
			st::whoReadDateStyle,
			{ data.date },
			MenuTextOptions);
	}
	_type = data.type;
	_custom = _customEmojiFactory
		? _customEmojiFactory(data.customEntityData, [=] { update(); })
		: nullptr;
	const auto ratio = style::DevicePixelRatio();
	const auto size = Emoji::GetSizeNormal() / ratio;
	_customSize = Text::AdjustCustomEmojiSize(size);
	const auto textWidth = std::max(
		_text.maxWidth(),
		st::whoReadDateSkip + _date.maxWidth());
	const auto &padding = _st.itemPadding;
	const auto rightSkip = padding.right()
		+ (_custom ? (size + padding.right()) : 0);
	const auto goodWidth = st::defaultWhoRead.nameLeft
		+ textWidth
		+ rightSkip;
	const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);
	_textWidth = w - (goodWidth - textWidth);
	setMinWidth(w);
	update();
}

void WhoReactedEntryAction::paint(Painter &&p) {
	const auto enabled = isEnabled();
	const auto selected = isSelected();
	if (selected && _st.itemBgOver->c.alpha() < 255) {
		p.fillRect(0, 0, width(), _height, _st.itemBg);
	}
	p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
	if (enabled) {
		paintRipple(p, 0, 0);
	}
	const auto photoSize = st::defaultWhoRead.photoSize;
	const auto photoLeft = st::defaultWhoRead.photoLeft;
	const auto photoTop = (height() - photoSize) / 2;
	const auto preloader = (_type == WhoReactedType::Preloader);
	const auto preloaderBrush = preloader
		? [&] {
			auto color = _st.itemFg->c;
			color.setAlphaF(color.alphaF() * kPreloaderAlpha);
			return QBrush(color);
		}() : QBrush();
	if (preloader) {
		auto hq = PainterHighQualityEnabler(p);
		p.setPen(Qt::NoPen);
		p.setBrush(preloaderBrush);
		p.drawEllipse(photoLeft, photoTop, photoSize, photoSize);
	} else if (!_userpic.isNull()) {
		p.drawImage(photoLeft, photoTop, _userpic);
	} else if (!_custom) {
		st::menuIconReactions.paintInCenter(
			p,
			QRect(photoLeft, photoTop, photoSize, photoSize));
	}

	const auto withDate = !_date.isEmpty();
	const auto textTop = withDate
		? st::whoReadNameWithDateTop
		: (height() - _st.itemStyle.font->height) / 2;
	if (_type == WhoReactedType::Preloader) {
		auto hq = PainterHighQualityEnabler(p);
		p.setPen(Qt::NoPen);
		p.setBrush(preloaderBrush);
		const auto height = _st.itemStyle.font->height / 2;
		p.drawRoundedRect(
			st::defaultWhoRead.nameLeft,
			textTop + (_st.itemStyle.font->height - height) / 2,
			_textWidth,
			height,
			height / 2.,
			height / 2.);
	} else {
		p.setPen(selected
			? _st.itemFgOver
			: enabled
			? _st.itemFg
			: _st.itemFgDisabled);
		_text.drawLeftElided(
			p,
			st::defaultWhoRead.nameLeft,
			textTop,
			_textWidth,
			width());
	}
	if (withDate) {
		const auto iconPosition = QPoint(
			st::defaultWhoRead.nameLeft,
			st::whoReadDateTop) + st::whoReadDateChecksPosition;
		const auto icon = [&] {
			switch (_type) {
			case WhoReactedType::Viewed:
				return &(selected
					? st::whoReadDateChecksOver
					: st::whoReadDateChecks);
			case WhoReactedType::Reacted:
				return &(selected
					? st::whoLikedDateHeartOver
					: st::whoLikedDateHeart);
			case WhoReactedType::Reposted:
				return &(selected
					? st::whoRepostedDateHeartOver
					: st::whoRepostedDateHeart);
			case WhoReactedType::Forwarded:
				return &(selected
					? st::whoForwardedDateHeartOver
					: st::whoForwardedDateHeart);
			}
			Unexpected("Type in WhoReactedEntryAction::paint.");
		}();
		icon->paint(p, iconPosition, width());
		p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
		_date.drawLeftElided(
			p,
			st::defaultWhoRead.nameLeft + st::whoReadDateSkip,
			st::whoReadDateTop,
			_textWidth - st::whoReadDateSkip,
			width());
	}
	if (_custom) {
		const auto ratio = style::DevicePixelRatio();
		const auto size = Emoji::GetSizeNormal() / ratio;
		const auto skip = (size - _customSize) / 2;
		_custom->paint(p, {
			.textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,
			.now = crl::now(),
			.position = QPoint(
				width() - _st.itemPadding.right() - size + skip,
				(height() - _customSize) / 2),
		});
	}
}

bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {
	return (a.id == b.id)
		&& (a.name == b.name)
		&& (a.date == b.date)
		&& (a.userpicKey == b.userpicKey);
}

bool operator!=(const WhoReadParticipant &a, const WhoReadParticipant &b) {
	return !(a == b);
}

base::unique_qptr<Menu::ItemBase> WhoReactedContextAction(
		not_null<PopupMenu*> menu,
		rpl::producer<WhoReadContent> content,
		CustomEmojiFactory factory,
		Fn<void(uint64)> participantChosen,
		Fn<void()> showAllChosen) {
	return base::make_unique_q<Action>(
		menu,
		std::move(content),
		std::move(factory),
		std::move(participantChosen),
		std::move(showAllChosen));
}

WhoReactedListMenu::WhoReactedListMenu(
	CustomEmojiFactory factory,
	Fn<void(uint64)> participantChosen,
	Fn<void()> showAllChosen)
: _customEmojiFactory(std::move(factory))
, _participantChosen(std::move(participantChosen))
, _showAllChosen(std::move(showAllChosen)) {
}

void WhoReactedListMenu::clear() {
	_actions.clear();
}

void WhoReactedListMenu::populate(
		not_null<PopupMenu*> menu,
		const WhoReadContent &content,
		Fn<void()> refillTopActions,
		int addedToBottom,
		Fn<void()> appendBottomActions) {
	const auto reactions = ranges::count_if(
		content.participants,
		[](const auto &p) { return !p.customEntityData.isEmpty(); });
	const auto addShowAll = (content.fullReactionsCount > reactions);
	const auto actionsCount = int(content.participants.size())
		+ (addShowAll ? 1 : 0);
	if (_actions.size() > actionsCount) {
		_actions.clear();
		menu->clearActions();
		if (refillTopActions) {
			refillTopActions();
		}
		addedToBottom = 0;
	}
	auto index = 0;
	const auto append = [&](WhoReactedEntryData &&data) {
		if (index < _actions.size()) {
			_actions[index]->setData(std::move(data));
		} else {
			auto item = base::make_unique_q<WhoReactedEntryAction>(
				menu->menu(),
				_customEmojiFactory,
				menu->menu()->st(),
				std::move(data));
			_actions.push_back(item.get());
			const auto count = int(menu->actions().size());
			if (addedToBottom > 0 && addedToBottom <= count) {
				menu->insertAction(count - addedToBottom, std::move(item));
			} else {
				menu->addAction(std::move(item));
			}
		}
		++index;
	};
	for (const auto &participant : content.participants) {
		const auto chosen = [call = _participantChosen, id = participant.id]{
			call(id);
		};
		append({
			.text = participant.name,
			.date = participant.date,
			.type = (participant.dateReacted
				? WhoReactedType::Reacted
				: WhoReactedType::Viewed),
			.customEntityData = participant.customEntityData,
			.userpic = participant.userpicLarge,
			.callback = chosen,
		});
	}
	if (addShowAll) {
		append({
			.text = tr::lng_context_seen_reacted_all(tr::now),
			.callback = _showAllChosen,
		});
	}
	if (!addedToBottom && appendBottomActions) {
		appendBottomActions();
	}
}

} // namespace Ui
