/*
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 "core/click_handler_types.h"

#include "base/unixtime.h"
#include "lang/lang_keys.h"
#include "chat_helpers/bot_command.h"
#include "core/application.h"
#include "core/local_url_handlers.h"
#include "core/file_utilities.h"
#include "mainwidget.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/toast/toast.h"
#include "ui/widgets/popup_menu.h"
#include "base/qthelp_regex.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/random.h"
#include "storage/storage_account.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "history/history_item.h"
#include "inline_bots/bot_attach_web_view.h"
#include "data/data_game.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h"
#include "apiwrap.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/history_view_scheduled_section.h"
#include "menu/menu_send.h"
#include "data/data_types.h"
#include "styles/style_calls.h" // groupCallBoxLabel
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"

#include <QtCore/QDateTime>
#include <QtCore/QLocale>

namespace {

constexpr auto kReminderSetToastDuration = 4 * crl::time(1000);

[[nodiscard]] TextWithEntities BoldDomainInUrl(const QString &url) {
	auto result = TextWithEntities{ .text = url };

	if (const auto parsedUrl = QUrl(url); parsedUrl.isValid()) {
		if (const auto host = parsedUrl.host(); !host.isEmpty()) {
			if (const auto hostPos = url.indexOf(host); hostPos != -1) {
				auto boldEntity = EntityInText(
					EntityType::Bold,
					hostPos,
					host.length());

				if (host.startsWith("www.")) {
					boldEntity = EntityInText(
						EntityType::Bold,
						hostPos + 4,
						host.length() - 4);
				}

				result.entities.push_back(boldEntity);
			}
		}
	}
	return result;
}

[[nodiscard]] bool IsTelegramShortLinkHost(const QUrl &url) {
	using namespace qthelp;

	return regex_match(
		"(^|\\.)(telegram\\.(me|dog)|t\\.me)$",
		url.host(),
		RegExOption::CaseInsensitive).valid();
}

[[nodiscard]] bool HiddenUrlRequiresConfirmation(const QUrl &url) {
	return UrlRequiresConfirmation(url) || IsTelegramShortLinkHost(url);
}

[[nodiscard]] bool RequiresConfirmationAfterIvFallback(const QUrl &url) {
	const auto host = url.host().toLower();
	return (host == u"telegra.ph"_q) || (host == u"te.legra.ph"_q);
}

// Possible context owners: media viewer, profile, history widget.

void SearchByHashtag(ClickContext context, const QString &tag) {
	const auto my = context.other.value<ClickHandlerContext>();
	if (const auto delegate = my.elementDelegate
		? my.elementDelegate()
		: nullptr) {
		delegate->elementSearchInList(tag, my.itemId);
		return;
	}
	const auto controller = my.sessionWindow.get();
	if (!controller) {
		return;
	}
	if (controller->openedFolder().current()) {
		controller->closeFolder();
	}

	controller->widget()->ui_hideSettingsAndLayer(anim::type::normal);
	Core::App().hideMediaView();

	auto &data = controller->session().data();
	const auto inPeer = my.peer
		? my.peer
		: my.itemId
		? data.message(my.itemId)->history()->peer.get()
		: nullptr;
	controller->content()->searchMessages(
		tag + ' ',
		(inPeer && !inPeer->isUser())
			? data.history(inPeer).get()
			: Dialogs::Key());
}

void ExportToCalendar(TimeId date, const QString &messageText) {
	const auto start = QDateTime::fromSecsSinceEpoch(
		date,
		Qt::UTC);
	const auto end = QDateTime::fromSecsSinceEpoch(
		date + 3600,
		Qt::UTC);
	const auto now = QDateTime::currentDateTimeUtc();
	const auto format = u"yyyyMMdd'T'HHmmss'Z'"_q;
	const auto locale = QLocale();
	const auto raw = locale.toString(
		base::unixtime::parse(date),
		QLocale::LongFormat);
	auto summary = raw;
	summary.replace('\\', u"\\\\"_q);
	summary.replace(';', u"\\;"_q);
	summary.replace(',', u"\\,"_q);
	summary.replace('\n', u"\\n"_q);
	auto description = messageText;
	description.replace('\\', u"\\\\"_q);
	description.replace(';', u"\\;"_q);
	description.replace(',', u"\\,"_q);
	description.replace('\n', u"\\n"_q);
	const auto uid = base::RandomValue<uint64>();
	const auto content = u"BEGIN:VCALENDAR\r\n"
		"VERSION:2.0\r\n"
		"PRODID:-//Telegram Desktop//EN\r\n"
		"BEGIN:VEVENT\r\n"
		"DTSTART:%1\r\n"
		"DTEND:%2\r\n"
		"DTSTAMP:%3\r\n"
		"UID:telegram-%4-%7@telegram.org\r\n"
		"SUMMARY:%5\r\n"
		"DESCRIPTION:%6\r\n"
		"END:VEVENT\r\n"
		"END:VCALENDAR\r\n"_q
			.arg(start.toString(format))
			.arg(end.toString(format))
			.arg(now.toString(format))
			.arg(date)
			.arg(summary)
			.arg(description)
			.arg(uid, 0, 16);
	const auto dir = cWorkingDir() + u"tdata/temp"_q;
	QDir().mkpath(dir);
	const auto path = u"%1/event_%2.ics"_q
		.arg(dir)
		.arg(date);
	auto file = QFile(path);
	if (file.open(QIODevice::WriteOnly)) {
		file.write(content.toUtf8());
		file.close();
		File::Launch(path);
	}
}

void DoneSetReminder(std::shared_ptr<ChatHelpers::Show> show) {
	if (!show->valid()) {
		return;
	}
	const auto text = tr::lng_reminder_scheduled_in(
		tr::now,
		lt_link,
		tr::link(tr::bold(tr::lng_saved_messages(tr::now))),
		tr::marked);
	const auto session = &show->session();
	const auto filter = [=](
			const ClickHandlerPtr &,
			Qt::MouseButton) {
		if (const auto controller = show->resolveWindow()) {
			controller->showSection(
				std::make_shared<HistoryView::ScheduledMemento>(
					session->data().history(session->user())));
		}
		return false;
	};
	show->showToast({
		.text = text,
		.filter = filter,
		.iconLottie = u"toast/saved_messages"_q,
		.iconPadding = st::selfForwardsTaggerIconPadding,
		.st = &st::selfForwardsTaggerToast,
		.attach = RectPart::Top,
		.duration = kReminderSetToastDuration,
	});
};

} // namespace

bool UrlRequiresConfirmation(const QUrl &url) {
	using namespace qthelp;

	return !regex_match(
		"(^|\\.)("
		"telegram\\.(org|me|dog)"
		"|t\\.me"
		"|te\\.?legra\\.ph"
		"|graph\\.org"
		"|fragment\\.com"
		"|telesco\\.pe"
		")$",
		url.host(),
		RegExOption::CaseInsensitive);
}

QString HiddenUrlClickHandler::copyToClipboardText() const {
	const auto original = originalUrl();
	const auto originalExternal = UrlClickHandler::ExternalUrlFromInternalUrl(
		original);
	if (!originalExternal.isEmpty()) {
		return originalExternal;
	}
	const auto value = url();
	const auto external = UrlClickHandler::ExternalUrlFromInternalUrl(value);
	return external.isEmpty() ? value : external;
}

QString HiddenUrlClickHandler::copyToClipboardContextItemText() const {
	const auto original = originalUrl();
	const auto originalExternal = UrlClickHandler::ExternalUrlFromInternalUrl(
		original);
	const auto value = originalExternal.isEmpty() ? url() : original;
	const auto external = originalExternal.isEmpty()
		? UrlClickHandler::ExternalUrlFromInternalUrl(value)
		: originalExternal;
	return value.isEmpty()
		? QString()
		: !value.startsWith(u"internal:"_q)
		? UrlClickHandler::copyToClipboardContextItemText()
		: !external.isEmpty()
		? UrlClickHandler::copyToClipboardContextItemText()
		: QString();
}

QString HiddenUrlClickHandler::dragText() const {
	const auto result = HiddenUrlClickHandler::copyToClipboardText();
	return result.startsWith(u"internal:"_q) ? QString() : result;
}

void HiddenUrlClickHandler::Open(QString url, QVariant context) {
	if (const auto external = UrlClickHandler::ExternalUrlFromInternalUrl(url);
			!external.isEmpty()) {
		url = external;
	}
	url = Core::TryConvertUrlToLocal(url);
	if (Core::InternalPassportOrOAuthLink(url)) {
		return;
	}

	if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)
		|| url.startsWith(u"internal:"_q, Qt::CaseInsensitive)) {
		UrlClickHandler::Open(url, QVariant::fromValue([&] {
			auto result = context.value<ClickHandlerContext>();
			result.mayShowConfirmation = !base::IsCtrlPressed();
			return result;
		}()));
	} else {
		const auto parsedUrl = url.startsWith(u"tonsite://"_q)
			? QUrl(url)
			: QUrl::fromUserInput(url);
		auto my = context.value<ClickHandlerContext>();
		auto openContext = context;
		const auto forceConfirmation = my.forceExternalUrlConfirmation
			&& my.ignoreIv;
		const auto skipConfirmation = base::IsCtrlPressed();
		if (forceConfirmation) {
			my.forceExternalUrlConfirmation = false;
			openContext = QVariant::fromValue(my);
		}
		const auto confirmAfterIvFallback
			= RequiresConfirmationAfterIvFallback(parsedUrl)
			&& !my.ignoreIv
			&& !skipConfirmation;
		const auto canTryIv = (my.sessionWindow.get() != nullptr);
		if (confirmAfterIvFallback && canTryIv) {
			my.forceExternalUrlConfirmation = true;
			openContext = QVariant::fromValue(my);
		}
		const auto open = [=] {
			UrlClickHandler::Open(url, openContext);
		};
		if (forceConfirmation
			|| (confirmAfterIvFallback && !canTryIv)
			|| (HiddenUrlRequiresConfirmation(parsedUrl)
				&& !skipConfirmation)) {
			if (!my.show) {
				Core::App().hideMediaView();
			}
			const auto displayed = parsedUrl.isValid()
				? parsedUrl.toDisplayString()
				: url;
			const auto displayUrl = !IsSuspicious(displayed)
				? displayed
				: parsedUrl.isValid()
				? QString::fromUtf8(parsedUrl.toEncoded())
				: ShowEncoded(displayed);
			const auto controller = my.sessionWindow.get();
			const auto use = controller
				? &controller->window()
				: Core::App().activeWindow();
			auto box = Box([=](not_null<Ui::GenericBox*> box) {
				Ui::ConfirmBox(box, {
					.text = (tr::lng_open_this_link(tr::now)),
					.confirmed = [=](Fn<void()> hide) { hide(); open(); },
					.confirmText = tr::lng_open_link(),
					.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,
				});
				const auto &st = my.dark
					? st::groupCallBoxLabel
					: st::boxLabel;
				box->addSkip(st.style.lineHeight - st::boxPadding.bottom());
				const auto url = box->addRow(
					object_ptr<Ui::FlatLabel>(
						box,
						rpl::single(BoldDomainInUrl(displayUrl)),
						st));
				url->setContextMenuHook([=](
						Ui::FlatLabel::ContextMenuRequest request) {
					const auto copyContextText = [=] {
						TextUtilities::SetClipboardText(
							TextForMimeData::Simple(displayUrl));
					};
					if (request.fullSelection) {
						request.menu->addAction(
							tr::lng_context_copy_link(tr::now),
							copyContextText);
					} else if (request.uponSelection
						&& !request.fullSelection) {
						const auto selection = request.selection;
						const auto copySelectedText = [=] {
							TextUtilities::SetClipboardText(
								TextForMimeData::Simple(
									displayUrl.mid(
										selection.from,
										selection.to - selection.from)));
						};
						request.menu->addAction(
							tr::lng_context_copy_selected(tr::now),
							copySelectedText);
					} else if (request.selection.empty()) {
						request.menu->addAction(
							tr::lng_context_copy_link(tr::now),
							copyContextText);
					}
				});
				url->setSelectable(true);
				url->setContextCopyText(tr::lng_context_copy_link(tr::now));
			});
			if (my.show) {
				my.show->showBox(std::move(box));
			} else if (use) {
				use->show(std::move(box));
				use->activate();
			}
		} else {
			open();
		}
	}
}

void BotGameUrlClickHandler::onClick(ClickContext context) const {
	const auto url = Core::TryConvertUrlToLocal(this->url());
	if (Core::InternalPassportOrOAuthLink(url)) {
		return;
	}
	const auto openLink = [=] {
		UrlClickHandler::Open(url, context.other);
	};
	const auto my = context.other.value<ClickHandlerContext>();
	const auto weakController = my.sessionWindow;
	const auto controller = weakController.get();
	const auto item = controller
		? controller->session().data().message(my.itemId)
		: nullptr;
	const auto media = item ? item->media() : nullptr;
	const auto game = media ? media->game() : nullptr;
	if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive) || !_bot || !game) {
		openLink();
		return;
	}
	const auto bot = _bot;
	const auto title = game->title;
	const auto itemId = my.itemId;
	const auto openGame = [=] {
		bot->session().attachWebView().open({
			.bot = bot,
			.context = { .controller = weakController },
			.button = {.url = url.toUtf8() },
			.source = InlineBots::WebViewSourceGame{
				.messageId = itemId,
				.title = title,
			},
		});
	};
	if (_bot->isVerified()
		|| _bot->session().local().isPeerTrustedOpenGame(_bot->id)) {
		openGame();
	} else {
		if (const auto controller = my.sessionWindow.get()) {
			const auto callback = [=, bot = _bot](Fn<void()> close) {
				close();
				bot->session().local().markPeerTrustedOpenGame(bot->id);
				openGame();
			};
			controller->show(Ui::MakeConfirmBox({
				.text = tr::lng_allow_bot_pass(
					tr::now,
					lt_bot_name,
					_bot->name()),
				.confirmed = callback,
				.confirmText = tr::lng_allow_bot(),
				.labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr,
			}));
		}
	}
}

auto HiddenUrlClickHandler::getTextEntity() const -> TextEntity {
	const auto original = originalUrl();
	return {
		EntityType::CustomUrl,
		UrlClickHandler::ExternalUrlFromInternalUrl(original).isEmpty()
			? url()
			: original
	};
}

QString MentionClickHandler::copyToClipboardContextItemText() const {
	return tr::lng_context_copy_mention(tr::now);
}

void MentionClickHandler::onClick(ClickContext context) const {
	const auto button = context.button;
	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
		const auto my = context.other.value<ClickHandlerContext>();
		const auto controller = my.sessionWindow.get();
		const auto use = controller
			? controller
			: Core::App().activeWindow()
			? Core::App().activeWindow()->sessionController()
			: nullptr;
		if (use) {
			use->showPeerByLink(Window::PeerByLinkInfo{
				.usernameOrId = _tag.mid(1),
				.resolveType = Window::ResolveType::Mention,
			});
		}
	}
}

auto MentionClickHandler::getTextEntity() const -> TextEntity {
	return { EntityType::Mention };
}

void MentionNameClickHandler::onClick(ClickContext context) const {
	const auto button = context.button;
	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
		const auto my = context.other.value<ClickHandlerContext>();
		if (const auto controller = my.sessionWindow.get()) {
			if (auto user = _session->data().userLoaded(_userId)) {
				controller->showPeerInfo(user);
			}
		}
	}
}

auto MentionNameClickHandler::getTextEntity() const -> TextEntity {
	const auto data = TextUtilities::MentionNameDataFromFields({
		.selfId = _session->userId().bare,
		.userId = _userId.bare,
		.accessHash = _accessHash,
	});
	return { EntityType::MentionName, data };
}

QString MentionNameClickHandler::tooltip() const {
	if (const auto user = _session->data().userLoaded(_userId)) {
		const auto name = user->name();
		if (name != _text) {
			return name;
		}
	}
	return QString();
}

QString HashtagClickHandler::copyToClipboardContextItemText() const {
	return tr::lng_context_copy_hashtag(tr::now);
}

void HashtagClickHandler::onClick(ClickContext context) const {
	const auto button = context.button;
	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
		SearchByHashtag(context, _tag);
	}
}

auto HashtagClickHandler::getTextEntity() const -> TextEntity {
	return { EntityType::Hashtag };
}

QString CashtagClickHandler::copyToClipboardContextItemText() const {
	return tr::lng_context_copy_hashtag(tr::now);
}

void CashtagClickHandler::onClick(ClickContext context) const {
	const auto button = context.button;
	if (button == Qt::LeftButton || button == Qt::MiddleButton) {
		SearchByHashtag(context, _tag);
	}
}

auto CashtagClickHandler::getTextEntity() const -> TextEntity {
	return { EntityType::Cashtag };
}

void BotCommandClickHandler::onClick(ClickContext context) const {
	const auto button = context.button;
	if (button != Qt::LeftButton && button != Qt::MiddleButton) {
		return;
	}
	const auto my = context.other.value<ClickHandlerContext>();
	if (const auto delegate = my.elementDelegate
		? my.elementDelegate()
		: nullptr) {
		delegate->elementSendBotCommand(_cmd, my.itemId);
	} else if (const auto controller = my.sessionWindow.get()) {
		auto &data = controller->session().data();
		const auto peer = my.peer
			? my.peer
			: my.itemId
			? data.message(my.itemId)->history()->peer.get()
			: nullptr;
		// Can't find context.
		if (!peer) {
			return;
		}
		controller->widget()->ui_hideSettingsAndLayer(anim::type::normal);
		Core::App().hideMediaView();
		controller->content()->sendBotCommand({
			.peer = peer,
			.command = _cmd,
			.context = my.itemId,
		});
	}
}

auto BotCommandClickHandler::getTextEntity() const -> TextEntity {
	return { EntityType::BotCommand };
}

MonospaceClickHandler::MonospaceClickHandler(
	const QString &text,
	EntityType type)
: _text(text)
, _entity({ type }) {
}

void MonospaceClickHandler::onClick(ClickContext context) const {
	const auto button = context.button;
	if (button != Qt::LeftButton && button != Qt::MiddleButton) {
		return;
	}
	const auto my = context.other.value<ClickHandlerContext>();
	if (const auto controller = my.sessionWindow.get()) {
		controller->showToast(tr::lng_text_copied(tr::now));
	}
	TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed()));
}

auto MonospaceClickHandler::getTextEntity() const -> TextEntity {
	return _entity;
}

QString MonospaceClickHandler::url() const {
	return _text;
}

FormattedDateClickHandler::FormattedDateClickHandler(
	int32 date,
	FormattedDateFlags flags)
: _date(date)
, _entityData(SerializeFormattedDateData(date, flags)) {
}

void FormattedDateClickHandler::onClick(ClickContext context) const {
	if (context.button != Qt::LeftButton) {
		return;
	}
	const auto my = context.other.value<ClickHandlerContext>();
	const auto controller = my.sessionWindow.get();
	if (!controller) {
		return;
	}
	const auto menu = Ui::CreateChild<Ui::PopupMenu>(
		controller->content(),
		st::popupMenuWithIcons);

	const auto date = _date;
	const auto show = controller->uiShow();

	menu->addAction(
		tr::lng_context_copy_date(tr::now),
		[date, show] {
			const auto text = QLocale().toString(
				base::unixtime::parse(date),
				QLocale::LongFormat);
			TextUtilities::SetClipboardText(TextForMimeData::Simple(text));
			show->showToast(tr::lng_date_copied(tr::now));
		},
		&st::menuIconCopy);

	const auto itemId = my.itemId;
	const auto &owner = controller->session().data();
	const auto item = owner.message(itemId);

	const auto messageText = item ? item->originalText().text : QString();
	menu->addAction(
		tr::lng_context_add_to_calendar(tr::now),
		[date, messageText] { ExportToCalendar(date, messageText); },
		&st::menuIconSchedule);

	const auto canForward = item
		&& !item->forbidsForward()
		&& item->history()->peer->allowsForwarding();
	if (canForward) {
		menu->addAction(
			tr::lng_context_set_reminder(tr::now),
			[date, itemId, show] {
				const auto session = &show->session();
				const auto item = session->data().message(itemId);
				if (!item) {
					return;
				}
				const auto self = session->user();
				const auto history = self->owner().history(self);
				const auto now = base::unixtime::now();
				const auto scheduleTime = (date > now + 60)
					? date
					: HistoryView::DefaultScheduleTime();
				show->showBox(HistoryView::PrepareScheduleBox(
					session,
					show,
					SendMenu::Details{ .type = SendMenu::Type::Reminder },
					[=](Api::SendOptions options) {
						auto action = Api::SendAction(history, options);
						action.clearDraft = false;
						action.generateLocal = false;
						session->api().forwardMessages(
							Data::ResolvedForwardDraft{
								.items = { item },
							},
							action,
							[=] { DoneSetReminder(show); });
					},
					Api::SendOptions(),
					scheduleTime));
			},
			&st::menuIconNotifications);
	}

	menu->popup(QCursor::pos());
}

auto FormattedDateClickHandler::getTextEntity() const -> TextEntity {
	return { EntityType::FormattedDate, _entityData };
}

QString FormattedDateClickHandler::tooltip() const {
	return QLocale().toString(
		base::unixtime::parse(_date),
		QLocale::LongFormat);
}
