/*
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 "platform/mac/notifications_manager_mac.h"

#include "core/application.h"
#include "core/core_settings.h"
#include "base/platform/base_platform_info.h"
#include "platform/platform_specific.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/random.h"
#include "data/data_forum_topic.h"
#include "history/history.h"
#include "history/history_item.h"
#include "ui/empty_userpic.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "window/notifications_utilities.h"
#include "styles/style_window.h"

#include <thread>
#include <Cocoa/Cocoa.h>

namespace {

constexpr auto kQuerySettingsEachMs = crl::time(1000);

crl::time LastSettingsQueryMs/* = 0*/;
bool DoNotDisturbEnabled/* = false*/;

[[nodiscard]] bool ShouldQuerySettings() {
	const auto now = crl::now();
	if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
		return false;
	}
	LastSettingsQueryMs = now;
	return true;
}

[[nodiscard]] QString LibraryPath() {
	static const auto result = [] {
		NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
		return url
			? QString::fromUtf8([[url path] fileSystemRepresentation])
			: QString();
	}();
	return result;
}

void queryDoNotDisturbState() {
	if (!ShouldQuerySettings()) {
		return;
	}
	Boolean isKeyValid;
	const auto doNotDisturb = CFPreferencesGetAppBooleanValue(
		CFSTR("doNotDisturb"),
		CFSTR("com.apple.notificationcenterui"),
		&isKeyValid);
	DoNotDisturbEnabled = isKeyValid
		? doNotDisturb
		: false;
}

using Manager = Platform::Notifications::Manager;

} // namespace

@interface NotificationDelegate : NSObject<NSUserNotificationCenterDelegate> {
}

- (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId;
- (void) userNotificationCenter:(NSUserNotificationCenter*)center didActivateNotification:(NSUserNotification*)notification;
- (BOOL) userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification;

@end // @interface NotificationDelegate

@implementation NotificationDelegate {
	base::weak_ptr<Manager> _manager;
	uint64 _managerId;

}

- (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId {
	if (self = [super init]) {
		_manager = manager;
		_managerId = managerId;
	}
	return self;
}

- (void) userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
	NSDictionary *notificationUserInfo = [notification userInfo];
	NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
	auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
	DEBUG_LOG(("Received notification with instance %1, mine: %2").arg(notificationManagerId).arg(_managerId));
	if (notificationManagerId != _managerId) { // other app instance notification
		crl::on_main([] {
			// Usually we show and activate main window when the application
			// is activated (receives applicationDidBecomeActive: notification).
			//
			// This is used for window show in Cmd+Tab switching to the application.
			//
			// But when a notification arrives sometimes macOS still activates the app
			// and we receive applicationDidBecomeActive: notification even if the
			// notification was sent by another instance of the application. In that case
			// we set a flag for a couple of seconds to ignore this app activation.
			objc_ignoreApplicationActivationRightNow();
		});
		return;
	}

	NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
	const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
	if (!notificationSessionId) {
		LOG(("App Error: A notification with unknown session was received"));
		return;
	}
	NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
	const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0ULL;
	if (!notificationPeerId) {
		LOG(("App Error: A notification with unknown peer was received"));
		return;
	}
	NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
	if (!topicObject) {
		LOG(("App Error: A notification with unknown topic was received"));
		return;
	}
	const auto notificationTopicRootId = [topicObject longLongValue];

	NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
	const auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL;

	const auto my = Window::Notifications::Manager::NotificationId{
		.contextId = Manager::ContextId{
			.sessionId = notificationSessionId,
			.peerId = PeerId(notificationPeerId),
			.topicRootId = MsgId(notificationTopicRootId),
		},
		.msgId = notificationMsgId,
	};
	if (notification.activationType == NSUserNotificationActivationTypeReplied) {
		const auto notificationReply = QString::fromUtf8([[[notification response] string] UTF8String]);
		const auto manager = _manager;
		crl::on_main(manager, [=] {
			manager->notificationReplied(my, { notificationReply, {} });
		});
	} else if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) {
		const auto manager = _manager;
		crl::on_main(manager, [=] {
			manager->notificationActivated(my);
		});
	}

	[center removeDeliveredNotification: notification];
}

- (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
	return YES;
}

@end // @implementation NotificationDelegate

