/*
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/peers/edit_peer_reactions.h"

#include "apiwrap.h"
#include "base/event_filter.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h"
#include "data/data_peer_values.h" // UniqueReactionsLimit.
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/boost_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"

#include <QtWidgets/QTextEdit>
#include <QtGui/QTextBlock>
#include <QtGui/QTextDocumentFragment>

namespace {

constexpr auto kDisabledEmojiOpacity = 0.4;

struct UniqueCustomEmojiContext {
	std::vector<DocumentId> ids;
	Fn<bool(DocumentId)> applyHardLimit;
	int hardLimit = 0;
	int hardLimitChecked = 0;
	bool hardLimitHit = false;
};

class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {
public:
	MaybeDisabledEmoji(
		std::unique_ptr<CustomEmoji> wrapped,
		Fn<bool()> enabled);

	int width() override;
	QString entityData() override;
	void paint(QPainter &p, const Context &context) override;
	void unload() override;
	bool ready() override;
	bool readyInDefaultState() override;

private:
	const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
	const Fn<bool()> _enabled;

};

MaybeDisabledEmoji::MaybeDisabledEmoji(
	std::unique_ptr<CustomEmoji> wrapped,
	Fn<bool()> enabled)
: _wrapped(std::move(wrapped))
, _enabled(std::move(enabled)) {
}

int MaybeDisabledEmoji::width() {
	return _wrapped->width();
}

QString MaybeDisabledEmoji::entityData() {
	return _wrapped->entityData();
}

void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {
	const auto disabled = !_enabled();
	const auto was = disabled ? p.opacity() : 1.;
	if (disabled) {
		p.setOpacity(kDisabledEmojiOpacity);
	}
	_wrapped->paint(p, context);
	if (disabled) {
		p.setOpacity(was);
	}
}

void MaybeDisabledEmoji::unload() {
	_wrapped->unload();
}

bool MaybeDisabledEmoji::ready() {
	return _wrapped->ready();
}

bool MaybeDisabledEmoji::readyInDefaultState() {
	return _wrapped->readyInDefaultState();
}

[[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
	auto all = TextUtilities::SplitTags(mimeTag);
	for (auto i = all.begin(); i != all.end();) {
		if (Ui::InputField::IsCustomEmojiLink(*i)) {
			++i;
		} else {
			i = all.erase(i);
		}
	}
	return TextUtilities::JoinTag(all);
}

[[nodiscard]] bool AllowOnlyCustomEmojiMimeDataHook(
		not_null<const QMimeData*> data,
		Ui::InputField::MimeAction action) {
	if (action == Ui::InputField::MimeAction::Check) {
		const auto textMime = TextUtilities::TagsTextMimeType();
		const auto tagsMime = TextUtilities::TagsMimeType();
		if (!data->hasFormat(textMime) || !data->hasFormat(tagsMime)) {
			return false;
		}
		auto text = QString::fromUtf8(data->data(textMime));
		auto tags = TextUtilities::DeserializeTags(
			data->data(tagsMime),
			text.size());
		auto checkedTill = 0;
		ranges::sort(tags, ranges::less(), &TextWithTags::Tag::offset);
		for (const auto &tag : tags) {
			if (tag.offset != checkedTill
				|| AllowOnlyCustomEmojiProcessor(tag.id) != tag.id) {
				return false;
			}
			checkedTill += tag.length;
		}
		return true;
	} else if (action == Ui::InputField::MimeAction::Insert) {
		return false;
	}
	Unexpected("Action in MimeData hook.");
}

[[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {
	const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
	const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
	return { Data::ReactionId{ like }, Data::ReactionId{ dislike } };
}

[[nodiscard]] bool RemoveNonCustomEmojiFragment(
		not_null<QTextDocument*> document,
		UniqueCustomEmojiContext &context) {
	context.ids.clear();
	context.hardLimitChecked = 0;
	auto removeFrom = 0;
	auto removeTill = 0;
	auto block = document->begin();
	for (auto j = block.begin(); !j.atEnd(); ++j) {
		const auto fragment = j.fragment();
		Assert(fragment.isValid());

		removeTill = removeFrom = fragment.position();
		const auto format = fragment.charFormat();
		if (format.objectType() != Ui::InputField::kCustomEmojiFormat) {
			removeTill += fragment.length();
			break;
		}
		const auto id = format.property(Ui::InputField::kCustomEmojiId);
		const auto documentId = id.toULongLong();
		const auto applyHardLimit = context.applyHardLimit(documentId);
		if (ranges::contains(context.ids, documentId)) {
			removeTill += fragment.length();
			break;
		} else if (applyHardLimit
			&& context.hardLimitChecked >= context.hardLimit) {
			context.hardLimitHit = true;
			removeTill += fragment.length();
			break;
		}
		context.ids.push_back(documentId);
		if (applyHardLimit) {
			++context.hardLimitChecked;
		}
	}
	while (removeTill == removeFrom) {
		block = block.next();
		if (block == document->end()) {
			return false;
		}
		removeTill = block.position();
	}
	Ui::PrepareFormattingOptimization(document);
	auto cursor = QTextCursor(document);
	cursor.setPosition(removeFrom);
	cursor.setPosition(removeTill, QTextCursor::KeepAnchor);
	cursor.removeSelectedText();
	return true;
}

bool RemoveNonCustomEmoji(
		not_null<QTextDocument*> document,
		UniqueCustomEmojiContext &context) {
	if (!RemoveNonCustomEmojiFragment(document, context)) {
		return false;
	}
	while (RemoveNonCustomEmojiFragment(document, context)) {
	}
	return true;
}

void SetupOnlyCustomEmojiField(
		not_null<Ui::InputField*> field,
		Fn<void(std::vector<DocumentId>, bool)> callback,
		Fn<bool(DocumentId)> applyHardLimit,
		int customHardLimit) {
	field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
	field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);

	struct State {
		bool processing = false;
		bool pending = false;
	};
	const auto state = field->lifetime().make_state<State>();

	field->changes(
	) | rpl::start_with_next([=] {
		state->pending = true;
		if (state->processing) {
			return;
		}
		auto context = UniqueCustomEmojiContext{
			.applyHardLimit = applyHardLimit,
			.hardLimit = customHardLimit,
		};
		auto changed = false;
		state->processing = true;
		while (state->pending) {
			state->pending = false;
			const auto document = field->rawTextEdit()->document();
			const auto pageSize = document->pageSize();
			QTextCursor(document).joinPreviousEditBlock();
			if (RemoveNonCustomEmoji(document, context)) {
				changed = true;
			}
			state->processing = false;
			QTextCursor(document).endEditBlock();
			if (document->pageSize() != pageSize) {
				document->setPageSize(pageSize);
			}
		}
		callback(context.ids, context.hardLimitHit);
		if (changed) {
			field->forceProcessContentsChanges();
		}
	}, field->lifetime());
}

[[nodiscard]] TextWithTags ComposeEmojiList(
		not_null<Data::Reactions*> reactions,
		const std::vector<Data::ReactionId> &list) {
	auto result = TextWithTags();
	const auto size = [&] {
		return int(result.text.size());
	};
	auto added = base::flat_set<Data::ReactionId>();
	const auto &all = reactions->list(Data::Reactions::Type::All);
	const auto add = [&](Data::ReactionId id) {
		if (!added.emplace(id).second) {
			return;
		}
		auto unifiedId = id.custom();
		const auto offset = size();
		if (unifiedId) {
			result.text.append('@');
		} else if (id.paid()) {
			result.text.append(QChar(0x2B50));
			unifiedId = reactions->lookupPaid()->selectAnimation->id;
		} else {
			result.text.append(id.emoji());
			const auto i = ranges::find(all, id, &Data::Reaction::id);
			if (i == end(all)) {
				return;
			}
			unifiedId = i->selectAnimation->id;
		}
		const auto data = Data::SerializeCustomEmojiId(unifiedId);
		const auto tag = Ui::InputField::CustomEmojiLink(data);
		result.tags.append({ offset, size() - offset, tag });
	};
	for (const auto &id : list) {
		add(id);
	}
	return result;
}

enum class ReactionsSelectorState {
	Active,
	Disabled,
	Hidden,
};

struct ReactionsSelectorArgs {
	not_null<QWidget*> outer;
	not_null<Window::SessionController*> controller;
	rpl::producer<QString> title;
	std::vector<Data::Reaction> list;
	std::vector<Data::ReactionId> selected;
	rpl::producer<bool> paid;
	Fn<void(std::vector<Data::ReactionId>, bool)> callback;
	rpl::producer<ReactionsSelectorState> stateValue;
	int customAllowed = 0;
	int customHardLimit = 0;
	bool all = false;
};

object_ptr<Ui::RpWidget> AddReactionsSelector(
		not_null<Ui::RpWidget*> parent,
		ReactionsSelectorArgs &&args) {
	using namespace ChatHelpers;
	using HistoryView::Reactions::UnifiedFactoryOwner;

	auto result = object_ptr<Ui::InputField>(
		parent,
		st::manageGroupReactionsField,
		Ui::InputField::Mode::MultiLine,
		std::move(args.title));
	const auto raw = result.data();
	const auto session = &args.controller->session();
	const auto owner = &session->data();
	const auto reactions = &owner->reactions();
	const auto customAllowed = args.customAllowed;

	struct State {
		std::unique_ptr<Ui::RpWidget> overlay;
		std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
		UnifiedFactoryOwner::RecentFactory factory;
		base::flat_set<DocumentId> allowed;
		std::vector<Data::ReactionId> reactions;
		rpl::lifetime focusLifetime;
	};
	const auto paid = reactions->lookupPaid();
	auto normal = reactions->list(Data::Reactions::Type::Active);
	normal.push_back(*paid);
	const auto state = raw->lifetime().make_state<State>();
	state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
		session,
		normal);
	state->factory = state->unifiedFactoryOwner->factory();
	state->reactions = std::move(args.selected);

	const auto customEmojiPaused = [controller = args.controller] {
		return controller->isGifPausedAtLeastFor(PauseReason::Layer);
	};
	auto factory = [=](QStringView data, Fn<void()> update)
		-> std::unique_ptr<Ui::Text::CustomEmoji> {
		const auto id = Data::ParseCustomEmojiData(data);
		auto result = owner->customEmojiManager().create(
			data,
			std::move(update));
		if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
			return std::make_unique<MaybeDisabledEmoji>(
				std::move(result),
				[=] { return state->allowed.contains(id); });
		}
		using namespace Ui::Text;
		return std::make_unique<FirstFrameEmoji>(std::move(result));
	};
	raw->setCustomTextContext([=](Fn<void()> repaint) {
		return std::any(Core::MarkedTextContext{
			.session = session,
			.customEmojiRepaint = std::move(repaint),
		});
	}, customEmojiPaused, customEmojiPaused, std::move(factory));

	const auto callback = args.callback;
	const auto isCustom = [=](DocumentId id) {
		return state->unifiedFactoryOwner->lookupReactionId(id).custom();
	};
	SetupOnlyCustomEmojiField(raw, [=](
			std::vector<DocumentId> ids,
			bool hardLimitHit) {
		auto allowed = base::flat_set<DocumentId>();
		auto reactions = std::vector<Data::ReactionId>();
		reactions.reserve(ids.size());
		allowed.reserve(std::min(customAllowed, int(ids.size())));
		const auto owner = state->unifiedFactoryOwner.get();
		for (const auto id : ids) {
			const auto reactionId = owner->lookupReactionId(id);
			if (reactionId.custom() && allowed.size() < customAllowed) {
				allowed.emplace(id);
			}
			reactions.push_back(reactionId);
		}
		if (state->allowed != allowed) {
			state->allowed = std::move(allowed);
			raw->rawTextEdit()->update();
		}
		state->reactions = reactions;
		callback(std::move(reactions), hardLimitHit);
	}, isCustom, args.customHardLimit);
	const auto applyFromState = [=] {
		raw->setTextWithTags(ComposeEmojiList(reactions, state->reactions));
	};

	applyFromState();
	std::move(
		args.paid
	) | rpl::start_with_next([=](bool paid) {
		const auto id = Data::ReactionId::Paid();
		if (paid && !ranges::contains(state->reactions, id)) {
			state->reactions.insert(begin(state->reactions), id);
			applyFromState();
		} else if (!paid && ranges::contains(state->reactions, id)) {
			state->reactions.erase(
				ranges::remove(state->reactions, id),
				end(state->reactions));
			applyFromState();
		}
	}, raw->lifetime());

	const auto toggle = Ui::CreateChild<Ui::IconButton>(
		parent.get(),
		st::manageGroupReactions);

	using SelectorState = ReactionsSelectorState;
	std::move(
		args.stateValue
	) | rpl::start_with_next([=](SelectorState value) {
		switch (value) {
		case SelectorState::Active:
			state->overlay = nullptr;
			state->focusLifetime.destroy();
			if (raw->empty()) {
				raw->setTextWithTags(
					ComposeEmojiList(reactions, DefaultSelected()));
			}
			raw->setDisabled(false);
			raw->setFocusFast();
			break;
		case SelectorState::Disabled:
			state->overlay = std::make_unique<Ui::RpWidget>(parent);
			state->overlay->show();
			raw->geometryValue() | rpl::start_with_next([=](QRect rect) {
				state->overlay->setGeometry(rect);
			}, state->overlay->lifetime());
			state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) {
				auto color = st::boxBg->c;
				color.setAlphaF(0.5);
				QPainter(state->overlay.get()).fillRect(
					clip,
					color);
			}, state->overlay->lifetime());
			[[fallthrough]];
		case SelectorState::Hidden:
			if (Ui::InFocusChain(raw)) {
				raw->parentWidget()->setFocus();
			}
			raw->setDisabled(true);
			raw->focusedChanges(
			) | rpl::start_with_next([=](bool focused) {
				if (focused) {
					raw->parentWidget()->setFocus();
				}
			}, state->focusLifetime);
			break;
		}
	}, raw->lifetime());

	const auto panel = Ui::CreateChild<TabbedPanel>(
		args.outer.get(),
		args.controller,
		object_ptr<TabbedSelector>(
			nullptr,
			args.controller->uiShow(),
			Window::GifPauseReason::Layer,
			(args.all
				? TabbedSelector::Mode::FullReactions
				: TabbedSelector::Mode::RecentReactions)));
	auto panelList = state->unifiedFactoryOwner->unifiedIdsList();
	panelList.erase(
		ranges::remove(panelList, paid->selectAnimation->id),
		end(panelList));
	panel->selector()->provideRecentEmoji(panelList);
	panel->setDesiredHeightValues(
		1.,
		st::emojiPanMinHeight / 2,
		st::emojiPanMinHeight);
	panel->hide();
	panel->selector()->customEmojiChosen(
	) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
		Data::InsertCustomEmoji(raw, data.document);
	}, panel->lifetime());

	const auto updateEmojiPanelGeometry = [=] {
		const auto parent = panel->parentWidget();
		const auto global = toggle->mapToGlobal({ 0, 0 });
		const auto local = parent->mapFromGlobal(global);
		panel->moveBottomRight(
			local.y(),
			local.x() + toggle->width() * 3);
	};
	const auto scheduleUpdateEmojiPanelGeometry = [=] {
		// updateEmojiPanelGeometry uses not only container geometry, but
		// also container children geometries that will be updated later.
		crl::on_main(raw, updateEmojiPanelGeometry);
	};
	const auto filterCallback = [=](not_null<QEvent*> event) {
		const auto type = event->type();
		if (type == QEvent::Move || type == QEvent::Resize) {
			scheduleUpdateEmojiPanelGeometry();
		}
		return base::EventFilterResult::Continue;
	};
	for (auto widget = (QWidget*)raw
		; widget && widget != args.outer
		; widget = widget->parentWidget()) {
		base::install_event_filter(raw, widget, filterCallback);
	}
	base::install_event_filter(raw, args.outer, filterCallback);
	scheduleUpdateEmojiPanelGeometry();

	toggle->installEventFilter(panel);
	toggle->addClickHandler([=] {
		panel->toggleAnimated();
	});

	raw->geometryValue() | rpl::start_with_next([=](QRect geometry) {
		toggle->move(
			geometry.x() + geometry.width() - toggle->width(),
			geometry.y() + geometry.height() - toggle->height());
		updateEmojiPanelGeometry();
	}, toggle->lifetime());

	return result;
}

void AddReactionsText(
		not_null<Ui::VerticalLayout*> container,
		not_null<Window::SessionNavigation*> navigation,
		int allowedCustomReactions,
		rpl::producer<int> customCountValue,
		Fn<void(int required)> askForBoosts) {
	auto ownedInner = object_ptr<Ui::VerticalLayout>(container);
	const auto inner = ownedInner.data();
	const auto count = inner->lifetime().make_state<rpl::variable<int>>(
		std::move(customCountValue));

	container->add(
		object_ptr<Ui::DividerLabel>(
			container,
			std::move(ownedInner),
			st::defaultBoxDividerLabelPadding),
		QMargins(0, st::manageGroupReactionsTextSkip, 0, 0));
	const auto label = inner->add(
		object_ptr<Ui::FlatLabel>(
			inner,
			tr::lng_manage_peer_reactions_own(
				lt_link,
				tr::lng_manage_peer_reactions_own_link(
				) | Ui::Text::ToLink(),
				Ui::Text::WithEntities),
			st::boxDividerLabel));
	const auto weak = base::make_weak(navigation);
	label->setClickHandlerFilter([=](const auto &...) {
		if (const auto strong = weak.get()) {
			strong->showPeerByLink(Window::PeerByLinkInfo{
				.usernameOrId = u"stickers"_q,
				.resolveType = Window::ResolveType::Mention,
			});
		}
		return false;
	});
	auto countString = count->value() | rpl::map([](int count) {
		return TextWithEntities{ QString::number(count) };
	});
	auto needs = rpl::combine(
		tr::lng_manage_peer_reactions_level(
			lt_count,
			count->value() | tr::to_count(),
			lt_same_count,
			std::move(countString),
			Ui::Text::RichLangValue),
		tr::lng_manage_peer_reactions_boost(
			lt_link,
			tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(),
			Ui::Text::RichLangValue)
	) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {
		a.append(' ').append(std::move(b));
		return std::move(a);
	});
	const auto wrap = inner->add(
		object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
			inner,
			object_ptr<Ui::FlatLabel>(
				inner,
				std::move(needs),
				st::boxDividerLabel),
			QMargins{ 0, st::normalFont->height, 0, 0 }));
	wrap->toggleOn(count->value() | rpl::map(
		rpl::mappers::_1 > allowedCustomReactions
	));
	wrap->finishAnimating();

	wrap->entity()->setClickHandlerFilter([=](const auto &...) {
		askForBoosts(count->current());
		return false;
	});
}

} // namespace

void EditAllowedReactionsBox(
		not_null<Ui::GenericBox*> box,
		EditAllowedReactionsArgs &&args) {
	using namespace Data;
	using namespace rpl::mappers;

	box->setTitle(tr::lng_manage_peer_reactions());
	box->setWidth(st::boxWideWidth);

	enum class Option {
		All,
		Some,
		None,
	};
	using SelectorState = ReactionsSelectorState;
	struct State {
		rpl::variable<Option> option; // For groups.
		rpl::variable<SelectorState> selectorState;
		std::vector<Data::ReactionId> selected;
		rpl::variable<int> customCount;
		rpl::variable<bool> paidEnabled;
	};
	const auto allowed = args.allowed;
	const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
		? Option::All
		: (allowed.some.empty() && !allowed.paidEnabled)
		? Option::None
		: Option::Some;
	const auto state = box->lifetime().make_state<State>(State{
		.option = optionInitial,
		.paidEnabled = allowed.paidEnabled,
	});

	const auto container = box->verticalLayout();
	const auto isGroup = args.isGroup;
	const auto enabled = isGroup
		? nullptr
		: container->add(object_ptr<Ui::SettingsButton>(
			container.get(),
			tr::lng_manage_peer_reactions_enable(),
			st::manageGroupNoIconButton.button));
	if (enabled) {
		enabled->toggleOn(rpl::single(optionInitial != Option::None));
		enabled->toggledValue(
		) | rpl::start_with_next([=](bool value) {
			state->selectorState = value
				? SelectorState::Active
				: SelectorState::Disabled;
		}, enabled->lifetime());
	}
	const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
		state->option.current());
	group->setChangedCallback([=](Option value) {
		state->option = value;
	});
	const auto addOption = [&](Option option, const QString &text) {
		if (!isGroup) {
			return;
		}
		container->add(
			object_ptr<Ui::Radioenum<Option>>(
				container,
				group,
				option,
				text,
				st::settingsSendType),
			st::settingsSendTypePadding);
	};
	addOption(Option::All, tr::lng_manage_peer_reactions_all(tr::now));
	addOption(Option::Some, tr::lng_manage_peer_reactions_some(tr::now));
	addOption(Option::None, tr::lng_manage_peer_reactions_none(tr::now));

	const auto about = [](Option option) {
		switch (option) {
		case Option::All: return tr::lng_manage_peer_reactions_all_about();
		case Option::Some: return tr::lng_manage_peer_reactions_some_about();
		case Option::None: return tr::lng_manage_peer_reactions_none_about();
		}
		Unexpected("Option value in EditAllowedReactionsBox.");
	};
	Ui::AddSkip(container);
	Ui::AddDividerText(
		container,
		(isGroup
			? (state->option.value()
				| rpl::map(about)
				| rpl::flatten_latest())
			: tr::lng_manage_peer_reactions_about_channel()));

	const auto wrap = enabled ? nullptr : container->add(
		object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
			container,
			object_ptr<Ui::VerticalLayout>(container)));
	if (wrap) {
		wrap->toggleOn(state->option.value(
		) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {
			if (!some) {
				state->selectorState = SelectorState::Hidden;
			}
		}) | rpl::after_next([=](bool some) {
			if (some) {
				state->selectorState = SelectorState::Active;
			}
		}));
		wrap->finishAnimating();
	}
	const auto reactions = wrap ? wrap->entity() : container.get();

	Ui::AddSkip(reactions);

	const auto all = args.list;
	auto selected = (allowed.type != AllowedReactionsType::Some)
		? (all
			| ranges::views::transform(&Data::Reaction::id)
			| ranges::to_vector)
		: allowed.some;
	if (allowed.paidEnabled) {
		selected.insert(begin(selected), Data::ReactionId::Paid());
	}
	const auto changed = [=](
			std::vector<Data::ReactionId> chosen,
			bool hardLimitHit) {
		state->selected = std::move(chosen);
		state->customCount = ranges::count_if(
			state->selected,
			&Data::ReactionId::custom);
		state->paidEnabled = ranges::contains(
			state->selected,
			Data::ReactionId::Paid());
		if (hardLimitHit) {
			box->uiShow()->showToast(
				tr::lng_manage_peer_reactions_limit(tr::now));
		}
	};
	changed(selected.empty() ? DefaultSelected() : std::move(selected), {});
	Ui::AddSubsectionTitle(
		reactions,
		enabled
			? tr::lng_manage_peer_reactions_available()
			: tr::lng_manage_peer_reactions_some_title(),
		st::manageGroupReactionsFieldPadding);
	reactions->add(AddReactionsSelector(reactions, {
		.outer = box->getDelegate()->outerContainer(),
		.controller = args.navigation->parentController(),
		.title = tr::lng_manage_peer_reactions_available_ph(),
		.list = all,
		.selected = state->selected,
		.paid = state->paidEnabled.value(),
		.callback = changed,
		.stateValue = state->selectorState.value(),
		.customAllowed = args.allowedCustomReactions,
		.customHardLimit = args.customReactionsHardLimit,
		.all = !args.isGroup,
	}), st::boxRowPadding);

	box->setFocusCallback([=] {
		if (state->option.current() == Option::Some) {
			state->selectorState.force_assign(SelectorState::Active);
		}
	});

	const auto reactionsLimit = container->lifetime().make_state<int>(0);
	if (!isGroup) {
		AddReactionsText(
			container,
			args.navigation,
			args.allowedCustomReactions,
			state->customCount.value(),
			args.askForBoosts);

		const auto session = &args.navigation->parentController()->session();

		const auto wrap = container->add(
			object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
				container,
				object_ptr<Ui::VerticalLayout>(container)));
		const auto max = Data::UniqueReactionsLimit(session->user());
		const auto inactiveColor = std::make_optional(st::windowSubTextFg->c);
		const auto activeColor = std::make_optional(
			st::windowActiveTextFg->c);
		const auto inner = wrap->entity();
		Ui::AddSkip(inner);
		Ui::AddSubsectionTitle(
			inner,
			tr::lng_manage_peer_reactions_max_title(),
			st::manageGroupReactionsMaxSubtitlePadding);
		Ui::AddSkip(inner);
		const auto line = inner->add(
			object_ptr<Ui::RpWidget>(inner),
			st::boxRowPadding);
		Ui::AddSkip(inner);
		Ui::AddSkip(inner);
		const auto left = Ui::CreateChild<Ui::FlatLabel>(
			line,
			QString::number(1),
			st::defaultFlatLabel);
		const auto center = Ui::CreateChild<Ui::FlatLabel>(
			line,
			st::defaultFlatLabel);
		const auto right = Ui::CreateChild<Ui::FlatLabel>(
			line,
			QString::number(max),
			st::defaultFlatLabel);
		const auto slider = Ui::CreateChild<Ui::MediaSlider>(
			line,
			st::settingsScale);
		rpl::combine(
			line->sizeValue(),
			left->sizeValue(),
			center->sizeValue(),
			right->sizeValue()
		) | rpl::start_with_next([=](
				const QSize &s,
				const QSize &leftSize,
				const QSize &centerSize,
				const QSize &rightSize) {
			const auto sliderHeight = st::settingsScale.seekSize.height();
			line->resize(
				line->width(),
				leftSize.height() + sliderHeight * 2);
			{
				const auto r = line->rect();
				slider->setGeometry(
					0,
					r.height() - sliderHeight * 1.5,
					r.width(),
					sliderHeight);
			}
			left->moveToLeft(0, 0);
			right->moveToRight(0, 0);
			center->moveToLeft((s.width() - centerSize.width()) / 2, 0);
		}, line->lifetime());

		const auto updateLabels = [=](int limit) {
			left->setTextColorOverride((limit <= 1)
				? activeColor
				: inactiveColor);

			center->setText(tr::lng_manage_peer_reactions_max_slider(
				tr::now,
				lt_count,
				limit));
			center->setTextColorOverride(activeColor);

			right->setTextColorOverride((limit >= max)
				? activeColor
				: inactiveColor);

			(*reactionsLimit) = limit;
		};
		const auto current = args.allowed.maxCount
			? std::clamp(1, args.allowed.maxCount, max)
			: max / 2;
		slider->setPseudoDiscrete(
			max,
			[=](int index) { return index + 1; },
			current,
			updateLabels,
			updateLabels);
		updateLabels(current);

		wrap->toggleOn(rpl::single(
			optionInitial != Option::None
		) | rpl::then(
			state->selectorState.value(
			) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));

		Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());

		Ui::AddSkip(inner);
		const auto paid = inner->add(object_ptr<Ui::SettingsButton>(
			inner,
			tr::lng_manage_peer_reactions_paid(),
			st::manageGroupNoIconButton.button));
		paid->toggleOn(state->paidEnabled.value());
		paid->toggledValue(
		) | rpl::start_with_next([=](bool value) {
			state->paidEnabled = value;
		}, paid->lifetime());
		Ui::AddSkip(inner);

		Ui::AddDividerText(
			inner,
			tr::lng_manage_peer_reactions_paid_about(
				lt_link,
				tr::lng_manage_peer_reactions_paid_link([=](QString text) {
					return Ui::Text::Link(
						text,
						u"https://telegram.org/tos/stars"_q);
				}),
				Ui::Text::WithEntities));
	}
	const auto collect = [=] {
		auto result = AllowedReactions();
		result.maxCount = (*reactionsLimit);
		if (isGroup
			? (state->option.current() == Option::Some)
			: (enabled->toggled())) {
			result.some = state->selected;
		}
		if (!isGroup && enabled->toggled())	{
			result.paidEnabled = state->paidEnabled.current();
		}
		auto some = result.some;
		auto simple = all | ranges::views::transform(
			&Data::Reaction::id
		) | ranges::to_vector;
		ranges::sort(some);
		ranges::sort(simple);
		result.type = isGroup
			? (state->option.current() != Option::All
				? AllowedReactionsType::Some
				: AllowedReactionsType::All)
			: (some == simple)
			? AllowedReactionsType::Default
			: AllowedReactionsType::Some;
		return result;
	};

	box->addButton(tr::lng_settings_save(), [=] {
		const auto result = collect();
		if (!isGroup) {
			const auto custom = ranges::count_if(
				result.some,
				&Data::ReactionId::custom);
			if (custom > args.allowedCustomReactions) {
				args.askForBoosts(custom);
				return;
			}
		}
		box->closeBox();
		args.save(result);
	});
	box->addButton(tr::lng_cancel(), [=] {
		box->closeBox();
	});
}

void SaveAllowedReactions(
		not_null<PeerData*> peer,
		const Data::AllowedReactions &allowed) {
	auto ids = allowed.some | ranges::views::transform(
		Data::ReactionToMTP
	) | ranges::to<QVector<MTPReaction>>;

	using Flag = MTPmessages_SetChatAvailableReactions::Flag;
	using Type = Data::AllowedReactionsType;
	const auto updated = (allowed.type != Type::Some)
		? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
			? MTPDchatReactionsAll::Flag(0)
			: MTPDchatReactionsAll::Flag::f_allow_custom))
		: allowed.some.empty()
		? MTP_chatReactionsNone()
		: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
	const auto editPaidEnabled = peer->isBroadcast();
	const auto paidEnabled = editPaidEnabled && allowed.paidEnabled;
	const auto maxCount = allowed.maxCount;
	peer->session().api().request(MTPmessages_SetChatAvailableReactions(
		MTP_flags(Flag()
			| (maxCount ? Flag::f_reactions_limit : Flag())
			| (editPaidEnabled ? Flag::f_paid_enabled : Flag())),
		peer->input,
		updated,
		MTP_int(maxCount),
		MTP_bool(paidEnabled)
	)).done([=](const MTPUpdates &result) {
		peer->session().api().applyUpdates(result);
		auto parsed = Data::Parse(updated, maxCount, paidEnabled);
		if (const auto chat = peer->asChat()) {
			chat->setAllowedReactions(parsed);
		} else if (const auto channel = peer->asChannel()) {
			channel->setAllowedReactions(parsed);
		} else {
			Unexpected("Invalid peer type in SaveAllowedReactions.");
		}
	}).fail([=](const MTP::Error &error) {
		if (error.type() == u"REACTION_INVALID"_q) {
			peer->updateFullForced();
			peer->owner().reactions().refreshDefault();
		}
	}).send();
}
