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

#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/style/style_core_font.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"

#include <QtGui/QFontDatabase>

namespace Ui {
namespace {

constexpr auto kMinTextWidth = 120;
constexpr auto kMaxTextWidth = 320;
constexpr auto kMaxTextLines = 3;

struct PreviewRequest {
	QString family;
	QColor msgBg;
	QColor msgShadow;
	QColor replyBar;
	QColor replyNameFg;
	QColor textFg;
	QImage bubbleTail;
};

class PreviewPainter {
public:
	PreviewPainter(const QImage &bg, PreviewRequest request);

	QImage takeResult();

private:
	void layout();

	void paintBubble(Painter &p);
	void paintContent(Painter &p);
	void paintReply(Painter &p);
	void paintMessage(Painter &p);

	void validateBubbleCache();

	const PreviewRequest _request;
	const style::owned_color _msgBg;
	const style::owned_color _msgShadow;

	style::owned_font _nameFontOwned;
	style::font _nameFont;
	style::TextStyle _nameStyle;
	style::owned_font _textFontOwned;
	style::font _textFont;
	style::TextStyle _textStyle;

	Ui::Text::String _nameText;
	Ui::Text::String _replyText;
	Ui::Text::String _messageText;

	QRect _replyRect;
	QRect _name;
	QRect _reply;
	QRect _message;
	QRect _content;
	QRect _bubble;
	QSize _outer;

	Ui::CornersPixmaps _bubbleCorners;
	QPixmap _bubbleShadowBottomRight;

	QImage _result;

};

class Selector final : public Ui::RpWidget {
public:
	Selector(
		not_null<QWidget*> parent,
		const QString &now,
		rpl::producer<QString> filter,
		rpl::producer<> submits,
		Fn<void(QString)> chosen,
		Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo);

	void initScroll(anim::type animated);
	void setMinHeight(int height);
	void selectSkip(Qt::Key direction);

private:
	struct Entry {
		QString id;
		QString key;
		QString text;
		QStringList keywords;
		QImage cache;
		std::unique_ptr<Ui::RadioView> check;
		std::unique_ptr<Ui::RippleAnimation> ripple;
		int paletteVersion = 0;
	};
	[[nodiscard]] static std::vector<Entry> FullList(const QString &now);

	int resizeGetHeight(int newWidth) override;
	void paintEvent(QPaintEvent *e) override;
	void leaveEventHook(QEvent *e) override;
	void mouseMoveEvent(QMouseEvent *e) override;
	void mousePressEvent(QMouseEvent *e) override;
	void mouseReleaseEvent(QMouseEvent *e) override;

	[[nodiscard]] bool searching() const;
	[[nodiscard]] int shownRowsCount() const;
	[[nodiscard]] Entry &shownRowAt(int index);

	void applyFilter(const QString &query);
	void updateSelected(int selected);
	void updatePressed(int pressed);
	void updateRow(int index);
	void updateRow(not_null<Entry*> row, int hint);
	void addRipple(int index, QPoint position);
	void validateCache(Entry &row);
	void choose(Entry &row);

	const style::SettingsButton &_st;
	std::vector<Entry> _rows;
	std::vector<not_null<Entry*>> _filtered;
	QString _chosen;
	int _selected = -1;
	int _pressed = -1;

	std::optional<QPoint> _lastGlobalPoint;
	bool _selectedByKeyboard = false;

	Fn<void(QString)> _callback;
	Fn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;

	int _rowsSkip = 0;
	int _rowHeight = 0;
	int _minHeight = 0;

	QString _query;
	QStringList _queryWords;