namespace Platform {
namespace Notifications {

bool SkipToastForCustom() {
	return false;
}

void MaybePlaySoundForCustom(Fn<void()> playSound) {
	playSound();
}

void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
	flashBounce();
}

bool WaitForInputForCustom() {
	return true;
}

bool Supported() {
	return true;
}

bool Enforced() {
	return Supported();
}

bool ByDefault() {
	return Supported();
}

void Create(Window::Notifications::System *system) {
	if (Supported()) {
		system->setManager(std::make_unique<Manager>(system));
	} else {
		system->setManager(nullptr);
	}
}

class Manager::Private : public QObject {
public:
	Private(Manager *manager);

	void showNotification(
		not_null<PeerData*> peer,
		MsgId topicRootId,
		Ui::PeerUserpicView &userpicView,
		MsgId msgId,
		const QString &title,
		const QString &subtitle,
		const QString &msg,
		DisplayOptions options);
	void clearAll();
	void clearFromItem(not_null<HistoryItem*> item);
	void clearFromTopic(not_null<Data::ForumTopic*> topic);
	void clearFromHistory(not_null<History*> history);
	void clearFromSession(not_null<Main::Session*> session);
	void updateDelegate();

	void invokeIfNotFocused(Fn<void()> callback);

	~Private();

private:
	template <typename Task>
	void putClearTask(Task task);

	void clearingThreadLoop();
	void checkFocusState();

	const uint64 _managerId = 0;
	QString _managerIdString;

	NotificationDelegate *_delegate = nullptr;

	std::thread _clearingThread;
	std::mutex _clearingMutex;
	std::condition_variable _clearingCondition;

	struct ClearFromItem {
		NotificationId id;
	};
	struct ClearFromTopic {
		ContextId contextId;
	};
	struct ClearFromHistory {
		ContextId partialContextId;
	};
	struct ClearFromSession {
		uint64 sessionId = 0;
	};
	struct ClearAll {
	};
	struct ClearFinish {
	};
	using ClearTask = std::variant<
		ClearFromItem,
		ClearFromTopic,
		ClearFromHistory,
		ClearFromSession,
		ClearAll,
		ClearFinish>;
	std::vector<ClearTask> _clearingTasks;

	QProcess _dnd;
	QProcess _focus;
	std::vector<Fn<void()>> _focusedCallbacks;
	bool _waitingDnd = false;
	bool _waitingFocus = false;
	bool _focused = false;
	bool _processesInited = false;

	rpl::lifetime _lifetime;

};

Manager::Private::Private(Manager *manager)
: _managerId(base::RandomValue<uint64>())
, _managerIdString(QString::number(_managerId))
, _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId]) {
	Core::App().settings().workModeValue(
	) | rpl::start_with_next([=](Core::Settings::WorkMode mode) {
		// We need to update the delegate _after_ the tray icon change was done in Qt.
		// Because Qt resets the delegate.
		crl::on_main(this, [=] {
			updateDelegate();
		});
	}, _lifetime);
}

void Manager::Private::showNotification(
		not_null<PeerData*> peer,
		MsgId topicRootId,
		Ui::PeerUserpicView &userpicView,
		MsgId msgId,
		const QString &title,
		const QString &subtitle,
		const QString &msg,
		DisplayOptions options) {
	@autoreleasepool {

	NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];
	if ([notification respondsToSelector:@selector(setIdentifier:)]) {
		auto identifier = _managerIdString
			+ '_'
			+ QString::number(peer->id.value)
			+ '_'
			+ QString::number(msgId.bare);
		auto identifierValue = Q2NSString(identifier);
		[notification setIdentifier:identifierValue];
	}
	[notification setUserInfo:
		[NSDictionary dictionaryWithObjectsAndKeys:
			[NSNumber numberWithUnsignedLongLong:peer->session().uniqueId()],
			@"session",
			[NSNumber numberWithUnsignedLongLong:peer->id.value],
			@"peer",
			[NSNumber numberWithLongLong:topicRootId.bare],
			@"topic",
			[NSNumber numberWithLongLong:msgId.bare],
			@"msgid",
			[NSNumber numberWithUnsignedLongLong:_managerId],
			@"manager",
			nil]];

	[notification setTitle:Q2NSString(title)];
	[notification setSubtitle:Q2NSString(subtitle)];
	[notification setInformativeText:Q2NSString(msg)];
	if (!options.hideNameAndPhoto
		&& [notification respondsToSelector:@selector(setContentImage:)]) {
		NSImage *img = Q2NSImage(
			Window::Notifications::GenerateUserpic(peer, userpicView));
		[notification setContentImage:img];
	}

	if (!options.hideReplyButton
		&& [notification respondsToSelector:@selector(setHasReplyButton:)]) {
		[notification setHasReplyButton:YES];
	}

	[notification setSoundName:nil];

	NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
	[center deliverNotification:notification];

	}
}

