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

#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "base/parse_helper.h"
#include "core/application.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "window/window_controller.h"
#include "media/player/media_player_instance.h"
#include "platform/platform_specific.h"

#include <QAction>
#include <QShortcut>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>

namespace Shortcuts {
namespace {

constexpr auto kCountLimit = 256; // How many shortcuts can be in json file.

rpl::event_stream<not_null<Request*>> RequestsStream;
bool Paused/* = false*/;

const auto kChatSwitchSpecialKeys = std::array{
	Qt::Key_Left,
	Qt::Key_Right,
	Qt::Key_Up,
	Qt::Key_Down,
	Qt::Key_Q,
};
Qt::Key ChatSwitchModifier/* = Qt::Key()*/;
bool ChatSwitchStarted/* = false*/;
QObject *ChatSwitchFilter/* = nullptr*/;
rpl::event_stream<ChatSwitchRequest> ChatSwitchStream;
std::array<Qt::Key, kChatSwitchSpecialKeys.size()> ChatSwitchKeyPressHandled;

const auto AutoRepeatCommands = base::flat_set<Command>{
	Command::MediaPrevious,
	Command::MediaNext,
	Command::ChatPrevious,
	Command::ChatNext,
	Command::ChatFirst,
	Command::ChatLast,
};

const auto MediaCommands = base::flat_set<Command>{
	Command::MediaPlay,
	Command::MediaPause,
	Command::MediaPlayPause,
	Command::MediaStop,
	Command::MediaPrevious,
	Command::MediaNext,
};

const auto SupportCommands = base::flat_set<Command>{
	Command::SupportReloadTemplates,
	Command::SupportToggleMuted,
	Command::SupportScrollToCurrent,
	Command::SupportHistoryBack,
	Command::SupportHistoryForward,
};

const auto CommandByName = base::flat_map<QString, Command>{
	{ u"close_telegram"_q    , Command::Close },
	{ u"lock_telegram"_q     , Command::Lock },
	{ u"minimize_telegram"_q , Command::Minimize },
	{ u"quit_telegram"_q     , Command::Quit },
	{ u"global_search"_q     , Command::GlobalSearch },

	{ u"media_play"_q        , Command::MediaPlay },
	{ u"media_pause"_q       , Command::MediaPause },
	{ u"media_playpause"_q   , Command::MediaPlayPause },
	{ u"media_stop"_q        , Command::MediaStop },
	{ u"media_previous"_q    , Command::MediaPrevious },
	{ u"media_next"_q        , Command::MediaNext },

	{ u"search"_q            , Command::Search },

	{ u"previous_chat"_q     , Command::ChatPrevious },
	{ u"next_chat"_q         , Command::ChatNext },
	{ u"first_chat"_q        , Command::ChatFirst },
	{ u"last_chat"_q         , Command::ChatLast },
	{ u"self_chat"_q         , Command::ChatSelf },
	{ u"pinned_chat1"_q      , Command::ChatPinned1 },
	{ u"pinned_chat2"_q      , Command::ChatPinned2 },
	{ u"pinned_chat3"_q      , Command::ChatPinned3 },
	{ u"pinned_chat4"_q      , Command::ChatPinned4 },
	{ u"pinned_chat5"_q      , Command::ChatPinned5 },
	{ u"pinned_chat6"_q      , Command::ChatPinned6 },
	{ u"pinned_chat7"_q      , Command::ChatPinned7 },
	{ u"pinned_chat8"_q      , Command::ChatPinned8 },

	{ u"previous_folder"_q   , Command::FolderPrevious },
	{ u"next_folder"_q       , Command::FolderNext },
	{ u"all_chats"_q         , Command::ShowAllChats },

	{ u"account1"_q          , Command::ShowAccount1 },
	{ u"account2"_q          , Command::ShowAccount2 },
	{ u"account3"_q          , Command::ShowAccount3 },
	{ u"account4"_q          , Command::ShowAccount4 },
	{ u"account5"_q          , Command::ShowAccount5 },
	{ u"account6"_q          , Command::ShowAccount6 },

	{ u"folder1"_q           , Command::ShowFolder1 },
	{ u"folder2"_q           , Command::ShowFolder2 },
	{ u"folder3"_q           , Command::ShowFolder3 },
	{ u"folder4"_q           , Command::ShowFolder4 },
	{ u"folder5"_q           , Command::ShowFolder5 },
	{ u"folder6"_q           , Command::ShowFolder6 },
	{ u"last_folder"_q       , Command::ShowFolderLast },

	{ u"show_archive"_q      , Command::ShowArchive },
	{ u"show_contacts"_q     , Command::ShowContacts },

	{ u"read_chat"_q         , Command::ReadChat },

	{ u"show_chat_menu"_q    , Command::ShowChatMenu },
	{ u"show_chat_preview"_q , Command::ShowChatPreview },

	{ u"record_voice"_q      , Command::RecordVoice },

	// Shortcuts that have no default values.
	{ u"message"_q                       , Command::JustSendMessage },
	{ u"message_silently"_q              , Command::SendSilentMessage },
	{ u"message_scheduled"_q             , Command::ScheduleMessage },
	{ u"ai_compose_apply"_q              , Command::ComposeAiApplyInPlace },
	{ u"toggle_webpage_preview"_q        , Command::ToggleWebPagePreview },
	{ u"media_viewer_video_fullscreen"_q , Command::MediaViewerFullscreen },
	{ u"show_scheduled"_q                , Command::ShowScheduled },
	{ u"archive_chat"_q                  , Command::ArchiveChat },
	{ u"record_round"_q                  , Command::RecordRound },
	{ u"show_admin_log"_q                , Command::ShowAdminLog },
	//
	{ u"fast_forward"_q      , Command::FastForward },
	{ u"fast_copy"_q         , Command::FastCopy },
};

const base::flat_map<Command, QString> &CommandNames() {
	static const auto result = [&] {
		auto result = base::flat_map<Command, QString>();
		for (const auto &[name, command] : CommandByName) {
			result.emplace(command, name);
		}
		return result;
	}();
	return result;
};

[[maybe_unused]] constexpr auto kNoValue = {
	Command::JustSendMessage,
	Command::SendSilentMessage,
	Command::ScheduleMessage,
	Command::ComposeAiApplyInPlace,
	Command::ToggleWebPagePreview,
	Command::MediaViewerFullscreen,
	Command::ShowScheduled,
	Command::ArchiveChat,
	Command::RecordRound,
};

class Manager {
public:
	void fill();
	void clear();