	rpl::lifetime _lifetime;

};

Selector::Selector(
	not_null<QWidget*> parent,
	const QString &now,
	rpl::producer<QString> filter,
	rpl::producer<> submits,
	Fn<void(QString)> chosen,
	Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo)
: RpWidget(parent)
, _st(st::settingsButton)
, _rows(FullList(now))
, _chosen(now)
, _callback(std::move(chosen))
, _scrollTo(std::move(scrollTo))
, _rowsSkip(st::settingsInfoPhotoSkip)
, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {
	setMouseTracking(true);

	std::move(filter) | rpl::start_with_next([=](const QString &query) {
		applyFilter(query);
	}, _lifetime);

	std::move(
		submits
	) | rpl::start_with_next([=] {
		if (_selected >= 0) {
			choose(shownRowAt(_selected));
		} else if (searching() && !_filtered.empty()) {
			choose(*_filtered.front());
		}
	}, _lifetime);
}

void Selector::applyFilter(const QString &query) {
	if (_query == query) {
		return;
	}
	_query = query;

	updateSelected(-1);
	updatePressed(-1);

	_queryWords = TextUtilities::PrepareSearchWords(_query);

	const auto skip = [](
			const QStringList &haystack,
			const QStringList &needles) {
		const auto find = [](
				const QStringList &haystack,
				const QString &needle) {
			for (const auto &item : haystack) {
				if (item.startsWith(needle)) {
					return true;
				}
			}
			return false;
		};
		for (const auto &needle : needles) {
			if (!find(haystack, needle)) {
				return true;
			}
		}
		return false;
	};

	_filtered.clear();
	if (!_queryWords.isEmpty()) {
		_filtered.reserve(_rows.size());
		for (auto &row : _rows) {
			if (!skip(row.keywords, _queryWords)) {
				_filtered.push_back(&row);
			} else {
				row.ripple = nullptr;
			}
		}
	}

	resizeToWidth(width());
	Ui::SendPendingMoveResizeEvents(this);
	update();
}

void Selector::updateSelected(int selected) {
	if (_selected == selected) {
		return;
	}
	const auto was = (_selected >= 0);
	updateRow(_selected);
	_selected = selected;
	updateRow(_selected);
	const auto now = (_selected >= 0);
	if (was != now) {
		setCursor(now ? style::cur_pointer : style::cur_default);
	}
	if (_selectedByKeyboard) {
		const auto top = (_selected > 0)
			? (_rowsSkip + _selected * _rowHeight)
			: 0;
		const auto bottom = (_selected > 0)
			? (top + _rowHeight)
			: _selected
			? 0
			: _rowHeight;
		_scrollTo({ top, bottom }, anim::type::instant);
	}
}

void Selector::updatePressed(int pressed) {
	if (_pressed == pressed) {
		return;
	} else if (_pressed >= 0) {
		if (auto &ripple = shownRowAt(_pressed).ripple) {
			ripple->lastStop();
		}
	}
	updateRow(_pressed);
	_pressed = pressed;
	updateRow(_pressed);
}

void Selector::updateRow(int index) {
	if (index >= 0) {
		update(0, _rowsSkip + index * _rowHeight, width(), _rowHeight);
	}
}

void Selector::updateRow(not_null<Entry*> row, int hint) {
	if (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) {
		updateRow(hint);
	} else if (searching()) {
		const auto i = ranges::find(_filtered, row);
		if (i != end(_filtered)) {
			updateRow(int(i - begin(_filtered)));
		}
	} else {
		const auto index = int(row.get() - &_rows[0]);
		Assert(index >= 0 && index < _rows.size());
		updateRow(index);
	}
}

void Selector::validateCache(Entry &row) {
	const auto version = style::PaletteVersion();
	if (row.cache.isNull()) {
		const auto ratio = style::DevicePixelRatio();
		row.cache = QImage(
			QSize(width(), _rowHeight) * ratio,
			QImage::Format_ARGB32_Premultiplied);
		row.cache.setDevicePixelRatio(ratio);
	} else if (row.paletteVersion == version) {
		return;
	}
	row.paletteVersion = version;
	row.cache.fill(Qt::transparent);
	auto owned = style::owned_font(row.id, 0, st::boxFontSize);
	const auto font = owned.font();
	auto p = QPainter(&row.cache);
	p.setFont(font);
	p.setPen(st::windowFg);

	const auto textw = width() - _st.padding.left() - _st.padding.right();
	const auto textt = (_rowHeight - font->height) / 2.;
	p.drawText(
		_st.padding.left(),
		textt + font->ascent,
		font->elided(row.text, textw));
}

bool Selector::searching() const {
	return !_queryWords.isEmpty();
}

int Selector::shownRowsCount() const {
	return searching() ? int(_filtered.size()) : int(_rows.size());
}

Selector::Entry &Selector::shownRowAt(int index) {
	return searching() ? *_filtered[index] : _rows[index];
}

void Selector::setMinHeight(int height) {
	_minHeight = height;
	if (_minHeight > 0) {
		resizeToWidth(width());
	}
}

void Selector::selectSkip(Qt::Key key) {
	const auto count = shownRowsCount();
	if (key == Qt::Key_Down) {
		if (_selected + 1 < count) {
			_selectedByKeyboard = true;
			updateSelected(_selected + 1);
		}
	} else if (key == Qt::Key_Up) {
		if (_selected >= 0) {
			_selectedByKeyboard = true;
			updateSelected(_selected - 1);
		}
	} else if (key == Qt::Key_PageDown) {
		const auto change = _minHeight / _rowHeight;
		if (_selected + 1 < count) {
			_selectedByKeyboard = true;
			updateSelected(std::min(_selected + change, count - 1));
		}
	} else if (key == Qt::Key_PageUp) {
		const auto change = _minHeight / _rowHeight;
		if (_selected > 0) {
			_selectedByKeyboard = true;
			updateSelected(std::max(_selected - change, 0));
		} else if (!_selected) {
			_selectedByKeyboard = true;
			updateSelected(-1);
		}
	}
}

void Selector::initScroll(anim::type animated) {
	const auto index = [&] {
		if (searching()) {
			const auto i = ranges::find(_filtered, _chosen, &Entry::id);
			if (i != end(_filtered)) {
				return int(i - begin(_filtered));
			}
			return -1;
		}
		const auto i = ranges::find(_rows, _chosen, &Entry::id);
		Assert(i != end(_rows));
		return int(i - begin(_rows));
	}();
	if (index >= 0) {
		const auto top = _rowsSkip + index * _rowHeight;
		const auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0);
		_scrollTo({ use, use + _minHeight }, animated);
	}
}

int Selector::resizeGetHeight(int newWidth) {
	const auto added = 2 * _rowsSkip;
	return std::max(added + shownRowsCount() * _rowHeight, _minHeight);
}

void Selector::paintEvent(QPaintEvent *e) {
	auto p = QPainter(this);

	const auto rows = shownRowsCount();
	if (!rows) {
		p.setFont(st::normalFont);
		p.setPen(st::windowSubTextFg);
		p.drawText(
			QRect(0, 0, width(), height() * 2 / 3),
			tr::lng_font_not_found(tr::now),
			style::al_center);
		return;
	}
	const auto clip = e->rect();
	const auto clipped = std::max(clip.y() - _rowsSkip, 0);
	const auto from = std::min(clipped / _rowHeight, rows);
	const auto till = std::min(
		(clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight,
		rows);
	const auto active = (_pressed >= 0) ? _pressed : _selected;
	for (auto i = from; i != till; ++i) {
		auto &row = shownRowAt(i);
		const auto y = _rowsSkip + i * _rowHeight;
		const auto bg = (i == active) ? st::windowBgOver : st::windowBg;
		const auto rect = QRect(0, y, width(), _rowHeight);
		p.fillRect(rect, bg);

		if (row.ripple) {
			row.ripple->paint(p, 0, y, width());
			if (row.ripple->empty()) {
				row.ripple = nullptr;
			}
		}

		validateCache(row);
		p.drawImage(0, y, row.cache);

		if (!row.check) {
			row.check = std::make_unique<Ui::RadioView>(
				st::langsRadio,
				(row.id == _chosen),
				[=, row = &row] { updateRow(row, i); });
		}
		row.check->paint(
			p,
			_st.iconLeft,
			y + (_rowHeight - st::langsRadio.diameter) / 2,
			width());
	}
}

void Selector::leaveEventHook(QEvent *e) {
	_lastGlobalPoint = std::nullopt;
	if (!_selectedByKeyboard) {
		updateSelected(-1);
	}
}

void Selector::mouseMoveEvent(QMouseEvent *e) {
	if (!_lastGlobalPoint) {
		_lastGlobalPoint = e->globalPos();
		if (_selectedByKeyboard) {
			return;
		}
	} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {
		return;
	} else {
		_lastGlobalPoint = e->globalPos();
	}
	_selectedByKeyboard = false;
	const auto y = e->y() - _rowsSkip;
	const auto index = (y >= 0) ? (y / _rowHeight) : -1;
	updateSelected((index >= 0 && index < shownRowsCount()) ? index : -1);
}

void Selector::mousePressEvent(QMouseEvent *e) {
	updatePressed(_selected);
	if (_pressed >= 0) {
		addRipple(_pressed, e->pos());
	}
}

void Selector::mouseReleaseEvent(QMouseEvent *e) {
	const auto pressed = _pressed;
	updatePressed(-1);
	if (pressed >= 0 && pressed == _selected) {
		choose(shownRowAt(pressed));
	}
}

void Selector::choose(Entry &row) {
	const auto id = row.id;
	if (_chosen != id) {
		const auto i = ranges::find(_rows, _chosen, &Entry::id);
		Assert(i != end(_rows));
		if (i->check) {
			i->check->setChecked(false, anim::type::normal);
		}
		_chosen = id;
		if (row.check) {
			row.check->setChecked(true, anim::type::normal);
		}
	}
	const auto animated = searching()
		? anim::type::instant
		: anim::type::normal;
	_callback(id);
	initScroll(animated);
}

void Selector::addRipple(int index, QPoint position) {
	Expects(index >= 0 && index < shownRowsCount());

	const auto row = &shownRowAt(index);
	if (!row->ripple) {
		row->ripple = std::make_unique<Ui::RippleAnimation>(
			st::defaultRippleAnimation,
			Ui::RippleAnimation::RectMask({ width(), _rowHeight }),
			[=] { updateRow(row, index); });
	}
	row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));
}

