/*
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/chat/attach/attach_bot_webview.h"

#include "ui/chat/attach/attach_bot_downloads.h"
#include "ui/chat/attach/attach_bot_webview_linux_shell.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/box_content.h"
#include "ui/layers/standalone_layer_stack.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/round_rect.h"
#include "ui/style/style_core_palette.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/separate_panel.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/integration.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "core/file_utilities.h"
#include "webview/webview_embed.h"
#include "webview/webview_dialog.h"
#include "webview/webview_interface.h"
#include "base/debug_log.h"
#include "base/invoke_queued.h"
#include "base/platform/base_platform_info.h"
#include "base/qt_signal_producer.h"
#include "base/random.h"
#include "styles/style_chat.h"
#include "styles/style_info.h"
#include "styles/style_payments.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"

#include <QtCore/QBuffer>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QUrl>
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
#include <QtGui/QWindow>
#include <QtGui/QScreen>
#include <QtGui/qpa/qplatformscreen.h>

#include <algorithm>
#include <memory>

namespace Ui::BotWebView {

namespace {

constexpr auto kClipboardReadTimeout = crl::time(10000);
constexpr auto kProgressDuration = crl::time(200);
constexpr auto kProgressOpacity = 0.3;
constexpr auto kLightnessThreshold = 128;
constexpr auto kLightnessDelta = 32;
constexpr auto kExternalShellButtonIconSize = 20;
constexpr auto kMaxNativeMessageBytes = 1024 * 1024;
constexpr auto kExternalMessageType = "tdesktop_external_bot_webapp";

enum class NativeMessageSource {
	LegacyWebApp,
	ExternalWebApp,
	ExternalShell,
};

struct NativeMessage {
	NativeMessageSource source = NativeMessageSource::LegacyWebApp;
	QString origin;
	QString command;
	QJsonObject arguments;
};

[[nodiscard]] QString GenerateExternalShellToken() {
	auto bytes = QByteArray();
	bytes.resize(32);
	base::RandomFill(bytes.data(), bytes.size());
	return QString::fromLatin1(bytes.toHex());
}

[[nodiscard]] QString ExternalShellTopUrl() {
	return u"https://web.telegram.org:443/blank.html"_q;
}

[[nodiscard]] TextWithEntities WebviewErrorText(
		const QString &text,
		const Webview::Available &information) {
	Expects(information.error != Webview::Available::Error::None);

	return TextWithEntities{ text }.append("\n\n").append(
		ErrorText(information));
}

[[nodiscard]] bool IsExternalShellOrigin(const QString &origin) {
	const auto url = QUrl(origin);
	return url.isValid()
		&& url.scheme() == u"https"_q
		&& url.host() == u"web.telegram.org"_q
		&& url.port(443) == 443
		&& url.userInfo().isEmpty()
		&& url.path().isEmpty()
		&& url.query().isEmpty()
		&& url.fragment().isEmpty();
}

[[nodiscard]] int EffectivePort(const QUrl &url) {
	const auto explicitPort = url.port(-1);
	if (explicitPort >= 0) {
		return explicitPort;
	}
	const auto scheme = url.scheme().toLower();
	if (scheme == u"http"_q) {
		return 80;
	} else if (scheme == u"https"_q) {
		return 443;
	}
	return -1;
}

[[nodiscard]] QString OriginFromUrl(const QString &url) {
	const auto parsed = QUrl(url);
	if (!parsed.isValid()) {
		return {};
	}
	const auto scheme = parsed.scheme().toLower();
	auto host = parsed.host().toLower();
	const auto port = EffectivePort(parsed);
	if (scheme.isEmpty() || host.isEmpty() || port < 0) {
		return {};
	}
	if (host.contains(':') && !host.startsWith('[')) {
		host = u"["_q + host + u"]"_q;
	}
	return u"%1://%2:%3"_q.arg(scheme, host, QString::number(port));
}

[[nodiscard]] bool OriginsMatch(const QString &a, const QString &b) {
	if (a.isEmpty() || b.isEmpty()) {
		return false;
	}
	const auto normalizedA = OriginFromUrl(a);
	const auto normalizedB = OriginFromUrl(b);
	return !normalizedA.isEmpty()
		&& !normalizedB.isEmpty()
		&& normalizedA == normalizedB;
}

[[nodiscard]] RectPart ParsePosition(const QString &position) {
	if (position == u"left"_q) {
		return RectPart::Left;
	} else if (position == u"top"_q) {
		return RectPart::Top;
	} else if (position == u"right"_q) {
		return RectPart::Right;
	} else if (position == u"bottom"_q) {
		return RectPart::Bottom;
	}
	return RectPart::Left;
}

[[nodiscard]] bool IsNoArgumentsSentinel(const QString &string) {
	return string.isEmpty() || string == u"\"\""_q;
}

[[nodiscard]] bool CanParseArguments(QJsonValue value) {
	if (value.isObject()) {
		return true;
	} else if (!value.isString()) {
		return false;
	} else if (IsNoArgumentsSentinel(value.toString())) {
		return true;
	}
	auto error = QJsonParseError();
	const auto document = QJsonDocument::fromJson(
		value.toString().toUtf8(),
		&error);
	return (error.error == QJsonParseError::NoError) && document.isObject();
}

[[nodiscard]] QJsonObject ParseArguments(QJsonValue value) {
	if (value.isObject()) {
		return value.toObject();
	} else if (!value.isString()) {
		return {};
	}
	auto error = QJsonParseError();
	const auto document = QJsonDocument::fromJson(
		value.toString().toUtf8(),
		&error);
	return (error.error == QJsonParseError::NoError && document.isObject())
		? document.object()
		: QJsonObject();
}

[[nodiscard]] bool IsShellNamespaceCommand(const QString &command) {
	return command.startsWith(u"shell_"_q)
		|| command.startsWith(u"tdesktop_shell_"_q);
}

[[nodiscard]] QString SafeCommandForLog(const QString &command) {
	if (command.isEmpty() || command.size() > 80) {
		return {};
	}
	if (!command.startsWith(u"web_app_"_q)
		&& !command.startsWith(u"shell_"_q)
		&& !command.startsWith(u"tdesktop_shell_"_q)
		&& command != u"share_score"_q) {
		return {};
	}
	const auto safe = std::all_of(
		command.begin(),
		command.end(),
		[](QChar ch) {
			return ch.isLetterOrNumber() || ch == QChar('_');
		});
	return safe ? command : QString();
}

void LogNativeMessageRejected(
		const QString &reason,
		quint64 bytes,
		const QString &command = QString()) {
	const auto safeCommand = SafeCommandForLog(command);
	if (safeCommand.isEmpty()) {
		LOG(("BotWebView Error: Native message rejected: %1 (%2 bytes)."
			).arg(reason, QString::number(bytes)));
	} else {
		LOG(("BotWebView Error: Native message rejected: %1 "
			"(%2 bytes, command: %3)."
			).arg(reason, QString::number(bytes), safeCommand));
	}
}

[[nodiscard]] std::optional<NativeMessage> ParseNativeMessage(
		const QByteArray &bytes,
		const QString &sourceUrl,
		bool externalShell,
		const QString &shellToken) {
	const auto byteCount = quint64(bytes.size());
	if (bytes.size() > kMaxNativeMessageBytes) {
		LogNativeMessageRejected(u"payload too large"_q, byteCount);
		return std::nullopt;
	}
	auto error = QJsonParseError();
	const auto document = QJsonDocument::fromJson(bytes, &error);
	if (error.error != QJsonParseError::NoError) {
		LogNativeMessageRejected(u"invalid json"_q, byteCount);
		return std::nullopt;
	}
	if (externalShell) {
		if (!document.isObject()) {
			LogNativeMessageRejected(
				u"external payload is not an object"_q,
				byteCount);
			return std::nullopt;
		}
		const auto object = document.object();
		const auto command = object.value(u"eventType"_q).toString();
		const auto reject = [&](const QString &reason) {
			LogNativeMessageRejected(reason, byteCount, command);
			return std::optional<NativeMessage>();
		};
		if (object.value(u"type"_q).toString()
			!= QString::fromLatin1(kExternalMessageType)) {
			return reject(u"bad external type"_q);
		}
		const auto sourceText = object.value(u"source"_q).toString();
		auto source = NativeMessageSource::ExternalWebApp;
		if (sourceText == u"webapp"_q) {
			source = NativeMessageSource::ExternalWebApp;
		} else if (sourceText == u"shell"_q) {
			source = NativeMessageSource::ExternalShell;
		} else {
			return reject(u"bad external source"_q);
		}
		const auto token = object.value(u"token"_q);
		if (!token.isString()
			|| shellToken.isEmpty()
			|| token.toString() != shellToken) {
			return reject(u"bad external token"_q);
		}
		const auto origin = object.value(u"origin"_q);
		if (source == NativeMessageSource::ExternalShell) {
			if (!origin.isString()
				|| !IsExternalShellOrigin(origin.toString())) {
				return reject(u"bad shell origin"_q);
			}
		}
		if (!object.value(u"eventType"_q).isString() || command.isEmpty()) {
			return reject(u"bad command"_q);
		}
		const auto data = object.value(u"eventData"_q);
		if (!CanParseArguments(data)) {
			return reject(u"bad arguments"_q);
		}
		const auto arguments = ParseArguments(data);
		const auto shellCommand = IsShellNamespaceCommand(command);
		if (source == NativeMessageSource::ExternalShell) {
			if (!shellCommand) {
				return reject(u"non-shell command from shell"_q);
			}
		} else if (shellCommand) {
			return reject(u"shell command from webapp"_q);
		}
		return NativeMessage{
			.source = source,
			.origin = (source == NativeMessageSource::ExternalWebApp)
				? origin.toString()
				: QString(),
			.command = command,
			.arguments = arguments,
		};
	}
	if (!document.isArray()) {
		LogNativeMessageRejected(
			u"legacy payload is not an array"_q,
			byteCount);
		return std::nullopt;
	}
	const auto list = document.array();
	const auto command = list.at(0).toString();
	const auto reject = [&](const QString &reason) {
		LogNativeMessageRejected(reason, byteCount, command);
		return std::optional<NativeMessage>();
	};
	if (!list.at(0).isString() || command.isEmpty()) {
		return reject(u"bad command"_q);
	} else if (IsShellNamespaceCommand(command)) {
		return reject(u"shell command from legacy"_q);
	}
	auto arguments = QJsonObject();
	if (list.size() > 1) {
		const auto value = list.at(1);
		if (!value.isNull()
			&& !value.isUndefined()
			&& !CanParseArguments(value)) {
			return reject(u"bad arguments"_q);
		}
		arguments = ParseArguments(value);
	}
	return NativeMessage{
		.source = NativeMessageSource::LegacyWebApp,
		.origin = OriginFromUrl(sourceUrl),
		.command = command,
		.arguments = arguments,
	};
}

[[nodiscard]] bool UseExternalBotWebApps() {
#ifdef Q_OS_LINUX
	return true;
#else // Q_OS_LINUX
	return false;
#endif // Q_OS_LINUX
}

[[nodiscard]] QColor ResolveExternalShellThemeColor(QColor color) {
	return (color.alpha() == 255) ? color : st::windowBg->c;
}

[[nodiscard]] QJsonObject ThemeChangedPayload(
		const Webview::ThemeParams &params) {
	const auto parsed = QJsonDocument::fromJson(params.json);
	return { { u"theme_params"_q, parsed.object() } };
}

[[nodiscard]] Platform::ForeignParent CompatibleForeignParent(
		Platform::ForeignParent parent) {
	switch (parent.type) {
	case Platform::ForeignParent::Type::None:
		return {};
	case Platform::ForeignParent::Type::X11:
		return ::Platform::IsX11() ? parent : Platform::ForeignParent();
	case Platform::ForeignParent::Type::Wayland:
		return ::Platform::IsWayland() ? parent : Platform::ForeignParent();
	}
	return {};
}

enum class SharedPanelMenuAction {
	None,
	Settings,
	OpenBot,
	Reload,
	ShareGame,
	Terms,
	Privacy,
	RemoveFromMenu,
	RemoveFromMainMenu,
	DownloadOpen,
	DownloadRetry,
	DownloadCancel,
};

struct SharedPanelMenuItem {
	QString id;
	QString text;
	QString subtitle;
	QString actionLabel;
	QString iconKey;
	const style::icon *icon = nullptr;
	bool isSeparator = false;
	bool isAttention = false;
	bool isEnabled = true;
	std::vector<SharedPanelMenuItem> children;
};

struct SharedPanelMenuBuildArgs {
	const std::vector<DownloadsEntry> *downloads = nullptr;
	bool hasSettings = false;
	MenuButtons buttons = {};
};

struct SharedPanelMenuDispatchArgs {
	Fn<void()> settings;
	Fn<void()> reload;
	Fn<void()> terms;
	Fn<void()> privacy;
	Fn<void(MenuButton)> menuButton;
	Fn<void(uint32, DownloadsAction)> download;
};

[[nodiscard]] QString SharedPanelMenuActionId(SharedPanelMenuAction action) {
	switch (action) {
	case SharedPanelMenuAction::Settings:
		return u"settings"_q;
	case SharedPanelMenuAction::OpenBot:
		return u"open_bot"_q;
	case SharedPanelMenuAction::Reload:
		return u"reload"_q;
	case SharedPanelMenuAction::ShareGame:
		return u"share_game"_q;
	case SharedPanelMenuAction::Terms:
		return u"terms"_q;
	case SharedPanelMenuAction::Privacy:
		return u"privacy"_q;
	case SharedPanelMenuAction::RemoveFromMenu:
		return u"remove_from_menu"_q;
	case SharedPanelMenuAction::RemoveFromMainMenu:
		return u"remove_from_main_menu"_q;
	case SharedPanelMenuAction::None:
	case SharedPanelMenuAction::DownloadOpen:
	case SharedPanelMenuAction::DownloadRetry:
	case SharedPanelMenuAction::DownloadCancel:
		break;
	}
	return QString();
}

[[nodiscard]] QString DownloadPanelMenuActionId(
		uint32 id,
		DownloadsAction action) {
	auto type = QString();
	switch (action) {
	case DownloadsAction::Open:
		type = u"open"_q;
		break;
	case DownloadsAction::Retry:
		type = u"retry"_q;
		break;
	case DownloadsAction::Cancel:
		type = u"cancel"_q;
		break;
	}
	return u"download:%1:%2"_q.arg(id).arg(type);
}

[[nodiscard]] SharedPanelMenuAction ParseSharedPanelMenuActionType(
		const QString &type) {
	if (type == u"open"_q) {
		return SharedPanelMenuAction::DownloadOpen;
	} else if (type == u"retry"_q) {
		return SharedPanelMenuAction::DownloadRetry;
	} else if (type == u"cancel"_q) {
		return SharedPanelMenuAction::DownloadCancel;
	}
	return SharedPanelMenuAction::None;
}

struct ParsedSharedPanelMenuAction {
	SharedPanelMenuAction action = SharedPanelMenuAction::None;
	uint32 downloadId = 0;
};

[[nodiscard]] ParsedSharedPanelMenuAction ParseSharedPanelMenuAction(
		const QString &id) {
	if (id == u"settings"_q) {
		return { SharedPanelMenuAction::Settings };
	} else if (id == u"open_bot"_q) {
		return { SharedPanelMenuAction::OpenBot };
	} else if (id == u"reload"_q) {
		return { SharedPanelMenuAction::Reload };
	} else if (id == u"share_game"_q) {
		return { SharedPanelMenuAction::ShareGame };
	} else if (id == u"terms"_q) {
		return { SharedPanelMenuAction::Terms };
	} else if (id == u"privacy"_q) {
		return { SharedPanelMenuAction::Privacy };
	} else if (id == u"remove_from_menu"_q) {
		return { SharedPanelMenuAction::RemoveFromMenu };
	} else if (id == u"remove_from_main_menu"_q) {
		return { SharedPanelMenuAction::RemoveFromMainMenu };
	} else if (id.startsWith(u"download:"_q)) {
		const auto parts = id.split(u':');
		const auto downloadId = (parts.size() == 3)
			? parts[1].toUInt()
			: 0;
		return {
			.action = (parts.size() == 3)
				? ParseSharedPanelMenuActionType(parts[2])
				: SharedPanelMenuAction::None,
			.downloadId = downloadId,
		};
	}
	return {};
}

void DispatchSharedPanelMenuAction(
		const QString &id,
		const SharedPanelMenuDispatchArgs &dispatch) {
	const auto parsed = ParseSharedPanelMenuAction(id);
	switch (parsed.action) {
	case SharedPanelMenuAction::Settings:
		if (dispatch.settings) {
			dispatch.settings();
		}
		break;
	case SharedPanelMenuAction::OpenBot:
		if (dispatch.menuButton) {
			dispatch.menuButton(MenuButton::OpenBot);
		}
		break;
	case SharedPanelMenuAction::Reload:
		if (dispatch.reload) {
			dispatch.reload();
		}
		break;
	case SharedPanelMenuAction::ShareGame:
		if (dispatch.menuButton) {
			dispatch.menuButton(MenuButton::ShareGame);
		}
		break;
	case SharedPanelMenuAction::Terms:
		if (dispatch.terms) {
			dispatch.terms();
		}
		break;
	case SharedPanelMenuAction::Privacy:
		if (dispatch.privacy) {
			dispatch.privacy();
		}
		break;
	case SharedPanelMenuAction::RemoveFromMenu:
		if (dispatch.menuButton) {
			dispatch.menuButton(MenuButton::RemoveFromMenu);
		}
		break;
	case SharedPanelMenuAction::RemoveFromMainMenu:
		if (dispatch.menuButton) {
			dispatch.menuButton(MenuButton::RemoveFromMainMenu);
		}
		break;
	case SharedPanelMenuAction::DownloadOpen:
		if (dispatch.download) {
			dispatch.download(parsed.downloadId, DownloadsAction::Open);
		}
		break;
	case SharedPanelMenuAction::DownloadRetry:
		if (dispatch.download) {
			dispatch.download(parsed.downloadId, DownloadsAction::Retry);
		}
		break;
	case SharedPanelMenuAction::DownloadCancel:
		if (dispatch.download) {
			dispatch.download(parsed.downloadId, DownloadsAction::Cancel);
		}
		break;
	case SharedPanelMenuAction::None:
		break;
	}
}

[[nodiscard]] SharedPanelMenuItem BuildDownloadPanelMenuItem(
		const DownloadsEntry &entry) {
	auto item = SharedPanelMenuItem();
	item.text = entry.path.section(u'/', -1);
	if (item.text.isEmpty()) {
		item.text = entry.path;
	}
	if (item.text.isEmpty()) {
		item.text = entry.url;
	}
	if (entry.total && (entry.total == entry.ready)) {
		item.id = DownloadPanelMenuActionId(entry.id, DownloadsAction::Open);
		item.subtitle = FormatSizeText(entry.total);
	} else if (entry.loading) {
		item.id = DownloadPanelMenuActionId(entry.id, DownloadsAction::Cancel);
		item.subtitle = entry.total
			? FormatProgressText(entry.ready, entry.total)
			: tr::lng_bot_download_starting(tr::now);
		item.actionLabel = tr::lng_cancel(tr::now);
	} else {
		item.id = DownloadPanelMenuActionId(entry.id, DownloadsAction::Retry);
		item.subtitle = tr::lng_bot_download_failed(
			tr::now,
			lt_retry,
			tr::lng_bot_download_retry(tr::now));
		item.actionLabel = tr::lng_bot_download_retry(tr::now);
	}
	item.isEnabled = !item.id.isEmpty();
	return item;
}

[[nodiscard]] std::vector<SharedPanelMenuItem> BuildSharedPanelMenuItems(
		const SharedPanelMenuBuildArgs &args) {
	auto result = std::vector<SharedPanelMenuItem>();
	if (args.downloads && !args.downloads->empty()) {
		auto children = std::vector<SharedPanelMenuItem>();
		children.reserve(args.downloads->size());
		for (const auto &entry : *args.downloads | ranges::views::reverse) {
			children.push_back(BuildDownloadPanelMenuItem(entry));
		}
		result.push_back({
			.id = u"downloads"_q,
			.text = tr::lng_downloads_section(tr::now),
			.iconKey = u"downloads"_q,
			.icon = &st::menuIconDownload,
			.children = std::move(children),
		});
		result.push_back({
			.isSeparator = true,
		});
	}
	if (args.hasSettings) {
		result.push_back({
			.id = SharedPanelMenuActionId(SharedPanelMenuAction::Settings),
			.text = tr::lng_bot_settings(tr::now),
			.iconKey = u"settings"_q,
			.icon = &st::menuIconSettings,
		});
	}
	if (args.buttons & MenuButton::OpenBot) {
		result.push_back({
			.id = SharedPanelMenuActionId(SharedPanelMenuAction::OpenBot),
			.text = tr::lng_bot_open(tr::now),
			.iconKey = u"open_bot"_q,
			.icon = &st::menuIconLeave,
		});
	}
	result.push_back({
		.id = SharedPanelMenuActionId(SharedPanelMenuAction::Reload),
		.text = tr::lng_bot_reload_page(tr::now),
		.iconKey = u"reload"_q,
		.icon = &st::menuIconRestore,
	});
	if (args.buttons & MenuButton::ShareGame) {
		result.push_back({
			.id = SharedPanelMenuActionId(SharedPanelMenuAction::ShareGame),
			.text = tr::lng_iv_share(tr::now),
			.iconKey = u"share_game"_q,
			.icon = &st::menuIconShare,
		});
	} else {
		result.push_back({
			.id = SharedPanelMenuActionId(SharedPanelMenuAction::Terms),
			.text = tr::lng_bot_terms(tr::now),
			.iconKey = u"terms"_q,
			.icon = &st::menuIconGroupLog,
		});
		result.push_back({
			.id = SharedPanelMenuActionId(SharedPanelMenuAction::Privacy),
			.text = tr::lng_bot_privacy(tr::now),
			.iconKey = u"privacy"_q,
			.icon = &st::menuIconAntispam,
		});
	}
	if (args.buttons & MenuButton::RemoveFromMainMenu) {
		result.push_back({
			.id = SharedPanelMenuActionId(
				SharedPanelMenuAction::RemoveFromMainMenu),
			.text = tr::lng_bot_remove_from_side_menu(tr::now),
			.iconKey = u"remove"_q,
			.icon = &st::menuIconDeleteAttention,
			.isAttention = true,
		});
	} else if (args.buttons & MenuButton::RemoveFromMenu) {
		result.push_back({
			.id = SharedPanelMenuActionId(
				SharedPanelMenuAction::RemoveFromMenu),
			.text = tr::lng_bot_remove_from_menu(tr::now),
			.iconKey = u"remove"_q,
			.icon = &st::menuIconDeleteAttention,
			.isAttention = true,
		});
	}
	return result;
}

void FillNativeSharedPanelMenu(
		const Ui::Menu::MenuCallback &callback,
		const std::vector<SharedPanelMenuItem> &items,
		Fn<rpl::producer<std::vector<DownloadsEntry>>()> makeDownloads,
		const SharedPanelMenuDispatchArgs &dispatch) {
	for (const auto &item : items) {
		if (item.isSeparator) {
			callback({
				.separatorSt = &st::expandedMenuSeparator,
				.isSeparator = true,
			});
		} else if (!item.children.empty()) {
			callback(Ui::Menu::MenuCallback::Args{
				.text = item.text,
				.icon = item.icon,
				.fillSubmenu = FillAttachBotDownloadsSubmenu(
					makeDownloads(),
					[download = dispatch.download](
							uint32 id,
							DownloadsAction type) {
						if (download) {
							download(id, type);
						}
					}),
			});
		} else {
			callback(Ui::Menu::MenuCallback::Args{
				.text = item.text,
				.handler = [=] {
					DispatchSharedPanelMenuAction(item.id, dispatch);
				},
				.icon = item.icon,
				.isAttention = item.isAttention,
			});
		}
	}
}

[[nodiscard]] QImage RasterizeStyleIcon(const style::icon &icon) {
	const auto size = icon.size();
	const auto ratio = style::DevicePixelRatio();
	auto image = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
	image.setDevicePixelRatio(ratio);
	image.fill(Qt::transparent);
	auto painter = Painter(&image);
	icon.paintInCenter(painter, QRect(QPoint(), size));
	return image;
}

[[nodiscard]] QImage RasterizeVerifiedBadge() {
	const auto size = st::infoVerifiedStar.size() + QSize(0, st::lineWidth);
	const auto ratio = style::DevicePixelRatio();
	auto image = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
	image.setDevicePixelRatio(ratio);
	image.fill(Qt::transparent);
	auto painter = Painter(&image);
	const auto width = size.width();
	st::infoVerifiedStar.paint(painter, st::lineWidth, 0, width);
	st::infoPeerBadge.verifiedCheck.paint(painter, st::lineWidth, 0, width);
	return image;
}

[[nodiscard]] QString PngDataUrl(const QImage &image) {
	auto bytes = QByteArray();
	auto buffer = QBuffer(&bytes);
	buffer.open(QIODevice::WriteOnly);
	image.save(&buffer, "PNG");
	return u"data:image/png;base64,"_q + QString::fromLatin1(bytes.toBase64());
}

[[nodiscard]] QJsonObject SerializeRasterAsset(
		const QImage &image,
		QSize size,
		QString alt = QString()) {
	auto result = QJsonObject{
		{ u"url"_q, PngDataUrl(image) },
		{ u"width"_q, size.width() },
		{ u"height"_q, size.height() },
	};
	if (!alt.isEmpty()) {
		result.insert(u"alt"_q, alt);
	}
	return result;
}

[[nodiscard]] QJsonObject SerializeStyleIconAsset(const style::icon &icon) {
	return SerializeRasterAsset(RasterizeStyleIcon(icon), icon.size());
}

[[nodiscard]] QJsonObject SerializeVerifiedBadgeAsset() {
	const auto size = st::infoVerifiedStar.size() + QSize(0, st::lineWidth);
	return SerializeRasterAsset(
		RasterizeVerifiedBadge(),
		size,
		tr::lng_sr_verified_badge(tr::now));
}

void CollectSharedPanelMenuIcons(
		const std::vector<SharedPanelMenuItem> &items,
		QJsonObject &result) {
	for (const auto &item : items) {
		if (!item.iconKey.isEmpty()
			&& item.icon
			&& !result.contains(item.iconKey)) {
			result.insert(item.iconKey, SerializeStyleIconAsset(*item.icon));
		}
		if (!item.children.empty()) {
			CollectSharedPanelMenuIcons(item.children, result);
		}
	}
}

[[nodiscard]] QJsonObject SerializeSharedPanelMenuItem(
		const SharedPanelMenuItem &item) {
	if (item.isSeparator) {
		return { { u"separator"_q, true } };
	}
	auto result = QJsonObject{
		{ u"id"_q, item.id },
		{ u"text"_q, item.text },
		{ u"attention"_q, item.isAttention },
		{ u"enabled"_q, item.isEnabled },
	};
	if (!item.subtitle.isEmpty()) {
		result.insert(u"subtitle"_q, item.subtitle);
	}
	if (!item.actionLabel.isEmpty()) {
		result.insert(u"actionLabel"_q, item.actionLabel);
	}
	if (!item.iconKey.isEmpty()) {
		result.insert(u"icon"_q, item.iconKey);
	}
	if (!item.children.empty()) {
		auto children = QJsonArray();
		for (const auto &child : item.children) {
			children.push_back(SerializeSharedPanelMenuItem(child));
		}
		result.insert(u"children"_q, children);
	}
	return result;
}

[[nodiscard]] QJsonArray SerializeSharedPanelMenu(
		const std::vector<SharedPanelMenuItem> &items) {
	auto result = QJsonArray();
	for (const auto &item : items) {
		result.push_back(SerializeSharedPanelMenuItem(item));
	}
	return result;
}

[[nodiscard]] std::optional<QColor> ParseColor(const QString &text) {
	if (!text.startsWith('#') || text.size() != 7) {
		return {};
	}
	const auto data = text.data() + 1;
	const auto hex = [&](int from) -> std::optional<int> {
		const auto parse = [](QChar ch) -> std::optional<int> {
			const auto code = ch.unicode();
			return (code >= 'a' && code <= 'f')
				? std::make_optional(10 + (code - 'a'))
				: (code >= 'A' && code <= 'F')
				? std::make_optional(10 + (code - 'A'))
				: (code >= '0' && code <= '9')
				? std::make_optional(code - '0')
				: std::nullopt;
		};
		const auto h = parse(data[from]), l = parse(data[from + 1]);
		return (h && l) ? std::make_optional(*h * 16 + *l) : std::nullopt;
	};
	const auto r = hex(0), g = hex(2), b = hex(4);
	return (r && g && b) ? QColor(*r, *g, *b) : std::optional<QColor>();
}

[[nodiscard]] QColor ResolveRipple(QColor background) {
	auto hue = 0;
	auto saturation = 0;
	auto lightness = 0;
	auto alpha = 0;
	background.getHsv(&hue, &saturation, &lightness, &alpha);
	return QColor::fromHsv(
		hue,
		saturation,
		lightness - (lightness > kLightnessThreshold
			? kLightnessDelta
			: -kLightnessDelta),
		alpha);
}

[[nodiscard]] const style::color *LookupNamedColor(const QString &key) {
	if (key == u"secondary_bg_color"_q) {
		return &st::boxDividerBg;
	} else if (key == u"bottom_bar_bg_color"_q) {
		return &st::windowBg;
	}
	return nullptr;
}

} // namespace

class Panel::Button final : public RippleButton {
public:
	Button(
		QWidget *parent,
		const style::RoundButton &st,
		Text::MarkedContext textContext);
	~Button();

	void updateBg(QColor bg);
	void updateBg(not_null<const style::color*> paletteBg);
	void updateFg(QColor fg);
	void updateFg(not_null<const style::color*> paletteFg);

	void updateArgs(Panel::ButtonArgs &&args);

private:
	void paintEvent(QPaintEvent *e) override;

	QImage prepareRippleMask() const override;
	QPoint prepareRippleStartPosition() const override;

	void toggleProgress(bool shown);
	void setupProgressGeometry();
	void updateText(uint64 iconCustomEmojiId, const QString &text);

	std::unique_ptr<Progress> _progress;
	Ui::Text::String _text;

	Text::MarkedContext _textContext;
	const style::RoundButton &_st;
	QColor _fg;
	style::owned_color _bg;
	RoundRect _roundRect;

	rpl::lifetime _bgLifetime;
	rpl::lifetime _fgLifetime;

};

struct Panel::Progress {
	Progress(QWidget *parent, Fn<QRect()> rect);

	RpWidget widget;
	InfiniteRadialAnimation animation;
	Animations::Simple shownAnimation;
	bool shown = true;
	rpl::lifetime geometryLifetime;
};

struct Panel::WebviewWithLifetime {
	WebviewWithLifetime(
		QWidget *parent = nullptr,
		Webview::WindowConfig config = Webview::WindowConfig());

	Webview::Window window;
	std::vector<QPointer<RpWidget>> boxes;
	rpl::lifetime boxesLifetime;
	rpl::lifetime lifetime;
};

Panel::Button::Button(
	QWidget *parent,
	const style::RoundButton &st,
	Text::MarkedContext textContext)
: RippleButton(parent, st.ripple)
, _textContext(std::move(textContext))
, _st(st)
, _bg(st::windowBgActive->c)
, _roundRect(st::callRadius, st::windowBgActive) {
	resize(
		_st.padding.left() + _text.maxWidth() + _st.padding.right(),
		_st.padding.top() + _st.height + _st.padding.bottom());
}

Panel::Button::~Button() = default;

void Panel::Button::updateText(
		uint64 iconCustomEmojiId,
		const QString &text) {
	if (iconCustomEmojiId) {
		auto result = Text::SingleCustomEmoji(
			QString::number(iconCustomEmojiId));
		if (!text.isEmpty()) {
			result.append(' ').append(text);
		}
		auto context = _textContext;
		context.repaint = [=] { update(); };
		_text.setMarkedText(
			st::semiboldTextStyle,
			result,
			kMarkupTextOptions,
			context);
	} else {
		_text.setText(st::semiboldTextStyle, text);
	}
}

void Panel::Button::updateBg(QColor bg) {
	_bg.update(bg);
	_roundRect.setColor(_bg.color());
	_bgLifetime.destroy();
	update();
}

void Panel::Button::updateBg(not_null<const style::color*> paletteBg) {
	updateBg((*paletteBg)->c);
	_bgLifetime = style::PaletteChanged(
	) | rpl::on_next([=] {
		updateBg((*paletteBg)->c);
	});
}

void Panel::Button::updateFg(QColor fg) {
	_fg = fg;
	_fgLifetime.destroy();
	update();
}

void Panel::Button::updateFg(not_null<const style::color*> paletteFg) {
	updateFg((*paletteFg)->c);
	_fgLifetime = style::PaletteChanged(
	) | rpl::on_next([=] {
		updateFg((*paletteFg)->c);
	});
}

void Panel::Button::updateArgs(Panel::ButtonArgs &&args) {
	updateText(args.iconCustomEmojiId, args.text);
	setDisabled(!args.isActive);
	setPointerCursor(false);
	setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor);
	setVisible(args.isVisible);
	toggleProgress(args.isProgressVisible);
	update();
}

void Panel::Button::toggleProgress(bool shown) {
	if (!_progress) {
		if (!shown) {
			return;
		}
		_progress = std::make_unique<Progress>(
			this,
			[=] { return _progress->widget.rect(); });
		_progress->widget.paintRequest(
		) | rpl::on_next([=](QRect clip) {
			auto p = QPainter(&_progress->widget);
			p.setOpacity(
				_progress->shownAnimation.value(_progress->shown ? 1. : 0.));
			auto thickness = st::paymentsLoading.thickness;
			const auto rect = _progress->widget.rect().marginsRemoved(
				{ thickness, thickness, thickness, thickness });
			InfiniteRadialAnimation::Draw(
				p,
				_progress->animation.computeState(),
				rect.topLeft(),
				rect.size() - QSize(),
				_progress->widget.width(),
				_fg,
				thickness);
		}, _progress->widget.lifetime());
		_progress->widget.show();
		_progress->animation.start();
	} else if (_progress->shown == shown) {
		return;
	}
	const auto callback = [=] {
		if (!_progress->shownAnimation.animating() && !_progress->shown) {
			_progress = nullptr;
		} else {
			_progress->widget.update();
		}
	};
	_progress->shown = shown;
	_progress->shownAnimation.start(
		callback,
		shown ? 0. : 1.,
		shown ? 1. : 0.,
		kProgressDuration);
	if (shown) {
		setupProgressGeometry();
	}
}

void Panel::Button::setupProgressGeometry() {
	if (!_progress || !_progress->shown) {
		return;
	}
	_progress->geometryLifetime.destroy();
	sizeValue(
	) | rpl::on_next([=](QSize outer) {
		const auto height = outer.height();
		const auto size = st::paymentsLoading.size;
		const auto skip = (height - size.height()) / 2;
		const auto right = outer.width();
		const auto top = outer.height() - height;
		_progress->widget.setGeometry(QRect{
			QPoint(right - skip - size.width(), top + skip),
			size });
	}, _progress->geometryLifetime);

	_progress->widget.show();
	_progress->widget.raise();
	if (_progress->shown
		&& Ui::AppInFocus()
		&& Ui::InFocusChain(_progress->widget.window())) {
		_progress->widget.setFocus();
	}
}

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

	_roundRect.paint(p, rect());

	if (!isDisabled()) {
		const auto ripple = ResolveRipple(_bg.color()->c);
		paintRipple(p, rect().topLeft(), &ripple);
	}

	p.setFont(_st.style.font);

	const auto height = rect().height();
	const auto progress = st::paymentsLoading.size;
	const auto minPad = (height - progress.height()) / 2;
	const auto rightReserved = _progress
		? (minPad + progress.width() + minPad)
		: minPad;
	const auto maxTextEnd = width() - rightReserved;
	const auto maxSpace = std::max(maxTextEnd - minPad, 0);
	const auto textWidth = std::min(_text.maxWidth(), maxSpace);
	const auto centered = (width() - textWidth) / 2;
	const auto textLeft = std::max(
		std::min(centered, maxTextEnd - textWidth),
		minPad);
	const auto textTop = _st.padding.top() + _st.textTop;
	p.setPen(_fg);
	_text.drawLeftElided(p, textLeft, textTop, textWidth, width());
}

QImage Panel::Button::prepareRippleMask() const {
	return RippleAnimation::RoundRectMask(size(), st::callRadius);
}

QPoint Panel::Button::prepareRippleStartPosition() const {
	return mapFromGlobal(QCursor::pos())
		- QPoint(_st.padding.left(), _st.padding.top());
}

Panel::WebviewWithLifetime::WebviewWithLifetime(
	QWidget *parent,
	Webview::WindowConfig config)
: window(parent, std::move(config)) {
}

Panel::Progress::Progress(QWidget *parent, Fn<QRect()> rect)
: widget(parent)
, animation(
	[=] { if (!anim::Disabled()) widget.update(rect()); },
	st::paymentsLoading) {
}

Panel::Panel(Args &&args)
: _storageId(args.storageId)
, _delegate(args.delegate)
, _externalShell(UseExternalBotWebApps())
, _menuButtons(args.menuButtons)
, _externalPanelParent(_externalShell ? std::make_unique<RpWidget>() : nullptr)
, _widget(std::make_unique<SeparatePanel>(Ui::SeparatePanelArgs{
			.parent = _externalPanelParent.get(),
			.menuSt = &st::botWebViewMenu,
		}))
, _externalLayer(_externalShell
	? std::make_unique<StandaloneLayerStack>()
	: nullptr)
, _fullscreen(args.fullscreen)
, _allowClipboardRead(args.allowClipboardRead)
, _sameOrigin(args.sameOrigin) {
	if (_externalShell) {
		_widget->setAttribute(Qt::WA_DontShowOnScreen);
		_externalLayer->boxAdded(
		) | rpl::on_next([=] {
			setExternalShellBlocked(true);
		}, _widget->lifetime());
		_externalLayer->boxClosed(
		) | rpl::on_next([=] {
			setExternalShellBlocked(false);
		}, _widget->lifetime());
	}
	_widget->setWindowFlag(Qt::WindowStaysOnTopHint, false);
	_widget->setInnerSize(st::botWebViewPanelSize, true);

	const auto panel = _widget.get();
	rpl::duplicate(
		args.title
	) | rpl::on_next([=](const QString &title) {
		const auto value = tr::lng_credits_box_history_entry_miniapp(tr::now)
			+ u": "_q
			+ title;
		panel->window()->setWindowTitle(value);
	}, panel->lifetime());

	const auto params = _delegate->botThemeParams();
	updateColorOverrides(params);

	_fullscreen.value(
	) | rpl::on_next([=](bool fullscreen) {
		if (_externalShell) {
			if (!_webview) {
				return;
			}
			applyExternalShellFullscreen(fullscreen);
			sendFullScreen();
			sendSafeArea();
			sendContentSafeArea();
			sendViewport();
			return;
		}
		_widget->toggleFullScreen(fullscreen);
		layoutButtons();
		sendFullScreen();
		sendSafeArea();
		sendContentSafeArea();
	}, _widget->lifetime());

	_widget->fullScreenValue(
	) | rpl::on_next([=](bool fullscreen) {
		if (!_externalShell) {
			_fullscreen = fullscreen;
		}
	}, _widget->lifetime());

	_widget->closeRequests(
	) | rpl::on_next([=] {
		if (_closeNeedConfirmation) {
			scheduleCloseWithConfirmation();
		} else {
			_delegate->botClose();
		}
	}, _widget->lifetime());

	_widget->closeEvents(
	) | rpl::filter([=] {
		return !_hiddenForPayment;
	}) | rpl::on_next([=] {
		_delegate->botClose();
	}, _widget->lifetime());

	_widget->backRequests(
	) | rpl::on_next([=] {
		postEvent("back_button_pressed");
	}, _widget->lifetime());

	rpl::merge(
		style::PaletteChanged(),
		_themeUpdateForced.events()
	) | rpl::filter([=] {
		return !_themeUpdateScheduled;
	}) | rpl::on_next([=] {
		_themeUpdateScheduled = true;
		crl::on_main(_widget.get(), [=] {
			_themeUpdateScheduled = false;
			updateThemeParams(_delegate->botThemeParams());
		});
	}, _widget->lifetime());

	setTitle(std::move(args.title));
	_bottomText.value() | rpl::on_next([=](const QString &text) {
		if (_externalShell) {
			return;
		}
	}, _widget->lifetime());
	_externalTitleBadgeVisible = (args.titleBadge.paint != nullptr);
	_widget->setTitleBadge(std::move(args.titleBadge));

	if (!showWebview(std::move(args), params)) {
		if (_externalShell && _externalLayer) {
			const auto available = Webview::Availability();
			if (available.error != Webview::Available::Error::None) {
				showExternalShellError(WebviewErrorText(
					tr::lng_bot_no_webview(tr::now),
					available));
			} else {
				showExternalShellError({ tr::lng_bot_webview_failed(tr::now) });
			}
		} else {
			_externalShell = false;
			const auto available = Webview::Availability();
			if (available.error != Webview::Available::Error::None) {
				showWebviewError(tr::lng_bot_no_webview(tr::now), available);
			} else {
				showCriticalError({ tr::lng_bot_webview_failed(tr::now) });
			}
		}
	}
}

Panel::~Panel() {
	base::take(_webview);
	_progress = nullptr;
	_externalLayer = nullptr;
	_externalWebviewParent = nullptr;
	_widget = nullptr;
	_externalPanelParent = nullptr;
}

void Panel::setupDownloadsProgress(
		not_null<RpWidget*> button,
		rpl::producer<DownloadsProgress> progress,
		bool fullscreen) {
	const auto widget = Ui::CreateChild<RpWidget>(button.get());
	widget->show();
	widget->setAttribute(Qt::WA_TransparentForMouseEvents);

	button->sizeValue() | rpl::on_next([=](QSize size) {
		widget->setGeometry(QRect(QPoint(), size));
	}, widget->lifetime());

	struct State {
		State(QWidget *parent)
		: animation([=](crl::time now) {
			const auto total = progress.total;
			const auto current = total
				? (progress.ready / float64(total))
				: 0.;
			const auto updated = animation.update(current, false, now);
			if (!anim::Disabled() || updated) {
				parent->update();
			}
		}) {
		}

		DownloadsProgress progress;
		RadialAnimation animation;
		Animations::Simple fade;
		bool shown = false;
	};
	const auto state = widget->lifetime().make_state<State>(widget);
	std::move(
		progress
	) | rpl::on_next([=](DownloadsProgress progress) {
		const auto toggle = [&](bool shown) {
			if (state->shown == shown) {
				return;
			}
			state->shown = shown;
			if (shown && !state->fade.animating()) {
				return;
			}
			state->fade.start([=] {
				widget->update();
				if (!state->shown
					&& !state->fade.animating()
					&& (!state->progress.total
						|| (state->progress.ready
							== state->progress.total))) {
					state->animation.stop();
				}
			}, shown ? 0. : 2., shown ? 2. : 0., st::radialDuration * 2);
		};
		if (!state->shown && progress.loading) {
			if (!state->animation.animating()) {
				state->animation.start(0.);
			}
			toggle(true);
		} else if (state->shown && !progress.loading) {
			state->animation.update(1., false, crl::now());
			toggle(false);
		}
		state->progress = progress;
	}, widget->lifetime());

	widget->paintRequest() | rpl::on_next([=] {
		const auto opacity = std::clamp(
			state->fade.value(state->shown ? 2. : 0.) - 1.,
			0.,
			1.);
		if (!opacity) {
			return;
		}
		auto p = QPainter(widget);
		p.setOpacity(opacity);
		const auto palette = _widget->titleOverridePalette();
		const auto color = fullscreen
			? st::radialFg
			: palette
			? palette->boxTitleCloseFg()
			: st::paymentsLoading.color;
		const auto &st = fullscreen
			? st::fullScreenPanelMenu
			: st::separatePanelMenu;
		const auto size = st.rippleAreaSize;
		const auto rect = QRect(st.rippleAreaPosition, QSize(size, size));
		const auto stroke = st::botWebViewRadialStroke;
		const auto shift = stroke * 1.5;
		const auto inner = QRectF(rect).marginsRemoved(
			QMarginsF{ shift, shift, shift, shift });
		state->animation.draw(p, inner, stroke, color);
	}, widget->lifetime());
}

void Panel::requestActivate() {
	if (_externalShell) {
		if (_webview) {
			_webview->window.focus();
		}
		return;
	}
	_widget->showAndActivate();
	if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
		InvokeQueued(widget, [=] {
			if (widget->isVisible()) {
				_webview->window.focus();
			}
		});
	}
}

void Panel::toggleProgress(bool shown) {
	if (_externalShell) {
		sendExternalShellMethod("setProgress", { { u"shown"_q, shown } });
		return;
	}
	if (!_progress) {
		if (!shown) {
			return;
		}
		_progress = std::make_unique<Progress>(
			_widget.get(),
			[=] { return progressRect(); });
		_progress->widget.paintRequest(
		) | rpl::on_next([=](QRect clip) {
			auto p = QPainter(&_progress->widget);
			p.setOpacity(
				_progress->shownAnimation.value(_progress->shown ? 1. : 0.));
			const auto thickness = st::paymentsLoading.thickness;
			if (progressWithBackground()) {
				auto color = st::windowBg->c;
				color.setAlphaF(kProgressOpacity);
				p.fillRect(clip, color);
			}
			const auto rect = progressRect() - Margins(thickness);
			InfiniteRadialAnimation::Draw(
				p,
				_progress->animation.computeState(),
				rect.topLeft(),
				rect.size() - QSize(),
				_progress->widget.width(),
				st::paymentsLoading.color,
				anim::Disabled() ? (thickness / 2.) : thickness);
		}, _progress->widget.lifetime());
		_progress->widget.show();
		_progress->animation.start();
	} else if (_progress->shown == shown) {
		return;
	}
	const auto callback = [=] {
		if (!_progress->shownAnimation.animating() && !_progress->shown) {
			_progress = nullptr;
		} else {
			_progress->widget.update();
		}
	};
	_progress->shown = shown;
	_progress->shownAnimation.start(
		callback,
		shown ? 0. : 1.,
		shown ? 1. : 0.,
		kProgressDuration);
	if (shown) {
		setupProgressGeometry();
	}
}

bool Panel::progressWithBackground() const {
	return (_progress->widget.width() == _widget->innerGeometry().width());
}

QRect Panel::progressRect() const {
	const auto rect = _progress->widget.rect();
	if (!progressWithBackground()) {
		return rect;
	}
	const auto size = st::defaultBoxButton.height;
	return QRect(
		rect.x() + (rect.width() - size) / 2,
		rect.y() + (rect.height() - size) / 2,
		size,
		size);
}

void Panel::setupProgressGeometry() {
	if (!_progress || !_progress->shown) {
		return;
	}
	_progress->geometryLifetime.destroy();
	if (_webviewBottom) {
		_webviewBottom->geometryValue(
		) | rpl::on_next([=](QRect bottom) {
			const auto height = bottom.height();
			const auto size = st::paymentsLoading.size;
			const auto skip = (height - size.height()) / 2;
			const auto inner = _widget->innerGeometry();
			const auto right = inner.x() + inner.width();
			const auto top = inner.y() + inner.height() - height;
			// This doesn't work, because first we get the correct bottom
			// geometry and after that we get the previous event (which
			// triggered the 'fire' of correct geometry before getting here).
			//const auto right = bottom.x() + bottom.width();
			//const auto top = bottom.y();
			_progress->widget.setGeometry(QRect{
				QPoint(right - skip - size.width(), top + skip),
				size });
		}, _progress->geometryLifetime);
	}
	_progress->widget.show();
	_progress->widget.raise();
	if (_progress->shown) {
		_progress->widget.setFocus();
	}
}

void Panel::showWebviewProgress() {
	if (_webviewProgress && _progress && _progress->shown) {
		return;
	}
	_webviewProgress = true;
	toggleProgress(true);
}

void Panel::hideWebviewProgress() {
	if (!_webviewProgress) {
		return;
	}
	_webviewProgress = false;
	toggleProgress(false);
}

bool Panel::showWebview(Args &&args, const Webview::ThemeParams &params) {
	_bottomText = std::move(args.bottom);
	_externalUrl = args.url;
	_sameOrigin = args.sameOrigin;
	_initialOrigin = OriginFromUrl(args.url);
	_currentOrigin = _initialOrigin;
	if (_externalShell && !_webview) {
		resetExternalShellIdentity();
	}
	if (!_webview && !createWebview(params)) {
		return false;
	}
	const auto allowBack = false;
	showWebviewProgress();
	if (!_externalShell) {
		_widget->hideLayer(anim::type::instant);
	}
	updateThemeParams(params);
	const auto url = args.url;
	if (_externalShell) {
		_externalShellBootstrapped = false;
		_webview->window.navigate(ExternalShellTopUrl());
	} else {
		_webview->window.navigate(url);
		_widget->setBackAllowed(allowBack);
	}

	rpl::duplicate(args.downloadsProgress) | rpl::on_next([=] {
		_downloadsUpdated.fire({});
		if (_externalShell && _externalShellBootstrapped) {
			sendExternalShellAssets();
			sendExternalShellMenu();
		}
	}, lifetime());

	if (_externalShell) {
		return true;
	}

	const auto dispatch = SharedPanelMenuDispatchArgs{
		.settings = [=] {
			postEvent("settings_button_pressed");
		},
		.reload = [=] {
			if (_webview && _webview->window.widget()) {
				_webview->window.reload();
			} else if (const auto params = _delegate->botThemeParams()
				; createWebview(params)) {
				showWebviewProgress();
				updateThemeParams(params);
				_webview->window.navigate(url);
			}
		},
		.terms = [=] {
			File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));
		},
		.privacy = [=] {
			_delegate->botOpenPrivacyPolicy();
		},
		.menuButton = [=](MenuButton button) {
			_delegate->botHandleMenuButton(button);
		},
		.download = [=](uint32 id, DownloadsAction type) {
			_delegate->botDownloadsAction(id, type);
		},
	};
	_widget->setMenuAllowed([=](
			const Ui::Menu::MenuCallback &callback) {
		const auto &downloads = _delegate->botDownloads(true);
		const auto items = BuildSharedPanelMenuItems({
			.downloads = &downloads,
			.hasSettings = _webview
				&& _webview->window.widget()
				&& _hasSettingsButton,
			.buttons = _menuButtons,
		});
		FillNativeSharedPanelMenu(
			callback,
			items,
			[=] {
				return rpl::single(_delegate->botDownloads(true))
					| rpl::then(_downloadsUpdated.events(
					) | rpl::map([=] {
						return _delegate->botDownloads();
					}));
			},
			dispatch);
	}, [=, progress = std::move(args.downloadsProgress)](
			not_null<RpWidget*> button,
			bool fullscreen) {
		setupDownloadsProgress(
			button,
			rpl::duplicate(progress),
			fullscreen);
	});

	return true;
}

void Panel::setExternalShellTitleColor(std::optional<QColor> color) {
	_externalShellColorState.titleUsesTheme = !color.has_value();
	_externalShellColorState.title = std::move(color);
}

void Panel::setExternalShellBodyColor(std::optional<QColor> color) {
	_externalShellColorState.bodyUsesTheme = !color.has_value();
	_externalShellColorState.body = std::move(color);
}

void Panel::setExternalShellBottomColor(std::optional<QColor> color) {
	_externalShellColorState.bottomUsesTheme = !color.has_value();
	_externalShellColorState.bottom = std::move(color);
}

LinuxShell::ResolvedColors Panel::externalShellColors(
		const Webview::ThemeParams &params) const {
	const auto body = _externalShellColorState.bodyUsesTheme
		? ResolveExternalShellThemeColor(params.bodyBg)
		: _externalShellColorState.body.value_or(params.bodyBg);
	return {
		.titleBg = _externalShellColorState.titleUsesTheme
			? ResolveExternalShellThemeColor(params.titleBg)
			: _externalShellColorState.title.value_or(params.titleBg),
		.bodyBg = body,
		.bottomBg = _externalShellColorState.bottomUsesTheme
			? body
			: _externalShellColorState.bottom.value_or(body),
	};
}

void Panel::sendExternalShellColors(const Webview::ThemeParams &params) {
	sendExternalShellMethod(
		"setColors",
		LinuxShell::ColorPayload(externalShellColors(params)));
}

void Panel::resetExternalShellIdentity() {
	_externalShellToken = GenerateExternalShellToken();
	++_externalShellGeneration;
	_externalShellBootstrapped = false;
}

void Panel::invalidateExternalShellSession() {
	const auto wasActive = _externalShellBootstrapped
		|| !_externalShellToken.isEmpty();
	_externalShellBootstrapped = false;
	_externalShellToken.clear();
	if (wasActive) {
		++_externalShellGeneration;
	}
}

void Panel::installExternalShellDocument() {
	if (!_externalShell
		|| !_externalShellBootstrapped
		|| !_webview
		|| _externalShellToken.isEmpty()) {
		return;
	}
	_webview->window.eval(LinuxShell::InstallScript(_externalShellToken));
}

void Panel::sendExternalShellBootstrap() {
	const auto params = _delegate->botThemeParams();
	sendExternalShellMethod("bootstrap", {
		{ u"url"_q, _externalUrl },
		{ u"sameOrigin"_q, bool(_sameOrigin) },
		{ u"initialOrigin"_q, _initialOrigin },
		{ u"title"_q, _externalTitle },
		{ u"metrics"_q, LinuxShell::Metrics() },
		{ u"colors"_q, LinuxShell::ColorPayload(externalShellColors(params)) },
		{ u"bottomText"_q, QString() },
		{ u"backVisible"_q, _externalBackVisible },
		{ u"menuVisible"_q, true },
		{ u"badgeVisible"_q, _externalTitleBadgeVisible },
	});
	sendExternalShellAssets();
	sendExternalShellMenu();
	postEvent("theme_changed", ThemeChangedPayload(params));
	sendFullScreen();
	sendSafeArea();
	sendContentSafeArea();
}

void Panel::sendExternalShellMethod(
		const QByteArray &method,
		const QJsonObject &data) {
	if (!_externalShell
		|| !_externalShellBootstrapped
		|| !_webview
		|| _externalShellToken.isEmpty()) {
		return;
	}
	_webview->window.eval(
		LinuxShell::MethodCallScript(method, data, _externalShellToken));
}

void Panel::sendExternalShellEvent(
		const QString &event,
		const QJsonObject &data) {
	if (!_externalShell
		|| !_externalShellBootstrapped
		|| !_webview
		|| _externalShellToken.isEmpty()) {
		return;
	}
	_webview->window.eval(
		LinuxShell::EventScript(event, data, _externalShellToken));
}

void Panel::sendExternalShellButton(
		const char *name,
		const QJsonObject &args) {
	auto &state = (name[0] == 'm')
		? _externalMainButton
		: _externalSecondaryButton;
	const auto text = args["text"].toString();
	const auto trimmed = text.trimmed();
	const auto iconCustomEmojiId
		= args["icon_custom_emoji_id"].toString().toULongLong();
	const auto color = ParseColor(args["color"].toString()).value_or(
		st::windowBgActive->c);
	const auto textColor = ParseColor(args["text_color"].toString()).value_or(
		st::windowFgActive->c);
	const auto iconAffectingChanged
		= (state.args.iconCustomEmojiId != iconCustomEmojiId)
		|| (iconCustomEmojiId && state.textColor != textColor);
	state.args = {
		.isActive = args["is_active"].toBool(),
		.isVisible = args["is_visible"].toBool()
			&& (!trimmed.isEmpty() || iconCustomEmojiId),
		.isProgressVisible = args["is_progress_visible"].toBool(),
		.iconCustomEmojiId = iconCustomEmojiId,
		.text = text,
	};
	state.color = color;
	state.textColor = textColor;
	state.position = args["position"].toString();
	if (iconAffectingChanged) {
		++state.iconGeneration;
	}
	const auto visible = state.args.isVisible;
	sendExternalShellMethod("setButton", {
		{ u"name"_q, QString::fromLatin1(name) },
		{ u"visible"_q, visible },
		{ u"active"_q, state.args.isActive },
		{ u"progress"_q, state.args.isProgressVisible },
		{ u"text"_q, state.args.text },
		{ u"color"_q, state.color.name(QColor::HexRgb) },
		{ u"textColor"_q, state.textColor.name(QColor::HexRgb) },
		{ u"position"_q, state.position },
		{ u"iconCustomEmojiId"_q, state.args.iconCustomEmojiId
			? QString::number(state.args.iconCustomEmojiId)
			: QString() },
		{ u"iconGeneration"_q, QString::number(state.iconGeneration) },
	});
}

void Panel::applyExternalShellFullscreen(bool fullscreen) {
	if (!_externalShell || !_webview) {
		return;
	}
	_webview->window.setFullscreen(fullscreen);
}

void Panel::requestExternalShellButtonEmoji(const QString &name) {
	auto *state = (name == u"main"_q)
		? &_externalMainButton
		: (name == u"secondary"_q)
		? &_externalSecondaryButton
		: nullptr;
	if (!state || !_externalShell) {
		return;
	}
	const auto generation = state->iconGeneration;
	const auto responseName = name;
	const auto weak = base::make_weak(this);
	auto send = [=](QImage image = QImage()) {
		const auto panel = weak.get();
		if (!panel) {
			return;
		}
		const auto *current = (responseName == u"main"_q)
			? &panel->_externalMainButton
			: &panel->_externalSecondaryButton;
		if (current->iconGeneration != generation) {
			return;
		}
		auto data = QJsonObject{
			{ u"name"_q, responseName },
			{ u"generation"_q, QString::number(generation) },
		};
		if (!image.isNull()) {
			data.insert(
				u"icon"_q,
				SerializeRasterAsset(
					image,
					QSize(
						kExternalShellButtonIconSize,
						kExternalShellButtonIconSize)));
		}
		panel->sendExternalShellMethod("setButtonIcon", data);
	};
	if (!state->args.isVisible || !state->args.iconCustomEmojiId) {
		send();
		return;
	}
	_delegate->botResolveButtonEmoji({
		.customEmojiId = state->args.iconCustomEmojiId,
		.textColor = state->textColor,
		.size = kExternalShellButtonIconSize,
		.callback = std::move(send),
	});
}

void Panel::sendExternalShellMenu() {
	const auto &downloads = _delegate->botDownloads(true);
	const auto items = BuildSharedPanelMenuItems({
		.downloads = &downloads,
		.hasSettings = _webview && _webview->window.widget() && _hasSettingsButton,
		.buttons = _menuButtons,
	});
	sendExternalShellMethod("setMenu", {
		{ u"items"_q, SerializeSharedPanelMenu(items) },
	});
}

void Panel::sendExternalShellAssets() {
	const auto &downloads = _delegate->botDownloads(true);
	const auto items = BuildSharedPanelMenuItems({
		.downloads = &downloads,
		.hasSettings = _webview && _webview->window.widget() && _hasSettingsButton,
		.buttons = _menuButtons,
	});
	auto icons = QJsonObject();
	CollectSharedPanelMenuIcons(items, icons);
	sendExternalShellMethod("setAssets", {
		{ u"icons"_q, icons },
		{ u"titleMenuIcon"_q,
			SerializeStyleIconAsset(st::separatePanelMenu.icon) },
		{ u"verifiedBadge"_q, SerializeVerifiedBadgeAsset() },
		{ u"menuPalette"_q, LinuxShell::MenuPalette() },
	});
}

void Panel::handleExternalShellMenuAction(const QString &id) {
	DispatchSharedPanelMenuAction(id, {
		.settings = [=] {
			postEvent("settings_button_pressed");
		},
		.reload = [=] {
			if (_webview && _webview->window.widget()) {
				sendExternalShellMethod("reloadFrame", {});
			} else {
				const auto params = _delegate->botThemeParams();
				resetExternalShellIdentity();
				if (!createWebview(params)) {
					return;
				}
				showWebviewProgress();
				updateThemeParams(params);
				_webview->window.navigate(ExternalShellTopUrl());
			}
		},
		.terms = [=] {
			File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now));
		},
		.privacy = [=] {
			_delegate->botOpenPrivacyPolicy();
		},
		.menuButton = [=](MenuButton button) {
			_delegate->botHandleMenuButton(button);
		},
		.download = [=](uint32 downloadId, DownloadsAction type) {
			_delegate->botDownloadsAction(downloadId, type);
		},
	});
}

void Panel::sendExternalShellChrome() {
	sendExternalShellMethod("setChrome", {
		{ u"backVisible"_q, _externalBackVisible },
		{ u"menuVisible"_q, true },
		{ u"badgeVisible"_q, _externalTitleBadgeVisible },
	});
}

void Panel::setExternalShellBlocked(bool blocked) {
	if (!_externalShell) {
		return;
	}
	const auto was = (_externalBlockCount > 0);
	if (blocked) {
		++_externalBlockCount;
	} else if (_externalBlockCount > 0) {
		--_externalBlockCount;
	}
	const auto now = (_externalBlockCount > 0);
	if (was != now) {
		sendExternalShellMethod("setBlocked", { { u"blocked"_q, now } });
	}
}

void Panel::closeExternalShellLayer() {
	if (!_externalShell) {
		return;
	}
	if (_externalLayer) {
		_externalLayer->hideLayers(anim::type::normal);
	}
	Webview::CloseBlockingPopup();
}

void Panel::showExternalShellError(TextWithEntities text) {
	const auto anchor = externalShellAnchor();
	invalidateExternalShellSession();
	_progress = nullptr;
	_webviewProgress = false;
	base::take(_webview);
	_externalWebviewParent = nullptr;
	_webviewParent = nullptr;
	Webview::CloseBlockingPopup();
	if (!_externalLayer) {
		showCriticalError(text);
		return;
	}
	_externalLayer->setAnchor(
		anchor.anchorGeometry,
		anchor.outerSize,
		anchor.transientParent);
	const auto weak = base::make_weak(this);
	const auto botClosed = std::make_shared<bool>(false);
	const auto closeBot = [=] {
		if (*botClosed) {
			return;
		}
		*botClosed = true;
		if (const auto panel = weak.get()) {
			panel->_delegate->botClose();
		}
	};
	auto box = Ui::MakeInformBox({
		.text = std::move(text),
		.confirmed = [=](Fn<void()> close) {
			close();
			closeBot();
		},
	});
	box->boxClosing() | rpl::on_next(closeBot, box->lifetime());
	_externalLayer->showBox(
		std::move(box),
		LayerOption::CloseOther,
		anim::type::normal);
}

Panel::ExternalShellAnchor Panel::externalShellAnchor() const {
	if (!_webview) {
		return {};
	}
	auto popupAnchor = _webview->window.popupAnchor();
	auto result = ExternalShellAnchor{
		.outerSize = std::move(popupAnchor.outerSize),
		.transientParent = CompatibleForeignParent(
			std::move(popupAnchor.transientParent)),
	};
	switch (result.transientParent.type) {
	case Ui::Platform::ForeignParent::Type::X11:
		result.anchorGeometry = Ui::Platform::ForeignWindowGeometry(
			result.transientParent);
		if (result.anchorGeometry) {
			result.outerSize = std::nullopt;
		}
		break;
	case Ui::Platform::ForeignParent::Type::None:
	case Ui::Platform::ForeignParent::Type::Wayland:
		result.anchorGeometry = std::move(popupAnchor.geometry);
		break;
	}
	if (!result.transientParent
		&& !result.anchorGeometry
		&& !result.outerSize) {
		return {};
	}
	return result;
}

QWidget *Panel::webviewWindowForPopup() const {
	const auto widget = _webview ? _webview->window.widget() : nullptr;
	return widget ? widget->window() : nullptr;
}

void Panel::showPopup(
		Webview::PopupArgs &&args,
		Fn<void(Webview::PopupResult)> done) {
	if (!_externalShell) {
		auto result = Webview::ShowBlockingPopup(std::move(args));
		if (done) {
			done(std::move(result));
		}
		return;
	}
	const auto anchor = externalShellAnchor();
	args.anchorGeometry = anchor.anchorGeometry;
	args.transientParent = anchor.transientParent;
	args.parent = nullptr;
	setExternalShellBlocked(true);
	const auto weak = base::make_weak(this);
	Webview::ShowPopupAsync(
		std::move(args),
		[=, done = std::move(done)](
				Webview::PopupResult result) mutable {
			if (weak) {
				weak->setExternalShellBlocked(false);
			}
			if (done) {
				done(std::move(result));
			}
		},
		false);
}

void Panel::createWebviewBottom() {
	_webviewBottom = std::make_unique<RpWidget>(_widget.get());
	const auto bottom = _webviewBottom.get();
	bottom->setVisible(!_fullscreen.current());

	const auto &padding = st::paymentsPanelPadding;
	const auto label = CreateChild<FlatLabel>(
		_webviewBottom.get(),
		_bottomText.value(),
		st::paymentsWebviewBottom);
	_webviewBottomLabel = label;

	const auto height = padding.top()
		+ label->heightNoMargins()
		+ padding.bottom();
	rpl::combine(
		_webviewBottom->widthValue(),
		label->widthValue()
	) | rpl::on_next([=](int outerWidth, int width) {
		label->move((outerWidth - width) / 2, padding.top());
	}, label->lifetime());
	label->show();
	_webviewBottom->resize(_webviewBottom->width(), height);

	rpl::combine(
		_webviewParent->geometryValue() | rpl::map([=] {
			return _widget->innerGeometry();
		}),
		bottom->heightValue()
	) | rpl::on_next([=](QRect inner, int height) {
		bottom->move(inner.x(), inner.y() + inner.height() - height);
		bottom->resizeToWidth(inner.width());
		layoutButtons();
	}, bottom->lifetime());
}

bool Panel::createWebview(const Webview::ThemeParams &params) {
	RpWidget *container = nullptr;
	if (_externalShell) {
		_externalWebviewParent = std::make_unique<RpWidget>();
		container = _externalWebviewParent.get();
	} else {
		auto outer = base::make_unique_q<RpWidget>(_widget.get());
		container = outer.get();
		_widget->showInner(std::move(outer));
	}
	_webviewParent = container;

	_headerColorReceived = false;
	_bodyColorReceived = false;
	_bottomColorReceived = false;
	updateColorOverrides(params);
	if (!_externalShell) {
		createWebviewBottom();
	}

	if (!_externalShell) {
		container->show();
	}
	_externalWindowCloseRequested = false;
	_webview = std::make_unique<WebviewWithLifetime>(
		container,
		Webview::WindowConfig{
			.opaqueBg = params.bodyBg,
			.storageId = _storageId,
			.mode = _externalShell
				? Webview::WindowMode::External
				: Webview::WindowMode::Embedded,
			.windowStyle = _externalShell
				? Webview::WindowStyle::Frameless
				: Webview::WindowStyle::Default,
			.windowMargins = _externalShell
				? st::botWebViewShellShadowPadding
				: QMargins(),
			.initialSize = _externalShell
				? LinuxShell::WindowSize(st::botWebViewPanelSize)
				: QSize(),
			.shellMessageToken = _externalShell
				? _externalShellToken
				: QString(),
		});
	const auto raw = &_webview->window;

	const auto bottom = _webviewBottom.get();
	QObject::connect(container, &QObject::destroyed, [=] {
		if (_webview && &_webview->window == raw) {
			if (_externalShell) {
				_externalShellBootstrapped = false;
				++_externalShellGeneration;
			}
			base::take(_webview);
			if (_webviewProgress) {
				hideWebviewProgress();
				if (_progress && !_progress->shown) {
					_progress = nullptr;
				}
			}
		}
		if (_webviewBottom.get() == bottom) {
			_webviewBottomLabel = nullptr;
			_webviewBottom = nullptr;
			_secondaryButton = nullptr;
			_mainButton = nullptr;
			_bottomButtonsBg = nullptr;
		}
	});
	if (!raw->widget()) {
		return false;
	}

#if !defined Q_OS_WIN && !defined Q_OS_MAC
	if (!_externalShell) {
		_widget->allowChildFullScreenControls(
			!raw->widget()->inherits("QWindowContainer"));
	}
#endif // !Q_OS_WIN && !Q_OS_MAC

	raw->setInteractionHandler([=] {
		_lastWebviewInteraction = crl::now();
	});
	raw->setExternalWindowCloseHandler([=] {
		if (!_externalShell
			|| !_webview
			|| &_webview->window != raw) {
			return;
		}
		_externalWindowCloseRequested = true;
		invalidateExternalShellSession();
		_delegate->botClose();
	});

	QObject::connect(raw->widget(), &QObject::destroyed, [=] {
		const auto parent = _webviewParent.data();
		if (!_webview
			|| &_webview->window != raw
			|| (_externalShell && _externalWindowCloseRequested)
			|| (!_externalShell
				&& (!parent || _widget->inner() != parent))) {
			// If we destroyed _webview ourselves,
			// or if we changed _widget->inner ourselves,
			// we don't show any message, nothing crashed.
			return;
		}
		if (_externalShell) {
			invalidateExternalShellSession();
		}
		crl::on_main(this, [=] {
			if (!_webview || &_webview->window != raw) {
				return;
			}
			if (_externalShell) {
				showExternalShellError({ tr::lng_bot_webview_crashed(tr::now) });
			} else {
				showCriticalError({ tr::lng_bot_webview_crashed(tr::now) });
			}
		});
	});

	if (_externalShell) {
		applyExternalShellFullscreen(_fullscreen.current());
	} else {
		rpl::combine(
			container->geometryValue(),
			_footerHeight.value()
		) | rpl::on_next([=](QRect geometry, int footer) {
			if (const auto view = raw->widget()) {
				view->setGeometry(geometry.marginsRemoved({ 0, 0, 0, footer }));
				crl::on_main(view, [=] {
					sendViewport();
					InvokeQueued(view, [=] { sendViewport(); });
				});
			}
		}, _webview->lifetime);
	}

	raw->setMessageHandler([=](Webview::Message message) {
		if (message.text.size() > size_t(kMaxNativeMessageBytes)) {
			LogNativeMessageRejected(
				u"payload too large"_q,
				quint64(message.text.size()));
			return;
		}
		const auto bytes = QByteArray::fromRawData(
			message.text.data(),
			int(message.text.size()));
		const auto parsed = ParseNativeMessage(
			bytes,
			QString::fromStdString(message.sourceUrl),
			_externalShell,
			_externalShellToken);
		if (!parsed) {
			return;
		}
		if (_sameOrigin
			&& parsed->source != NativeMessageSource::ExternalShell
			&& !OriginsMatch(parsed->origin, _initialOrigin)) {
			LogNativeMessageRejected(
				u"bad webapp origin"_q,
				quint64(message.text.size()),
				parsed->command);
			return;
		}
		const auto &command = parsed->command;
		const auto &arguments = parsed->arguments;
		if (parsed->source == NativeMessageSource::ExternalShell) {
			if (!_externalShell || !_externalShellBootstrapped) {
				return;
			}
			if (command == "shell_close") {
				if (_closeNeedConfirmation) {
					scheduleCloseWithConfirmation();
				} else {
					_delegate->botClose();
				}
			} else if (command == "shell_menu_request") {
				if (_externalBlockCount <= 0) {
					sendExternalShellAssets();
					sendExternalShellMenu();
				}
			} else if (command == "shell_menu_action") {
				if (_externalBlockCount <= 0) {
					handleExternalShellMenuAction(arguments["id"].toString());
				}
			} else if (command == "shell_request_button_icon") {
				const auto name = arguments["name"];
				if (name.isString()) {
					requestExternalShellButtonEmoji(name.toString());
				}
			} else if (command == "shell_close_layer") {
				closeExternalShellLayer();
			}
			return;
		}
		if (command == "web_app_close") {
			_delegate->botClose();
		} else if (command == "web_app_data_send") {
			sendDataMessage(arguments);
		} else if (command == "web_app_switch_inline_query") {
			switchInlineQueryMessage(arguments);
		} else if (command == "web_app_setup_main_button") {
			processButtonMessage(_mainButton, arguments);
		} else if (command == "web_app_setup_secondary_button") {
			processButtonMessage(_secondaryButton, arguments);
		} else if (command == "web_app_setup_back_button") {
			processBackButtonMessage(arguments);
		} else if (command == "web_app_setup_settings_button") {
			processSettingsButtonMessage(arguments);
		} else if (command == "web_app_request_theme") {
			_themeUpdateForced.fire({});
		} else if (command == "web_app_request_viewport") {
			sendViewport();
		} else if (command == "web_app_request_safe_area") {
			sendSafeArea();
		} else if (command == "web_app_request_content_safe_area") {
			sendContentSafeArea();
		} else if (command == "web_app_request_fullscreen") {
			if (!_fullscreen.current()) {
				_fullscreen = true;
			} else {
				sendFullScreen();
			}
		} else if (command == "web_app_request_file_download") {
			processDownloadRequest(arguments);
		} else if (command == "web_app_exit_fullscreen") {
			if (_fullscreen.current()) {
				_fullscreen = false;
			} else {
				sendFullScreen();
			}
		} else if (command == "web_app_check_home_screen") {
			postEvent("home_screen_checked", QJsonObject{
				{ u"status"_q, u"unsupported"_q },
			});
		} else if (command == "web_app_start_accelerometer") {
			postEvent("accelerometer_failed", QJsonObject{
				{ u"error"_q, u"UNSUPPORTED"_q },
			});
		} else if (command == "web_app_start_device_orientation") {
			postEvent("device_orientation_failed", QJsonObject{
				{ u"error"_q, u"UNSUPPORTED"_q },
			});
		} else if (command == "web_app_start_gyroscope") {
			postEvent("gyroscope_failed", QJsonObject{
				{ u"error"_q, u"UNSUPPORTED"_q },
			});
		} else if (command == "web_app_check_location") {
			postEvent("location_checked", QJsonObject{
				{ u"available"_q, false },
			});
		} else if (command == "web_app_request_location") {
			postEvent("location_requested", QJsonObject{
				{ u"available"_q, false },
			});
		} else if (command == "web_app_biometry_get_info") {
			postEvent("biometry_info_received", QJsonObject{
				{ u"available"_q, false },
			});
		} else if (command == "web_app_open_tg_link") {
			openTgLink(arguments);
		} else if (command == "web_app_open_link") {
			openExternalLink(arguments);
		} else if (command == "web_app_open_invoice") {
			openInvoice(arguments);
		} else if (command == "web_app_open_popup") {
			openPopup(arguments);
		} else if (command == "web_app_open_scan_qr_popup") {
			openScanQrPopup(arguments);
		} else if (command == "web_app_share_to_story") {
			openShareStory(arguments);
		} else if (command == "web_app_request_write_access") {
			requestWriteAccess();
		} else if (command == "web_app_request_phone") {
			requestPhone();
		} else if (command == "web_app_invoke_custom_method") {
			invokeCustomMethod(arguments);
		} else if (command == "web_app_setup_closing_behavior") {
			setupClosingBehaviour(arguments);
		} else if (command == "web_app_read_text_from_clipboard") {
			requestClipboardText(arguments);
		} else if (command == "web_app_set_header_color") {
			processHeaderColor(arguments);
		} else if (command == "web_app_set_background_color") {
			processBackgroundColor(arguments);
		} else if (command == "web_app_set_bottom_bar_color") {
			processBottomBarColor(arguments);
		} else if (command == "web_app_send_prepared_message") {
			processSendMessageRequest(arguments);
		} else if (command == "web_app_request_chat") {
			processRequestChat(arguments);
		} else if (command == "web_app_set_emoji_status") {
			processEmojiStatusRequest(arguments);
		} else if (command == "web_app_request_emoji_status_access") {
			processEmojiStatusAccessRequest();
		} else if (command == "web_app_device_storage_save_key") {
			processStorageSaveKey(arguments);
		} else if (command == "web_app_device_storage_get_key") {
			processStorageGetKey(arguments);
		} else if (command == "web_app_device_storage_clear") {
			processStorageClear(arguments);
		} else if (command == "web_app_secure_storage_save_key") {
			secureStorageFailed(arguments);
		} else if (command == "web_app_secure_storage_get_key") {
			secureStorageFailed(arguments);
		} else if (command == "web_app_secure_storage_restore_key") {
			secureStorageFailed(arguments);
		} else if (command == "web_app_secure_storage_clear") {
			secureStorageFailed(arguments);
		} else if (command == "web_app_verify_age") {
			const auto passed = arguments["passed"];
			const auto detected = arguments["age"];
			const auto valid = passed.isBool()
				&& passed.toBool()
				&& detected.isDouble();
			const auto age = valid
				? int(std::floor(detected.toDouble()))
				: 0;
			_delegate->botVerifyAge(age);
		} else if (command == "share_score") {
			_delegate->botHandleMenuButton(MenuButton::ShareGame);
		}
	});

	raw->setNavigationStartHandler([=](const QString &uri, bool newWindow) {
		if (_delegate->botHandleLocalUri(uri, false)) {
			return false;
		} else if (newWindow) {
			return true;
		}
		_currentOrigin = OriginFromUrl(uri);
		showWebviewProgress();
		return true;
	});
	raw->setNavigationDoneHandler([=](bool success) {
		hideWebviewProgress();
		if (_externalShell && !_externalShellBootstrapped) {
			if (!success) {
				invalidateExternalShellSession();
				crl::on_main(this, [=] {
					if (_externalShell && _webview && &_webview->window == raw) {
						showExternalShellError({
							tr::lng_bot_webview_failed(tr::now),
						});
					}
				});
				return;
			}
			_externalShellBootstrapped = true;
			installExternalShellDocument();
			sendExternalShellBootstrap();
		}
	});
	if (_externalShell) {
		raw->setDialogHandler([=](Webview::DialogArgs args) {
			const auto anchor = externalShellAnchor();
			args.anchorGeometry = anchor.anchorGeometry;
			args.transientParent = anchor.transientParent;
			args.parent = nullptr;
			setExternalShellBlocked(true);
			const auto weak = base::make_weak(this);
			const auto guard = gsl::finally([=] {
				if (weak) {
					weak->setExternalShellBlocked(false);
				}
			});
			return Webview::DefaultDialogHandler(std::move(args));
		});
		raw->setAsyncDialogHandler([=](
				Webview::DialogArgs args,
				std::function<void(Webview::DialogResult)> done) {
			const auto anchor = externalShellAnchor();
			args.anchorGeometry = anchor.anchorGeometry;
			args.transientParent = anchor.transientParent;
			args.parent = nullptr;
			setExternalShellBlocked(true);
			const auto weak = base::make_weak(this);
			Webview::DefaultDialogHandlerAsync(
				std::move(args),
				[=, done = std::move(done)](
						Webview::DialogResult result) mutable {
					if (weak) {
						weak->setExternalShellBlocked(false);
					}
					done(std::move(result));
				},
				false);
			return true;
		});
	}

	auto initScript = QByteArray(R"(
window.TelegramWebviewProxy = {
postEvent: function(eventType, eventData) {
	if (window.external && window.external.invoke) {
		window.external.invoke(JSON.stringify([eventType, eventData]));
	}
}
};)");
		raw->init(initScript);

	if (!_webview) {
		return false;
	}

	if (!_externalShell) {
		layoutButtons();
		setupProgressGeometry();
	}

	base::qt_signal_producer(
		qApp,
		&QGuiApplication::focusWindowChanged
	) | rpl::filter([=](QWindow *focused) {
		const auto handle = _widget->window()->windowHandle();
		const auto widget = _webview ? _webview->window.widget() : nullptr;
		return widget
			&& !widget->isHidden()
			&& handle
			&& (focused == handle);
	}) | rpl::on_next([=] {
		_webview->window.focus();
	}, _webview->lifetime);

	return true;
}

void Panel::sendViewport() {
	if (_externalShell) {
		sendExternalShellMethod("sendViewport", {});
		return;
	}
	postEvent("viewport_changed", "{ "
		"height: window.innerHeight, "
		"is_state_stable: true, "
		"is_expanded: true }");
}

void Panel::sendFullScreen() {
	postEvent("fullscreen_changed", QJsonObject{
		{ u"is_fullscreen"_q, _fullscreen.current() },
	});
}

void Panel::sendSafeArea() {
	postEvent("safe_area_changed", QJsonObject{
		{ u"top"_q, 0 },
		{ u"right"_q, 0 },
		{ u"bottom"_q, 0 },
		{ u"left"_q, 0 },
	});
}

void Panel::sendContentSafeArea() {
	const auto shift = st::separatePanelClose.rippleAreaPosition.y();
	const auto top = _fullscreen.current()
		? (shift + st::fullScreenPanelClose.height + (shift / 2))
		: 0;
	const auto scaled = top * style::DevicePixelRatio();
	auto report = 0;
	if (const auto screen = QGuiApplication::primaryScreen()) {
		const auto dpi = screen->logicalDotsPerInch();
		const auto ratio = screen->devicePixelRatio();
		const auto basePair = screen->handle()->logicalBaseDpi();
		const auto base = (basePair.first + basePair.second) * 0.5;
		const auto systemScreenScale = dpi * ratio / base;
		report = int(base::SafeRound(scaled / systemScreenScale));
	}
	postEvent("content_safe_area_changed", QJsonObject{
		{ u"top"_q, report },
		{ u"right"_q, 0 },
		{ u"bottom"_q, 0 },
		{ u"left"_q, 0 },
	});
}

void Panel::setTitle(rpl::producer<QString> title) {
	if (!_externalShell) {
		_widget->setTitle(std::move(title));
		return;
	}
	std::move(title) | rpl::on_next([=](const QString &title) {
		_externalTitle = title;
		sendExternalShellMethod("setTitle", { { u"title"_q, title } });
	}, _widget->lifetime());
}

void Panel::sendDataMessage(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto data = args["data"].toString();
	if (data.isEmpty()) {
		LOG(("BotWebView Error: Bad 'data' in sendDataMessage."));
		_delegate->botClose();
		return;
	}
	_delegate->botSendData(data.toUtf8());
}

void Panel::switchInlineQueryMessage(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	} else if (!args.contains("query")) {
		LOG(("BotWebView Error: No 'query' in switchInlineQueryMessage."));
		_delegate->botClose();
		return;
	}
	const auto query = args["query"].toString();
	const auto valid = base::flat_set<QString>{
		u"users"_q,
		u"bots"_q,
		u"groups"_q,
		u"channels"_q,
	};
	const auto typeArray = args["chat_types"].toArray();
	auto types = std::vector<QString>();
	for (const auto &value : typeArray) {
		const auto type = value.toString();
		if (valid.contains(type)) {
			types.push_back(type);
		} else {
			LOG(("BotWebView Error: "
				"Bad chat type in switchInlineQueryMessage: %1.").arg(type));
			types.clear();
			break;
		}
	}
	_delegate->botSwitchInlineQuery(types, query);
}

void Panel::processSendMessageRequest(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto id = args["id"].toString();
	auto callback = crl::guard(this, [=](QString error) {
		if (error.isEmpty()) {
			postEvent("prepared_message_sent");
		} else {
			postEvent("prepared_message_failed", QJsonObject{
				{ u"error"_q, error },
			});
		}
	});
	_delegate->botSendPreparedMessage({
		.id = id,
		.callback = std::move(callback),
	});
}

void Panel::processRequestChat(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto requestId = args["req_id"].toString();
	if (requestId.isEmpty()) {
		return;
	}
	auto callback = crl::guard(this, [=](QString error) {
		if (error.isEmpty()) {
			postEvent("requested_chat_sent", QJsonObject{
				{ u"req_id"_q, requestId },
			});
		} else {
			postEvent("requested_chat_failed", QJsonObject{
				{ u"req_id"_q, requestId },
				{ u"error"_q, error },
			});
		}
	});
	_delegate->botRequestChat({
		.requestId = requestId,
		.callback = std::move(callback),
	});
}

void Panel::processEmojiStatusRequest(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto emojiId = args["custom_emoji_id"].toString().toULongLong();
	const auto duration = TimeId(base::SafeRound(
		args["duration"].toDouble()));
	if (!emojiId) {
		postEvent("emoji_status_failed", QJsonObject{
			{ u"error"_q, u"SUGGESTED_EMOJI_INVALID"_q },
		});
		return;
	} else if (duration < 0) {
		postEvent("emoji_status_failed", QJsonObject{
			{ u"error"_q, u"DURATION_INVALID"_q },
		});
		return;
	}
	auto callback = crl::guard(this, [=](QString error) {
		if (error.isEmpty()) {
			postEvent("emoji_status_set");
		} else {
			postEvent("emoji_status_failed", QJsonObject{
				{ u"error"_q, error },
			});
		}
	});
	_delegate->botSetEmojiStatus({
		.customEmojiId = emojiId,
		.duration = duration,
		.callback = std::move(callback),
	});
}

void Panel::processEmojiStatusAccessRequest() {
	auto callback = crl::guard(this, [=](bool allowed) {
		postEvent("emoji_status_access_requested", QJsonObject{
			{ u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q },
		});
	});
	_delegate->botRequestEmojiStatusAccess(std::move(callback));
}

void Panel::processStorageSaveKey(const QJsonObject &args) {
	const auto keyObject = args["key"];
	const auto valueObject = args["value"];
	const auto key = keyObject.toString();
	if (!keyObject.isString()) {
		deviceStorageFailed(args, u"KEY_INVALID"_q);
	} else if (valueObject.isNull()) {
		_delegate->botStorageWrite(key, std::nullopt);
		replyDeviceStorage(args, u"device_storage_key_saved"_q, {});
	} else if (!valueObject.isString()) {
		deviceStorageFailed(args, u"VALUE_INVALID"_q);
	} else if (_delegate->botStorageWrite(key, valueObject.toString())) {
		replyDeviceStorage(args, u"device_storage_key_saved"_q, {});
	} else {
		deviceStorageFailed(args, u"QUOTA_EXCEEDED"_q);
	}
}

void Panel::processStorageGetKey(const QJsonObject &args) {
	const auto keyObject = args["key"];
	const auto key = keyObject.toString();
	if (!keyObject.isString()) {
		deviceStorageFailed(args, u"KEY_INVALID"_q);
	} else {
		const auto value = _delegate->botStorageRead(key);
		replyDeviceStorage(args, u"device_storage_key_received"_q, {
			{ u"value"_q, value ? QJsonValue(*value) : QJsonValue::Null },
		});
	}
}

void Panel::processStorageClear(const QJsonObject &args) {
	_delegate->botStorageClear();
	replyDeviceStorage(args, u"device_storage_cleared"_q, {});
}

void Panel::replyDeviceStorage(
		const QJsonObject &args,
		const QString &event,
		QJsonObject response) {
	response[u"req_id"_q] = args[u"req_id"_q];
	postEvent(event, response);
}

void Panel::deviceStorageFailed(const QJsonObject &args, QString error) {
	replyDeviceStorage(args, u"device_storage_failed"_q, {
		{ u"error"_q, error },
	});
}

void Panel::secureStorageFailed(const QJsonObject &args) {
	postEvent(u"secure_storage_failed"_q, QJsonObject{
		{ u"req_id"_q, args["req_id"] },
		{ u"error"_q, u"UNSUPPORTED"_q },
	});
}

void Panel::openTgLink(const QJsonObject &args) {
	if (args.isEmpty()) {
		LOG(("BotWebView Error: Bad arguments in 'web_app_open_tg_link'."));
		_delegate->botClose();
		return;
	}
	const auto path = args["path_full"].toString();
	if (path.isEmpty()) {
		LOG(("BotWebView Error: Bad 'path_full' in 'web_app_open_tg_link'."));
		_delegate->botClose();
		return;
	}
	_delegate->botHandleLocalUri("https://t.me" + path, true);
}

void Panel::openExternalLink(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto iv = args["try_instant_view"].toBool();
	const auto url = args["url"].toString();
	if (!_delegate->botValidateExternalLink(url)) {
		LOG(("BotWebView Error: Bad url in openExternalLink: %1").arg(url));
		_delegate->botClose();
		return;
	} else if (!allowOpenLink()) {
		return;
	} else if (iv) {
		_delegate->botOpenIvLink(url);
	} else {
		File::OpenUrl(url);
	}
}

void Panel::openInvoice(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto slug = args["slug"].toString();
	if (slug.isEmpty()) {
		LOG(("BotWebView Error: Bad 'slug' in openInvoice."));
		_delegate->botClose();
		return;
	}
	_delegate->botHandleInvoice(slug);
}

void Panel::openPopup(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	using Button = Webview::PopupArgs::Button;
	using Type = Button::Type;
	const auto message = args["message"].toString();
	const auto types = base::flat_map<QString, Button::Type>{
		{ "default", Type::Default },
		{ "ok", Type::Ok },
		{ "close", Type::Close },
		{ "cancel", Type::Cancel },
		{ "destructive", Type::Destructive },
	};
	const auto buttonArray = args["buttons"].toArray();
	auto buttons = std::vector<Webview::PopupArgs::Button>();
	for (const auto button : buttonArray) {
		const auto fields = button.toObject();
		const auto i = types.find(fields["type"].toString());
		if (i == end(types)) {
			LOG(("BotWebView Error: Bad 'type' in openPopup buttons."));
			_delegate->botClose();
			return;
		}
		buttons.push_back({
			.id = fields["id"].toString(),
			.text = fields["text"].toString(),
			.type = i->second,
		});
	}
	if (message.isEmpty()) {
		LOG(("BotWebView Error: Bad 'message' in openPopup."));
		_delegate->botClose();
		return;
	} else if (buttons.empty()) {
		LOG(("BotWebView Error: Bad 'buttons' in openPopup."));
		_delegate->botClose();
		return;
	}
	const auto weak = base::make_weak(this);
	showPopup({
		.parent = webviewWindowForPopup(),
		.title = args["title"].toString(),
		.text = message,
		.buttons = std::move(buttons),
	}, [=](Webview::PopupResult result) {
		if (!weak) {
			return;
		}
		postEvent("popup_closed", result.id
			? QJsonObject{ { u"button_id"_q, *result.id } }
			: EventData());
	});
}

void Panel::openScanQrPopup(const QJsonObject &args) {
	showPopup({
		.parent = webviewWindowForPopup(),
		.text = tr::lng_bot_no_scan_qr(tr::now),
		.buttons = { {
			.id = "ok",
			.text = tr::lng_box_ok(tr::now),
			.type = Webview::PopupArgs::Button::Type::Ok,
		}},
	}, [](Webview::PopupResult) {});
}

void Panel::openShareStory(const QJsonObject &args) {
	showPopup({
		.parent = webviewWindowForPopup(),
		.text = tr::lng_bot_no_share_story(tr::now),
		.buttons = { {
			.id = "ok",
			.text = tr::lng_box_ok(tr::now),
			.type = Webview::PopupArgs::Button::Type::Ok,
		}},
	}, [](Webview::PopupResult) {});
}

void Panel::requestWriteAccess() {
	if (_inBlockingRequest) {
		replyRequestWriteAccess(false);
		return;
	}
	_inBlockingRequest = true;
	const auto finish = [=](bool allowed) {
		_inBlockingRequest = false;
		replyRequestWriteAccess(allowed);
	};
	const auto weak = base::make_weak(this);
	_delegate->botCheckWriteAccess([=](bool allowed) {
		if (!weak) {
			return;
		} else if (allowed) {
			finish(true);
			return;
		}
		using Button = Webview::PopupArgs::Button;
		if (!_webview) {
			_inBlockingRequest = false;
			return;
		}
		const auto integration = &Ui::Integration::Instance();
		showPopup({
			.parent = webviewWindowForPopup(),
			.title = integration->phraseBotAllowWriteTitle(),
			.text = integration->phraseBotAllowWrite(),
			.buttons = {
				{
					.id = "allow",
					.text = integration->phraseBotAllowWriteConfirm(),
				},
				{ .id = "cancel", .type = Button::Type::Cancel },
			},
		}, [=](Webview::PopupResult result) {
			if (!weak) {
				return;
			} else if (result.id == "allow") {
				_delegate->botAllowWriteAccess(crl::guard(this, finish));
			} else {
				finish(false);
			}
		});
	});
}

void Panel::replyRequestWriteAccess(bool allowed) {
	postEvent("write_access_requested", QJsonObject{
		{ u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q }
	});
}

void Panel::requestPhone() {
	if (_inBlockingRequest) {
		replyRequestPhone(false);
		return;
	}
	_inBlockingRequest = true;
	const auto finish = [=](bool shared) {
		_inBlockingRequest = false;
		replyRequestPhone(shared);
	};
	using Button = Webview::PopupArgs::Button;
	const auto weak = base::make_weak(this);
	if (!_webview) {
		_inBlockingRequest = false;
		return;
	}
	const auto integration = &Ui::Integration::Instance();
	showPopup({
		.parent = webviewWindowForPopup(),
		.title = integration->phraseBotSharePhoneTitle(),
		.text = integration->phraseBotSharePhone(),
		.buttons = {
			{
				.id = "share",
				.text = integration->phraseBotSharePhoneConfirm(),
			},
			{ .id = "cancel", .type = Button::Type::Cancel },
		},
	}, [=](Webview::PopupResult result) {
		if (!weak) {
			return;
		} else if (result.id == "share") {
			_delegate->botSharePhone(crl::guard(this, finish));
		} else {
			finish(false);
		}
	});
}

void Panel::replyRequestPhone(bool shared) {
	postEvent("phone_requested", QJsonObject{
		{ u"status"_q, shared ? u"sent"_q : u"cancelled"_q }
	});
}

void Panel::invokeCustomMethod(const QJsonObject &args) {
	const auto requestId = args["req_id"];
	if (requestId.isUndefined()) {
		return;
	}
	const auto finish = [=](QJsonObject response) {
		replyCustomMethod(requestId, std::move(response));
	};
	auto callback = crl::guard(this, [=](CustomMethodResult result) {
		if (result) {
			auto error = QJsonParseError();
			const auto parsed = QJsonDocument::fromJson(
				"{ \"result\": " + *result + '}',
				&error);
			if (error.error != QJsonParseError::NoError
				|| !parsed.isObject()
				|| parsed.object().size() != 1) {
				finish({ { u"error"_q, u"Could not parse response."_q } });
			} else {
				finish(parsed.object());
			}
		} else {
			finish({ { u"error"_q, result.error() } });
		}
	});
	const auto params = QJsonDocument(
		args["params"].toObject()
	).toJson(QJsonDocument::Compact);
	_delegate->botInvokeCustomMethod({
		.method = args["method"].toString(),
		.params = params,
		.callback = std::move(callback),
	});
}

void Panel::replyCustomMethod(QJsonValue requestId, QJsonObject response) {
	response["req_id"] = requestId;
	postEvent(u"custom_method_invoked"_q, response);
}

void Panel::requestClipboardText(const QJsonObject &args) {
	const auto requestId = args["req_id"];
	if (requestId.isUndefined()) {
		return;
	}
	auto result = QJsonObject();
	result["req_id"] = requestId;
	if (allowClipboardQuery()) {
		result["data"] = QGuiApplication::clipboard()->text();
	}
	postEvent(u"clipboard_text_received"_q, result);
}

bool Panel::allowOpenLink() const {
	//const auto now = crl::now();
	//if (_mainButtonLastClick
	//	&& _mainButtonLastClick + kProcessClickTimeout >= now) {
	//	_mainButtonLastClick = 0;
	//	return true;
	//}
	return true;
}

bool Panel::allowClipboardQuery() const {
	if (!_allowClipboardRead) {
		return false;
	}
	const auto now = crl::now();
	return _lastWebviewInteraction
		&& (_lastWebviewInteraction + kClipboardReadTimeout >= now);
}

void Panel::scheduleCloseWithConfirmation() {
	if (!_closeWithConfirmationScheduled) {
		_closeWithConfirmationScheduled = true;
		const auto generation = _externalShellGeneration;
		InvokeQueued(_widget.get(), [=] {
			if (_externalShell && generation != _externalShellGeneration) {
				_closeWithConfirmationScheduled = false;
				return;
			}
			closeWithConfirmation();
		});
	}
}

void Panel::closeWithConfirmation() {
	if (!_webview) {
		_closeWithConfirmationScheduled = false;
		_delegate->botClose();
		return;
	}
	using Button = Webview::PopupArgs::Button;
	const auto weak = base::make_weak(this);
	const auto integration = &Ui::Integration::Instance();
	showPopup({
		.parent = webviewWindowForPopup(),
		.title = integration->phrasePanelCloseWarning(),
		.text = integration->phrasePanelCloseUnsaved(),
		.buttons = {
			{
				.id = "close",
				.text = integration->phrasePanelCloseAnyway(),
				.type = Button::Type::Destructive,
			},
			{ .id = "cancel", .type = Button::Type::Cancel },
		},
		.ignoreFloodCheck = true,
	}, [=](Webview::PopupResult result) {
		if (!weak) {
			return;
		} else if (result.id == "close") {
			_delegate->botClose();
		} else {
			_closeWithConfirmationScheduled = false;
		}
	});
}

void Panel::setupClosingBehaviour(const QJsonObject &args) {
	_closeNeedConfirmation = args["need_confirmation"].toBool();
}

void Panel::processButtonMessage(
		std::unique_ptr<Button> &button,
		const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	if (_externalShell) {
		sendExternalShellButton(
			(&button == &_mainButton) ? "main" : "secondary",
			args);
		sendViewport();
		return;
	}

	const auto shown = [&] {
		return button && !button->isHidden();
	};
	const auto wasShown = shown();
	const auto guard = gsl::finally([&] {
		if (shown() != wasShown) {
			crl::on_main(this, [=] {
				sendViewport();
			});
		}
	});

	const auto text = args["text"].toString().trimmed();
	const auto iconCustomEmojiId
		= args["icon_custom_emoji_id"].toString().toULongLong();
	const auto visible = args["is_visible"].toBool()
		&& (!text.isEmpty() || iconCustomEmojiId);
	if (!button) {
		if (visible) {
			createButton(button);
			_bottomButtonsBg->show();
		} else {
			return;
		}
	}

	if (const auto bg = ParseColor(args["color"].toString())) {
		button->updateBg(*bg);
	} else {
		button->updateBg(&st::windowBgActive);
	}

	if (const auto fg = ParseColor(args["text_color"].toString())) {
		button->updateFg(*fg);
	} else {
		button->updateFg(&st::windowFgActive);
	}

	button->updateArgs({
		.isActive = args["is_active"].toBool(),
		.isVisible = visible,
		.isProgressVisible = args["is_progress_visible"].toBool(),
		.iconCustomEmojiId = iconCustomEmojiId,
		.text = args["text"].toString(),
	});
	if (button.get() == _secondaryButton.get()) {
		const auto position = ParsePosition(args["position"].toString());
		if (_secondaryPosition != position) {
			_secondaryPosition = position;
			layoutButtons();
		}
	}
}

void Panel::processBackButtonMessage(const QJsonObject &args) {
	if (_externalShell) {
		_externalBackVisible = args["is_visible"].toBool();
		sendExternalShellChrome();
		return;
	}
	_widget->setBackAllowed(args["is_visible"].toBool());
}

void Panel::processSettingsButtonMessage(const QJsonObject &args) {
	_hasSettingsButton = args["is_visible"].toBool();
	if (_externalShell) {
		sendExternalShellChrome();
		sendExternalShellAssets();
		sendExternalShellMenu();
	}
}

void Panel::processHeaderColor(const QJsonObject &args) {
	_headerColorReceived = true;
	const auto apply = [&](std::optional<QColor> color) {
		if (_externalShell) {
			setExternalShellTitleColor(color);
			sendExternalShellColors(_delegate->botThemeParams());
		} else {
			_widget->overrideTitleColor(color);
		}
	};
	if (const auto color = ParseColor(args["color"].toString())) {
		apply(*color);
		_headerColorLifetime.destroy();
	} else if (const auto color = LookupNamedColor(
			args["color_key"].toString())) {
		apply((*color)->c);
		_headerColorLifetime = style::PaletteChanged(
		) | rpl::on_next([=] {
			apply((*color)->c);
		});
	} else {
		apply(std::nullopt);
		_headerColorLifetime.destroy();
	}
}

void Panel::overrideBodyColor(std::optional<QColor> color) {
	if (_externalShell) {
		if (_bodyColorReceived) {
			setExternalShellBodyColor(color);
		}
		sendExternalShellColors(_delegate->botThemeParams());
		return;
	}
	_widget->overrideBodyColor(color);
	const auto raw = _webviewBottomLabel.data();
	if (!raw) {
		return;
	} else if (!color) {
		raw->setTextColorOverride(std::nullopt);
		return;
	}
	const auto contrast = 2.5;
	const auto luminance = 0.2126 * color->redF()
		+ 0.7152 * color->greenF()
		+ 0.0722 * color->blueF();
	const auto textColor = (luminance > 0.5)
		? QColor(0, 0, 0)
		: QColor(255, 255, 255);
	const auto textLuminance = (luminance > 0.5) ? 0 : 1;
	const auto adaptiveOpacity = (luminance - textLuminance + contrast)
		/ contrast;
	const auto opacity = std::clamp(adaptiveOpacity, 0.5, 0.64);
	auto buttonColor = textColor;
	buttonColor.setAlphaF(opacity);
	raw->setTextColorOverride(buttonColor);
}

void Panel::processBackgroundColor(const QJsonObject &args) {
	_bodyColorReceived = true;
	if (const auto color = ParseColor(args["color"].toString())) {
		overrideBodyColor(*color);
		_bodyColorLifetime.destroy();
	} else if (const auto color = LookupNamedColor(
			args["color_key"].toString())) {
		overrideBodyColor((*color)->c);
		_bodyColorLifetime = style::PaletteChanged(
		) | rpl::on_next([=] {
			overrideBodyColor((*color)->c);
		});
	} else {
		overrideBodyColor(std::nullopt);
		_bodyColorLifetime.destroy();
	}
	if (const auto raw = _bottomButtonsBg.get()) {
		raw->update();
	}
	if (const auto raw = _webviewBottom.get()) {
		raw->update();
	}
}

void Panel::processBottomBarColor(const QJsonObject &args) {
	_bottomColorReceived = true;
	const auto apply = [&](std::optional<QColor> color) {
		_bottomBarColor = color;
		if (_externalShell) {
			setExternalShellBottomColor(color);
			sendExternalShellColors(_delegate->botThemeParams());
		} else {
			_widget->overrideBottomBarColor(color);
		}
	};
	if (const auto color = ParseColor(args["color"].toString())) {
		apply(*color);
		_bottomBarColorLifetime.destroy();
	} else if (const auto color = LookupNamedColor(
			args["color_key"].toString())) {
		apply((*color)->c);
		_bottomBarColorLifetime = style::PaletteChanged(
		) | rpl::on_next([=] {
			apply((*color)->c);
		});
	} else {
		apply(std::nullopt);
		_bottomBarColorLifetime.destroy();
	}
	if (_externalShell) {
		return;
	}
	if (const auto raw = _bottomButtonsBg.get()) {
		raw->update();
	}
}

void Panel::processDownloadRequest(const QJsonObject &args) {
	if (args.isEmpty()) {
		_delegate->botClose();
		return;
	}
	const auto url = args["url"].toString();
	const auto name = args["file_name"].toString();
	if (url.isEmpty()) {
		LOG(("BotWebView Error: Bad 'url' in download request."));
		_delegate->botClose();
		return;
	} else if (name.isEmpty()) {
		LOG(("BotWebView Error: Bad 'file_name' in download request."));
		_delegate->botClose();
		return;
	}
	const auto done = crl::guard(this, [=](bool started) {
		postEvent("file_download_requested", QJsonObject{
			{ u"status"_q, started ? u"downloading"_q : u"cancelled"_q },
		});
	});
	_delegate->botDownloadFile({
		.url = url,
		.name = name,
		.callback = done,
	});
}

void Panel::createButton(std::unique_ptr<Button> &button) {
	if (!_bottomButtonsBg) {
		_bottomButtonsBg = std::make_unique<RpWidget>(_widget.get());

		const auto raw = _bottomButtonsBg.get();
		raw->paintRequest() | rpl::on_next([=] {
			auto p = QPainter(raw);
			auto hq = PainterHighQualityEnabler(p);
			p.setPen(Qt::NoPen);
			p.setBrush(_bottomBarColor.value_or(st::windowBg->c));
			p.drawRoundedRect(
				raw->rect().marginsAdded({ 0, 2 * st::callRadius, 0, 0 }),
				st::callRadius,
				st::callRadius);
		}, raw->lifetime());
	}
	button = std::make_unique<Button>(
		_bottomButtonsBg.get(),
		st::botWebViewBottomButton,
		_delegate->botTextContext());
	const auto raw = button.get();

	raw->setClickedCallback([=] {
		if (!raw->isDisabled()) {
			if (raw == _mainButton.get()) {
				postEvent("main_button_pressed");
			} else if (raw == _secondaryButton.get()) {
				postEvent("secondary_button_pressed");
			}
		}
	});
	raw->hide();

	rpl::combine(
		raw->shownValue(),
		raw->heightValue()
	) | rpl::on_next([=] {
		layoutButtons();
	}, raw->lifetime());
}

void Panel::layoutButtons() {
	if (!_webviewBottom) {
		return;
	}
	const auto inner = _widget->innerGeometry();
	const auto shown = [](std::unique_ptr<Button> &button) {
		return button && !button->isHidden();
	};
	const auto any = shown(_mainButton) || shown(_secondaryButton);
	_webviewBottom->setVisible(!any
		&& !_fullscreen.current()
		&& !_layerShown);
	if (any) {
		_bottomButtonsBg->setVisible(!_layerShown);

		const auto one = shown(_mainButton)
			? _mainButton.get()
			: _secondaryButton.get();
		const auto both = shown(_mainButton) && shown(_secondaryButton);
		const auto vertical = both
			&& ((_secondaryPosition == RectPart::Top)
				|| (_secondaryPosition == RectPart::Bottom));
		const auto padding = st::botWebViewBottomPadding;
		const auto height = padding.top()
			+ (vertical
				? (_mainButton->height()
					+ st::botWebViewBottomSkip.y()
					+ _secondaryButton->height())
				: one->height())
			+ padding.bottom();
		_bottomButtonsBg->setGeometry(
			inner.x(),
			inner.y() + inner.height() - height,
			inner.width(),
			height);
		auto left = padding.left();
		auto bottom = height - padding.bottom();
		auto available = inner.width() - padding.left() - padding.right();
		if (!both) {
			one->resizeToWidth(available);
			one->move(left, bottom - one->height());
		} else if (_secondaryPosition == RectPart::Top) {
			_mainButton->resizeToWidth(available);
			bottom -= _mainButton->height();
			_mainButton->move(left, bottom);
			bottom -= st::botWebViewBottomSkip.y();
			_secondaryButton->resizeToWidth(available);
			bottom -= _secondaryButton->height();
			_secondaryButton->move(left, bottom);
		} else if (_secondaryPosition == RectPart::Bottom) {
			_secondaryButton->resizeToWidth(available);
			bottom -= _secondaryButton->height();
			_secondaryButton->move(left, bottom);
			bottom -= st::botWebViewBottomSkip.y();
			_mainButton->resizeToWidth(available);
			bottom -= _mainButton->height();
			_mainButton->move(left, bottom);
		} else if (_secondaryPosition == RectPart::Left) {
			available = (available - st::botWebViewBottomSkip.x()) / 2;
			_secondaryButton->resizeToWidth(available);
			bottom -= _secondaryButton->height();
			_secondaryButton->move(left, bottom);
			_mainButton->resizeToWidth(available);
			_mainButton->move(
				inner.width() - padding.right() - available,
				bottom);
		} else {
			available = (available - st::botWebViewBottomSkip.x()) / 2;
			_mainButton->resizeToWidth(available);
			bottom -= _mainButton->height();
			_mainButton->move(left, bottom);
			_secondaryButton->resizeToWidth(available);
			_secondaryButton->move(
				inner.width() - padding.right() - available,
				bottom);
		}
	} else if (_bottomButtonsBg) {
		_bottomButtonsBg->hide();
	}
	const auto footer = _layerShown
		? 0
		: any
		? _bottomButtonsBg->height()
		: _fullscreen.current()
		? 0
		: _webviewBottom->height();
	_widget->setBottomBarHeight((!_layerShown && any) ? footer : 0);
	_footerHeight = footer;
}

void Panel::showBox(object_ptr<BoxContent> box) {
	showBox(std::move(box), LayerOption::KeepOther, anim::type::normal);
}

void Panel::showBox(
		object_ptr<BoxContent> box,
		LayerOptions options,
		anim::type animated) {
	if (_externalShell) {
		const auto anchor = externalShellAnchor();
		_externalLayer->setAnchor(
			anchor.anchorGeometry,
			anchor.outerSize,
			anchor.transientParent);
		_externalLayer->showBox(std::move(box), options, animated);
		return;
	}
	if (const auto widget = _webview ? _webview->window.widget() : nullptr) {
		_layerShown = true;
		const auto hideNow = !widget->isHidden();
		const auto raw = box.data();
		_webview->boxes.push_back(raw);
		raw->boxClosing(
		) | rpl::filter([=] {
			return _webview != nullptr;
		}) | rpl::on_next([=] {
			auto &list = _webview->boxes;
			list.erase(ranges::remove_if(list, [&](QPointer<RpWidget> b) {
				return !b || (b == raw);
			}), end(list));
			if (list.empty()) {
				_webview->boxesLifetime.destroy();
				_layerShown = false;
				const auto widget = _webview
					? _webview->window.widget()
					: nullptr;
				if (widget && widget->isHidden()) {
					widget->show();
					layoutButtons();
				}
			}
		}, _webview->boxesLifetime);

		if (hideNow) {
			widget->hide();
			layoutButtons();
		}
	}
	const auto raw = box.data();

	InvokeQueued(raw, [=] {
		if (raw->window()->isActiveWindow()) {
			// In case focus is somewhat in a native child window,
			// like a webview, Qt glitches here with input fields showing
			// focused state, but not receiving any keyboard input:
			//
			// window()->windowHandle()->isActive() == false.
			//
			// Steps were: SeparatePanel with a WebView2 child,
			// some interaction with mouse inside the WebView2,
			// so that WebView2 gets focus and active window state,
			// then we call setSearchAllowed() and after animation
			// is finished try typing -> nothing happens.
			//
			// With this workaround it works fine.
			_widget->activateWindow();
		}
	});

	_widget->showBox(
		std::move(box),
		LayerOption::KeepOther,
		anim::type::normal);
}

void Panel::showToast(TextWithEntities &&text) {
	_widget->showToast(std::move(text));
}

not_null<QWidget*> Panel::toastParent() const {
	return _widget->uiShow()->toastParent();
}

void Panel::hideLayer(anim::type animated) {
	if (_externalShell && _externalLayer) {
		_externalLayer->hideLayers(animated);
		return;
	}
	_widget->hideLayer(animated);
}

void Panel::showCriticalError(const TextWithEntities &text) {
	_progress = nullptr;
	_webviewProgress = false;
	auto wrap = base::make_unique_q<RpWidget>(_widget.get());
	const auto raw = wrap.get();

	const auto error = CreateChild<PaddingWrap<FlatLabel>>(
		raw,
		object_ptr<FlatLabel>(
			raw,
			rpl::single(text),
			st::paymentsCriticalError),
		st::paymentsCriticalErrorPadding);
	error->entity()->setClickHandlerFilter([=](
			const ClickHandlerPtr &handler,
			Qt::MouseButton) {
		const auto entity = handler->getTextEntity();
		if (entity.type != EntityType::CustomUrl) {
			return true;
		}
		File::OpenUrl(entity.data);
		return false;
	});

	raw->widthValue() | rpl::on_next([=](int width) {
		error->resizeToWidth(width);
		raw->resize(width, error->height());
	}, raw->lifetime());

	_widget->showInner(std::move(wrap));
}

void Panel::updateThemeParams(const Webview::ThemeParams &params) {
	updateColorOverrides(params);
	if (!_webview || !_webview->window.widget()) {
		return;
	}
	if (_externalShell) {
		_webview->window.updateTheme(
			params.bodyBg,
			params.scrollBg,
			params.scrollBgOver,
			params.scrollBarBg,
			params.scrollBarBgOver);
		sendExternalShellColors(params);
		sendExternalShellAssets();
		postEvent("theme_changed", ThemeChangedPayload(params));
		return;
	}
	_webview->window.updateTheme(
		params.bodyBg,
		params.scrollBg,
		params.scrollBgOver,
		params.scrollBarBg,
		params.scrollBarBgOver);
	postEvent("theme_changed", "{\"theme_params\": " + params.json + "}");
}

void Panel::updateColorOverrides(const Webview::ThemeParams &params) {
	if (!_headerColorReceived && params.titleBg.alpha() == 255) {
		if (_externalShell) {
			sendExternalShellColors(params);
		} else {
			_widget->overrideTitleColor(params.titleBg);
		}
	}
	if (!_bodyColorReceived && params.bodyBg.alpha() == 255) {
		overrideBodyColor(params.bodyBg);
	}
}

void Panel::invoiceClosed(const QString &slug, const QString &status) {
	if (!_webview || !_webview->window.widget()) {
		return;
	}
	postEvent("invoice_closed", QJsonObject{
		{ u"slug"_q, slug },
		{ u"status"_q, status },
	});
	if (_hiddenForPayment) {
		_hiddenForPayment = false;
		if (_externalShell) {
			setExternalShellBlocked(false);
		} else {
			_widget->showAndActivate();
		}
	}
}

void Panel::hideForPayment() {
	_hiddenForPayment = true;
	if (_externalShell) {
		setExternalShellBlocked(true);
	} else {
		_widget->hideGetDuration();
	}
}

void Panel::postEvent(const QString &event) {
	postEvent(event, {});
}

void Panel::postEvent(const QString &event, EventData data) {
	if (!_webview) {
		LOG(("BotWebView Error: Post event \"%1\" on crashed webview."
			).arg(event));
		return;
	}
	if (_externalShell) {
		if (v::is<QJsonObject>(data)) {
			sendExternalShellEvent(event, v::get<QJsonObject>(data));
		} else if (v::get<QString>(data).isEmpty()) {
			sendExternalShellEvent(event, {});
		} else {
			LOG(("BotWebView Error: Drop raw external event \"%1\"."
				).arg(event));
		}
		return;
	}
	if (_sameOrigin && !OriginsMatch(_currentOrigin, _initialOrigin)) {
		return;
	}
	auto written = v::is<QString>(data)
		? v::get<QString>(data).toUtf8()
		: QJsonDocument(
			v::get<QJsonObject>(data)).toJson(QJsonDocument::Compact);
	_webview->window.eval(R"(
if (window.TelegramGameProxy) {
window.TelegramGameProxy.receiveEvent(
		")"
		+ event.toUtf8()
		+ '"' + (written.isEmpty() ? QByteArray() : ", " + written)
		+ R"();
}
)");
}

TextWithEntities ErrorText(const Webview::Available &info) {
	Expects(info.error != Webview::Available::Error::None);

	using Error = Webview::Available::Error;
	switch (info.error) {
	case Error::NoWebview2:
		return tr::lng_payments_webview_install_edge(
			tr::now,
			lt_link,
			Text::Link(
				"Microsoft Edge WebView2 Runtime",
				"https://go.microsoft.com/fwlink/p/?LinkId=2124703"),
			tr::marked);
	case Error::NoWebKitGTK:
		return { tr::lng_payments_webview_install_webkit(tr::now) };
	case Error::OldWindows:
		return { tr::lng_payments_webview_update_windows(tr::now) };
	default:
		return { QString::fromStdString(info.details) };
	}
}

void Panel::showWebviewError(
		const QString &text,
		const Webview::Available &information) {
	showCriticalError(WebviewErrorText(text, information));
}

rpl::lifetime &Panel::lifetime() {
	return _widget->lifetime();
}

std::unique_ptr<Panel> Show(Args &&args) {
	return std::make_unique<Panel>(std::move(args));
}

} // namespace Ui::BotWebView
