/*
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 "history/view/history_view_reply.h"

#include "core/click_handler_types.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_story.h"
#include "data/data_user.h"
#include "history/view/history_view_item_preview.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"

namespace HistoryView {
namespace {

constexpr auto kNonExpandedLinesLimit = 5;

} // namespace

void ValidateBackgroundEmoji(
		DocumentId backgroundEmojiId,
		not_null<Ui::BackgroundEmojiData*> data,
		not_null<Ui::BackgroundEmojiCache*> cache,
		not_null<Ui::Text::QuotePaintCache*> quote,
		not_null<const Element*> view) {
	if (data->firstFrameMask.isNull() && !data->emoji) {
		data->emoji = CreateBackgroundEmojiInstance(
			&view->history()->owner(),
			backgroundEmojiId,
			crl::guard(view, [=] { view->repaint(); }));
	}
	ValidateBackgroundEmoji(backgroundEmojiId, data, cache, quote);
}

void ValidateBackgroundEmoji(
		DocumentId backgroundEmojiId,
		not_null<Ui::BackgroundEmojiData*> data,
		not_null<Ui::BackgroundEmojiCache*> cache,
		not_null<Ui::Text::QuotePaintCache*> quote) {
	Expects(!data->firstFrameMask.isNull() || data->emoji != nullptr);

	if (data->firstFrameMask.isNull()) {
		if (!cache->frames[0].isNull()) {
			for (auto &frame : cache->frames) {
				frame = QImage();
			}
		}
		if (!data->emoji->ready()) {
			return;
		}
		const auto tag = Data::CustomEmojiSizeTag::Isolated;
		const auto size = Data::FrameSizeFromTag(tag);
		data->firstFrameMask = QImage(
			QSize(size, size),
			QImage::Format_ARGB32_Premultiplied);
		data->firstFrameMask.fill(Qt::transparent);
		data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio());
		auto p = Painter(&data->firstFrameMask);
		data->emoji->paint(p, {
			.textColor = QColor(255, 255, 255),
			.position = QPoint(0, 0),
			.internal = {
				.forceFirstFrame = true,
			},
		});
		p.end();

		data->emoji = nullptr;
	}
	if (!cache->frames[0].isNull() && cache->color == quote->icon) {
		return;
	}
	cache->color = quote->icon;
	const auto ratio = style::DevicePixelRatio();
	auto colorized = QImage(
		data->firstFrameMask.size(),
		QImage::Format_ARGB32_Premultiplied);
	colorized.setDevicePixelRatio(ratio);
	style::colorizeImage(
		data->firstFrameMask,
		cache->color,
		&colorized,
		QRect(), // src
		QPoint(), // dst
		true); // use alpha
	const auto make = [&](int size) {
		size = style::ConvertScale(size) * ratio;
		auto result = colorized.scaled(
			size,
			size,
			Qt::IgnoreAspectRatio,
			Qt::SmoothTransformation);
		result.setDevicePixelRatio(ratio);
		return result;
	};

	constexpr auto kSize1 = 12;
	constexpr auto kSize2 = 16;
	constexpr auto kSize3 = 20;
	cache->frames[0] = make(kSize1);
	cache->frames[1] = make(kSize2);
	cache->frames[2] = make(kSize3);
}

auto CreateBackgroundEmojiInstance(
	not_null<Data::Session*> owner,
	DocumentId backgroundEmojiId,
	Fn<void()> repaint)
-> std::unique_ptr<Ui::Text::CustomEmoji> {
	return owner->customEmojiManager().create(
		backgroundEmojiId,
		repaint,
		Data::CustomEmojiSizeTag::Isolated);
}

void FillBackgroundEmoji(
		QPainter &p,
		const QRect &rect,
		bool quote,
		const Ui::BackgroundEmojiCache &cache) {
	p.setClipRect(rect);

	const auto &frames = cache.frames;
	const auto right = rect.x() + rect.width();
	const auto paint = [&](int x, int y, int index, float64 opacity) {
		y = style::ConvertScale(y);
		if (y >= rect.height()) {
			return;
		}
		p.setOpacity(opacity);
		p.drawImage(
			right - style::ConvertScale(x + (quote ? 12 : 0)),
			rect.y() + y,
			frames[index]);
	};

	paint(28, 4, 2, 0.32);
	paint(51, 15, 1, 0.32);
	paint(64, -2, 0, 0.28);
	paint(87, 11, 1, 0.24);
	paint(125, -2, 2, 0.16);

	paint(28, 31, 1, 0.24);
	paint(72, 33, 2, 0.2);

	paint(46, 52, 1, 0.24);
	paint(24, 55, 2, 0.18);

	if (quote) {
		paint(4, 23, 1, 0.28);
		paint(0, 48, 0, 0.24);
	}

	p.setClipping(false);
	p.setOpacity(1.);
}

Reply::Reply()
: _name(st::maxSignatureSize / 2)
, _text(st::maxSignatureSize / 2) {
}

Reply &Reply::operator=(Reply &&other) = default;

Reply::~Reply() = default;

void Reply::update(
		not_null<Element*> view,
		not_null<HistoryMessageReply*> data) {
	const auto item = view->data();
	const auto &fields = data->fields();
	const auto message = data->resolvedMessage.get();
	const auto story = data->resolvedStory.get();
	const auto externalMedia = fields.externalMedia.get();
	if (!_externalSender) {
		if (const auto id = fields.externalSenderId) {
			_externalSender = view->history()->owner().peer(id);
		}
	}
	_colorPeer = message
		? message->contentColorsFrom()
		: story
		? story->peer().get()
		: _externalSender
		? _externalSender
		: nullptr;
	_hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
		? (message->originalHiddenSenderInfo()->colorIndex + 1)
		: 0;

	const auto hasPreview = (story && story->hasReplyPreview())
		|| (message
			&& message->media()
			&& message->media()->hasReplyPreview())
		|| (externalMedia && externalMedia->hasReplyPreview());
	_hasPreview = hasPreview ? 1 : 0;
	_displaying = data->displaying() ? 1 : 0;
	_multiline = data->multiline() ? 1 : 0;
	_replyToStory = (fields.storyId != 0);
	const auto hasQuoteIcon = _displaying
		&& fields.manualQuote
		&& !fields.quote.empty();
	_hasQuoteIcon = hasQuoteIcon ? 1 : 0;

	const auto text = (!_displaying && data->unavailable())
		? TextWithEntities()
		: (message && (fields.quote.empty() || !fields.manualQuote))
		? message->inReplyText()
		: !fields.quote.empty()
		? fields.quote
		: story
		? story->inReplyText()
		: externalMedia
		? externalMedia->toPreview({
			.hideSender = true,
			.hideCaption = true,
			.ignoreMessageText = true,
			.generateImages = false,
			.ignoreGroup = true,
			.ignoreTopic = true,
		}).text
		: TextWithEntities();
	const auto repaint = [=] { item->customEmojiRepaint(); };
	const auto context = Core::MarkedTextContext{
		.session = &view->history()->session(),
		.customEmojiRepaint = repaint,
	};
	_text.setMarkedText(
		st::defaultTextStyle,
		text,
		_multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(),
		context);

	updateName(view, data);

	if (_displaying) {
		setLinkFrom(view, data);
		const auto media = message ? message->media() : nullptr;
		if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) {
			_spoiler = nullptr;
		} else if (!_spoiler) {
			_spoiler = std::make_unique<Ui::SpoilerAnimation>(repaint);
		}
	} else {
		_spoiler = nullptr;
	}
}

bool Reply::expand() {
	if (!_expandable || _expanded) {
		return false;
	}
	_expanded = true;
	return true;
}

void Reply::setLinkFrom(
		not_null<Element*> view,
		not_null<HistoryMessageReply*> data) {
	const auto weak = base::make_weak(view);
	const auto &fields = data->fields();
	const auto externalChannelId = peerToChannel(fields.externalPeerId);
	const auto messageId = fields.messageId;
	const auto quote = fields.manualQuote
		? fields.quote
		: TextWithEntities();
	const auto quoteOffset = fields.quoteOffset;
	const auto returnToId = view->data()->fullId();
	const auto externalLink = [=](ClickContext context) {
		const auto my = context.other.value<ClickHandlerContext>();
		if (const auto controller = my.sessionWindow.get()) {
			auto error = QString();
			const auto owner = &controller->session().data();
			if (const auto view = weak.get()) {
				if (const auto reply = view->Get<Reply>()) {
					if (reply->expand()) {
						owner->requestViewResize(view);
						return;
					}
				}
			}
			if (externalChannelId) {
				const auto channel = owner->channel(externalChannelId);
				if (!channel->isForbidden()) {
					if (messageId) {
						JumpToMessageClickHandler(
							channel,
							messageId,
							returnToId,
							quote,
							quoteOffset
						)->onClick(context);
					} else {
						controller->showPeerInfo(channel);
					}
				} else if (channel->isBroadcast()) {
					error = tr::lng_channel_not_accessible(tr::now);
				} else {
					error = tr::lng_group_not_accessible(tr::now);
				}
			} else {
				error = tr::lng_reply_from_private_chat(tr::now);
			}
			if (!error.isEmpty()) {
				controller->showToast(error);
			}
		}
	};
	const auto message = data->resolvedMessage.get();
	const auto story = data->resolvedStory.get();
	_link = message
		? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset)
		: story
		? JumpToStoryClickHandler(story)
		: (data->external()
			&& (!fields.messageId
				|| (data->unavailable() && externalChannelId)))
		? std::make_shared<LambdaClickHandler>(externalLink)
		: nullptr;
}

PeerData *Reply::sender(
		not_null<const Element*> view,
		not_null<HistoryMessageReply*> data) const {
	const auto message = data->resolvedMessage.get();
	if (const auto story = data->resolvedStory.get()) {
		return story->peer();
	} else if (!message) {
		return _externalSender;
	} else if (view->data()->Has<HistoryMessageForwarded>()) {
		// Forward of a reply. Show reply-to original sender.
		const auto forwarded = message->Get<HistoryMessageForwarded>();
		if (forwarded) {
			return forwarded->originalSender;
		}
	}
	if (const auto from = message->displayFrom()) {
		return from;
	}
	return message->author().get();
}

QString Reply::senderName(
		not_null<const Element*> view,
		not_null<HistoryMessageReply*> data,
		bool shorten) const {
	if (const auto peer = sender(view, data)) {
		return senderName(peer, shorten);
	} else if (!data->resolvedMessage) {
		return data->fields().externalSenderName;
	} else if (view->data()->Has<HistoryMessageForwarded>()) {
		// Forward of a reply. Show reply-to original sender.
		const auto forwarded
			= data->resolvedMessage->Get<HistoryMessageForwarded>();
		if (forwarded) {
			Assert(forwarded->originalHiddenSenderInfo != nullptr);
			return forwarded->originalHiddenSenderInfo->name;
		}
	}
	return QString();
}

QString Reply::senderName(
		not_null<PeerData*> peer,
		bool shorten) const {
	const auto user = shorten ? peer->asUser() : nullptr;
	return user ? user->firstName : peer->name();
}

bool Reply::isNameUpdated(
		not_null<const Element*> view,
		not_null<HistoryMessageReply*> data) const {
	if (const auto from = sender(view, data)) {
		if (_nameVersion < from->nameVersion()) {
			updateName(view, data, from);
			return true;
		}
	}
	return false;
}

void Reply::updateName(
		not_null<const Element*> view,
		not_null<HistoryMessageReply*> data,
		std::optional<PeerData*> resolvedSender) const {
	auto viaBotUsername = QString();
	const auto message = data->resolvedMessage.get();
	const auto forwarded = message
		? message->Get<HistoryMessageForwarded>()
		: nullptr;
	if (message && !forwarded) {
		if (const auto bot = message->viaBot()) {
			viaBotUsername = bot->username();
		}
	}
	const auto history = view->history();
	const auto &fields = data->fields();
	const auto sender = resolvedSender.value_or(this->sender(view, data));
	const auto externalPeer = fields.externalPeerId
		? history->owner().peer(fields.externalPeerId).get()
		: nullptr;
	const auto displayAsExternal = data->displayAsExternal(view->data());
	const auto groupNameAdded = displayAsExternal
		&& externalPeer
		&& (externalPeer != sender)
		&& (externalPeer->isChat() || externalPeer->isMegagroup());
	const auto originalNameAdded = !displayAsExternal
		&& forwarded
		&& !message->isDiscussionPost()
		&& (forwarded->forwardOfForward()
			|| (!message->showForwardsFromSender(forwarded)
				&& !view->data()->Has<HistoryMessageForwarded>()));
	const auto shorten = !viaBotUsername.isEmpty()
		|| groupNameAdded
		|| originalNameAdded;
	const auto name = sender
		? senderName(sender, shorten)
		: senderName(view, data, shorten);
	const auto previewSkip = _hasPreview
		? (st::messageQuoteStyle.outline
			+ st::historyReplyPreviewMargin.left()
			+ st::historyReplyPreview
			+ st::historyReplyPreviewMargin.right()
			- st::historyReplyPadding.left())
		: 0;
	auto nameFull = TextWithEntities();
	if (displayAsExternal && !groupNameAdded && !fields.storyId) {
		nameFull.append(PeerEmoji(history, sender));
	}
	nameFull.append(name);
	if (groupNameAdded) {
		nameFull.append(' ').append(PeerEmoji(history, externalPeer));
		nameFull.append(externalPeer->name());
	} else if (originalNameAdded) {
		nameFull.append(' ').append(ForwardEmoji(&history->owner()));
		nameFull.append(forwarded->originalSender
			? forwarded->originalSender->name()
			: forwarded->originalHiddenSenderInfo->name);
	}
	if (!viaBotUsername.isEmpty()) {
		nameFull.append(u" @"_q).append(viaBotUsername);
	}
	const auto context = Core::MarkedTextContext{
		.session = &history->session(),
		.customEmojiRepaint = [] {},
		.customEmojiLoopLimit = 1,
	};
	_name.setMarkedText(
		st::fwdTextStyle,
		nameFull,
		Ui::NameTextOptions(),
		context);
	if (sender) {
		_nameVersion = sender->nameVersion();
	}
	const auto nameMaxWidth = previewSkip
		+ _name.maxWidth()
		+ (_hasQuoteIcon
			? st::messageTextStyle.blockquote.icon.width()
			: 0);
	const auto storySkip = fields.storyId
		? (st::dialogsMiniReplyStory.skipText
			+ st::dialogsMiniReplyStory.icon.icon.width())
		: 0;
	const auto optimalTextSize = _multiline
		? countMultilineOptimalSize(previewSkip)
		: QSize(
			(previewSkip
				+ storySkip
				+ std::min(_text.maxWidth(), st::maxSignatureSize)),
			st::normalFont->height);
	_maxWidth = std::max(nameMaxWidth, optimalTextSize.width());
	if (!data->displaying()) {
		const auto unavailable = data->unavailable();
		_stateText = ((fields.messageId || fields.storyId) && !unavailable)
			? tr::lng_profile_loading(tr::now)
			: fields.storyId
			? tr::lng_deleted_story(tr::now)
			: tr::lng_deleted_message(tr::now);
		const auto phraseWidth = st::msgDateFont->width(_stateText);
		_maxWidth = unavailable
			? phraseWidth
			: std::max(_maxWidth, phraseWidth);
	} else {
		_stateText = QString();
	}
	_maxWidth = st::historyReplyPadding.left()
		+ _maxWidth
		+ st::historyReplyPadding.right();
	_minHeight = st::historyReplyPadding.top()
		+ st::msgServiceNameFont->height
		+ optimalTextSize.height()
		+ st::historyReplyPadding.bottom();
}

int Reply::resizeToWidth(int width) const {
	_ripple.animation = nullptr;

	const auto previewSkip = _hasPreview
		? (st::messageQuoteStyle.outline
			+ st::historyReplyPreviewMargin.left()
			+ st::historyReplyPreview
			+ st::historyReplyPreviewMargin.right()
			- st::historyReplyPadding.left())
		: 0;
	if (width >= _maxWidth || !_multiline) {
		_nameTwoLines = 0;
		_expandable = _minHeightExpandable;
		_height = _minHeight;
		return height();
	}
	const auto innerw = width
		- st::historyReplyPadding.left()
		- st::historyReplyPadding.right();
	const auto namew = innerw - previewSkip;
	const auto desiredNameHeight = _name.countHeight(namew);
	_nameTwoLines = (desiredNameHeight > st::semiboldFont->height) ? 1 : 0;
	const auto nameh = (_nameTwoLines ? 2 : 1) * st::semiboldFont->height;
	const auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;
	auto elided = false;
	const auto texth = _text.countDimensions(
		textGeometry(innerw, firstLineSkip, &elided)).height;
	_expandable = elided ? 1 : 0;
	_height = st::historyReplyPadding.top()
		+ nameh
		+ std::max(texth, st::normalFont->height)
		+ st::historyReplyPadding.bottom();
	return height();
}

Ui::Text::GeometryDescriptor Reply::textGeometry(
		int available,
		int firstLineSkip,
		bool *outElided) const {
	return { .layout = [=](int line) {
		const auto skip = (line ? 0 : firstLineSkip);
		const auto elided = !_multiline
			|| (!_expanded && (line + 1 >= kNonExpandedLinesLimit));
		return Ui::Text::LineGeometry{
			.left = skip,
			.width = available - skip,
			.elided = elided,
		};
	}, .outElided = outElided };
}

int Reply::height() const {
	return _height + st::historyReplyTop + st::historyReplyBottom;
}

QMargins Reply::margins() const {
	return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom);
}

QSize Reply::countMultilineOptimalSize(
		int previewSkip) const {
	auto elided = false;
	const auto max = previewSkip + _text.maxWidth();
	const auto result = _text.countDimensions(
		textGeometry(max, previewSkip, &elided));
	_minHeightExpandable = elided ? 1 : 0;
	return {
		result.width + st::historyReplyPadding.right(),
		std::max(result.height, st::normalFont->height),
	};
}

void Reply::paint(
		Painter &p,
		not_null<const Element*> view,
		const Ui::ChatPaintContext &context,
		int x,
		int y,
		int w,
		bool inBubble) const {
	const auto st = context.st;
	const auto stm = context.messageStyle();

	y += st::historyReplyTop;
	const auto rect = QRect(x, y, w, _height);
	const auto selected = context.selected();
	const auto backgroundEmojiId = _colorPeer
		? _colorPeer->backgroundEmojiId()
		: DocumentId();
	const auto colorIndexPlusOne = _colorPeer
		? (_colorPeer->colorIndex() + 1)
		: _hiddenSenderColorIndexPlusOne;
	const auto useColorIndex = colorIndexPlusOne && !context.outbg;
	const auto colorPattern = colorIndexPlusOne
		? st->colorPatternIndex(colorIndexPlusOne - 1)
		: 0;
	const auto cache = !inBubble
		? (_hasQuoteIcon
			? st->serviceQuoteCache(colorPattern)
			: st->serviceReplyCache(colorPattern)).get()
		: useColorIndex
		? (_hasQuoteIcon
			? st->coloredQuoteCache(selected, colorIndexPlusOne - 1)
			: st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get()
		: (_hasQuoteIcon
			? stm->quoteCache[colorPattern]
			: stm->replyCache[colorPattern]).get();
	const auto &quoteSt = _hasQuoteIcon
		? st::messageTextStyle.blockquote
		: st::messageQuoteStyle;
	const auto backgroundEmoji = backgroundEmojiId
		? st->backgroundEmojiData(backgroundEmojiId).get()
		: nullptr;
	const auto backgroundEmojiCache = backgroundEmoji
		? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex(
			selected,
			context.outbg,
			inBubble,
			colorIndexPlusOne)]
		: nullptr;
	const auto rippleColor = cache->bg;
	if (!inBubble) {
		cache->bg = QColor(0, 0, 0, 0);
	}
	Ui::Text::ValidateQuotePaintCache(*cache, quoteSt);
	Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt);
	if (backgroundEmoji) {
		ValidateBackgroundEmoji(
			backgroundEmojiId,
			backgroundEmoji,
			backgroundEmojiCache,
			cache,
			view);
		if (!backgroundEmojiCache->frames[0].isNull()) {
			FillBackgroundEmoji(p, rect, _hasQuoteIcon, *backgroundEmojiCache);
		}
	}
	if (!inBubble) {
		cache->bg = rippleColor;
	}

	if (_ripple.animation) {
		_ripple.animation->paint(p, x, y, w, &rippleColor);
		if (_ripple.animation->empty()) {
			_ripple.animation.reset();
		}
	}

	auto hasPreview = (_hasPreview != 0);
	auto previewSkip = hasPreview
		? (st::messageQuoteStyle.outline
			+ st::historyReplyPreviewMargin.left()
			+ st::historyReplyPreview
			+ st::historyReplyPreviewMargin.right()
			- st::historyReplyPadding.left())
		: 0;
	if (hasPreview && w <= st::historyReplyPadding.left() + previewSkip) {
		hasPreview = false;
		previewSkip = 0;
	}

	const auto pausedSpoiler = context.paused
		|| On(PowerSaving::kChatSpoiler);
	auto textLeft = x + st::historyReplyPadding.left();
	auto textTop = y
		+ st::historyReplyPadding.top()
		+ (st::msgServiceNameFont->height * (_nameTwoLines ? 2 : 1));
	if (w > st::historyReplyPadding.left()) {
		if (_displaying) {
			if (hasPreview) {
				const auto data = view->data()->Get<HistoryMessageReply>();
				const auto message = data
					? data->resolvedMessage.get()
					: nullptr;
				const auto media = message ? message->media() : nullptr;
				const auto image = media
					? media->replyPreview()
					: !data
					? nullptr
					: data->resolvedStory
					? data->resolvedStory->replyPreview()
					: data->fields().externalMedia
					? data->fields().externalMedia->replyPreview()
					: nullptr;
				if (image) {
					auto to = style::rtlrect(
						x + st::historyReplyPreviewMargin.left(),
						y + st::historyReplyPreviewMargin.top(),
						st::historyReplyPreview,
						st::historyReplyPreview,
						w + 2 * x);
					const auto preview = image->pixSingle(
						image->size() / style::DevicePixelRatio(),
						{
							.colored = (context.selected()
								? &st->msgStickerOverlay()
								: nullptr),
							.options = Images::Option::RoundSmall,
							.outer = to.size(),
						});
					p.drawPixmap(to.x(), to.y(), preview);
					if (_spoiler) {
						view->clearCustomEmojiRepaint();
						Ui::FillSpoilerRect(
							p,
							to,
							Ui::DefaultImageSpoiler().frame(
								_spoiler->index(
									context.now,
									pausedSpoiler)));
					}
				}
			}
			const auto textw = w
				- st::historyReplyPadding.left()
				- st::historyReplyPadding.right();
			const auto namew = textw
				- previewSkip
				- (_hasQuoteIcon
					? st::messageTextStyle.blockquote.icon.width()
					: 0);
			auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;
			if (namew > 0) {
				p.setPen(!inBubble
					? st->msgImgReplyBarColor()->c
					: useColorIndex
					? FromNameFg(context, colorIndexPlusOne - 1)
					: stm->msgServiceFg->c);
				_name.drawLeftElided(
					p,
					x + st::historyReplyPadding.left() + previewSkip,
					y + st::historyReplyPadding.top(),
					namew,
					w + 2 * x,
					_nameTwoLines ? 2 : 1);

				p.setPen(inBubble
					? stm->historyTextFg
					: st->msgImgReplyBarColor());
				view->prepareCustomEmojiPaint(p, context, _text);
				auto replyToTextPalette = &(!inBubble
					? st->imgReplyTextPalette()
					: useColorIndex
					? st->coloredTextPalette(selected, colorIndexPlusOne - 1)
					: stm->replyTextPalette);
				auto owned = std::optional<style::owned_color>();
				auto copy = std::optional<style::TextPalette>();
				if (inBubble && colorIndexPlusOne) {
					copy.emplace(*replyToTextPalette);
					owned.emplace(cache->icon);
					copy->linkFg = owned->color();
					replyToTextPalette = &*copy;
				}
				if (_replyToStory) {
					st::dialogsMiniReplyStory.icon.icon.paint(
						p,
						textLeft + firstLineSkip,
						textTop,
						w + 2 * x,
						replyToTextPalette->linkFg->c);
					firstLineSkip += st::dialogsMiniReplyStory.skipText
						+ st::dialogsMiniReplyStory.icon.icon.width();
				}
				_text.draw(p, {
					.position = { textLeft, textTop },
					.geometry = textGeometry(textw, firstLineSkip),
					.palette = replyToTextPalette,
					.spoiler = Ui::Text::DefaultSpoilerCache(),
					.now = context.now,
					.pausedEmoji = (context.paused
						|| On(PowerSaving::kEmojiChat)),
					.pausedSpoiler = pausedSpoiler,
					.elisionLines = 1,
				});
				p.setTextPalette(stm->textPalette);
			}
		} else {
			p.setFont(st::msgDateFont);
			p.setPen(cache->icon);
			p.drawTextLeft(
				textLeft,
				(y
					+ st::historyReplyPadding.top()
					+ (st::msgDateFont->height / 2)),
				w + 2 * x,
				st::msgDateFont->elided(
					_stateText,
					x + w - textLeft - st::historyReplyPadding.right()));
		}
	}
}

void Reply::createRippleAnimation(
		not_null<const Element*> view,
		QSize size) {
	_ripple.animation = std::make_unique<Ui::RippleAnimation>(
		st::defaultRippleAnimation,
		Ui::RippleAnimation::RoundRectMask(
			size,
			st::messageQuoteStyle.radius),
		[=] { view->repaint(); });
}

void Reply::saveRipplePoint(QPoint point) const {
	_ripple.lastPoint = point;
}

void Reply::addRipple() {
	if (_ripple.animation) {
		_ripple.animation->add(_ripple.lastPoint);
	}
}

void Reply::stopLastRipple() {
	if (_ripple.animation) {
		_ripple.animation->lastStop();
	}
}

TextWithEntities Reply::PeerEmoji(
		not_null<History*> history,
		PeerData *peer) {
	return PeerEmoji(&history->owner(), peer);
}

TextWithEntities Reply::PeerEmoji(
		not_null<Data::Session*> owner,
		PeerData *peer) {
	using namespace std;
	const auto icon = !peer
		? pair(&st::historyReplyUser, st::historyReplyUserPadding)
		: peer->isBroadcast()
		? pair(&st::historyReplyChannel, st::historyReplyChannelPadding)
		: (peer->isChannel() || peer->isChat())
		? pair(&st::historyReplyGroup, st::historyReplyGroupPadding)
		: pair(&st::historyReplyUser, st::historyReplyUserPadding);
	return Ui::Text::SingleCustomEmoji(
		owner->customEmojiManager().registerInternalEmoji(
			*icon.first,
			icon.second));
}

TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
	return Ui::Text::SingleCustomEmoji(
		owner->customEmojiManager().registerInternalEmoji(
			st::historyReplyForward,
			st::historyReplyForwardPadding));
}

TextWithEntities Reply::ComposePreviewName(
		not_null<History*> history,
		not_null<HistoryItem*> to,
		bool quote) {
	const auto sender = [&] {
		if (const auto from = to->displayFrom()) {
			return not_null(from);
		}
		return to->author();
	}();
	const auto toPeer = to->history()->peer;
	const auto displayAsExternal = (to->history() != history);
	const auto groupNameAdded = displayAsExternal
		&& (toPeer != sender)
		&& (toPeer->isChat() || toPeer->isMegagroup());
	const auto shorten = groupNameAdded || quote;

	auto nameFull = TextWithEntities();
	using namespace HistoryView;
	if (displayAsExternal && !groupNameAdded) {
		nameFull.append(Reply::PeerEmoji(history, sender));
	}
	nameFull.append(shorten ? sender->shortName() : sender->name());
	if (groupNameAdded) {
		nameFull.append(' ').append(Reply::PeerEmoji(history, toPeer));
		nameFull.append(toPeer->name());
	}
	return (quote
		? tr::lng_preview_reply_to_quote
		: tr::lng_preview_reply_to)(
			tr::now,
			lt_name,
			nameFull,
			Ui::Text::WithEntities);

}

void Reply::unloadPersistentAnimation() {
	_text.unloadPersistentAnimation();
}

} // namespace HistoryView