std::vector<Selector::Entry> Selector::FullList(const QString &now) {
	using namespace TextUtilities;

	auto database = QFontDatabase();
	auto families = database.families();
	auto result = std::vector<Entry>();
	result.reserve(families.size() + 3);
	const auto add = [&](const QString &text, const QString &id = {}) {
		result.push_back({
			.id = id,
			.text = text,
			.keywords = PrepareSearchWords(text),
		});
	};
	add(tr::lng_font_default(tr::now));
	add(tr::lng_font_system(tr::now), style::SystemFontTag());
	for (const auto &family : families) {
		if (database.isScalable(family)) {
			result.push_back({ .id = family });
		}
	}
	auto nowIt = ranges::find(result, now, &Entry::id);
	if (nowIt == end(result)) {
		result.push_back({ .id = now });
		nowIt = end(result) - 1;
	}
	for (auto i = begin(result) + 2; i != end(result); ++i) {
		i->key = TextUtilities::RemoveAccents(i->id).toLower();
		i->text = i->id;
		i->keywords = TextUtilities::PrepareSearchWords(i->id);
	}
	auto skip = 2;
	if (nowIt - begin(result) >= skip) {
		std::swap(result[2], *nowIt);
		++skip;
	}
	ranges::sort(
		begin(result) + skip, end(result),
		std::less<>(),
		&Entry::key);
	return result;
}