	[[nodiscard]] std::vector<Command> lookup(
		not_null<QObject*> object) const;
	void toggleMedia(bool toggled);
	void toggleSupport(bool toggled);
	void listen(not_null<QWidget*> widget);
	[[nodiscard]] bool handles(const QKeySequence &sequence) const;

	[[nodiscard]] const QStringList &errors() const;

	[[nodiscard]] auto keysDefaults() const
		-> base::flat_map<QKeySequence, base::flat_set<Command>>;
	[[nodiscard]] auto keysCurrents() const
		-> base::flat_map<QKeySequence, base::flat_set<Command>>;

	void change(
		QKeySequence was,
		QKeySequence now,
		Command command,
		std::optional<Command> restore);
	void resetToDefaults();

private:
	void fillDefaults();
	void writeDefaultFile();
	void writeCustomFile();
	bool readCustomFile();

	void set(const QString &keys, Command command, bool replace = false);
	void set(const QKeySequence &result, Command command, bool replace);
	void remove(const QString &keys);
	void remove(const QKeySequence &keys);
	void remove(const QKeySequence &keys, Command command);
	void unregister(base::unique_qptr<QAction> shortcut);

	void pruneListened();

	QStringList _errors;

	base::flat_map<QKeySequence, base::unique_qptr<QAction>> _shortcuts;
	base::flat_multi_map<not_null<QObject*>, Command> _commandByObject;
	std::vector<QPointer<QWidget>> _listened;

	base::flat_map<QKeySequence, base::flat_set<Command>> _defaults;

