/*
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 "boxes/peer_list_controllers.h"

#include "api/api_chat_participants.h"
#include "api/api_premium.h" // MessageMoneyRestriction.
#include "base/random.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "settings/settings_common.h"
#include "settings/sections/settings_premium.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/round_checkbox.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "main/main_session.h"
#include "data/data_peer_values.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_folder.h"
#include "data/data_histories.h"
#include "data/data_changes.h"
#include "dialogs/ui/dialogs_layout.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
#include "dialogs/dialogs_main_list.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/effects/outline_segments.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_separate_id.h"
#include "window/window_session_controller.h" // showAddContact()
#include "base/unixtime.h"
#include "styles/style_boxes.h"
#include "styles/style_profile.h"
#include "styles/style_dialogs.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"

namespace {

constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
constexpr auto kSearchPerPage = 50;

} // namespace

object_ptr<Ui::BoxContent> PrepareContactsBox(
		not_null<Window::SessionController*> window) {
	using Mode = ContactsBoxController::SortMode;
	class Controller final : public ContactsBoxController {
	public:
		using ContactsBoxController::ContactsBoxController;

		[[nodiscard]] rpl::producer<not_null<PeerData*>> wheelClicks() const {
			return _wheelClicks.events();
		}

	protected:
		std::unique_ptr<PeerListRow> createRow(
				not_null<UserData*> user) override {
			return !user->isSelf()
				? ContactsBoxController::createRow(user)
				: nullptr;
		}

		void rowMiddleClicked(
				not_null<PeerListRow*> row) override {
			_wheelClicks.fire(row->peer());
		}

	private:
		rpl::event_stream<not_null<PeerData*>> _wheelClicks;

	};
	auto controller = std::make_unique<Controller>(
		&window->session());
	controller->setStyleOverrides(&st::contactsWithStories);
	controller->setStoriesShown(true);
	const auto raw = controller.get();
	auto init = [=](not_null<PeerListBox*> box) {
		struct State {
			QPointer<::Ui::IconButton> toggleSort;
			rpl::variable<Mode> mode = Mode::Online;
			::Ui::Animations::Simple scrollAnimation;
		};

		const auto state = box->lifetime().make_state<State>();
		box->addButton(tr::lng_close(), [=] { box->closeBox(); });
		box->addLeftButton(
			tr::lng_profile_add_contact(),
			[=] { window->showAddContact(); });
		state->toggleSort = box->addTopButton(st::contactsSortButton, [=] {
			const auto online = (state->mode.current() == Mode::Online);
			const auto mode = online ? Mode::Alphabet : Mode::Online;
			state->mode = mode;
			raw->setSortMode(mode);
			state->toggleSort->setIconOverride(
				online ? &st::contactsSortOnlineIcon : nullptr,
				online ? &st::contactsSortOnlineIconOver : nullptr);
		});
		raw->setSortMode(Mode::Online);

		raw->wheelClicks() | rpl::on_next([=](not_null<PeerData*> p) {
			window->showInNewWindow(p);
		}, box->lifetime());

		raw->setShowFinishedCallback([=] {
			window->checkHighlightControl(
				u"contacts/sort"_q,
				state->toggleSort,
				{ .rippleShape = true });
		});
	};
	return Box<PeerListBox>(std::move(controller), std::move(init));
}

QBrush PeerListStoriesGradient(const style::PeerList &st) {
	const auto left = st.item.photoPosition.x();
	const auto top = st.item.photoPosition.y();
	const auto size = st.item.photoSize;
	return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));
}

std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
		PeerListStoriesCounts counts,
		const QBrush &unreadBrush) {
	Expects(counts.unread <= counts.count);
	Expects(counts.count > 0);

	auto result = std::vector<Ui::OutlineSegment>();
	const auto add = [&](bool unread) {
		result.push_back({
			.brush = (counts.videoStream
				? st::attentionButtonFg->b
				: unread
				? unreadBrush
				: st::dialogsUnreadBgMuted->b),
			.width = (unread
				? st::dialogsStoriesFull.lineTwice / 2.
				: st::dialogsStoriesFull.lineReadTwice / 2.),
		});
	};
	if (counts.videoStream) {
		add(true);
	} else {
		const auto count = counts.count;
		const auto unread = counts.unread;
		result.reserve(count);
		for (auto i = 0, till = count - unread; i != till; ++i) {
			add(false);
		}
		for (auto i = 0; i != unread; ++i) {
			add(true);
		}
	}
	return result;
}

void PeerListRowWithLink::setActionLink(const QString &action) {
	_action = action;
	refreshActionLink();
}

void PeerListRowWithLink::refreshActionLink() {
	if (!isInitialized()) return;
	_actionWidth = _action.isEmpty() ? 0 : st::normalFont->width(_action);
}

void PeerListRowWithLink::lazyInitialize(const style::PeerListItem &st) {
	PeerListRow::lazyInitialize(st);
	refreshActionLink();
}

QSize PeerListRowWithLink::rightActionSize() const {
	return QSize(_actionWidth, st::normalFont->height);
}

QMargins PeerListRowWithLink::rightActionMargins() const {
	return QMargins(
		st::contactsCheckPosition.x(),
		(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom() - st::normalFont->height) / 2,
		st::defaultPeerListItem.photoPosition.x() + st::contactsCheckPosition.x(),
		0);
}

void PeerListRowWithLink::rightActionPaint(
		Painter &p,
		int x,
		int y,
		int outerWidth,
		bool selected,
		bool actionSelected) {
	p.setFont(actionSelected ? st::linkFontOver : st::linkFont);
	p.setPen(actionSelected ? st::defaultLinkButton.overColor : st::defaultLinkButton.color);
	p.drawTextLeft(x, y, outerWidth, _action, _actionWidth);
}

PeerListGlobalSearchController::PeerListGlobalSearchController(
	not_null<Main::Session*> session)
: _session(session)
, _api(&session->mtp()) {
	_timer.setCallback([this] { searchOnServer(); });
}

void PeerListGlobalSearchController::searchQuery(const QString &query) {
	if (_query != query) {
		_query = query;
		_requestId = 0;
		if (!_query.isEmpty() && !searchInCache()) {
			_timer.callOnce(AutoSearchTimeout);
		} else {
			_timer.cancel();
		}
	}
}

bool PeerListGlobalSearchController::searchInCache() {
	auto it = _cache.find(_query);
	if (it != _cache.cend()) {
		_requestId = 0;
		searchDone(it->second, _requestId);
		return true;
	}
	return false;
}

void PeerListGlobalSearchController::searchOnServer() {
	_requestId = _api.request(MTPcontacts_Search(
		MTP_string(_query),
		MTP_int(SearchPeopleLimit)
	)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {
		searchDone(result, requestId);
	}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
		if (_requestId == requestId) {
			_requestId = 0;
			delegate()->peerListSearchRefreshRows();
		}
	}).send();
	_queries.emplace(_requestId, _query);
}

void PeerListGlobalSearchController::searchDone(
		const MTPcontacts_Found &result,
		mtpRequestId requestId) {
	Expects(result.type() == mtpc_contacts_found);

	auto &contacts = result.c_contacts_found();
	auto query = _query;
	if (requestId) {
		_session->data().processUsers(contacts.vusers());
		_session->data().processChats(contacts.vchats());
		auto it = _queries.find(requestId);
		if (it != _queries.cend()) {
			query = it->second;
			_cache[query] = result;
			_queries.erase(it);
		}
	}
	const auto feedList = [&](const MTPVector<MTPPeer> &list) {
		for (const auto &mtpPeer : list.v) {
			const auto peer = _session->data().peerLoaded(
				peerFromMTP(mtpPeer));
			if (peer) {
				delegate()->peerListSearchAddRow(peer);
			}
		}
	};
	if (_requestId == requestId) {
		_requestId = 0;
		feedList(contacts.vmy_results());
		feedList(contacts.vresults());
		delegate()->peerListSearchRefreshRows();
	}
}

bool PeerListGlobalSearchController::isLoading() {
	return _timer.isActive() || _requestId;
}

struct RecipientRow::Restriction {
	Api::MessageMoneyRestriction value;
	RestrictionBadgeCache cache;
};

RecipientRow::RecipientRow(
	not_null<PeerData*> peer,
	const style::PeerListItem *maybeLockedSt,
	History *maybeHistory)
: PeerListRow(peer)
, _maybeHistory(maybeHistory)
, _maybeLockedSt(maybeLockedSt) {
	if (_maybeLockedSt) {
		setRestriction(Api::ResolveMessageMoneyRestrictions(
			peer,
			maybeHistory));
	}
}

Api::MessageMoneyRestriction RecipientRow::restriction() const {
	return _restriction
		? _restriction->value
		: Api::MessageMoneyRestriction();
}

void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) {
	if (!restriction) {
		_restriction = nullptr;
		return;
	} else if (!_restriction) {
		_restriction = std::make_unique<Restriction>();
	}
	_restriction->value = restriction;
}

void RecipientRow::paintUserpicOverlay(
		Painter &p,
		const style::PeerListItem &st,
		int x,
		int y,
		int outerWidth) {
	if (const auto &r = _restriction) {
		PaintRestrictionBadge(
			p,
			_maybeLockedSt,
			r->value.starsPerMessage,
			r->cache,
			x,
			y,
			outerWidth,
			st.photoSize);
	}
}

bool RecipientRow::refreshLock(
		not_null<const style::PeerListItem*> maybeLockedSt) {
	if (const auto user = peer()->asUser()) {
		using Restriction = Api::MessageMoneyRestriction;
		const auto r = _maybeLockedSt
			? Api::ResolveMessageMoneyRestrictions(
				user,
				_maybeHistory)
			: Restriction();
		if ((_restriction ? _restriction->value : Restriction()) != r) {
			setRestriction(r);
			return true;
		}
	}
	return false;
}

void RecipientRow::preloadUserpic() {
	PeerListRow::preloadUserpic();

	if (!_maybeLockedSt) {
		return;
	}
	const auto peer = this->peer();
	const auto known = Api::ResolveMessageMoneyRestrictions(
		peer,
		_maybeHistory).known;
	if (known) {
		return;
	} else if (const auto user = peer->asUser()) {
		const auto api = &user->session().api();
		api->premium().resolveMessageMoneyRestrictions(user);
	} else if (const auto group = peer->asChannel()) {
		group->updateFull();
	}
}

void TrackMessageMoneyRestrictionsChanges(
		not_null<PeerListController*> controller,
		rpl::lifetime &lifetime) {
	const auto session = &controller->session();
	rpl::merge(
		Data::AmPremiumValue(session) | rpl::to_empty,
		session->api().premium().someMessageMoneyRestrictionsResolved()
	) | rpl::on_next([=] {
		const auto st = &controller->computeListSt().item;
		const auto delegate = controller->delegate();
		const auto process = [&](not_null<PeerListRow*> raw) {
			if (static_cast<RecipientRow*>(raw.get())->refreshLock(st)) {
				delegate->peerListUpdateRow(raw);
			}
		};
		auto count = delegate->peerListFullRowsCount();
		for (auto i = 0; i != count; ++i) {
			process(delegate->peerListRowAt(i));
		}
		count = delegate->peerListSearchRowsCount();
		for (auto i = 0; i != count; ++i) {
			process(delegate->peerListSearchRowAt(i));
		}
	}, lifetime);
}

ChatsListBoxController::Row::Row(
	not_null<History*> history,
	const style::PeerListItem *maybeLockedSt)
: RecipientRow(history->peer, maybeLockedSt, history) {
}

ChatsListBoxController::ChatsListBoxController(
	not_null<Main::Session*> session)
: ChatsListBoxController(
	std::make_unique<PeerListGlobalSearchController>(session)) {
}

ChatsListBoxController::ChatsListBoxController(
	std::unique_ptr<PeerListSearchController> searchController)
: PeerListController(std::move(searchController)) {
}

void ChatsListBoxController::prepare() {
	setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
	delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);

	prepareViewHook();

	if (!session().data().chatsListLoaded()) {
		session().data().chatsListLoadedEvents(
		) | rpl::filter([=](Data::Folder *folder) {
			return !folder;
		}) | rpl::on_next([=] {
			checkForEmptyRows();
		}, lifetime());
	}

	session().data().chatsListChanges(
	) | rpl::on_next([=] {
		rebuildRows();
	}, lifetime());

	session().data().contactsLoaded().value(
	) | rpl::on_next([=] {
		rebuildRows();
	}, lifetime());
}

void ChatsListBoxController::rebuildRows() {
	auto wasEmpty = !delegate()->peerListFullRowsCount();
	auto appendList = [this](auto chats) {
		auto count = 0;
		for (const auto &row : chats->all()) {
			if (const auto history = row->history()) {
				if (appendRow(history)) {
					++count;
				}
			}
		}
		return count;
	};
	auto added = 0;
	if (!savedMessagesChatStatus().isEmpty()) {
		if (appendRow(session().data().history(session().user()))) {
			++added;
		}
	}
	added += appendList(session().data().chatsList()->indexed());
	const auto id = Data::Folder::kId;
	if (const auto folder = session().data().folderLoaded(id)) {
		added += appendList(folder->chatsList()->indexed());
	}
	added += appendList(session().data().contactsNoChatsList());
	if (!wasEmpty && added > 0) {
		// Place dialogs list before contactsNoDialogs list.
		delegate()->peerListPartitionRows([](const PeerListRow &a) {
			const auto history = static_cast<const Row&>(a).history();
			return history->inChatList();
		});
		if (!savedMessagesChatStatus().isEmpty()) {
			delegate()->peerListPartitionRows([](const PeerListRow &a) {
				return a.peer()->isSelf();
			});
		}
	}
	checkForEmptyRows();
	delegate()->peerListRefreshRows();
}

void ChatsListBoxController::checkForEmptyRows() {
	if (delegate()->peerListFullRowsCount()) {
		setDescriptionText(QString());
	} else {
		const auto loaded = session().data().contactsLoaded().current()
			&& session().data().chatsListLoaded();
		setDescriptionText(loaded ? emptyBoxText() : tr::lng_contacts_loading(tr::now));
	}
}

QString ChatsListBoxController::emptyBoxText() const {
	return tr::lng_contacts_not_found(tr::now);
}

std::unique_ptr<PeerListRow> ChatsListBoxController::createSearchRow(
		not_null<PeerData*> peer) {
	return createRow(peer->owner().history(peer));
}

bool ChatsListBoxController::appendRow(not_null<History*> history) {
	if (auto row = delegate()->peerListFindRow(history->peer->id.value)) {
		updateRowHook(static_cast<Row*>(row));
		return false;
	}
	if (auto row = createRow(history)) {
		delegate()->peerListAppendRow(std::move(row));
		return true;
	}
	return false;
}

PeerListStories::PeerListStories(
	not_null<PeerListController*> controller,
	not_null<Main::Session*> session)
: _controller(controller)
, _session(session) {
}

void PeerListStories::updateColors() {
	for (auto i = begin(_counts); i != end(_counts); ++i) {
		if (const auto row = _delegate->peerListFindRow(i->first)) {
			if (i->second.count >= 0 && i->second.unread >= 0) {
				applyForRow(row, i->second, true);
			}
		}
	}
}

void PeerListStories::updateFor(uint64 id, Counts counts) {
	if (const auto row = _delegate->peerListFindRow(id)) {
		applyForRow(row, counts);
		_delegate->peerListUpdateRow(row);
	}
}

void PeerListStories::process(not_null<PeerListRow*> row) {
	const auto user = row->peer()->asUser();
	if (!user) {
		return;
	}
	const auto stories = &_session->data().stories();
	const auto source = stories->source(user->id);
	const auto count = source
		? int(source->ids.size())
		: user->hasActiveStories()
		? 1
		: 0;
	const auto unread = source
		? int(source->info().unreadCount)
		: user->hasUnreadStories()
		? 1
		: 0;
	const auto videoStream = source
		? bool(source->info().hasVideoStream)
		: user->hasActiveVideoStream();
	applyForRow(row, { count, unread, videoStream }, true);
}

bool PeerListStories::handleClick(not_null<PeerData*> peer) {
	const auto point = _delegate->peerListLastRowMousePosition();
	const auto &st = _controller->computeListSt().item;
	if (point && point->x() < st.photoPosition.x() + st.photoSize) {
		if (const auto window = peer->session().tryResolveWindow()) {
			if (const auto user = peer->asUser()) {
				if (user->hasActiveStories()) {
					window->openPeerStories(peer->id);
					return true;
				}
			}
		}
	}
	return false;
}

void PeerListStories::prepare(not_null<PeerListDelegate*> delegate) {
	_delegate = delegate;

	_unreadBrush = PeerListStoriesGradient(_controller->computeListSt());
	style::PaletteChanged() | rpl::on_next([=] {
		_unreadBrush = PeerListStoriesGradient(_controller->computeListSt());
		updateColors();
	}, _lifetime);

	_session->changes().peerUpdates(
		Data::PeerUpdate::Flag::StoriesState
	) | rpl::on_next([=](const Data::PeerUpdate &update) {
		const auto id = update.peer->id.value;
		if (const auto row = _delegate->peerListFindRow(id)) {
			process(row);
		}
	}, _lifetime);

	const auto stories = &_session->data().stories();
	stories->sourceChanged() | rpl::on_next([=](PeerId id) {
		const auto source = stories->source(id);
		const auto info = source
			? source->info()
			: Data::StoriesSourceInfo();
		updateFor(id.value, {
			int(info.count),
			int(info.unreadCount),
			bool(info.hasVideoStream),
		});
	}, _lifetime);
}

void PeerListStories::applyForRow(
		not_null<PeerListRow*> row,
		Counts counts,
		bool force) {
	auto &existing = _counts[row->id()];
	if (!force && existing == counts) {
		return;
	}
	existing = counts;
	_delegate->peerListSetRowChecked(row, counts.count > 0);
	if (counts.count > 0) {
		row->setCustomizedCheckSegments(
			PeerListStoriesSegments(counts, _unreadBrush),
			counts.videoStream);
	}
}

ContactsBoxController::ContactsBoxController(
	not_null<Main::Session*> session)
: ContactsBoxController(
	session,
	std::make_unique<PeerListGlobalSearchController>(session)) {
}

ContactsBoxController::ContactsBoxController(
	not_null<Main::Session*> session,
	std::unique_ptr<PeerListSearchController> searchController)
: PeerListController(std::move(searchController))
, _session(session)
, _sortByOnlineTimer([=] { sort(); }) {
}

Main::Session &ContactsBoxController::session() const {
	return *_session;
}

void ContactsBoxController::prepare() {
	setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
	delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
	delegate()->peerListSetTitle(tr::lng_contacts_header());

	prepareViewHook();

	if (_stories) {
		_stories->prepare(delegate());
	}

	session().data().contactsLoaded().value(
	) | rpl::on_next([=] {
		rebuildRows();
	}, lifetime());
}

void ContactsBoxController::rebuildRows() {
	const auto appendList = [&](auto chats) {
		auto count = 0;
		for (const auto &row : chats->all()) {
			if (const auto history = row->history()) {
				if (const auto user = history->peer->asUser()) {
					if (appendRow(user)) {
						++count;
					}
				}
			}
		}
		return count;
	};
	appendList(session().data().contactsList());
	checkForEmptyRows();
	sort();
	delegate()->peerListRefreshRows();
}

void ContactsBoxController::checkForEmptyRows() {
	setDescriptionText(delegate()->peerListFullRowsCount()
		? QString()
		: session().data().contactsLoaded().current()
		? tr::lng_contacts_not_found(tr::now)
		: tr::lng_contacts_loading(tr::now));
}

std::unique_ptr<PeerListRow> ContactsBoxController::createSearchRow(
		not_null<PeerData*> peer) {
	if (const auto user = peer->asUser()) {
		return createRow(user);
	}
	return nullptr;
}

void ContactsBoxController::rowClicked(not_null<PeerListRow*> row) {
	const auto peer = row->peer();
	if (_stories && _stories->handleClick(peer)) {
		return;
	} else if (const auto window = peer->session().tryResolveWindow()) {
		window->showPeerHistory(peer);
	}
}

void ContactsBoxController::setSortMode(SortMode mode) {
	if (_sortMode == mode) {
		return;
	}
	_sortMode = mode;
	sort();
	if (_sortMode == SortMode::Online) {
		session().changes().peerUpdates(
			Data::PeerUpdate::Flag::OnlineStatus
		) | rpl::filter([=](const Data::PeerUpdate &update) {
			return !_sortByOnlineTimer.isActive()
				&& delegate()->peerListFindRow(update.peer->id.value);
		}) | rpl::on_next([=] {
			_sortByOnlineTimer.callOnce(kSortByOnlineThrottle);
		}, _sortByOnlineLifetime);
	} else {
		_sortByOnlineTimer.cancel();
		_sortByOnlineLifetime.destroy();
	}
}

void ContactsBoxController::setStoriesShown(bool shown) {
	_stories = std::make_unique<PeerListStories>(this, _session);
}

void ContactsBoxController::sort() {
	switch (_sortMode) {
	case SortMode::Alphabet: sortByName(); break;
	case SortMode::Online: sortByOnline(); break;
	default: Unexpected("SortMode in ContactsBoxController.");
	}
}

void ContactsBoxController::sortByOnline() {
	const auto now = base::unixtime::now();
	const auto key = [&](const PeerListRow &row) {
		const auto user = row.peer()->asUser();
		return user
			? (std::min(user->lastseen().onlineTill(), now + 1) + 1)
			: TimeId();
	};
	const auto predicate = [&](const PeerListRow &a, const PeerListRow &b) {
		return key(a) > key(b);
	};
	delegate()->peerListSortRows(predicate);
}

bool ContactsBoxController::appendRow(not_null<UserData*> user) {
	if (auto row = delegate()->peerListFindRow(user->id.value)) {
		updateRowHook(row);
		return false;
	}
	if (auto row = createRow(user)) {
		const auto raw = row.get();
		delegate()->peerListAppendRow(std::move(row));
		if (_stories) {
			_stories->process(raw);
		}
		return true;
	}
	return false;
}

std::unique_ptr<PeerListRow> ContactsBoxController::createRow(
		not_null<UserData*> user) {
	return std::make_unique<PeerListRow>(user);
}

RecipientMoneyRestrictionError WriteMoneyRestrictionError(
		not_null<UserData*> user) {
	return {
		.text = tr::lng_send_non_premium_message_toast(
			tr::now,
			lt_user,
			TextWithEntities{ user->shortName() },
			lt_link,
			tr::link(
				tr::bold(
					tr::lng_send_non_premium_message_toast_link(
						tr::now))),
			tr::rich),
	};
}

ChooseRecipientBoxController::ChooseRecipientBoxController(
	not_null<Main::Session*> session,
	FnMut<void(not_null<Data::Thread*>)> callback,
	Fn<bool(not_null<Data::Thread*>)> filter)
: ChooseRecipientBoxController({
	.session = session,
	.callback = std::move(callback),
	.filter = std::move(filter),
}) {
}

ChooseRecipientBoxController::ChooseRecipientBoxController(
	ChooseRecipientArgs &&args)
: ChatsListBoxController(args.session)
, _session(args.session)
, _callback(std::move(args.callback))
, _filter(std::move(args.filter))
, _moneyRestrictionError(std::move(args.moneyRestrictionError)) {
}

Main::Session &ChooseRecipientBoxController::session() const {
	return *_session;
}

void ChooseRecipientBoxController::prepareViewHook() {
	delegate()->peerListSetTitle(tr::lng_forward_choose());

	if (_moneyRestrictionError) {
		TrackMessageMoneyRestrictionsChanges(this, lifetime());
	}
}

bool ChooseRecipientBoxController::showLockedError(
		not_null<PeerListRow*> row) {
	return RecipientRow::ShowLockedError(
		this,
		row,
		_moneyRestrictionError);
}

void ChooseRecipientBoxController::rowClicked(not_null<PeerListRow*> row) {
	if (showLockedError(row)) {
		return;
	}
	auto guard = base::make_weak(this);
	const auto peer = row->peer();
	if (const auto forum = peer->forum()) {
		const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
		auto callback = [=](not_null<Data::Thread*> thread) {
			const auto exists = guard.get();
			if (!exists) {
				if (*weak) {
					(*weak)->closeBox();
				}
				return;
			}
			auto onstack = std::move(_callback);
			onstack(thread);
			if (guard) {
				_callback = std::move(onstack);
			} else if (*weak) {
				(*weak)->closeBox();
			}
		};
		const auto filter = [=](not_null<Data::Thread*> thread) {
			return guard && (!_filter || _filter(thread));
		};
		auto owned = Box<PeerListBox>(
			std::make_unique<ChooseTopicBoxController>(
				forum,
				std::move(callback),
				filter),
			[=](not_null<PeerListBox*> box) {
				box->addButton(tr::lng_cancel(), [=] {
					box->closeBox();
				});

				forum->destroyed(
				) | rpl::on_next([=] {
					box->closeBox();
				}, box->lifetime());
			});
		*weak = owned.data();
		delegate()->peerListUiShow()->showBox(std::move(owned));
		return;
	} else if (const auto monoforum = peer->monoforum()) {
		const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
		auto callback = [=](not_null<Data::SavedSublist*> sublist) {
			const auto exists = guard.get();
			if (!exists) {
				if (*weak) {
					(*weak)->closeBox();
				}
				return;
			}
			auto onstack = std::move(_callback);
			onstack(sublist);
			if (guard) {
				_callback = std::move(onstack);
			} else if (*weak) {
				(*weak)->closeBox();
			}
		};
		const auto filter = [=](not_null<Data::SavedSublist*> sublist) {
			return guard && (!_filter || _filter(sublist));
		};
		auto owned = Box<PeerListBox>(
			std::make_unique<ChooseSublistBoxController>(
				monoforum,
				std::move(callback),
				filter),
			[=](not_null<PeerListBox*> box) {
				box->addButton(tr::lng_cancel(), [=] {
					box->closeBox();
				});

				monoforum->destroyed(
				) | rpl::on_next([=] {
					box->closeBox();
				}, box->lifetime());
			});
		*weak = owned.data();
		delegate()->peerListUiShow()->showBox(std::move(owned));
		return;
	}
	const auto history = peer->owner().history(peer);
	auto callback = std::move(_callback);
	callback(history);
	if (guard) {
		_callback = std::move(callback);
	}
}

bool RecipientRow::ShowLockedError(
		not_null<PeerListController*> controller,
		not_null<PeerListRow*> row,
		Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> error) {
	const auto recipient = static_cast<RecipientRow*>(row.get());
	if (!recipient->restriction().premiumRequired) {
		return false;
	}
	::Settings::ShowPremiumPromoToast(
		controller->delegate()->peerListUiShow(),
		ChatHelpers::ResolveWindowDefault(),
		error(row->peer()->asUser()).text,
		u"require_premium"_q);
	return true;
}

QString ChooseRecipientBoxController::savedMessagesChatStatus() const {
	return tr::lng_saved_forward_here(tr::now);
}

auto ChooseRecipientBoxController::createRow(
		not_null<History*> history) -> std::unique_ptr<Row> {
	const auto peer = history->peer;
	const auto skip = _filter
		? !_filter(history)
		: ((peer->isBroadcast() && !Data::CanSendAnything(peer))
			|| peer->isRepliesChat()
			|| peer->isVerifyCodes()
			|| (peer->isUser() && (_moneyRestrictionError
				? !peer->asUser()->canSendIgnoreMoneyRestrictions()
				: !Data::CanSendAnything(peer))));
	if (skip) {
		return nullptr;
	}
	auto result = std::make_unique<Row>(
		history,
		_moneyRestrictionError ? &computeListSt().item : nullptr);
	return result;
}

ChooseTopicSearchController::ChooseTopicSearchController(
	not_null<Data::Forum*> forum)
: _forum(forum)
, _api(&forum->session().mtp())
, _timer([=] { searchOnServer(); }) {
}

void ChooseTopicSearchController::searchQuery(const QString &query) {
	if (_query != query) {
		_query = query;
		_api.request(base::take(_requestId)).cancel();
		_offsetDate = 0;
		_offsetId = 0;
		_offsetTopicId = 0;
		_allLoaded = false;
		if (!_query.isEmpty()) {
			_timer.callOnce(AutoSearchTimeout);
		} else {
			_timer.cancel();
		}
	}
}

void ChooseTopicSearchController::searchOnServer() {
	_requestId = _api.request(MTPmessages_GetForumTopics(
		MTP_flags(MTPmessages_GetForumTopics::Flag::f_q),
		_forum->peer()->input(),
		MTP_string(_query),
		MTP_int(_offsetDate),
		MTP_int(_offsetId),
		MTP_int(_offsetTopicId),
		MTP_int(kSearchPerPage)
	)).done([=](const MTPmessages_ForumTopics &result) {
		_requestId = 0;
		const auto savedTopicId = _offsetTopicId;
		const auto byCreation = result.data().is_order_by_create_date();
		_forum->applyReceivedTopics(result, [&](
				not_null<Data::ForumTopic*> topic) {
			_offsetTopicId = topic->rootId();
			if (byCreation) {
				_offsetDate = topic->creationDate();
				if (const auto last = topic->lastServerMessage()) {
					_offsetId = last->id;
				}
			} else if (const auto last = topic->lastServerMessage()) {
				_offsetId = last->id;
				_offsetDate = last->date();
			}
			delegate()->peerListSearchAddRow(topic->rootId().bare);
		});
		if (_offsetTopicId == savedTopicId) {
			_allLoaded = true;
		}
		delegate()->peerListSearchRefreshRows();
	}).fail([=] {
		_allLoaded = true;
	}).send();
}

bool ChooseTopicSearchController::isLoading() {
	return _timer.isActive() || _requestId;
}

bool ChooseTopicSearchController::loadMoreRows() {
	if (!isLoading()) {
		searchOnServer();
	}
	return !_allLoaded;
}

ChooseTopicBoxController::Row::Row(not_null<Data::ForumTopic*> topic)
: PeerListRow(topic->rootId().bare)
, _topic(topic) {
}

QString ChooseTopicBoxController::Row::generateName() {
	return _topic->title();
}

QString ChooseTopicBoxController::Row::generateShortName() {
	return _topic->title();
}

auto ChooseTopicBoxController::Row::generatePaintUserpicCallback(
	bool forceRound)
-> PaintRoundImageCallback {
	return [=](
			Painter &p,
			int x,
			int y,
			int outerWidth,
			int size) {
		const auto &st = st::forumTopicRow;
		x -= st.padding.left();
		y -= st.padding.top();
		auto view = Ui::PeerUserpicView();
		p.translate(x, y);
		_topic->paintUserpic(p, view, {
			.st = &st,
			.currentBg = st::windowBg,
			.now = crl::now(),
			.width = outerWidth,
			.paused = false,
		});
		p.translate(-x, -y);
	};
}

auto ChooseTopicBoxController::Row::generateNameFirstLetters() const
-> const base::flat_set<QChar> & {
	return _topic->chatListFirstLetters();
}

auto ChooseTopicBoxController::Row::generateNameWords() const
-> const base::flat_set<QString> & {
	return _topic->chatListNameWords();
}

QString ChooseTopicBoxController::AllMessagesRow::name() const {
	return _userCreatesTopics
		? tr::lng_forum_create_new_topic(tr::now)
		: tr::lng_forum_all_messages(tr::now);
}

ChooseTopicBoxController::AllMessagesRow::AllMessagesRow(bool userCreatesTopics)
: PeerListRow(PeerListRowId(0))
, _userCreatesTopics(userCreatesTopics) {
	const auto words = TextUtilities::PrepareSearchWords(name());
	for (const auto &word : words) {
		_nameWords.emplace(word);
		_nameFirstLetters.emplace(word[0]);
	}
}

QString ChooseTopicBoxController::AllMessagesRow::generateName() {
	return name();
}

QString ChooseTopicBoxController::AllMessagesRow::generateShortName() {
	return name();
}

auto ChooseTopicBoxController::AllMessagesRow::generatePaintUserpicCallback(
	bool forceRound)
-> PaintRoundImageCallback {
	return [userCreatesTopics = _userCreatesTopics](
			Painter &p,
			int x,
			int y,
			int outerWidth,
			int size) {
		const auto &icon = userCreatesTopics
			? st::menuIconDiscussion
			: st::menuIconChats;
		icon.paintInCenter(
			p,
			QRect(x, y - st::lineWidth, size, size));
	};
}

auto ChooseTopicBoxController::AllMessagesRow::generateNameFirstLetters() const
-> const base::flat_set<QChar> & {
	return _nameFirstLetters;
}

auto ChooseTopicBoxController::AllMessagesRow::generateNameWords() const
-> const base::flat_set<QString> & {
	return _nameWords;
}

ChooseTopicBoxController::ChooseTopicBoxController(
	not_null<Data::Forum*> forum,
	FnMut<void(not_null<Data::Thread*>)> callback,
	Fn<bool(not_null<Data::Thread*>)> filter)
: PeerListController(std::make_unique<ChooseTopicSearchController>(forum))
, _forum(forum)
, _callback(std::move(callback))
, _filter(std::move(filter)) {
	setStyleOverrides(&st::chooseTopicList);

	_forum->chatsListChanges(
	) | rpl::on_next([=] {
		refreshRows();
	}, lifetime());

	_forum->topicDestroyed(
	) | rpl::on_next([=](not_null<Data::ForumTopic*> topic) {
		const auto id = PeerListRowId(topic->rootId().bare);
		if (const auto row = delegate()->peerListFindRow(id)) {
			delegate()->peerListRemoveRow(row);
			delegate()->peerListRefreshRows();
		}
	}, lifetime());
}

Main::Session &ChooseTopicBoxController::session() const {
	return _forum->session();
}

void ChooseTopicBoxController::rowClicked(not_null<PeerListRow*> row) {
	const auto weak = base::make_weak(this);
	auto onstack = base::take(_callback);
	if (row->id() == PeerListRowId(0)) {
		onstack(_forum->history());
	} else {
		onstack(static_cast<Row*>(row.get())->topic());
	}
	if (weak) {
		_callback = std::move(onstack);
	}
}

void ChooseTopicBoxController::prepare() {
	delegate()->peerListSetTitle(tr::lng_forward_choose());
	setSearchNoResultsText(tr::lng_topics_not_found(tr::now));
	delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
	refreshRows(true);

	session().changes().entryUpdates(
		Data::EntryUpdate::Flag::Repaint
	) | rpl::on_next([=](const Data::EntryUpdate &update) {
		if (const auto topic = update.entry->asTopic()) {
			if (topic->forum() == _forum) {
				const auto id = topic->rootId().bare;
				if (const auto row = delegate()->peerListFindRow(id)) {
					delegate()->peerListUpdateRow(row);
				}
			}
		}
	}, lifetime());
}

void ChooseTopicBoxController::refreshRows(bool initial) {
	auto added = false;
	if (_forum->bot()
		&& !delegate()->peerListFindRow(PeerListRowId(0))
		&& (!_filter || _filter(_forum->history()))) {
		const auto userCreatesTopics = Data::IsBotUserCreatesTopics(
			_forum->peer());
		delegate()->peerListAppendRow(
			std::make_unique<AllMessagesRow>(userCreatesTopics));
		added = true;
	}
	for (const auto &row : _forum->topicsList()->indexed()->all()) {
		if (const auto topic = row->topic()) {
			const auto id = topic->rootId().bare;
			auto already = delegate()->peerListFindRow(id);
			if (initial || !already) {
				if (auto created = createRow(topic)) {
					delegate()->peerListAppendRow(std::move(created));
					added = true;
				}
			} else if (already->isSearchResult()) {
				delegate()->peerListAppendFoundRow(already);
				added = true;
			}
		}
	}
	if (added) {
		delegate()->peerListRefreshRows();
	}
}

void ChooseTopicBoxController::loadMoreRows() {
	_forum->requestTopics();
}

std::unique_ptr<PeerListRow> ChooseTopicBoxController::createSearchRow(
		PeerListRowId id) {
	if (const auto topic = _forum->topicFor(MsgId(id))) {
		return std::make_unique<Row>(topic);
	}
	return nullptr;
}

std::unique_ptr<PeerListRow> ChooseTopicBoxController::MakeRow(
		not_null<Data::ForumTopic*> topic) {
	return std::make_unique<Row>(topic);
}

auto ChooseTopicBoxController::createRow(not_null<Data::ForumTopic*> topic)
-> std::unique_ptr<Row> {
	const auto skip = _filter && !_filter(topic);
	return skip ? nullptr : std::make_unique<Row>(topic);
};

ChooseSublistBoxController::ChooseSublistBoxController(
	not_null<Data::SavedMessages*> monoforum,
	FnMut<void(not_null<Data::SavedSublist*>)> callback,
	Fn<bool(not_null<Data::SavedSublist*>)> filter)
: _monoforum(monoforum)
, _callback(std::move(callback))
, _filter(std::move(filter)) {
	setStyleOverrides(&st::chooseTopicList);

	_monoforum->chatsListChanges(
	) | rpl::on_next([=] {
		refreshRows();
	}, lifetime());

	_monoforum->sublistDestroyed(
	) | rpl::on_next([=](not_null<Data::SavedSublist*> sublist) {
		const auto id = sublist->sublistPeer()->id.value;
		if (const auto row = delegate()->peerListFindRow(id)) {
			delegate()->peerListRemoveRow(row);
			delegate()->peerListRefreshRows();
		}
	}, lifetime());
}

Main::Session &ChooseSublistBoxController::session() const {
	return _monoforum->session();
}

void ChooseSublistBoxController::rowClicked(not_null<PeerListRow*> row) {
	const auto weak = base::make_weak(this);
	auto onstack = base::take(_callback);
	onstack(_monoforum->sublist(row->peer()));
	if (weak) {
		_callback = std::move(onstack);
	}
}

void ChooseSublistBoxController::prepare() {
	delegate()->peerListSetTitle(tr::lng_forward_choose());
	setSearchNoResultsText(tr::lng_topics_not_found(tr::now));
	delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
	refreshRows(true);

	session().changes().entryUpdates(
		Data::EntryUpdate::Flag::Repaint
	) | rpl::on_next([=](const Data::EntryUpdate &update) {
		if (const auto sublist = update.entry->asSublist()) {
			if (sublist->parent() == _monoforum) {
				const auto id = sublist->sublistPeer()->id.value;
				if (const auto row = delegate()->peerListFindRow(id)) {
					delegate()->peerListUpdateRow(row);
				}
			}
		}
	}, lifetime());
}

void ChooseSublistBoxController::refreshRows(bool initial) {
	auto added = false;
	for (const auto &row : _monoforum->chatsList()->indexed()->all()) {
		if (const auto sublist = row->sublist()) {
			const auto id = sublist->sublistPeer()->id.value;
			auto already = delegate()->peerListFindRow(id);
			if (initial || !already) {
				if (auto created = createRow(sublist)) {
					delegate()->peerListAppendRow(std::move(created));
					added = true;
				}
			} else if (already->isSearchResult()) {
				delegate()->peerListAppendFoundRow(already);
				added = true;
			}
		}
	}
	if (added) {
		delegate()->peerListRefreshRows();
	}
}

void ChooseSublistBoxController::loadMoreRows() {
	_monoforum->loadMore();
}

std::unique_ptr<PeerListRow> ChooseSublistBoxController::createSearchRow(
		PeerListRowId id) {
	const auto peer = session().data().peer(PeerId(id));
	if (const auto sublist = _monoforum->sublistLoaded(peer)) {
		auto result = std::make_unique<PeerListRow>(sublist->sublistPeer());
		result->setCustomStatus(QString());
		return result;
	}
	return nullptr;
}

auto ChooseSublistBoxController::createRow(
	not_null<Data::SavedSublist*> sublist)
-> std::unique_ptr<PeerListRow> {
	if (_filter && !_filter(sublist)) {
		return nullptr;
	}
	auto result = std::make_unique<PeerListRow>(sublist->sublistPeer());
	result->setCustomStatus(QString());
	return result;
};

void PaintRestrictionBadge(
		Painter &p,
		not_null<const style::PeerListItem*> st,
		int stars,
		RestrictionBadgeCache &cache,
		int x,
		int y,
		int outerWidth,
		int size) {
	const auto paletteVersion = style::PaletteVersion();
	const auto good = !cache.badge.isNull()
		&& (cache.stars == stars)
		&& (cache.paletteVersion == paletteVersion);
	const auto &check = st->checkbox.check;
	const auto add = check.width;
	if (!good) {
		cache.stars = stars;
		cache.paletteVersion = paletteVersion;
		if (stars) {
			const auto text = (stars >= 1000)
				? (QString::number(stars / 1000) + 'K')
				: QString::number(stars);
			cache.badge = Ui::GenerateSmallBadgeImage(
				text,
				st::paidReactTopStarIcon,
				check.bgActive->c,
				st::premiumButtonFg->c,
				&check);
		} else {
			auto hq = PainterHighQualityEnabler(p);
			const auto &icon = st::stickersPremiumLock;
			const auto width = icon.width();
			const auto height = icon.height();
			const auto rect = QRect(
				QPoint(x + size - width, y + size - height),
				icon.size());
			const auto added = QMargins(add, add, add, add);
			const auto ratio = style::DevicePixelRatio();
			cache.badge = QImage(
				(rect + added).size() * ratio,
				QImage::Format_ARGB32_Premultiplied);
			cache.badge.setDevicePixelRatio(ratio);
			cache.badge.fill(Qt::transparent);
			const auto inner = QRect(add, add, rect.width(), rect.height());
			auto q = QPainter(&cache.badge);
			auto pen = check.border->p;
			pen.setWidthF(check.width);
			q.setPen(pen);
			q.setBrush(st::premiumButtonBg2);
			q.drawEllipse(inner);
			icon.paintInCenter(q, inner);
		}
	}
	const auto cached = cache.badge.size() / cache.badge.devicePixelRatio();
	const auto left = x + size + add - cached.width();
	const auto top = stars ? (y - add) : (y + size + add - cached.height());
	p.drawImage(left, top, cache.badge);
}