[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) {
	return {
		.family = family,
		.msgBg = st::msgInBg->c,
		.msgShadow = st::msgInShadow->c,
		.replyBar = st::msgInReplyBarColor->c,
		.replyNameFg = st::msgInServiceFg->c,
		.textFg = st::historyTextInFg->c,
		.bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c),
	};
}

PreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request)
: _request(request)
, _msgBg(_request.msgBg)
, _msgShadow(_request.msgShadow)
, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)
, _nameFont(_nameFontOwned.font())
, _nameStyle(st::semiboldTextStyle)
, _textFontOwned(_request.family, 0, st::fsize)
, _textFont(_textFontOwned.font())
, _textStyle(st::defaultTextStyle) {
	_nameStyle.font = _nameFont;
	_textStyle.font = _textFont;

	layout();

	const auto ratio = style::DevicePixelRatio();
	_result = QImage(
		_outer * ratio,
		QImage::Format_ARGB32_Premultiplied);
	_result.setDevicePixelRatio(ratio);

	auto p = Painter(&_result);
	p.drawImage(0, 0, bg);

	p.translate(_bubble.topLeft());
	paintBubble(p);
}

void PreviewPainter::paintBubble(Painter &p) {
	validateBubbleCache();
	const auto bubble = QRect(QPoint(), _bubble.size());
	const auto cornerShadow = _bubbleShadowBottomRight.size()
		/ _bubbleShadowBottomRight.devicePixelRatio();
	p.drawPixmap(
		bubble.width() - cornerShadow.width(),
		bubble.height() + st::msgShadow - cornerShadow.height(),
		_bubbleShadowBottomRight);
	Ui::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners);
	const auto &bubbleTail = _request.bubbleTail;
	const auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio();
	p.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail);
	p.fillRect(
		-tail.width(),
		bubble.height(),
		tail.width() + bubble.width() - cornerShadow.width(),
		st::msgShadow,
		_request.msgShadow);
	p.translate(_content.topLeft());
	const auto local = _content.translated(-_content.topLeft());
	p.setClipRect(local);
	paintContent(p);
}