	base::flat_set<QAction*> _mediaShortcuts;
	base::flat_set<QAction*> _supportShortcuts;

};

QString DefaultFilePath() {
	return cWorkingDir() + u"tdata/shortcuts-default.json"_q;
}

QString CustomFilePath() {
	return cWorkingDir() + u"tdata/shortcuts-custom.json"_q;
}

bool DefaultFileIsValid() {
	QFile file(DefaultFilePath());
	if (!file.open(QIODevice::ReadOnly)) {
		return false;
	}
	auto error = QJsonParseError{ 0, QJsonParseError::NoError };
	const auto document = QJsonDocument::fromJson(
		base::parse::stripComments(file.readAll()),
		&error);
	file.close();

	if (error.error != QJsonParseError::NoError || !document.isArray()) {
		return false;
	}
	const auto shortcuts = document.array();
	if (shortcuts.isEmpty() || !(*shortcuts.constBegin()).isObject()) {
		return false;
	}
	const auto versionObject = (*shortcuts.constBegin()).toObject();
	const auto version = versionObject.constFind(u"version"_q);
	if (version == versionObject.constEnd()
		|| !(*version).isString()
		|| (*version).toString() != QString::number(AppVersion)) {
		return false;
	}
	return true;
}

void WriteDefaultCustomFile() {
	const auto path = CustomFilePath();
	auto input = QFile(":/misc/default_shortcuts-custom.json");
	auto output = QFile(path);
	if (input.open(QIODevice::ReadOnly)
		&& output.open(QIODevice::WriteOnly)) {
#ifdef Q_OS_MAC
		auto text = qs(input.readAll());
		const auto note = R"(
// Note:
// On Apple platforms, reference to "ctrl" corresponds to the Command keys )"
			+ QByteArray()
			+ R"(on the Macintosh keyboard.
// On Apple platforms, reference to "meta" corresponds to the Control keys.

[
)";
		text.replace(u"\n\n["_q, QString(note));
		output.write(text.toUtf8());
#else
		output.write(input.readAll());
#endif // !Q_OS_MAC
	}
}

void Manager::fill() {
	fillDefaults();

	if (!DefaultFileIsValid()) {
		writeDefaultFile();
	}
	if (!readCustomFile()) {
		WriteDefaultCustomFile();
	}
}

void Manager::clear() {
	_errors.clear();
	_shortcuts.clear();
	_commandByObject.clear();
	_mediaShortcuts.clear();
	_supportShortcuts.clear();
}

const QStringList &Manager::errors() const {
	return _errors;
}

auto Manager::keysDefaults() const
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
	return _defaults;
}

auto Manager::keysCurrents() const
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
	auto result = base::flat_map<QKeySequence, base::flat_set<Command>>();
	for (const auto &[keys, command] : _shortcuts) {
		auto i = _commandByObject.findFirst(command);
		const auto end = _commandByObject.end();
		for (; i != end && (i->first == command); ++i) {
			result[keys].emplace(i->second);
		}
	}
	return result;
}

void Manager::change(
		QKeySequence was,
		QKeySequence now,
		Command command,
		std::optional<Command> restore) {
	if (!was.isEmpty()) {
		remove(was, command);
	}
	if (!now.isEmpty()) {
		set(now, command, true);
	}
	if (restore) {
		Assert(!was.isEmpty());
		set(was, *restore, true);
	}
	writeCustomFile();
}

void Manager::resetToDefaults() {
	while (!_shortcuts.empty()) {
		remove(_shortcuts.begin()->first);
	}
	for (const auto &[sequence, commands] : _defaults) {
		for (const auto command : commands) {
			set(sequence, command, false);
		}
	}
	writeCustomFile();
}

std::vector<Command> Manager::lookup(not_null<QObject*> object) const {
	auto result = std::vector<Command>();
	auto i = _commandByObject.findFirst(object);
	const auto end = _commandByObject.end();
	for (; i != end && (i->first == object); ++i) {
		result.push_back(i->second);
	}
	return result;
}

void Manager::toggleMedia(bool toggled) {
	for (const auto shortcut : _mediaShortcuts) {
		shortcut->setEnabled(toggled);
	}
}

void Manager::toggleSupport(bool toggled) {
	for (const auto shortcut : _supportShortcuts) {
		shortcut->setEnabled(toggled);
	}
}

void Manager::listen(not_null<QWidget*> widget) {
	pruneListened();
	_listened.push_back(widget.get());
	for (const auto &[keys, shortcut] : _shortcuts) {
		widget->addAction(shortcut.get());
	}
}

bool Manager::handles(const QKeySequence &sequence) const {
	return _shortcuts.contains(sequence);
}

void Manager::pruneListened() {
	for (auto i = begin(_listened); i != end(_listened);) {
		if (i->data()) {
			++i;
		} else {
			i = _listened.erase(i);
		}
	}
}