void Manager::Private::clearingThreadLoop() {
	auto finished = false;
	while (!finished) {
		auto clearAll = false;
		auto clearFromItems = base::flat_set<NotificationId>();
		auto clearFromTopics = base::flat_set<ContextId>();
		auto clearFromHistories = base::flat_set<ContextId>();
		auto clearFromSessions = base::flat_set<uint64>();
		{
			std::unique_lock<std::mutex> lock(_clearingMutex);
			while (_clearingTasks.empty()) {
				_clearingCondition.wait(lock);
			}
			for (auto &task : _clearingTasks) {
				v::match(task, [&](ClearFinish) {
					finished = true;
					clearAll = true;
				}, [&](ClearAll) {
					clearAll = true;
				}, [&](const ClearFromItem &value) {
					clearFromItems.emplace(value.id);
				}, [&](const ClearFromTopic &value) {
					clearFromTopics.emplace(value.contextId);
				}, [&](const ClearFromHistory &value) {
					clearFromHistories.emplace(value.partialContextId);
				}, [&](const ClearFromSession &value) {
					clearFromSessions.emplace(value.sessionId);
				});
			}
			_clearingTasks.clear();
		}

		@autoreleasepool {

		auto clearBySpecial = [&](NSDictionary *notificationUserInfo) {
			NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
			const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
			if (!notificationSessionId) {
				return true;
			}
			NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
			const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0;
			if (!notificationPeerId) {
				return true;
			}
			NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
			if (!topicObject) {
				return true;
			}
			const auto notificationTopicRootId = [topicObject longLongValue];
			NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
			const auto msgId = msgObject ? [msgObject longLongValue] : 0LL;
			const auto partialContextId = ContextId{
				.sessionId = notificationSessionId,
				.peerId = PeerId(notificationPeerId),
			};
			const auto contextId = ContextId{
				.sessionId = notificationSessionId,
				.peerId = PeerId(notificationPeerId),
				.topicRootId = MsgId(notificationTopicRootId),
			};
			const auto id = NotificationId{ contextId, MsgId(msgId) };
			return clearFromSessions.contains(notificationSessionId)
				|| clearFromHistories.contains(partialContextId)
				|| clearFromTopics.contains(contextId)
				|| (msgId && clearFromItems.contains(id));
		};

		NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
		NSArray *notificationsList = [center deliveredNotifications];
		for (id notification in notificationsList) {
			NSDictionary *notificationUserInfo = [notification userInfo];
			NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
			auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
			if (notificationManagerId == _managerId) {
				if (clearAll || clearBySpecial(notificationUserInfo)) {
					[center removeDeliveredNotification:notification];
				}
			}
		}

		}
	}
}

template <typename Task>
void Manager::Private::putClearTask(Task task) {
	if (!_clearingThread.joinable()) {
		_clearingThread = std::thread([this] { clearingThreadLoop(); });
	}

	std::unique_lock<std::mutex> lock(_clearingMutex);
	_clearingTasks.push_back(task);
	_clearingCondition.notify_one();
}

void Manager::Private::clearAll() {
	putClearTask(ClearAll());
}

void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
	putClearTask(ClearFromItem{ ContextId{
		.sessionId = item->history()->session().uniqueId(),
		.peerId = item->history()->peer->id,
		.topicRootId = item->topicRootId(),
	}, item->id });
}

void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
	putClearTask(ClearFromTopic{ ContextId{
		.sessionId = topic->session().uniqueId(),
		.peerId = topic->history()->peer->id,
		.topicRootId = topic->rootId(),
	} });
}

void Manager::Private::clearFromHistory(not_null<History*> history) {
	putClearTask(ClearFromHistory{ ContextId{
		.sessionId = history->session().uniqueId(),
		.peerId = history->peer->id,
	} });
}