void PreviewPainter::validateBubbleCache() {
	if (!_bubbleCorners.p[0].isNull()) {
		return;
	}
	const auto radius = st::bubbleRadiusLarge;
	_bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color());
	_bubbleCorners.p[2] = {};
	_bubbleShadowBottomRight
		= Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3];
}

void PreviewPainter::paintContent(Painter &p) {
	paintReply(p);

	p.translate(_message.topLeft());
	const auto local = _message.translated(-_message.topLeft());
	p.setClipRect(local);
	paintMessage(p);
}

void PreviewPainter::paintReply(Painter &p) {
	{
		auto hq = PainterHighQualityEnabler(p);
		p.setPen(Qt::NoPen);
		p.setBrush(_request.replyBar);

		const auto outline = st::messageTextStyle.blockquote.outline;
		const auto radius = st::messageTextStyle.blockquote.radius;
		p.setOpacity(Ui::kDefaultOutline1Opacity);
		p.setClipRect(
			_replyRect.x(),
			_replyRect.y(),
			outline,
			_replyRect.height());
		p.drawRoundedRect(_replyRect, radius, radius);
		p.setOpacity(Ui::kDefaultBgOpacity);
		p.setClipRect(
			_replyRect.x() + outline,
			_replyRect.y(),
			_replyRect.width() - outline,
			_replyRect.height());
		p.drawRoundedRect(_replyRect, radius, radius);
	}
	p.setOpacity(1.);
	p.setClipping(false);

	p.setPen(_request.replyNameFg);
	_nameText.drawLeftElided(
		p,
		_name.x(),
		_name.y(),
		_name.width(),
		_outer.width());

	p.setPen(_request.textFg);
	_replyText.drawLeftElided(
		p,
		_reply.x(),
		_reply.y(),
		_reply.width(),
		_outer.width());
}

void PreviewPainter::paintMessage(Painter &p) {
	p.setPen(_request.textFg);
	_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());
}

QImage PreviewPainter::takeResult() {
	return std::move(_result);
}