bool Manager::readCustomFile() {
	// read custom shortcuts from file if it exists or write an empty custom shortcuts file
	QFile file(CustomFilePath());
	if (!file.exists()) {
		return false;
	}
	const auto guard = gsl::finally([&] {
		if (!_errors.isEmpty()) {
			_errors.push_front((u"While reading file '%1'..."_q
			).arg(file.fileName()));
		}
	});
	if (!file.open(QIODevice::ReadOnly)) {
		_errors.push_back(u"Could not read the file!"_q);
		return true;
	}
	auto error = QJsonParseError{ 0, QJsonParseError::NoError };
	const auto document = QJsonDocument::fromJson(
		base::parse::stripComments(file.readAll()),
		&error);
	file.close();

	if (error.error != QJsonParseError::NoError) {
		_errors.push_back((u"Failed to parse! Error: %2"_q
		).arg(error.errorString()));
		return true;
	} else if (!document.isArray()) {
		_errors.push_back(u"Failed to parse! Error: array expected"_q);
		return true;
	}
	const auto shortcuts = document.array();
	auto limit = kCountLimit;
	for (auto i = shortcuts.constBegin(), e = shortcuts.constEnd(); i != e; ++i) {
		if (!(*i).isObject()) {
			_errors.push_back(u"Bad entry! Error: object expected"_q);
			continue;
		}
		const auto entry = (*i).toObject();
		const auto keys = entry.constFind(u"keys"_q);
		const auto command = entry.constFind(u"command"_q);
		const auto removed = entry.constFind(u"removed"_q);
		if (keys == entry.constEnd()
			|| command == entry.constEnd()
			|| !(*keys).isString()
			|| (!(*command).isString() && !(*command).isNull())) {
			_errors.push_back(qsl("Bad entry! "
				"{\"keys\": \"...\", \"command\": [ \"...\" | null ]} "
				"expected."));
		} else if ((*command).isNull()) {
			remove((*keys).toString());
		} else {
			const auto name = (*command).toString();
			const auto i = CommandByName.find(name);
			if (i != end(CommandByName)) {
				if (removed != entry.constEnd() && removed->toBool()) {
					remove((*keys).toString(), i->second);
				} else {
					set((*keys).toString(), i->second, true);
				}
			} else {
				LOG(("Shortcut Warning: "
					"could not find shortcut command handler '%1'"
					).arg(name));
			}
		}
		if (!--limit) {
			_errors.push_back(u"Too many entries! Limit is %1"_q.arg(
				kCountLimit));
			break;
		}
	}
	return true;
}

void Manager::fillDefaults() {
	const auto ctrl = Platform::IsMac() ? u"meta"_q : u"ctrl"_q;

	set(u"ctrl+w"_q, Command::Close);
	set(u"ctrl+f4"_q, Command::Close);
	set(u"ctrl+l"_q, Command::Lock);
	set(u"ctrl+m"_q, Command::Minimize);
	set(u"ctrl+q"_q, Command::Quit);
	set(qsl("ctrl+t"), Command::GlobalSearch);

	set(u"media play"_q, Command::MediaPlay);
	set(u"media pause"_q, Command::MediaPause);
	set(u"toggle media play/pause"_q, Command::MediaPlayPause);
	set(u"media stop"_q, Command::MediaStop);
	set(u"media previous"_q, Command::MediaPrevious);
	set(u"media next"_q, Command::MediaNext);

	set(u"ctrl+f"_q, Command::Search);
	set(u"search"_q, Command::Search);
	set(u"find"_q, Command::Search);

	set(u"ctrl+pgdown"_q, Command::ChatNext);
	set(u"alt+down"_q, Command::ChatNext);
	set(u"ctrl+pgup"_q, Command::ChatPrevious);
	set(u"alt+up"_q, Command::ChatPrevious);

	set(u"ctrl+alt+home"_q, Command::ChatFirst);
	set(u"ctrl+alt+end"_q, Command::ChatLast);

	set(u"f5"_q, Command::SupportReloadTemplates);
	set(u"ctrl+delete"_q, Command::SupportToggleMuted);
	set(u"ctrl+insert"_q, Command::SupportScrollToCurrent);
	set(u"ctrl+shift+x"_q, Command::SupportHistoryBack);
	set(u"ctrl+shift+c"_q, Command::SupportHistoryForward);

	set(u"ctrl+1"_q, Command::ChatPinned1);
	set(u"ctrl+2"_q, Command::ChatPinned2);
	set(u"ctrl+3"_q, Command::ChatPinned3);
	set(u"ctrl+4"_q, Command::ChatPinned4);
	set(u"ctrl+5"_q, Command::ChatPinned5);
	set(u"ctrl+6"_q, Command::ChatPinned6);
	set(u"ctrl+7"_q, Command::ChatPinned7);
	set(u"ctrl+8"_q, Command::ChatPinned8);

	auto &&folders = ranges::views::zip(
		kShowFolder,
		ranges::views::ints(1, ranges::unreachable));

	for (const auto &[command, index] : folders) {
		set(u"%1+%2"_q.arg(ctrl).arg(index), command);
	}

	set(u"%1+shift+down"_q.arg(ctrl), Command::FolderNext);
	set(u"%1+shift+up"_q.arg(ctrl), Command::FolderPrevious);

	set(u"ctrl+0"_q, Command::ChatSelf);

	set(u"ctrl+9"_q, Command::ShowArchive);
	set(u"ctrl+j"_q, Command::ShowContacts);

	set(u"ctrl+r"_q, Command::ReadChat);

	set(u"ctrl+\\"_q, Command::ShowChatMenu);
	set(u"ctrl+]"_q, Command::ShowChatPreview);

	set(u"ctrl+r"_q, Command::RecordVoice);

	set(u"alt+f"_q, Command::FastForward);
	set(u"alt+c"_q, Command::FastCopy);
	
	_defaults = keysCurrents();
}