void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
	putClearTask(ClearFromSession{ session->uniqueId() });
}

void Manager::Private::updateDelegate() {
	NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
	[center setDelegate:_delegate];
}

void Manager::Private::invokeIfNotFocused(Fn<void()> callback) {
	if (!Platform::IsMac11_0OrGreater()) {
		queryDoNotDisturbState();
		if (!DoNotDisturbEnabled) {
			callback();
		}
	} else if (Platform::IsMacStoreBuild() || LibraryPath().isEmpty()) {
		callback();
	} else if (!_focusedCallbacks.empty()) {
		_focusedCallbacks.push_back(std::move(callback));
	} else if (!ShouldQuerySettings()) {
		if (!_focused) {
			callback();
		}
	} else {
		if (!_processesInited) {
			_processesInited = true;
			QObject::connect(&_dnd, &QProcess::finished, [=] {
				_waitingDnd = false;
				checkFocusState();
			});
			QObject::connect(&_focus, &QProcess::finished, [=] {
				_waitingFocus = false;
				checkFocusState();
			});
		}
		const auto start = [](QProcess &process, QString keys) {
			auto arguments = QStringList()
				<< "-extract"
				<< keys
				<< "raw"
				<< "-o"
				<< "-"
				<< "--"
				<< (LibraryPath() + "/Preferences/com.apple.controlcenter.plist");
			DEBUG_LOG(("Focus Check: Started %1.").arg(u"plutil"_q + arguments.join(' ')));
			process.start(u"plutil"_q, arguments);
		};
		_focusedCallbacks.push_back(std::move(callback));
		_waitingFocus = _waitingDnd = true;
		start(_focus, u"NSStatusItem Visible FocusModes"_q);
		start(_dnd, u"NSStatusItem Visible DoNotDisturb"_q);
	}
}

void Manager::Private::checkFocusState() {
	if (_waitingFocus || _waitingDnd) {
		return;
	}
	const auto istrue = [](QProcess &process) {
		const auto output = process.readAllStandardOutput();
		DEBUG_LOG(("Focus Check: %1").arg(output));
		const auto result = (output.trimmed() == u"true"_q);
		return result;
	};
	_focused = istrue(_focus) || istrue(_dnd);
	auto callbacks = base::take(_focusedCallbacks);
	if (!_focused) {
		for (const auto &callback : callbacks) {
			callback();
		}
	}
}

Manager::Private::~Private() {
	if (_waitingDnd) {
		_dnd.kill();
	}
	if (_waitingFocus) {
		_focus.kill();
	}
	if (_clearingThread.joinable()) {
		putClearTask(ClearFinish());
		_clearingThread.join();
	}
	NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
	[center setDelegate:nil];
	[_delegate release];
}

Manager::Manager(Window::Notifications::System *system) : NativeManager(system)
, _private(std::make_unique<Private>(this)) {
}

Manager::~Manager() = default;

void Manager::doShowNativeNotification(
		not_null<PeerData*> peer,
		MsgId topicRootId,
		Ui::PeerUserpicView &userpicView,
		MsgId msgId,
		const QString &title,
		const QString &subtitle,
		const QString &msg,
		DisplayOptions options) {
	_private->showNotification(
		peer,
		topicRootId,
		userpicView,
		msgId,
		title,
		subtitle,
		msg,
		options);
}

void Manager::doClearAllFast() {
	_private->clearAll();
}

void Manager::doClearFromItem(not_null<HistoryItem*> item) {
	_private->clearFromItem(item);
}

void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
	_private->clearFromTopic(topic);
}

void Manager::doClearFromHistory(not_null<History*> history) {
	_private->clearFromHistory(history);
}

void Manager::doClearFromSession(not_null<Main::Session*> session) {
	_private->clearFromSession(session);
}

QString Manager::accountNameSeparator() {
	return QString::fromUtf8(" \xE2\x86\x92 ");
}

bool Manager::doSkipToast() const {
	return false;
}

void Manager::doMaybePlaySound(Fn<void()> playSound) {
	_private->invokeIfNotFocused(std::move(playSound));
}

void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
	_private->invokeIfNotFocused(std::move(flashBounce));
}

} // namespace Notifications
} // namespace Platform