void PreviewPainter::layout() {
	const auto skip = st::boxRowPadding.left();
	const auto minTextWidth = style::ConvertScale(kMinTextWidth);
	const auto maxTextWidth = st::boxWidth
		- 2 * skip
		- st::msgPadding.left()
		- st::msgPadding.right();

	_nameText = Ui::Text::String(
		_nameStyle,
		tr::lng_settings_chat_message_reply_from(tr::now));
	_replyText = Ui::Text::String(
		_textStyle,
		tr::lng_background_text2(tr::now));
	_messageText = Ui::Text::String(
		_textStyle,
		tr::lng_background_text1(tr::now),
		kDefaultTextOptions,
		st::msgMinWidth / 2);

	const auto namePosition = QPoint(
		st::historyReplyPadding.left(),
		st::historyReplyPadding.top());
	const auto replyPosition = QPoint(
		st::historyReplyPadding.left(),
		(st::historyReplyPadding.top() + _nameFont->height));
	const auto paddingRight = st::historyReplyPadding.right();

	const auto wantedWidth = std::max({
		namePosition.x() + _nameText.maxWidth() + paddingRight,
		replyPosition.x() + _replyText.maxWidth() + paddingRight,
		_messageText.maxWidth()
	});

	const auto messageWidth = std::clamp(
		wantedWidth,
		minTextWidth,
		maxTextWidth);
	const auto messageHeight = _messageText.countHeight(messageWidth);

	_replyRect = QRect(
		st::msgReplyBarPos.x(),
		st::historyReplyTop,
		messageWidth,
		(st::historyReplyPadding.top()
			+ _nameFont->height
			+ _textFont->height
			+ st::historyReplyPadding.bottom()));

	_name = QRect(
		_replyRect.topLeft() + namePosition,
		QSize(messageWidth - namePosition.x(), _nameFont->height));
	_reply = QRect(
		_replyRect.topLeft() + replyPosition,
		QSize(messageWidth - replyPosition.x(), _textFont->height));
	_message = QRect(0, 0, messageWidth, messageHeight);

	const auto replySkip = _replyRect.y()
		+ _replyRect.height()
		+ st::historyReplyBottom;
	_message.moveTop(replySkip);

	_content = QRect(0, 0, messageWidth, replySkip + messageHeight);

	const auto msgPadding = st::msgPadding;
	_bubble = _content.marginsAdded(msgPadding);
	_content.moveTopLeft(-_bubble.topLeft());
	_bubble.moveTopLeft({});

	_outer = QSize(st::boxWidth, st::boxWidth / 2);

	_bubble.moveTopLeft({ skip, std::max(
		(_outer.height() - _bubble.height()) / 2,
		st::msgMargin.top()) });
}

[[nodiscard]] QImage GeneratePreview(
		const QImage &bg,
		PreviewRequest request) {
	return PreviewPainter(bg, request).takeResult();
}

[[nodiscard]] object_ptr<Ui::RpWidget> MakePreview(
		not_null<QWidget*> parent,
		Fn<QImage()> generatePreviewBg,
		rpl::producer<QString> family) {
	auto result = object_ptr<Ui::RpWidget>(parent.get());
	const auto raw = result.data();

	struct State {
		QImage preview;
		QImage bg;
		QString family;
	};
	const auto state = raw->lifetime().make_state<State>();

	state->bg = generatePreviewBg();
	style::PaletteChanged() | rpl::start_with_next([=] {
		state->bg = generatePreviewBg();
	}, raw->lifetime());

	rpl::combine(
		rpl::single(rpl::empty) | rpl::then(style::PaletteChanged()),
		std::move(family)
	) | rpl::start_with_next([=](const auto &, QString family) {
		state->family = family;
		if (state->preview.isNull()) {
			state->preview = GeneratePreview(
				state->bg,
				PrepareRequest(family));
			const auto ratio = state->preview.devicePixelRatio();
			raw->resize(state->preview.size() / int(ratio));
		} else {
			const auto weak = Ui::MakeWeak(raw);
			const auto request = PrepareRequest(family);
			crl::async([=, bg = state->bg] {
				crl::on_main([
					weak,
					state,
					preview = GeneratePreview(bg, request)
				]() mutable {
					if (const auto strong = weak.data()) {
						state->preview = std::move(preview);
						const auto ratio = state->preview.devicePixelRatio();
						strong->resize(
							strong->width(),
							(state->preview.height() / int(ratio)));
						strong->update();
					}
				});
			});
		}
	}, raw->lifetime());

	raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
		QPainter(raw).drawImage(0, 0, state->preview);
	}, raw->lifetime());

	return result;
}

} // namespace