void Manager::writeDefaultFile() {
	auto file = QFile(DefaultFilePath());
	if (!file.open(QIODevice::WriteOnly)) {
		return;
	}
	const char *defaultHeader = R"HEADER(
// This is a list of default shortcuts for Telegram Desktop
// Please don't modify it, its content is not used in any way
// You can place your own shortcuts in the 'shortcuts-custom.json' file

)HEADER";
	file.write(defaultHeader);

	auto shortcuts = QJsonArray();
	auto version = QJsonObject();
	version.insert(u"version"_q, QString::number(AppVersion));
	shortcuts.push_back(version);

	for (const auto &[sequence, shortcut] : _shortcuts) {
		const auto object = shortcut.get();
		auto i = _commandByObject.findFirst(object);
		const auto end = _commandByObject.end();
		for (; i != end && i->first == object; ++i) {
			const auto j = CommandNames().find(i->second);
			if (j != CommandNames().end()) {
				QJsonObject entry;
				entry.insert(u"keys"_q, sequence.toString().toLower());
				entry.insert(u"command"_q, j->second);
				shortcuts.append(entry);
			}
		}
	}

	// Commands without a default value.
	for (const auto c : ranges::views::concat(kShowAccount, kNoValue)) {
		for (const auto &[name, command] : CommandByName) {
			if (c == command) {
				auto entry = QJsonObject();
				entry.insert(u"keys"_q, QJsonValue());
				entry.insert(u"command"_q, name);
				shortcuts.append(entry);
			}
		}
	}

	auto document = QJsonDocument();
	document.setArray(shortcuts);
	file.write(document.toJson(QJsonDocument::Indented));
}

