/*
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 "window/notifications_manager_default.h"

#include "platform/platform_notifications_manager.h"
#include "platform/platform_specific.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "chat_helpers/message_field.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/emoji_config.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "data/data_session.h"
#include "data/data_forum_topic.h"
#include "data/stickers/data_custom_emoji.h"
#include "dialogs/ui/dialogs_layout.h"
#include "window/window_controller.h"
#include "storage/file_download.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_item_preview.h"
#include "base/platform/base_platform_last_input.h"
#include "base/call_delayed.h"
#include "styles/style_dialogs.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"

#include <QtGui/QGuiApplication>
#include <QtGui/QScreen>

namespace Window {
namespace Notifications {
namespace Default {
namespace {

[[nodiscard]] QPoint notificationStartPosition() {
	const auto corner = Core::App().settings().notificationsCorner();
	const auto window = Core::App().activePrimaryWindow();
	const auto r = window
		? window->widget()->desktopRect()
		: QGuiApplication::primaryScreen()->availableGeometry();
	const auto isLeft = Core::Settings::IsLeftCorner(corner);
	const auto isTop = Core::Settings::IsTopCorner(corner);
	const auto x = (isLeft == rtl())
		? (r.x() + r.width() - st::notifyWidth - st::notifyDeltaX)
		: (r.x() + st::notifyDeltaX);
	const auto y = isTop ? r.y() : (r.y() + r.height());
	return QPoint(x, y);
}

internal::Widget::Direction notificationShiftDirection() {
	auto isTop = Core::Settings::IsTopCorner(Core::App().settings().notificationsCorner());
	return isTop ? internal::Widget::Direction::Down : internal::Widget::Direction::Up;
}

} // namespace

std::unique_ptr<Manager> Create(System *system) {
	return std::make_unique<Manager>(system);
}

Manager::Manager(System *system)
: Notifications::Manager(system)
, _inputCheckTimer([=] { checkLastInput(); }) {
	system->settingsChanged(
	) | rpl::start_with_next([=](ChangeType change) {
		settingsChanged(change);
	}, _lifetime);
}

Manager::QueuedNotification::QueuedNotification(NotificationFields &&fields)
: history(fields.item->history())
, topicRootId(fields.item->topicRootId())
, peer(history->peer)
, reaction(fields.reactionId)
, author(!fields.reactionFrom
	? fields.item->notificationHeader()
	: (fields.reactionFrom != peer)
	? fields.reactionFrom->name()
	: QString())
, item((fields.forwardedCount < 2) ? fields.item.get() : nullptr)
, forwardedCount(fields.forwardedCount)
, fromScheduled(reaction.empty() && (fields.item->out() || peer->isSelf())
	&& fields.item->isFromScheduled()) {
}

QPixmap Manager::hiddenUserpicPlaceholder() const {
	if (_hiddenUserpicPlaceholder.isNull()) {
		const auto ratio = style::DevicePixelRatio();
		_hiddenUserpicPlaceholder = Ui::PixmapFromImage(
			LogoNoMargin().scaled(
				st::notifyPhotoSize * ratio,
				st::notifyPhotoSize * ratio,
				Qt::IgnoreAspectRatio,
				Qt::SmoothTransformation));
		_hiddenUserpicPlaceholder.setDevicePixelRatio(ratio);
	}
	return _hiddenUserpicPlaceholder;
}

bool Manager::hasReplyingNotification() const {
	for (const auto &notification : _notifications) {
		if (notification->isReplying()) {
			return true;
		}
	}
	return false;
}

void Manager::settingsChanged(ChangeType change) {
	if (change == ChangeType::Corner) {
		auto startPosition = notificationStartPosition();
		auto shiftDirection = notificationShiftDirection();
		for (const auto &notification : _notifications) {
			notification->updatePosition(startPosition, shiftDirection);
		}
		if (_hideAll) {
			_hideAll->updatePosition(startPosition, shiftDirection);
		}
	} else if (change == ChangeType::MaxCount) {
		int allow = Core::App().settings().notificationsCount();
		for (int i = _notifications.size(); i != 0;) {
			auto &notification = _notifications[--i];
			if (notification->isUnlinked()) continue;
			if (--allow < 0) {
				notification->unlinkHistory();
			}
		}
		if (allow > 0) {
			for (int i = 0; i != allow; ++i) {
				showNextFromQueue();
			}
		}
	} else if ((change == ChangeType::DemoIsShown)
			|| (change == ChangeType::DemoIsHidden)) {
		_demoIsShown = (change == ChangeType::DemoIsShown);
		_demoMasterOpacity.start(
			[=] { demoMasterOpacityCallback(); },
			_demoIsShown ? 1. : 0.,
			_demoIsShown ? 0. : 1.,
			st::notifyFastAnim);
	}
}

void Manager::demoMasterOpacityCallback() {
	for (const auto &notification : _notifications) {
		notification->updateOpacity();
	}
	if (_hideAll) {
		_hideAll->updateOpacity();
	}
}

float64 Manager::demoMasterOpacity() const {
	return _demoMasterOpacity.value(_demoIsShown ? 0. : 1.);
}

void Manager::checkLastInput() {
	auto replying = hasReplyingNotification();
	auto waiting = false;
	const auto lastInputTime = base::Platform::LastUserInputTimeSupported()
		? std::make_optional(Core::App().lastNonIdleTime())
		: std::nullopt;
	for (const auto &notification : _notifications) {
		if (!notification->checkLastInput(replying, lastInputTime)) {
			waiting = true;
		}
	}
	if (waiting) {
		_inputCheckTimer.callOnce(300);
	}
}

void Manager::startAllHiding() {
	if (!hasReplyingNotification()) {
		for (const auto &notification : _notifications) {
			notification->startHiding();
		}
		if (_hideAll && _queuedNotifications.size() < 2) {
			_hideAll->startHiding();
		}
	}
}

void Manager::stopAllHiding() {
	for (const auto &notification : _notifications) {
		notification->stopHiding();
	}
	if (_hideAll) {
		_hideAll->stopHiding();
	}
}

void Manager::showNextFromQueue() {
	auto guard = gsl::finally([this] {
		if (_positionsOutdated) {
			moveWidgets();
		}
	});
	if (_queuedNotifications.empty()) {
		return;
	}
	int count = Core::App().settings().notificationsCount();
	for (const auto &notification : _notifications) {
		if (notification->isUnlinked()) continue;
		--count;
	}
	if (count <= 0) {
		return;
	}

	auto startPosition = notificationStartPosition();
	auto startShift = 0;
	auto shiftDirection = notificationShiftDirection();
	do {
		auto queued = _queuedNotifications.front();
		_queuedNotifications.pop_front();

		subscribeToSession(&queued.history->session());
		_notifications.push_back(std::make_unique<Notification>(
			this,
			queued.history,
			queued.topicRootId,
			queued.peer,
			queued.author,
			queued.item,
			queued.reaction,
			queued.forwardedCount,
			queued.fromScheduled,
			startPosition,
			startShift,
			shiftDirection));
		--count;
	} while (count > 0 && !_queuedNotifications.empty());

	_positionsOutdated = true;
	checkLastInput();
}

void Manager::subscribeToSession(not_null<Main::Session*> session) {
	auto i = _subscriptions.find(session);
	if (i == _subscriptions.end()) {
		i = _subscriptions.emplace(session).first;
		session->account().sessionChanges(
		) | rpl::start_with_next([=] {
			_subscriptions.remove(session);
		}, i->second.lifetime);
	} else if (i->second.subscription) {
		return;
	}
	session->downloaderTaskFinished(
	) | rpl::start_with_next([=] {
		auto found = false;
		for (const auto &notification : _notifications) {
			if (const auto history = notification->maybeHistory()) {
				if (&history->session() == session) {
					notification->updatePeerPhoto();
					found = true;
				}
			}
		}
		if (!found) {
			_subscriptions[session].subscription.destroy();
		}
	}, i->second.subscription);
}

void Manager::moveWidgets() {
	auto shift = st::notifyDeltaY;
	int lastShift = 0, lastShiftCurrent = 0, count = 0;
	for (int i = _notifications.size(); i != 0;) {
		auto &notification = _notifications[--i];
		if (notification->isUnlinked()) continue;

		notification->changeShift(shift);
		shift += notification->height() + st::notifyDeltaY;

		lastShiftCurrent = notification->currentShift();
		lastShift = shift;

		++count;
	}

	if (count > 1 || !_queuedNotifications.empty()) {
		if (!_hideAll) {
			_hideAll = std::make_unique<HideAllButton>(this, notificationStartPosition(), lastShiftCurrent, notificationShiftDirection());
		}
		_hideAll->changeShift(lastShift);
		_hideAll->stopHiding();
	} else if (_hideAll) {
		_hideAll->startHidingFast();
	}
}

void Manager::changeNotificationHeight(Notification *notification, int newHeight) {
	auto deltaHeight = newHeight - notification->height();
	if (!deltaHeight) return;

	notification->addToHeight(deltaHeight);
	auto it = std::find_if(_notifications.cbegin(), _notifications.cend(), [notification](auto &item) {
		return (item.get() == notification);
	});
	if (it != _notifications.cend()) {
		for (auto i = _notifications.cbegin(); i != it; ++i) {
			auto &notification = *i;
			if (notification->isUnlinked()) continue;

			notification->addToShift(deltaHeight);
		}
	}
	if (_hideAll) {
		_hideAll->addToShift(deltaHeight);
	}
}

void Manager::unlinkFromShown(Notification *remove) {
	if (remove) {
		if (remove->unlinkHistory()) {
			_positionsOutdated = true;
		}
	}
	showNextFromQueue();
}

void Manager::removeWidget(internal::Widget *remove) {
	if (remove == _hideAll.get()) {
		_hideAll.reset();
	} else if (remove) {
		const auto it = ranges::find(
			_notifications,
			remove,
			&std::unique_ptr<Notification>::get);
		if (it != end(_notifications)) {
			_notifications.erase(it);
			_positionsOutdated = true;
		}
	}
	showNextFromQueue();
}

void Manager::doShowNotification(NotificationFields &&fields) {
	_queuedNotifications.emplace_back(std::move(fields));
	showNextFromQueue();
}

void Manager::doClearAll() {
	_queuedNotifications.clear();
	for (const auto &notification : _notifications) {
		notification->unlinkHistory();
	}
	showNextFromQueue();
}

void Manager::doClearAllFast() {
	_queuedNotifications.clear();
	base::take(_notifications);
	base::take(_hideAll);
}

void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
	const auto history = topic->history();
	const auto topicRootId = topic->rootId();
	for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {
		if (i->history == history && i->topicRootId == topicRootId) {
			i = _queuedNotifications.erase(i);
		} else {
			++i;
		}
	}
	for (const auto &notification : _notifications) {
		if (notification->unlinkHistory(history, topicRootId)) {
			_positionsOutdated = true;
		}
	}
	showNextFromQueue();
}

void Manager::doClearFromHistory(not_null<History*> history) {
	for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {
		if (i->history == history) {
			i = _queuedNotifications.erase(i);
		} else {
			++i;
		}
	}
	for (const auto &notification : _notifications) {
		if (notification->unlinkHistory(history)) {
			_positionsOutdated = true;
		}
	}
	showNextFromQueue();
}

void Manager::doClearFromSession(not_null<Main::Session*> session) {
	for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) {
		if (&i->history->session() == session) {
			i = _queuedNotifications.erase(i);
		} else {
			++i;
		}
	}
	for (const auto &notification : _notifications) {
		if (notification->unlinkSession(session)) {
			_positionsOutdated = true;
		}
	}
	showNextFromQueue();
}

void Manager::doClearFromItem(not_null<HistoryItem*> item) {
	_queuedNotifications.erase(std::remove_if(_queuedNotifications.begin(), _queuedNotifications.end(), [&](auto &queued) {
		return (queued.item == item);
	}), _queuedNotifications.cend());

	auto showNext = false;
	for (const auto &notification : _notifications) {
		if (notification->unlinkItem(item)) {
			showNext = true;
		}
	}
	if (showNext) {
		// This call invalidates _notifications iterators.
		showNextFromQueue();
	}
}

bool Manager::doSkipToast() const {
	return Platform::Notifications::SkipToastForCustom();
}

void Manager::doMaybePlaySound(Fn<void()> playSound) {
	Platform::Notifications::MaybePlaySoundForCustom(std::move(playSound));
}

void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
	Platform::Notifications::MaybeFlashBounceForCustom(std::move(flashBounce));
}

void Manager::doUpdateAll() {
	for (const auto &notification : _notifications) {
		notification->updateNotifyDisplay();
	}
}

Manager::~Manager() {
	clearAllFast();
}

namespace internal {

Widget::Widget(
	not_null<Manager*> manager,
	QPoint startPosition,
	int shift,
	Direction shiftDirection)
: _manager(manager)
, _startPosition(startPosition)
, _direction(shiftDirection)
, _shift(shift)
, _shiftAnimation([=](crl::time now) {
	return shiftAnimationCallback(now);
}) {
	setWindowOpacity(0.);

	setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint)
		| Qt::WindowStaysOnTopHint
		| Qt::BypassWindowManagerHint
		| Qt::NoDropShadowWindowHint
		| Qt::Tool);
	setAttribute(Qt::WA_MacAlwaysShowToolWindow);
	setAttribute(Qt::WA_OpaquePaintEvent);

	Ui::Platform::InitOnTopPanel(this);

	_a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim);
}

void Widget::opacityAnimationCallback() {
	updateOpacity();
	update();
	if (!_a_opacity.animating() && _hiding) {
		if (underMouse()) {
			// The notification is leaving from under the cursor, but in such case leave hook is not
			// triggered automatically. But we still want the manager to start hiding notifications
			// (see #28813).
			manager()->startAllHiding();
		}
		manager()->removeWidget(this);  // Deletes `this`
	}
}

bool Widget::shiftAnimationCallback(crl::time now) {
	if (anim::Disabled()) {
		now += st::notifyFastAnim;
	}
	const auto dt = (now - _shiftAnimation.started())
		/ float64(st::notifyFastAnim);
	if (dt >= 1.) {
		_shift.finish();
	} else {
		_shift.update(dt, anim::linear);
	}
	moveByShift();
	return (dt < 1.);
}

void Widget::hideSlow() {
	if (anim::Disabled()) {
		_hiding = true;
		base::call_delayed(
			st::notifySlowHide,
			this,
			[=, guard = _hidingDelayed.make_guard()] {
				if (guard && _hiding) {
					hideFast();
				}
			});
	} else {
		hideAnimated(st::notifySlowHide, anim::easeInCirc);
	}
}

void Widget::hideFast() {
	hideAnimated(st::notifyFastAnim, anim::linear);
}

void Widget::hideStop() {
	if (_hiding) {
		_hiding = false;
		_hidingDelayed = {};
		_a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim);
	}
}

void Widget::hideAnimated(float64 duration, const anim::transition &func) {
	_hiding = true;
	// Stop the previous animation so as to make sure that the notification
	// is fully restored before hiding it again.
	// Relates to https://github.com/telegramdesktop/tdesktop/issues/28811.
	_a_opacity.stop();
	_a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., duration, func);
}

void Widget::updateOpacity() {
	setWindowOpacity(_a_opacity.value(_hiding ? 0. : 1.) * _manager->demoMasterOpacity());
}

void Widget::changeShift(int top) {
	_shift.start(top);
	_shiftAnimation.start();
}

void Widget::updatePosition(QPoint startPosition, Direction shiftDirection) {
	_startPosition = startPosition;
	_direction = shiftDirection;
	moveByShift();
}

void Widget::addToHeight(int add) {
	auto newHeight = height() + add;
	auto newPosition = computePosition(newHeight);
	updateGeometry(newPosition.x(), newPosition.y(), width(), newHeight);
	Ui::ForceFullRepaintSync(this);
}

void Widget::updateGeometry(int x, int y, int width, int height) {
	move(x, y);
	setFixedSize(width, height);
	update();
}

void Widget::addToShift(int add) {
	_shift.add(add);
	moveByShift();
}

void Widget::moveByShift() {
	move(computePosition(height()));
}

QPoint Widget::computePosition(int height) const {
	auto realShift = qRound(_shift.current());
	if (_direction == Direction::Up) {
		realShift = -realShift - height;
	}
	return QPoint(_startPosition.x(), _startPosition.y() + realShift);
}

Background::Background(QWidget *parent) : TWidget(parent) {
	setAttribute(Qt::WA_OpaquePaintEvent);
}

void Background::paintEvent(QPaintEvent *e) {
	Painter p(this);

	p.fillRect(rect(), st::notificationBg);
	p.fillRect(0, 0, st::notifyBorderWidth, height(), st::notifyBorder);
	p.fillRect(width() - st::notifyBorderWidth, 0, st::notifyBorderWidth, height(), st::notifyBorder);
	p.fillRect(st::notifyBorderWidth, height() - st::notifyBorderWidth, width() - 2 * st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder);
}

Notification::Notification(
	not_null<Manager*> manager,
	not_null<History*> history,
	MsgId topicRootId,
	not_null<PeerData*> peer,
	const QString &author,
	HistoryItem *item,
	const Data::ReactionId &reaction,
	int forwardedCount,
	bool fromScheduled,
	QPoint startPosition,
	int shift,
	Direction shiftDirection)
: Widget(manager, startPosition, shift, shiftDirection)
, _peer(peer)
, _started(crl::now())
, _history(history)
, _topic(history->peer->forumTopicFor(topicRootId))
, _topicRootId(topicRootId)
, _userpicView(_peer->createUserpicView())
, _author(author)
, _reaction(reaction)
, _item(item)
, _forwardedCount(forwardedCount)
, _fromScheduled(fromScheduled)
, _close(this, st::notifyClose)
, _reply(this, tr::lng_notification_reply(), st::defaultBoxButton) {
	Lang::Updated(
	) | rpl::start_with_next([=] {
		refreshLang();
	}, lifetime());

	if (_topic) {
		_topic->destroyed(
		) | rpl::start_with_next([=] {
			unlinkHistory();
		}, lifetime());
	}

	auto position = computePosition(st::notifyMinHeight);
	updateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyMinHeight);

	_userpicLoaded = !Ui::PeerUserpicLoading(_userpicView);
	updateNotifyDisplay();

	_hideTimer.setSingleShot(true);
	connect(&_hideTimer, &QTimer::timeout, [=] { startHiding(); });

	_close->setClickedCallback([this] {
		unlinkHistoryInManager();
	});
	_close->setAcceptBoth(true);
	_close->moveToRight(st::notifyClosePos.x(), st::notifyClosePos.y());
	_close->show();

	_reply->setClickedCallback([this] {
		showReplyField();
	});
	_replyPadding = st::notifyMinHeight - st::notifyPhotoPos.y() - st::notifyPhotoSize;
	updateReplyGeometry();
	_reply->hide();

	prepareActionsCache();

	style::PaletteChanged(
	) | rpl::start_with_next([=] {
		updateNotifyDisplay();
		if (!_buttonsCache.isNull()) {
			prepareActionsCache();
		}
		update();
		if (_background) {
			_background->update();
		}
	}, lifetime());

	show();
}

void Notification::updateReplyGeometry() {
	_reply->moveToRight(_replyPadding, height() - _reply->height() - _replyPadding);
}

void Notification::refreshLang() {
	InvokeQueued(this, [this] { updateReplyGeometry(); });
}

void Notification::prepareActionsCache() {
	auto replyCache = Ui::GrabWidget(_reply);
	auto fadeWidth = st::notifyFadeRight.width();
	auto actionsTop = st::notifyTextTop + st::semiboldFont->height;
	auto replyRight = _replyPadding - st::notifyBorderWidth;
	auto actionsCacheWidth = _reply->width() + replyRight + fadeWidth;
	auto actionsCacheHeight = height() - actionsTop - st::notifyBorderWidth;
	auto actionsCacheImg = QImage(
		QSize(actionsCacheWidth, actionsCacheHeight)
			* style::DevicePixelRatio(),
		QImage::Format_ARGB32_Premultiplied);
	actionsCacheImg.setDevicePixelRatio(style::DevicePixelRatio());
	actionsCacheImg.fill(Qt::transparent);
	{
		Painter p(&actionsCacheImg);
		st::notifyFadeRight.fill(p, style::rtlrect(0, 0, fadeWidth, actionsCacheHeight, actionsCacheWidth));
		p.fillRect(style::rtlrect(fadeWidth, 0, actionsCacheWidth - fadeWidth, actionsCacheHeight, actionsCacheWidth), st::notificationBg);
		p.drawPixmapRight(replyRight, _reply->y() - actionsTop, actionsCacheWidth, replyCache);
	}
	_buttonsCache = Ui::PixmapFromImage(std::move(actionsCacheImg));
}

bool Notification::checkLastInput(
		bool hasReplyingNotifications,
		std::optional<crl::time> lastInputTime) {
	if (!_waitingForInput) return true;

	using namespace Platform::Notifications;
	const auto waitForUserInput = WaitForInputForCustom()
		&& lastInputTime.has_value()
		&& (*lastInputTime <= _started);

	if (!waitForUserInput) {
		_waitingForInput = false;
		if (!hasReplyingNotifications) {
			_hideTimer.start(st::notifyWaitLongHide);
		}
		return true;
	}
	return false;
}

void Notification::replyResized() {
	changeHeight(st::notifyMinHeight + _replyArea->height() + st::notifyBorderWidth);
}

void Notification::replyCancel() {
	unlinkHistoryInManager();
}

void Notification::updateGeometry(int x, int y, int width, int height) {
	if (height > st::notifyMinHeight) {
		if (!_background) {
			_background.create(this);
		}
		_background->setGeometry(0, st::notifyMinHeight, width, height - st::notifyMinHeight);
	} else if (_background) {
		_background.destroy();
	}
	Widget::updateGeometry(x, y, width, height);
}

void Notification::paintEvent(QPaintEvent *e) {
	repaintText();

	Painter p(this);
	p.setClipRect(e->rect());
	p.drawImage(0, 0, _cache);

	auto buttonsTop = st::notifyTextTop + st::semiboldFont->height;
	if (a_actionsOpacity.animating()) {
		p.setOpacity(a_actionsOpacity.value(1.));
		p.drawPixmapRight(st::notifyBorderWidth, buttonsTop, width(), _buttonsCache);
	} else if (_actionsVisible) {
		p.drawPixmapRight(st::notifyBorderWidth, buttonsTop, width(), _buttonsCache);
	}
}

void Notification::actionsOpacityCallback() {
	update();
	if (!a_actionsOpacity.animating() && _actionsVisible) {
		_reply->show();
	}
}

void Notification::customEmojiCallback() {
	if (_textsRepaintScheduled) {
		return;
	}
	_textsRepaintScheduled = true;
	crl::on_main(this, [=] { repaintText(); });
}

void Notification::repaintText() {
	if (!_textsRepaintScheduled) {
		return;
	}
	_textsRepaintScheduled = false;
	if (_cache.isNull()) {
		return;
	}
	Painter p(&_cache);
	const auto adjusted = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);
	const auto skip = (adjusted - st::emojiSize + 1) / 2;
	const auto margin = QMargins{ skip, skip, skip, skip };
	p.fillRect(_titleRect.marginsAdded(margin), st::notificationBg);
	p.fillRect(_textRect.marginsAdded(margin), st::notificationBg);
	paintTitle(p);
	paintText(p);
	update();
}

void Notification::paintTitle(Painter &p) {
	p.setPen(st::dialogsNameFg);
	p.setFont(st::semiboldFont);
	_titleCache.draw(p, {
		.position = _titleRect.topLeft(),
		.availableWidth = _titleRect.width(),
		.palette = &st::dialogsTextPalette,
		.spoiler = Ui::Text::DefaultSpoilerCache(),
		.pausedEmoji = On(PowerSaving::kEmojiChat),
		.pausedSpoiler = On(PowerSaving::kChatSpoiler),
		.elisionLines = 1,
	});
}

void Notification::paintText(Painter &p) {
	p.setPen(st::dialogsTextFg);
	p.setFont(st::dialogsTextFont);
	_textCache.draw(p, {
		.position = _textRect.topLeft(),
		.availableWidth = _textRect.width(),
		.palette = &st::dialogsTextPalette,
		.spoiler = Ui::Text::DefaultSpoilerCache(),
		.pausedEmoji = On(PowerSaving::kEmojiChat),
		.pausedSpoiler = On(PowerSaving::kChatSpoiler),
		.elisionHeight = _textRect.height(),
	});
}

void Notification::updateNotifyDisplay() {
	if (!_history || (!_item && _forwardedCount < 2)) {
		return;
	}

	const auto options = manager()->getNotificationOptions(
		_item,
		(_reaction.empty()
			? Data::ItemNotificationType::Message
			: Data::ItemNotificationType::Reaction));
	_hideReplyButton = options.hideReplyButton;

	int32 w = width(), h = height();
	auto img = QImage(
		size() * style::DevicePixelRatio(),
		QImage::Format_ARGB32_Premultiplied);
	img.setDevicePixelRatio(style::DevicePixelRatio());
	img.fill(st::notificationBg->c);

	{
		Painter p(&img);
		p.fillRect(0, 0, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder);
		p.fillRect(w - st::notifyBorderWidth, 0, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder);
		p.fillRect(st::notifyBorderWidth, h - st::notifyBorderWidth, w - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorder);
		p.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, h - st::notifyBorderWidth, st::notifyBorder);

		if (!options.hideNameAndPhoto) {
			if (_fromScheduled && _history->peer->isSelf()) {
				Ui::EmptyUserpic::PaintSavedMessages(p, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize);
				_userpicLoaded = true;
			} else if (_history->peer->isRepliesChat()) {
				Ui::EmptyUserpic::PaintRepliesMessages(p, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize);
				_userpicLoaded = true;
			} else {
				_userpicView = _history->peer->createUserpicView();
				_history->peer->loadUserpic();
				_history->peer->paintUserpicLeft(p, _userpicView, st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), width(), st::notifyPhotoSize);
			}
		} else {
			p.drawPixmap(st::notifyPhotoPos.x(), st::notifyPhotoPos.y(), manager()->hiddenUserpicPlaceholder());
			_userpicLoaded = true;
		}

		int32 itemWidth = w - st::notifyPhotoPos.x() - st::notifyPhotoSize - st::notifyTextLeft - st::notifyClosePos.x() - st::notifyClose.width;

		QRect rectForName(st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft, st::notifyTextTop, itemWidth, st::semiboldFont->height);
		const auto reminder = _fromScheduled && _history->peer->isSelf();
		if (!options.hideNameAndPhoto) {
			if (_fromScheduled) {
				static const auto emoji = Ui::Emoji::Find(QString::fromUtf8("\xF0\x9F\x93\x85"));
				const auto size = Ui::Emoji::GetSizeNormal()
					/ style::DevicePixelRatio();
				const auto top = rectForName.top() + (st::semiboldFont->height - size) / 2;
				Ui::Emoji::Draw(p, emoji, Ui::Emoji::GetSizeNormal(), rectForName.left(), top);
				rectForName.setLeft(rectForName.left() + size + st::semiboldFont->spacew);
			}
			const auto chatTypeIcon = _topic
				? nullptr
				: Dialogs::Ui::ChatTypeIcon(_history->peer);
			if (chatTypeIcon) {
				chatTypeIcon->paint(p, rectForName.topLeft(), w);
				rectForName.setLeft(rectForName.left()
					+ chatTypeIcon->width()
					+ st::dialogsChatTypeSkip);
			}
		}

		const auto composeText = !options.hideMessageText
			|| (!_reaction.empty() && !options.hideNameAndPhoto);
		if (composeText) {
			auto old = base::take(_textCache);
			_textCache = Ui::Text::String(itemWidth);
			auto r = QRect(
				st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft,
				st::notifyItemTop + st::semiboldFont->height,
				itemWidth,
				2 * st::dialogsTextFont->height);
			const auto text = !_reaction.empty()
				? (!_author.isEmpty()
					? Ui::Text::Colorized(_author).append(' ')
					: TextWithEntities()
				).append(Manager::ComposeReactionNotification(
					_item,
					_reaction,
					options.hideMessageText))
				: _item
				? _item->toPreview({
					.hideSender = reminder,
					.generateImages = false,
					.spoilerLoginCode = options.spoilerLoginCode,
				}).text
				: ((!_author.isEmpty()
						? Ui::Text::Colorized(_author)
						: TextWithEntities()
					).append(_forwardedCount > 1
						? ('\n' + tr::lng_forward_messages(
							tr::now,
							lt_count,
							_forwardedCount))
						: QString()));
			const auto options = TextParseOptions{
				(TextParseColorized
					| TextParseMarkdown
					| (_forwardedCount > 1 ? TextParseMultiline : 0)),
				0,
				0,
				Qt::LayoutDirectionAuto,
			};
			const auto context = Core::TextContext({
				.session = &_history->session(),
				.repaint = [=] { customEmojiCallback(); },
			});
			_textCache.setMarkedText(
				st::dialogsTextStyle,
				text,
				options,
				context);
			_textRect = r;
			paintText(p);
			if (!_textCache.hasPersistentAnimation() && !_topic) {
				_textCache = Ui::Text::String();
			}
		} else {
			p.setFont(st::dialogsTextFont);
			p.setPen(st::dialogsTextFgService);
			p.drawText(
				st::notifyPhotoPos.x() + st::notifyPhotoSize + st::notifyTextLeft,
				st::notifyItemTop + st::semiboldFont->height + st::dialogsTextFont->ascent,
				st::dialogsTextFont->elided(
					tr::lng_notification_preview(tr::now),
					itemWidth));
		}

		const auto topicWithChat = [&]() -> TextWithEntities {
			const auto name = st::wrap_rtl(_history->peer->name());
			return _topic
				? _topic->titleWithIcon().append(u" ("_q + name + ')')
				: TextWithEntities{ name };
		};
		auto title = options.hideNameAndPhoto
			? TextWithEntities{ u"Telegram Desktop"_q }
			: reminder
			? tr::lng_notification_reminder(tr::now, Ui::Text::WithEntities)
			: topicWithChat();
		const auto fullTitle = manager()->addTargetAccountName(
			std::move(title),
			&_history->session());
		const auto context = Core::TextContext({
			.session = &_history->session(),
			.repaint = [=] { customEmojiCallback(); },
		});
		_titleCache.setMarkedText(
			st::semiboldTextStyle,
			fullTitle,
			Ui::NameTextOptions(),
			context);
		_titleRect = rectForName;
		paintTitle(p);
	}

	_cache = std::move(img);
	if (!canReply()) {
		toggleActionButtons(false);
	}
	update();
}

void Notification::updatePeerPhoto() {
	if (_userpicLoaded) {
		return;
	}
	_userpicView = _peer->createUserpicView();
	if (Ui::PeerUserpicLoading(_userpicView)) {
		return;
	}
	_userpicLoaded = true;

	Painter p(&_cache);
	p.fillRect(
		style::rtlrect(
			QRect(
				st::notifyPhotoPos,
				QSize(st::notifyPhotoSize, st::notifyPhotoSize)),
			width()),
		st::notificationBg);
	_peer->paintUserpicLeft(
		p,
		_userpicView,
		st::notifyPhotoPos.x(),
		st::notifyPhotoPos.y(),
		width(),
		st::notifyPhotoSize);
	_userpicView = {};
	update();
}

bool Notification::unlinkItem(HistoryItem *deleted) {
	auto unlink = (_item && _item == deleted);
	if (unlink) {
		_item = nullptr;
		unlinkHistory();
	}
	return unlink;
}

bool Notification::canReply() const {
	return !_hideReplyButton
		&& (_item != nullptr)
		&& !Core::App().passcodeLocked()
		&& (Core::App().settings().notifyView()
			<= Core::Settings::NotifyView::ShowPreview);
}

void Notification::unlinkHistoryInManager() {
	manager()->unlinkFromShown(this);
}

void Notification::toggleActionButtons(bool visible) {
	if (_actionsVisible != visible) {
		_actionsVisible = visible;
		a_actionsOpacity.start([this] { actionsOpacityCallback(); }, _actionsVisible ? 0. : 1., _actionsVisible ? 1. : 0., st::notifyActionsDuration);
		_reply->clearState();
		_reply->hide();
	}
}

void Notification::showReplyField() {
	if (!_item) {
		return;
	}
	raise();
	activateWindow();

	if (_replyArea) {
		_replyArea->setFocus();
		return;
	}
	stopHiding();

	_background.create(this);
	_background->setGeometry(0, st::notifyMinHeight, width(), st::notifySendReply.height + st::notifyBorderWidth);
	_background->show();

	_replyArea.create(
		this,
		st::notifyReplyArea,
		Ui::InputField::Mode::MultiLine,
		tr::lng_message_ph());
	_replyArea->resize(width() - st::notifySendReply.width - 2 * st::notifyBorderWidth, st::notifySendReply.height);
	_replyArea->moveToLeft(st::notifyBorderWidth, st::notifyMinHeight);
	_replyArea->show();
	_replyArea->setFocus();
	_replyArea->setMaxLength(MaxMessageSize);
	_replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
	InitMessageFieldHandlers({
		.session = &_item->history()->session(),
		.field = _replyArea.data(),
	});

	// Catch mouse press event to activate the window.
	QCoreApplication::instance()->installEventFilter(this);
	_replyArea->heightChanges(
	) | rpl::start_with_next([=] {
		replyResized();
	}, _replyArea->lifetime());
	_replyArea->submits(
	) | rpl::start_with_next([=] { sendReply(); }, _replyArea->lifetime());
	_replyArea->cancelled(
	) | rpl::start_with_next([=] {
		replyCancel();
	}, _replyArea->lifetime());

	_replySend.create(this, st::notifySendReply);
	_replySend->moveToRight(st::notifyBorderWidth, st::notifyMinHeight);
	_replySend->show();
	_replySend->setClickedCallback([this] { sendReply(); });

	toggleActionButtons(false);

	replyResized();
	update();
}

void Notification::sendReply() {
	if (!_history) return;

	manager()->notificationReplied(
		myId(),
		_replyArea->getTextWithAppliedMarkdown());

	manager()->startAllHiding();
}

Notifications::Manager::NotificationId Notification::myId() const {
	if (!_history) {
		return {};
	}
	return { .contextId = {
		.sessionId = _history->session().uniqueId(),
		.peerId = _history->peer->id,
		.topicRootId = _topicRootId,
	}, .msgId = _item ? _item->id : ShowAtUnreadMsgId };
}

void Notification::changeHeight(int newHeight) {
	manager()->changeNotificationHeight(this, newHeight);
}

bool Notification::unlinkHistory(History *history, MsgId topicRootId) {
	const auto unlink = _history
		&& (history == _history || !history)
		&& (topicRootId == _topicRootId || !topicRootId);
	if (unlink) {
		hideFast();
		_history = nullptr;
		_topic = nullptr;
		_item = nullptr;
	}
	return unlink;
}

bool Notification::unlinkSession(not_null<Main::Session*> session) {
	const auto unlink = _history && (&_history->session() == session);
	if (unlink) {
		hideFast();
		_history = nullptr;
		_item = nullptr;
	}
	return unlink;
}

void Notification::enterEventHook(QEnterEvent *e) {
	if (!_history) {
		return;
	}
	manager()->stopAllHiding();
	if (!_replyArea && canReply()) {
		toggleActionButtons(true);
	}
}

void Notification::leaveEventHook(QEvent *e) {
	if (!_history) {
		return;
	}
	manager()->startAllHiding();
	toggleActionButtons(false);
}

void Notification::startHiding() {
	if (!_history) return;
	hideSlow();
}

void Notification::mousePressEvent(QMouseEvent *e) {
	if (!_history) return;

	if (e->button() == Qt::RightButton) {
		unlinkHistoryInManager();
	} else {
		e->ignore();
		manager()->notificationActivated(myId());
	}
}

bool Notification::eventFilter(QObject *o, QEvent *e) {
	if (e->type() == QEvent::MouseButtonPress) {
		if (auto receiver = qobject_cast<QWidget*>(o)) {
			if (isAncestorOf(receiver)) {
				raise();
				activateWindow();
			}
		}
	}
	return false;
}

void Notification::stopHiding() {
	if (!_history) return;
	_hideTimer.stop();
	Widget::hideStop();
}

HideAllButton::HideAllButton(
	not_null<Manager*> manager,
	QPoint startPosition,
	int shift,
	Direction shiftDirection)
: Widget(manager, startPosition, shift, shiftDirection) {
	setCursor(style::cur_pointer);

	auto position = computePosition(st::notifyHideAllHeight);
	updateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyHideAllHeight);

	style::PaletteChanged(
	) | rpl::start_with_next([=] {
		update();
	}, lifetime());

	show();
}

void HideAllButton::startHiding() {
	hideSlow();
}

void HideAllButton::startHidingFast() {
	hideFast();
}

void HideAllButton::stopHiding() {
	hideStop();
}

void HideAllButton::enterEventHook(QEnterEvent *e) {
	_mouseOver = true;
	update();
}

void HideAllButton::leaveEventHook(QEvent *e) {
	_mouseOver = false;
	update();
}

void HideAllButton::mousePressEvent(QMouseEvent *e) {
	_mouseDown = true;
}

void HideAllButton::mouseReleaseEvent(QMouseEvent *e) {
	auto mouseDown = base::take(_mouseDown);
	if (mouseDown && _mouseOver) {
		manager()->clearAll();
	}
}

void HideAllButton::paintEvent(QPaintEvent *e) {
	Painter p(this);
	p.setClipRect(e->rect());

	p.fillRect(rect(), _mouseOver ? st::lightButtonBgOver : st::lightButtonBg);
	p.fillRect(0, 0, width(), st::notifyBorderWidth, st::notifyBorder);
	p.fillRect(0, height() - st::notifyBorderWidth, width(), st::notifyBorderWidth, st::notifyBorder);
	p.fillRect(0, st::notifyBorderWidth, st::notifyBorderWidth, height() - 2 * st::notifyBorderWidth, st::notifyBorder);
	p.fillRect(width() - st::notifyBorderWidth, st::notifyBorderWidth, st::notifyBorderWidth, height() - 2 * st::notifyBorderWidth, st::notifyBorder);

	p.setFont(st::defaultLinkButton.font);
	p.setPen(_mouseOver ? st::lightButtonFgOver : st::lightButtonFg);
	p.drawText(rect(), tr::lng_notification_hide_all(tr::now), style::al_center);
}

} // namespace internal
} // namespace Default
} // namespace Notifications
} // namespace Window