void ChooseFontBox(
		not_null<GenericBox*> box,
		Fn<QImage()> generatePreviewBg,
		const QString &family,
		Fn<void(QString)> save) {
	box->setTitle(tr::lng_font_box_title());

	struct State {
		rpl::variable<QString> family;
		rpl::variable<QString> query;
		rpl::event_stream<> submits;
	};
	const auto state = box->lifetime().make_state<State>(State{
		.family = family,
	});

	const auto top = box->setPinnedToTopContent(
		object_ptr<Ui::VerticalLayout>(box));
	top->add(MakePreview(top, generatePreviewBg, state->family.value()));
	const auto filter = top->add(object_ptr<Ui::MultiSelect>(
		top,
		st::defaultMultiSelect,
		tr::lng_participant_filter()));
	top->resizeToWidth(st::boxWidth);

	filter->setSubmittedCallback([=](Qt::KeyboardModifiers) {
		state->submits.fire({});
	});
	filter->setQueryChangedCallback([=](const QString &query) {
		state->query = query;
	});
	filter->setCancelledCallback([=] {
		filter->clearQuery();
	});

	const auto chosen = [=](const QString &value) {
		state->family = value;
		filter->clearQuery();
	};
	const auto scrollTo = [=](
			Ui::ScrollToRequest request,
			anim::type animated) {
		box->scrollTo(request, animated);
	};
	const auto selector = box->addRow(
		object_ptr<Selector>(
			box,
			state->family.current(),
			state->query.value(),
			state->submits.events(),
			chosen,
			scrollTo),
		QMargins());
	box->setMinHeight(st::boxMaxListHeight);
	box->setMaxHeight(st::boxMaxListHeight);

	base::install_event_filter(filter, [=](not_null<QEvent*> e) {
		if (e->type() == QEvent::KeyPress) {
			const auto key = static_cast<QKeyEvent*>(e.get())->key();
			if (key == Qt::Key_Up
				|| key == Qt::Key_Down
				|| key == Qt::Key_PageUp
				|| key == Qt::Key_PageDown) {
				selector->selectSkip(Qt::Key(key));
				return base::EventFilterResult::Cancel;
			}
		}
		return base::EventFilterResult::Continue;
	});

	rpl::combine(
		box->heightValue(),
		top->heightValue()
	) | rpl::start_with_next([=](int box, int top) {
		selector->setMinHeight(box - top);
	}, selector->lifetime());

	const auto apply = [=](QString chosen) {
		if (chosen == family) {
			box->closeBox();
			return;
		}
		box->getDelegate()->show(Ui::MakeConfirmBox({
			.text = tr::lng_settings_need_restart(),
			.confirmed = [=] { save(chosen); },
			.confirmText = tr::lng_settings_restart_now(),
		}));
	};
	const auto refreshButtons = [=](QString chosen) {
		box->clearButtons();
		// Doesn't fit in most languages.
		//if (!chosen.isEmpty()) {
		//	box->addLeftButton(tr::lng_background_reset_default(), [=] {
		//		apply(QString());
		//	});
		//}
		box->addButton(tr::lng_settings_save(), [=] {
			apply(chosen);
		});
		box->addButton(tr::lng_cancel(), [=] {
			box->closeBox();
		});
	};
	state->family.value(
	) | rpl::start_with_next(refreshButtons, box->lifetime());

	box->setFocusCallback([=] {
		filter->setInnerFocus();
	});
	box->setInitScrollCallback([=] {
		SendPendingMoveResizeEvents(box);
		selector->initScroll(anim::type::instant);
	});
}

} // namespace Ui