void Manager::writeCustomFile() {
	auto shortcuts = QJsonArray();
	for (const auto &[sequence, shortcut] : _shortcuts) {
		const auto object = shortcut.get();
		auto i = _commandByObject.findFirst(object);
		const auto end = _commandByObject.end();
		for (; i != end && i->first == object; ++i) {
			const auto d = _defaults.find(sequence);
			if (d == _defaults.end() || !d->second.contains(i->second)) {
				const auto j = CommandNames().find(i->second);
				if (j != CommandNames().end()) {
					QJsonObject entry;
					entry.insert(u"keys"_q, sequence.toString().toLower());
					entry.insert(u"command"_q, j->second);
					shortcuts.append(entry);
				}
			}
		}
	}
	const auto has = [&](not_null<QObject*> shortcut, Command command) {
		for (auto i = _commandByObject.findFirst(shortcut)
			; i != end(_commandByObject) && i->first == shortcut
			; ++i) {
			if (i->second == command) {
				return true;
			}
		}
		return false;
	};
	for (const auto &[sequence, commands] : _defaults) {
		const auto i = _shortcuts.find(sequence);
		if (i == end(_shortcuts)) {
			QJsonObject entry;
			entry.insert(u"keys"_q, sequence.toString().toLower());
			entry.insert(u"command"_q, QJsonValue());
			shortcuts.append(entry);
			continue;
		}
		for (const auto command : commands) {
			if (!has(i->second.get(), command)) {
				const auto j = CommandNames().find(command);
				if (j != CommandNames().end()) {
					QJsonObject entry;
					entry.insert(u"keys"_q, sequence.toString().toLower());
					entry.insert(u"command"_q, j->second);
					entry.insert(u"removed"_q, true);
					shortcuts.append(entry);
				}
			}
		}
	}

	if (shortcuts.isEmpty()) {
		WriteDefaultCustomFile();
		return;
	}

	auto file = QFile(CustomFilePath());
	if (!file.open(QIODevice::WriteOnly)) {
		LOG(("Shortcut Warning: could not write custom shortcuts file."));
		return;
	}
	const char *customHeader = R"HEADER(
// This is a list of changed shortcuts for Telegram Desktop
// You can edit them in Settings > Chat Settings > Keyboard Shortcuts.

)HEADER";
	file.write(customHeader);

	auto document = QJsonDocument();
	document.setArray(shortcuts);
	file.write(document.toJson(QJsonDocument::Indented));
}

void Manager::set(const QString &keys, Command command, bool replace) {
	if (keys.isEmpty()) {
		return;
	}

	const auto result = QKeySequence(keys, QKeySequence::PortableText);
	if (result.isEmpty()) {
		_errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys));
		return;
	}
	set(result, command, replace);
}

void Manager::set(
		const QKeySequence &keys,
		Command command,
		bool replace) {
	auto shortcut = base::make_unique_q<QAction>();
	shortcut->setShortcut(keys);
	shortcut->setShortcutContext(Qt::ApplicationShortcut);
	if (!AutoRepeatCommands.contains(command)) {
		shortcut->setAutoRepeat(false);
	}
	const auto isMediaShortcut = MediaCommands.contains(command);
	const auto isSupportShortcut = SupportCommands.contains(command);
	if (isMediaShortcut || isSupportShortcut) {
		shortcut->setEnabled(false);
	}
	auto object = shortcut.get();
	auto i = _shortcuts.find(keys);
	if (i == end(_shortcuts)) {
		i = _shortcuts.emplace(keys, std::move(shortcut)).first;
	} else if (replace) {
		unregister(std::exchange(i->second, std::move(shortcut)));
	} else {
		object = i->second.get();
	}
	_commandByObject.emplace(object, command);
	if (!shortcut) { // Added the new one.
		if (isMediaShortcut) {
			_mediaShortcuts.emplace(i->second.get());
		}
		if (isSupportShortcut) {
			_supportShortcuts.emplace(i->second.get());
		}
		pruneListened();
		for (const auto &widget : _listened) {
			widget->addAction(i->second.get());
		}
	}
}

void Manager::remove(const QString &keys) {
	if (keys.isEmpty()) {
		return;
	}

	const auto result = QKeySequence(keys, QKeySequence::PortableText);
	if (result.isEmpty()) {
		_errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys));
		return;
	}
	remove(result);
}

void Manager::remove(const QKeySequence &keys) {
	const auto i = _shortcuts.find(keys);
	if (i != end(_shortcuts)) {
		unregister(std::move(i->second));
		_shortcuts.erase(i);
	}
}

void Manager::remove(const QKeySequence &keys, Command command) {
	const auto i = _shortcuts.find(keys);
	if (i != end(_shortcuts)) {
		_commandByObject.remove(i->second.get(), command);
		if (!_commandByObject.contains(i->second.get())) {
			unregister(std::move(i->second));
			_shortcuts.erase(i);
		}
	}
}

void Manager::unregister(base::unique_qptr<QAction> shortcut) {
	if (shortcut) {
		_commandByObject.removeAll(shortcut.get());
		_mediaShortcuts.erase(shortcut.get());
		_supportShortcuts.erase(shortcut.get());
	}
}

Manager Data;

} // namespace

Request::Request(std::vector<Command> commands)
: _commands(std::move(commands)) {
}

bool Request::check(Command command, int priority) {
	if (ranges::contains(_commands, command)
		&& priority > _handlerPriority) {
		_handlerPriority = priority;
		return true;
	}
	return false;
}

bool Request::handle(FnMut<bool()> handler) {
	_handler = std::move(handler);
	return true;
}

FnMut<bool()> RequestHandler(std::vector<Command> commands) {
	auto request = Request(std::move(commands));
	RequestsStream.fire(&request);
	return std::move(request._handler);
}

FnMut<bool()> RequestHandler(Command command) {
	return RequestHandler(std::vector<Command>{ command });
}

bool Launch(Command command) {
	if (auto handler = RequestHandler(command)) {
		return handler();
	}
	return false;
}

bool Launch(std::vector<Command> commands) {
	if (Paused) {
		return false;
	} else if (auto handler = RequestHandler(std::move(commands))) {
		return handler();
	}
	return false;
}

rpl::producer<not_null<Request*>> Requests() {
	return RequestsStream.events();
}

void Start() {
	Data.fill();
}

const QStringList &Errors() {
	return Data.errors();
}

bool MarkChatSwitchKeyPressHandled(Qt::Key key, bool handled) {
	if (!key) {
		return false;
	} else if (handled) {
		if (ranges::contains(ChatSwitchKeyPressHandled, key)) {
			return false;
		}
		const auto i = ranges::find(ChatSwitchKeyPressHandled, Qt::Key(0));
		Assert(i != end(ChatSwitchKeyPressHandled));
		*i = key;
		return true;
	}
	const auto i = ranges::find(ChatSwitchKeyPressHandled, key);
	if (i == end(ChatSwitchKeyPressHandled)) {
		return false;
	}
	*i = Qt::Key(0);
	return true;
}

bool CancelChatSwitch(Qt::Key result) {
	ChatSwitchModifier = Qt::Key();
	if (!ChatSwitchStarted) {
		return false;
	}
	ChatSwitchStarted = false;
	delete base::take(ChatSwitchFilter);
	ChatSwitchStream.fire({ .action = result });
	return true;
}

bool NavigateChatSwitch(Qt::Key result) {
	if (!ChatSwitchStarted) {
		return false;
	}
	ChatSwitchStream.fire({ .action = result });
	return true;
}

bool CheckChatSwitchEvent(Qt::Key key) {
	const auto plain = Qt::Key(int(key)
		& ~(Qt::ControlModifier
			| Qt::ShiftModifier
			| Qt::AltModifier
			| Qt::MetaModifier));
	if (MarkChatSwitchKeyPressHandled(plain, false)) {
		return true;
	} else if (plain == Qt::Key_Escape) {
		return CancelChatSwitch(Qt::Key_Escape);
	} else if (plain == Qt::Key_Return || plain == Qt::Key_Enter) {
		return CancelChatSwitch(Qt::Key_Enter);
	} else if (ranges::contains(kChatSwitchSpecialKeys, plain)) {
		return NavigateChatSwitch(Qt::Key(plain));
	}
	return false;
}

bool HandleEvent(
		not_null<QObject*> object,
		not_null<QShortcutEvent*> event) {
	if (ChatSwitchStarted) {
		const auto full = event->key();
		for (auto i = 0; i != full.count(); ++i) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
			const auto raw = full[i].key();
#else // Qt >= 6.0.0
			const auto raw = full[i];
#endif // Qt < 6.0.0
			if (CheckChatSwitchEvent(Qt::Key(raw))) {
				return true;
			}
		}
		return false;
	}
	return Launch(Data.lookup(object));
}

rpl::producer<ChatSwitchRequest> ChatSwitchRequests() {
	return ChatSwitchStream.events();
}

bool HandlePossibleChatSwitch(not_null<QKeyEvent*> event) {
	const auto type = event->type();
	if (Paused) {
		return false;
	} else if (type == QEvent::ShortcutOverride) {
		const auto key = Qt::Key(event->key());
		const auto ctrl = Platform::IsMac()
			? Qt::MetaModifier
			: Qt::ControlModifier;
		if ((event->modifiers() & ctrl)
			&& ranges::contains(kChatSwitchSpecialKeys, key)) {
			// For Ctrl+Q in case of active chat switch we have
			// on Windows always ShortcutOverride + KeyPress,
			// while on macOS we have just ShortcutOverride for
			// the first press and KeyPress only for repeat events.
			//
			// This complex scheme allows to handle both cases with
			// firing the switch event Key_Q exactly once initially.
			if (CheckChatSwitchEvent(key)) {
				MarkChatSwitchKeyPressHandled(key, true);
			}
			crl::on_main([=] {
				MarkChatSwitchKeyPressHandled(key, false);
			});
		}
		if (Data.handles(QKeySequence(ctrl | Qt::Key_Tab))
			&& (Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Backtab)
				|| Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab)
				|| Data.handles(QKeySequence(ctrl | Qt::Key_Backtab)))) {
			return false;
		} else if (key == Qt::Key_Control || key == Qt::Key_Meta) {
			ChatSwitchModifier = key;
		} else if (key == Qt::Key_Tab || key == Qt::Key_Backtab) {
			const auto modifiers = event->modifiers();
			if (modifiers & ctrl) {
				if (ChatSwitchModifier == Qt::Key()) {
					ChatSwitchModifier = Platform::IsMac()
						? Qt::Key_Meta
						: Qt::Key_Control;
				}
				const auto action = (modifiers & Qt::ShiftModifier)
					? Qt::Key_Backtab
					: key;
				if (Data.handles(modifiers | key)) {
					return false;
				} else if (action == Qt::Key_Tab
					&& Data.handles(QKeySequence(ctrl | Qt::Key_Tab))) {
					return false;
				} else if (action == Qt::Key_Backtab
					&& Data.handles(QKeySequence(ctrl | Qt::Key_Backtab))) {
					return false;
				} else if (action == Qt::Key_Backtab
					&& Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab)) {
					return false;
				}
				const auto started = !std::exchange(ChatSwitchStarted, true);
				if (started) {
					Assert(!ChatSwitchFilter);
					ChatSwitchFilter = base::install_event_filter(qApp, [=](not_null<QEvent*> e) {
						return (e->type() == QEvent::InputMethod)
							? base::EventFilterResult::Cancel
							: base::EventFilterResult::Continue;
					});
				}
				ChatSwitchStream.fire({
					.action = action,
					.started = started,
				});
				return true;
			}
		}
	} else if (type == QEvent::KeyPress) {
		return CheckChatSwitchEvent(Qt::Key(event->key()));
	} else if (type == QEvent::KeyRelease) {
		const auto key = Qt::Key(event->key());
		if (key == ChatSwitchModifier) {
			CancelChatSwitch(Qt::Key_Enter);
		}
	}
	return false;
}

void ToggleMediaShortcuts(bool toggled) {
	Data.toggleMedia(toggled);
}

void ToggleSupportShortcuts(bool toggled) {
	Data.toggleSupport(toggled);
}

void Pause() {
	Paused = true;
}

void Unpause() {
	Paused = false;
}

auto KeysDefaults()
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
	return Data.keysDefaults();
}

auto KeysCurrents()
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
	return Data.keysCurrents();
}

void Change(
		QKeySequence was,
		QKeySequence now,
		Command command,
		std::optional<Command> restore) {
	Data.change(was, now, command, restore);
}

void ResetToDefaults() {
	Data.resetToDefaults();
}

bool AllowWithoutModifiers(int key) {
	const auto service = {
		Qt::Key_Escape,
		Qt::Key_Tab,
		Qt::Key_Backtab,
		Qt::Key_Backspace,
		Qt::Key_Return,
		Qt::Key_Enter,
		Qt::Key_Insert,
		Qt::Key_Delete,
		Qt::Key_Pause,
		Qt::Key_Print,
		Qt::Key_SysReq,
		Qt::Key_Clear,
		Qt::Key_Home,
		Qt::Key_End,
		Qt::Key_Left,
		Qt::Key_Up,
		Qt::Key_Right,
		Qt::Key_Down,
		Qt::Key_PageUp,
		Qt::Key_PageDown,
		Qt::Key_Shift,
		Qt::Key_Control,
		Qt::Key_Meta,
		Qt::Key_Alt,
		Qt::Key_CapsLock,
		Qt::Key_NumLock,
		Qt::Key_ScrollLock,
	};
	return (key >= 0x80) && !ranges::contains(service, key);
}

void Finish() {
	Data.clear();
}

void Listen(not_null<QWidget*> widget) {
	Data.listen(widget);
}

} // namespace Shortcuts
