// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2007 Konrad Twardowski

#include "mainwindow.h"

#include "bookmarks.h"
#include "commandline.h"
#include "config.h"
#include "fileshortcut.h"
#include "log.h"
#include "mod.h"
#include "password.h"
#include "plugins.h"
#include "preferences.h"
#include "progressbar.h"
#include "usystemtray.h"
#include "utils.h"
#include "uwidgets.h"
#include "actions/bootentry.h"
#include "actions/extras.h"
#include "actions/lock.h"
#include "actions/test.h"
#include "triggers/idlemonitor.h"
#include "triggers/processmonitor.h"

#ifdef KS_PURE_QT
	#include "version.h"
#endif // KS_PURE_QT

#include <QCloseEvent>
#include <QDebug>
#include <QMenuBar>
#include <QStatusBar>
#include <QTimer>
#include <QToolButton>

// TODO: review includes

#ifdef KS_KF5
	#include <KGlobalAccel>
	#include <KHelpMenu>
	#include <KNotification>
	#include <KNotifyConfigWidget>
	#include <KShortcutsDialog>
	#include <KStandardGuiItem>
#endif // KS_KF5

// public

MainWindow::~MainWindow() {
	//qDebug() << "MainWindow::~MainWindow()";

// FIXME: setAttribute(Qt::WA_DeleteOnClose, true);
	PluginManager::shutDown();
	Config::shutDown();
	Log::shutDown();
}

bool MainWindow::maybeShow(const bool forceShow) {
	if (CLI::isArg("hide-ui")) {
		hide();

		m_systemTray->setVisible(false);
		
		return true;
	}

// TEST: --ui-dialog shutdown:reboot:logout:-:hibernate:suspend:-:lock:logout:-:-:cancel:quit

// TODO: --ui-dialog auto|minimal

	const QString SEPARATOR = "-";

	auto layoutOption = CLI::getUILayoutOption("ui-dialog");
	if (!layoutOption.isEmpty()) {
		auto dialog = std::make_unique<UDialog>(nullptr, QApplication::applicationDisplayName(), true);

		int dialogSpacing = 20_px;

		auto *buttonsLayout = new QGridLayout();
		buttonsLayout->setSpacing(dialogSpacing);
		dialog->bottomWidget()->hide();
		dialog->mainLayout()->addLayout(buttonsLayout);
		Utils::setMargin(dialog->mainLayout(), dialogSpacing);

		bool confirm = CLI::isConfirm();
		int row = 0;
		int col = 0;
		const QString ROW_WRAP = "=";

		for (const QString &id : layoutOption) {
// TODO: icon size (?)
			QAction *buttonAction = nullptr;

			if (id == SEPARATOR) {
				col++;

				auto *spacer = new QSpacerItem(dialogSpacing * 2, 0_px);
				buttonsLayout->addItem(spacer, row, col);
			}
			else if (id == ROW_WRAP) {
				col = 0;
				row++;
			}
			else if (id == "cancel") {
				buttonAction = m_cancelAction;
				col++;
			}
			else if (id == "quit") {
				buttonAction = createQuitAction(false);
				col++;
			}
			else if (id == "title") {
				// title always visible in the dialog itself; ignore silently
			}
			else {
				auto *action = PluginManager::actionAlias(id);

				if (action != nullptr) {
					col++;

					if (confirm)
						buttonAction = action->createConfirmAction(confirm);
					else
						buttonAction = action->uiAction();
				}
				else {
					qCritical() << "Unknown ui-dialog element:" << id;
/* TODO:
					QStringList sl = { "x" };
					sl += SEPARATOR;
					sl += ROW_WRAP;
					sl += "cancel";
					sl += "quit";
					sl += "title";
					qInfo() << "Valid ui-dialog elements: " << sl.join(", ");
*/
				}
			}

			if (buttonAction != nullptr) {
				auto *button = new QToolButton();
				button->setDefaultAction(buttonAction);
				button->setIconSize(Utils::getLargeIconSize());
				button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
				button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // fill grid cell width
				button->setStyleSheet("QToolButton { padding: 1em; }");

				if (buttonAction->toolTip() == buttonAction->text())
					button->setToolTip("");

				Utils::setFont(button, 4, false);

				connect(button, SIGNAL(clicked()), dialog.get(), SLOT(close()));

// TODO: buttonsLayout->setColumnMinimumWidth(col, 200_px); (?)
// TODO:
				//buttonsLayout->setColumnMinimumWidth(col, 100_px);// (?)
				//buttonsLayout->setRowMinimumHeight(row, 150_px);// (?)

				buttonsLayout->addWidget(button, row, col);
			}
		}

		dialog->setWindowSize(UDialog::WindowSize::FIXED);
		dialog->moveToCenterOfScreen();
		dialog->exec();

		return false;
	}

	layoutOption/* reassign */ = CLI::getUILayoutOption("ui-menu");
	if (!layoutOption.isEmpty()) {
		auto *menu = new QMenu();
		Utils::showToolTips(menu);

		bool confirm = CLI::isConfirm();
		int count = 0;

		for (const QString &id : layoutOption) {
			if (id == SEPARATOR) {
				menu->addSeparator();
				//count++;
			}
			else if (id == "cancel") {
				menu->addAction(m_cancelAction);
				count++;
			}
			else if (id == "quit") {
				menu->addAction(createQuitAction(false));
				count++;
			}
			else if (id == "title") {
				Utils::addTitle(menu, QApplication::windowIcon(), QApplication::applicationDisplayName());
				count++;
			}
			else {
				auto *action = PluginManager::actionAlias(id);
// TODO: show confirmation dialog at cursor position (?)
				if (action != nullptr) {
					if (confirm)
						menu->addAction(action->createConfirmAction(confirm));
					else
						menu->addAction(action->uiAction());

					count++;
				}
				else {
					qCritical() << "Unknown ui-menu element:" << id;
				}
			}
		}

		if (count == 0) {
			qCritical() << "Empty ui-menu";

			return false;
		}

		Utils::execMenu(menu);

		return false;
	}

	if (!m_systemTray->isSupported()) {
		show();

		return true;
	}

	bool trayIconEnabled = Config::systemTrayIconEnabled.getBool();
	if (trayIconEnabled)
		m_systemTray->setVisible(true);

	if (forceShow) {
		show();
	}
	else if (CLI::isArg("init") || qApp->isSessionRestored()) {
		if (!trayIconEnabled)
			showMinimized();
	}
	else {
		show();
	}

	return true;
}

void MainWindow::setProgressWidgetsVisible() {
	bool dateTimeTrigger = (dynamic_cast<DateTimeTriggerBase *>(getSelectedTrigger()) != nullptr);

	progressBar()->setVisible(dateTimeTrigger && m_active && Config::progressBarEnabled);
	m_countdown  ->setVisible(dateTimeTrigger && m_active && Config::countdownEnabled);
}

void MainWindow::setTime(const QString &selectTrigger, const QTime &time) {
	setActive(false);

	setSelectedTrigger(selectTrigger);
	Trigger *trigger = getSelectedTrigger();

// TODO: common code
	auto *dateTimeTriggerBase = dynamic_cast<DateTimeTriggerBase *>(trigger);

	// ensure the trigger exists and is valid
	if ((dateTimeTriggerBase == nullptr) || (trigger->id() != selectTrigger)) {
		UDialog::error(this, i18n("Unsupported action: %0").arg(selectTrigger));
	
		return;
	}

	auto *dateTimeTrigger = dynamic_cast<DateTimeTrigger *>(trigger);
	auto dateTime = QDateTime(QDate::currentDate(), time);

	if (dateTimeTrigger != nullptr)
		dateTimeTrigger->setSelectedDateTimeAdjusted(dateTime);
	else
		dateTimeTriggerBase->setSelectedDateTime(dateTime);

	setActive(true);
}

void MainWindow::showOrHide() {
	if (isVisible() && !isMinimized()) {
		hide();
	}
	else {
		#ifdef Q_OS_WIN32
		if (isMinimized()) {
			// HACK: activateWindow() does not work and causes funny bugs
			showNormal();
		}
		else {
			show();
			activateWindow();
		}
		#else
// FIXME: KWin (?)
		show();
		activateWindow();
		#endif // Q_OS_WIN32
	}
}

// public slots

QStringList MainWindow::actionList(const bool showDescription) {
	QStringList sl;
	for (const Action *i : PluginManager::actionList()) {
		if (showDescription)
			sl.append(i->id() + " - " + i->originalText());
		else
			sl.append(i->id());
	}

	return sl;
}

QStringList MainWindow::triggerList(const bool showDescription) {
	QStringList sl;
	for (const Trigger *i : PluginManager::triggerList()) {
		if (showDescription)
			sl.append(i->id() + " - " + i->text());
		else
			sl.append(i->id());
	}

	return sl;
}

void MainWindow::setActive(const bool yes) {
	setActive(yes, true);
}

void MainWindow::setActive(const bool yes, const bool needAuthorization) { // private
	if (m_active == yes)
		return;

	qDebug() << "MainWindow::setActive( " << yes << " )";

	if (needAuthorization && !yes && !PasswordDialog::authorize(this, i18n("Cancel"), "kshutdown/action/cancel"))
		return;

	Action *action = getSelectedAction();
	
	if (yes && !action->isEnabled()) {
// TODO: GUI
		qDebug() << "MainWindow::setActive: action disabled: " << action->uiAction()->text() << ", " << action->status();
		action->activate(); // show error message only

		return;
	}

	if (yes && !action->authorize(this)) {
		return;
	}

	Trigger *trigger = getSelectedTrigger();
	if (yes && !PluginManager::triggerMap().contains(trigger->id())) {
// TODO: GUI
		qDebug() << "MainWindow::setActive: trigger disabled: " << trigger->text() << ", " << trigger->status();
	
		return;
	}
	
	m_active = yes;
	m_systemTray->updateIcon();
	ProgressBar::updateTaskbar(-1, -1s);

	//qDebug() << "\tMainWindow::getSelectedAction() == " << action->id();
	//qDebug() << "\tMainWindow::getSelectedTrigger() == " << trigger->id();

	// reset notifications
	m_lastNotificationID = QString();

	if (m_active) {
#ifdef Q_OS_WIN32
		// HACK: disable "Lock Screen" action if countdown is active
		if (action != LockAction::self())
			m_confirmLockAction->setEnabled(false);
#endif // Q_OS_WIN32

		m_triggerTimer->start(trigger->checkInterval());

		action->setState(Base::State::Start);
		trigger->setState(Base::State::Start);
	}
	else {
#ifdef Q_OS_WIN32
		if (action != LockAction::self())
			m_confirmLockAction->setEnabled(true);
#endif // Q_OS_WIN32
		m_triggerTimer->stop();

		action->setState(Base::State::Stop);
		trigger->setState(Base::State::Stop);
	}

	setProgressWidgetsVisible();
	setTitle(QString(), QString());

	action->updateStatusWidget(this);
	trigger->updateStatusWidget(this);
	updateWidgets();
}

void MainWindow::showNotification(const QString &id) { // public
	// do not display the same notification twice
	if (m_lastNotificationID == id)
		return;

	m_lastNotificationID = id;

	QString text = getSelectedTrigger()->createDisplayStatus(getSelectedAction(), Trigger::DISPLAY_STATUS_HTML);
	//qDebug() << "MainWindow::showNotification" << id << text;

	//qDebug() << "Raw notification:" << text;

	QString fixedHTML = QString(text);
	fixedHTML.remove("<qt>");
	fixedHTML.remove("</qt>");

// TODO: test other DE:
#ifdef Q_OS_WIN32
	fixedHTML.replace("<br>", "\n");
#endif // Q_OS_WIN32

	//qDebug() << "Fixed notification:" << fixedHTML;

#ifdef KS_KF5
	#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
	KNotification::event(
		id,
		fixedHTML,
		QPixmap(),
		this,
		KNotification::CloseOnTimeout
	);
	#else
	KNotification::event(
		id,
		fixedHTML,
		QPixmap(),
		KNotification::CloseOnTimeout
	);
	#endif // QT_VERSION
#endif // KS_KF5
#ifdef KS_PURE_QT
	// Qt supports notifications only in *some* DEs...
	if (!Utils::isKDE()) {
		// remove unsupported markup tags
		fixedHTML.remove(QRegularExpression(R"(\<\w+\>)"));
		fixedHTML.remove(QRegularExpression(R"(\</\w+\>)"));
	}
	m_systemTray->warning(fixedHTML);
#endif // KS_PURE_QT

	// flash taskbar button
	if ((id == "1m") || (id == "5m")) {
		milliseconds alertTime = 10s;
		QApplication::alert(this, alertTime.count());
	}
}

void MainWindow::setExtrasCommand(const QString &command) {
	setSelectedAction("extras");
	Extras::self()->setStringOption(command);
}

void MainWindow::setSelectedAction(const QString &id) {
	//qDebug() << "MainWindow::setSelectedAction( " << id << " )";

	int index = m_actions->findData(id);
	if (index == -1)
		index = m_actions->findData("test");
	if (index == -1)
		index = 0;
	m_actions->setCurrentIndex(index);
	onActionActivated(index);
}

void MainWindow::setSelectedTrigger(const QString &id) {
	//qDebug() << "MainWindow::setSelectedTrigger( " << id << " )";

	int index = m_triggers->findData(id);
	if (index == -1)
		index = 0;
	m_triggers->setCurrentIndex(index);
	onTriggerActivated(index);
}

void MainWindow::setTime(const QString &trigger, const QString &time) {
	if (!trigger.isEmpty())
		setSelectedTrigger(trigger);

// TODO: make the "time" parsing consistent with command line options
// TEST: qdbus net.sf.kshutdown /kshutdown net.sf.kshutdown.MainWindow.setTime time-from-now 12:00

	setTime(trigger, TimeOption::parseTime(time));
}

void MainWindow::setWaitForProcess(const qint64 pid) {
	//qDebug() << "MainWindow::setWaitForProcess( " << pid << " )";

	setActive(false);

	setSelectedTrigger("process-monitor");
	auto *processMonitor = dynamic_cast<ProcessMonitor *>(
		getSelectedTrigger()
	);
	
	if (processMonitor != nullptr) {
		processMonitor->setPID(pid);

		setActive(true);
	}
}

// protected

void MainWindow::closeEvent(QCloseEvent *e) {
	// normal close
	bool hideInTray = Config::minimizeToSystemTrayIcon && Config::systemTrayIconEnabled;
	if (!e->spontaneous() || m_forceQuit || Action::totalExit() || !hideInTray) {
		writeConfig();
	
		e->accept();

		// HACK: ?
		if (!hideInTray)
			qApp->quit();

		return;
	}
	
	e->ignore();

	// no system tray, minimize instead
	if (
		!m_systemTray->isSupported() ||
		Utils::isUnity() // HACK: QSystemTrayIcon::activated not called in Unity
	) {
		showMinimized();
	}
	// hide in system tray instead of close
	else {
		hide();
	}

	if (m_active) {
		if (m_showActiveWarning) {
			m_showActiveWarning = false;
			m_systemTray->info(i18n("KShutdown is still active!"));
		}
	}
	else {
		if (m_showMinimizeInfo) {
			m_showMinimizeInfo = false;
			m_systemTray->info(i18n("KShutdown has been minimized"));
		}
	}
}

#ifdef Q_OS_WIN32
// CREDITS: https://stackoverflow.com/questions/25916966/what-am-i-doing-wrong-with-qwintaskbarprogress
void MainWindow::showEvent(QShowEvent *e) {
	#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
	if (!m_winTaskbarButton) {
		m_winTaskbarButton = new QWinTaskbarButton(this);
		m_winTaskbarButton->setWindow(windowHandle());
	}
	#endif // QT_VERSION

	e->accept();
}
#endif // Q_OS_WIN32

// private

MainWindow::MainWindow() :
	QMainWindow(
		nullptr,
		Qt::Window |
		// disable "Maximize" button; no Qt::WindowMaximizeButtonHint
		Qt::CustomizeWindowHint | Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint
	),
	m_active(false),
	m_forceQuit(false),
	m_showActiveWarning(true),
	m_showMinimizeInfo(true),
	m_triggerTimer(new QTimer(this)) {

	//qDebug() << "MainWindow::MainWindow()";

#ifdef KS_KF5
	m_actionCollection = new KActionCollection(this);
#endif // KS_KF5

	// HACK: It seems that the "quit on last window closed"
	// does not work correctly if main window is hidden
	// "in" the system tray..
	qApp->setQuitOnLastWindowClosed(false);

	setObjectName("main-window");

#ifdef KS_PURE_QT
	// HACK: delete this on quit
	setAttribute(Qt::WA_DeleteOnClose, true);
#endif // KS_PURE_QT

// TODO: QIcon appIcon = QIcon::fromTheme("kshutdown");
	QIcon   appIcon(":/images/kshutdown.png");           // 32x32
	appIcon.addFile(":/images/hi64-app-kshutdown.png");  // 64x64
	appIcon.addFile(":/images/hi128-app-kshutdown.png"); // 128x128 - @2x High DPI version mostly used in UDialog::about
	QApplication::setWindowIcon(appIcon);
	//qDebug() << QApplication::windowIcon();
	//qDebug() << windowIcon();

	// NOTE: do not change the "init" order,
	// or your computer will explode
	initWidgets();

	QIcon alignFallbackIcon = UWidgets::isIconThemeSupported() ? UWidgets::emptyIcon(m_actions->iconSize()) : QIcon();

	// init actions
	int lastActionGroup = 0;
	for (const Action *action : PluginManager::actionList()) {
		if (action->visibleInWindow()) {
			int actionGroup = action->getUIGroup();
			if (actionGroup != lastActionGroup) {
				lastActionGroup = actionGroup;
				m_actions->insertSeparator(m_actions->count());
			}

			QString id = action->id();
// TODO: review "m_actions" usage

			QIcon icon = action->icon();
			m_actions->addItem(icon.isNull() ? alignFallbackIcon : icon, action->uiAction()->text(), id);
		}

		//qDebug() << "\tMainWindow::addAction( " << action->text() << " ) [ id=" << id << ", index=" << index << " ]";
	}
	m_actions->setMaxVisibleItems(m_actions->count());

	// init triggers
	for (const Trigger *trigger : PluginManager::triggerList()) {
		QString id = trigger->id();

		QIcon icon = trigger->icon();
		m_triggers->addItem(icon.isNull() ? alignFallbackIcon : icon, trigger->text(), id);

		// insert separator
		if (id == "date-time")
			m_triggers->insertSeparator(m_triggers->count());
	}
	m_triggers->setMaxVisibleItems(m_triggers->count());

	m_triggerTimer->callOnTimeout([this] { onCheckTrigger(); });

	m_systemTray = new USystemTray(this);
	initMenuBar();

	readConfig();

	setTitle(QString(), QString());

	updateWidgets();

	statusBar()->setSizeGripEnabled(true);

	// HACK: avoid insane window stretching
	// (dunno how to use setFixedSize correctly w/o breaking layout)
	QSize hint = sizeHint();
	setMaximumSize(hint.width() * 2, hint.height() * 2);

	connect(
		qApp, SIGNAL(focusChanged(QWidget *, QWidget *)),
		this, SLOT(onFocusChange(QWidget *, QWidget *))
	);

#ifdef QT_DBUS_LIB
	QDBusConnection dbus = QDBusConnection::sessionBus();
	#ifdef KS_PURE_QT
// TODO: allow only one application instance
	dbus.registerService("net.sf.kshutdown");
	#endif // KS_PURE_QT
	dbus.registerObject(
		"/kshutdown",
		this,
		QDBusConnection::ExportScriptableSlots
	);
#endif // QT_DBUS_LIB

	// Do not focus OK button on startup
	// to avoid accidental action activation via Enter key.
	m_actions->setFocus();

/* TEST:
	UDialog::error(this, "AutoText: <h1>UDialog::error</h1>", Qt::AutoText);
	UDialog::error(this, "Markdown: *UDialog*::_error_", Qt::MarkdownText);
	UDialog::error(this, "Plain: <h1>*UDialog*::_error_</h1>", Qt::PlainText);
	UDialog::info(this, "* UDialog::info");
	UDialog::warning(this, "UDialog\n::warning");
*/
}

Action *MainWindow::getSelectedAction() const { // public
	return PluginManager::action(m_actions->itemData(m_actions->currentIndex()).toString());
}

Trigger *MainWindow::getSelectedTrigger() const { // public
	return PluginManager::trigger(m_triggers->itemData(m_triggers->currentIndex()).toString());
}

QAction *MainWindow::createQuitAction(const bool mainMenu) {
	#ifdef KS_KF5
	auto *quitAction = KStandardAction::quit(this, SLOT(onQuit()), this);
	quitAction->setEnabled(!Utils::isRestricted("action/file_quit"));

	if (!mainMenu)
		quitAction->setShortcut(QKeySequence()); // hide shortcut
	#else
	auto *quitAction = new QAction(this);
	quitAction->setIcon(QIcon::fromTheme("application-exit"));

	if (mainMenu)
		quitAction->setShortcut(QKeySequence("Ctrl+Q"));
	//quitAction->setShortcuts(QKeySequence::Quit <- useless);

	connect(quitAction, SIGNAL(triggered()), SLOT(onQuit()));
	#endif // KS_KF5

	// NOTE: does not work with system tray icon context menu
	//quitAction->setShortcutVisibleInContextMenu(mainMenu);

	// NOTE: Use "Quit KShutdown" instead of "Quit" because
	// it may be too similar to "Turn Off" in some language translations.
	quitAction->setText(i18n("Quit KShutdown"));

	return quitAction;
}

void MainWindow::initFileMenu(QMenu *fileMenu, const bool mainMenu) {
	Utils::showToolTips(fileMenu);

	int lastActionGroup = 0;
	for (Action *action : PluginManager::actionList()) {
		if (mainMenu) {
			if (!action->visibleInMainMenu())
				continue; // for
		}
		else {
			if (!action->visibleInSystemTrayMenu())
				continue; // for
		}

		auto *confirmAction = action->createConfirmAction(false);
		if (action == LockAction::self())
			m_confirmLockAction = confirmAction;

		#ifdef KS_KF5
		if (mainMenu) {
			m_actionCollection->addAction("kshutdown/" + action->id(), confirmAction);
			KGlobalAccel::setGlobalShortcut(confirmAction, QList<QKeySequence>());
		}
// TODO: show global shortcuts: confirmAction->setShortcut(confirmAction->globalShortcut());
		#endif // KS_KF5

		int actionGroup = action->getUIGroup();
		if (actionGroup != lastActionGroup) {
			lastActionGroup = actionGroup;
			fileMenu->addSeparator();
		}

		fileMenu->addAction(confirmAction);

		#ifdef Q_OS_LINUX
		if ((action->id() == "reboot") && Config::bootEntriesVisibleInMenu) {
			auto *bootEntryMenu = new BootEntryMenu(this);
			bootEntryMenu->setTitle(action->originalText());
			fileMenu->addMenu(bootEntryMenu);
		}
		#endif // Q_OS_LINUX
	}
	fileMenu->addSeparator();
	fileMenu->addAction(m_cancelAction);
	fileMenu->addAction(createQuitAction(mainMenu));
}

void MainWindow::initMenuBar() {
	//qDebug() << "MainWindow::initMenuBar()";

	auto *menuBar = new QMenuBar();

	// HACK: Fixes Bookmarks menu and key shortcuts
	if (Utils::xfce)
		menuBar->setNativeMenuBar(false);

	// file menu

	auto *fileMenu = new QMenu(i18n("A&ction"), menuBar);

	Utils::addTitle(fileMenu, /*QIcon::fromTheme("dialog-warning")*/QIcon(), i18n("No Delay"));
	initFileMenu(fileMenu, true);
	menuBar->addMenu(fileMenu);

	// bookmarks menu

	if (!Mod::getBool("ui-hide-bookmarks-menu"))
		menuBar->addMenu(m_bookmarksMenu);

	// tools menu

	auto *toolsMenu = new QMenu(i18n("&Tools"), menuBar);

	QList<QStringList> runList {
		#ifdef Q_OS_LINUX
		// SLOW: { "journalctl", "--list-boots" },
		{ "last", "--hostlast", "--system" },

		{ "-", i18n("Current") },

		{ "journalctl", "--boot=0", "--system" },
		{ "journalctl", "--boot=0", "--user" },

		{ "-", i18n("Previous") },

		{ "journalctl", "--boot=-1", "--system" },
		{ "journalctl", "--boot=-1", "--user" },

		{ "-" },

		{ "uptime", "--pretty" },
		{ "w" },

		{ "-" },

		{ "systemd-analyze", "time" },
		{ "systemd-analyze", "blame" },
		{ "free", "--human" },
		#endif // Q_OS_LINUX
		//{ "__ERROR__", "TEST", "X" },
	};

	if (! runList.isEmpty()) { // cppcheck-suppress knownConditionTrueFalse
		auto *runMenu = new QMenu(i18n("Run"), toolsMenu);
		Utils::showToolTips(runMenu);

		for (const QStringList &programAndArgs : runList) {
			if (programAndArgs[0] == "-") {
				switch (programAndArgs.count()) {
					case 1:
						runMenu->addSeparator();
						break;
					case 2:
						Utils::addTitle(runMenu, QIcon(), programAndArgs[1]);
						break;
				}

				continue; // for
			}

			const QString title = programAndArgs.join(" ");

			auto *action = runMenu->addAction(title, [this, /* copy capture */programAndArgs, title] {
				QString program = programAndArgs[0];
				QStringList args = QStringList(programAndArgs);
				args.removeFirst();

				QString header = "$ " + title;
				QString text =
					header + "\n" +
					QString("-").repeated(header.length()) + "\n";

				QApplication::setOverrideCursor(Qt::WaitCursor);

				QProcess process;
				process.start(program, args);

				bool ok = false;
				text += Utils::read(process, ok);

				QApplication::restoreOverrideCursor();

				UDialog::plainText(this, text, title);
			});


			// NOTE: tool tip text from "man" page
			QString program = programAndArgs[0];

			if (program == "free")
				action->setToolTip("Show amount of free and used memory in the system");
			else if (program == "last")
				action->setToolTip("Show a listing of last logged in users");
			else if (program == "w")
				action->setToolTip("Show who is logged on and what they are doing");
		}

		toolsMenu->addMenu(runMenu);
		toolsMenu->addSeparator();
	}

	#ifdef Q_OS_LINUX
	toolsMenu->addAction(
		QIcon::fromTheme("system-run"), i18n("Create File Shortcut..."),
		[this] {
			auto fileShortcutDialog = std::make_unique<FileShortcutDialog>(this);
			fileShortcutDialog->exec();
		}
	);
	toolsMenu->addSeparator();
	#endif // Q_OS_LINUX

	#ifndef Q_OS_WIN32
	auto *systemSettingsAction = new QAction(QIcon::fromTheme("preferences-system"), i18n("System Settings..."));
	connect(systemSettingsAction, &QAction::triggered, [this]() {
		QString command = "kcmshell6";

		QStringList args = {
			"kcm_autostart",
			"kcm_kded",
			"kcm_notifications",
			"kcm_smserver",
			"kcm_energyinfo",
			"kcm_sddm",
			"kcm_splashscreen",
			"kcm_keys",
			// NOTE: for some reason "kcmshell5 --list" lists it with "kcm_" prefix but it does not work
			"powerdevilprofilesconfig",
			"screenlocker"
		};

		if (! Utils::run(command, args)) {
			command = "kcmshell5";

			if (! Utils::run(command, args)) {
				UDialog::error(this, i18n("Action failed: %0").arg("kcmshell5/kcmshell6"));
			}
		}
	});
	systemSettingsAction->setEnabled(Utils::isKDE());
	#endif // !Q_OS_WIN32

#ifdef KS_PURE_QT
	#ifndef Q_OS_WIN32
	toolsMenu->addAction(systemSettingsAction);
	#endif

	#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
	toolsMenu->addAction(
		QIcon::fromTheme("configure"), i18n("Preferences"),
		[] { Preferences::showDialog(); },
		QKeySequence::Preferences
	);
	#else
	toolsMenu->addAction(
		QIcon::fromTheme("configure"), i18n("Preferences"),
		QKeySequence::Preferences,
		[] { Preferences::showDialog(); }
	);
	#endif // QT_VERSION
#else

	// help menu

	auto *kdeHelpMenu = new KHelpMenu(this);

	// tools menu

	auto *configureLanguageAction = KStandardAction::switchApplicationLanguage(kdeHelpMenu, &KHelpMenu::switchApplicationLanguage, this);
	configureLanguageAction->setEnabled(!Utils::isRestricted("action/switch_application_language"));
	toolsMenu->addAction(configureLanguageAction);

	auto *configureShortcutsAction = KStandardAction::keyBindings(this, SLOT(onConfigureShortcuts()), this);
	configureShortcutsAction->setEnabled(!Utils::isRestricted("action/options_configure_keybinding"));
	toolsMenu->addAction(configureShortcutsAction);

	auto *configureNotificationsAction = KStandardAction::configureNotifications(this, SLOT(onConfigureNotifications()), this);
	configureNotificationsAction->setEnabled(!Utils::isRestricted("action/options_configure_notifications"));
	toolsMenu->addAction(configureNotificationsAction);
	
	toolsMenu->addSeparator();

	toolsMenu->addAction(systemSettingsAction);

	auto *preferencesAction = KStandardAction::preferences(this, [] { Preferences::showDialog(); }, this);
	preferencesAction->setEnabled(!Utils::isRestricted("action/options_configure"));
	toolsMenu->addAction(preferencesAction);
#endif // KS_PURE_QT

	menuBar->addMenu(toolsMenu);

	// help menu

	auto *helpMenu = new QMenu(i18n("&Help"), menuBar);

	Utils::showToolTips(helpMenu);

	auto *cliAction = new QAction(i18n("Command Line Options"), helpMenu);
	cliAction->setShortcut(QKeySequence("Ctrl+L,I"));
	connect(cliAction, &QAction::triggered, [this]() {
		CLI::showHelp(this);
	});

	#ifdef QT_DBUS_LIB
	auto *dbusAction = new QAction("D-Bus", helpMenu);
	connect(dbusAction, &QAction::triggered, [this]() {
		CLI::showDBusHelp(this);
	});
	#endif // QT_DBUS_LIB

	auto *systemInfoAction = new QAction(QIcon::fromTheme("dialog-information"), i18n("System Information"), helpMenu);
	connect(systemInfoAction, &QAction::triggered, [this]() {
		UDialog::systemInfo(this);
	});

// TODO: review QAction "parent"
	auto *whatsNewAction = UWidgets::newLinkAction(
		i18n("What's New?"),
		KS_HOME_PAGE + "releases/" + KS_APP_VERSION + ".html"
	);

	auto *homePageAction = UWidgets::newLinkAction(i18n("Home Page"), KS_HOME_PAGE);

// TODO: misc. help from Wiki

	helpMenu->addAction(cliAction);
	#ifdef QT_DBUS_LIB
	helpMenu->addAction(dbusAction);
	#endif // QT_DBUS_LIB

	helpMenu->addAction(systemInfoAction);

	helpMenu->addSeparator();

	helpMenu->addAction(whatsNewAction);

	if (Utils::isAntique()) {
		helpMenu->addAction(QIcon::fromTheme("dialog-warning"), i18n("Check for Updates"), [this]() {
			UDialog::warning(this, Utils::getAntiqueMessage());
		});
	}

	helpMenu->addSeparator();

	helpMenu->addAction(homePageAction);

// TODO: replace signal/slots with lambda (global)
#ifdef KS_KF5
	auto *aboutAction = KStandardAction::aboutApp(kdeHelpMenu, &KHelpMenu::aboutApplication, this);
	aboutAction->setEnabled(!Utils::isRestricted("action/help_about_app"));
	helpMenu->addAction(aboutAction);
#else
	#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
	helpMenu->addAction(
		i18n("About"),
		[this]() { UDialog::about(this); },
		QKeySequence("Ctrl+H,E,L,P")
	);
	#else
	helpMenu->addAction(
		i18n("About"),
		QKeySequence("Ctrl+H,E,L,P"),
		[this]() { UDialog::about(this); }
	);
	#endif // QT_VERSION
#endif // KS_KF5

	menuBar->addMenu(helpMenu);

	setMenuBar(menuBar);

	if (Mod::getBool("ui-hide-menu-bar"))
		menuBar->hide();
}

void MainWindow::initWidgets() {
	m_progressBar = std::make_shared<ProgressBar>();

	m_bookmarksMenu = new BookmarksMenu(this);

	const int BOX_MARGIN = 0_px;
	const int BOX_SPACING = 15_px;

	// actions

	m_actions = new QComboBox();
	m_actions->setObjectName("actions");
	connect(m_actions, SIGNAL(activated(int)), SLOT(onActionActivated(int)));

	m_actionInfoWidget = new InfoWidget();

	m_actionLayout = UWidgets::newVBoxLayout(
		{
			Utils::newHeader(i18n("Select an &action"), m_actions),
			m_actions,
			m_actionInfoWidget
		},
		BOX_SPACING, BOX_MARGIN
	);

	// triggers

	m_triggers = new QComboBox();
	m_triggers->setObjectName("triggers");
	connect(m_triggers, SIGNAL(activated(int)), SLOT(onTriggerActivated(int)));

	m_triggerInfoWidget = new InfoWidget();

	m_triggerLayout = UWidgets::newVBoxLayout(
		{
			Utils::newHeader(i18n("Se&lect a time/event"), m_triggers),
			m_triggers,
			m_triggerInfoWidget
		},
		BOX_SPACING, BOX_MARGIN
	);

	// lcd progress

	m_countdown = new QLCDNumber();
	m_countdown->setFrameShape(QLCDNumber::NoFrame);
	m_countdown->setMinimumHeight(50_px);
	m_countdown->setSegmentStyle(QLCDNumber::Flat);
	m_countdown->setToolTip(i18n("Countdown"));
	m_countdown->hide();

	// ok/cancel button

	m_okCancelButton = new QPushButton();
	m_okCancelButton->setDefault(true);
	m_okCancelButton->setObjectName("ok-cancel-button");
	m_okCancelButton->setToolTip(i18n("Click to activate/cancel the selected action"));
	Utils::setFont(m_okCancelButton, 1, true);

	connect(m_okCancelButton, &QPushButton::clicked, [this]() {
		setActive(!m_active);
	});

	// main layout/widget

	auto *mainLayout = UWidgets::newVBoxLayout({ }, BOX_SPACING, BOX_SPACING/* BOX_MARGIN */);
	mainLayout->addLayout(m_actionLayout);
	mainLayout->addSpacing(BOX_SPACING);
	mainLayout->addLayout(m_triggerLayout);
	mainLayout->addStretch();
	mainLayout->addSpacing(BOX_SPACING);
	mainLayout->addLayout(Utils::newHeader(""));
	mainLayout->addWidget(m_countdown);
	mainLayout->addWidget(m_okCancelButton);

	auto *mainWidget = new QWidget();
	mainWidget->setLayout(mainLayout);
	mainWidget->setObjectName("main-widget");
	setCentralWidget(mainWidget);

	// cancel action

	m_cancelAction = new QAction(this);
	#ifdef KS_KF5
	m_cancelAction->setIcon(KStandardGuiItem::cancel().icon());
	m_actionCollection->addAction("kshutdown/cancel", m_cancelAction);
	KGlobalAccel::setGlobalShortcut(m_cancelAction, QList<QKeySequence>());
// TODO: show global shortcut: m_cancelAction->setShortcut(m_cancelAction->globalShortcut());

	// NOTE: does not work with system tray icon context menu
	//m_cancelAction->setShortcutVisibleInContextMenu(false);
	#else
	m_cancelAction->setIcon(QIcon::fromTheme("dialog-cancel"));
	#endif // KS_KF5

	connect(m_cancelAction, &QAction::triggered, [this]() {
		setActive(false);
	});

	// show menu action

	#ifdef KS_KF5
	auto showMainWindowAction = new QAction(i18n("Show/Hide Main Window"), this);
	m_actionCollection->addAction("kshutdown/show-main-window", showMainWindowAction);
	KGlobalAccel::setGlobalShortcut(showMainWindowAction, QList<QKeySequence>());
	connect(showMainWindowAction, &QAction::triggered, [this]() {
// TODO: re-test all "hide-ui"
		if (!CLI::isArg("hide-ui"))
			showOrHide();
	});

	auto showMenuAction = new QAction(i18n("Show Action Menu"), this);
	showMenuAction->setIcon(QIcon::fromTheme("application-menu"));
	m_actionCollection->addAction("kshutdown/show-menu", showMenuAction);
	KGlobalAccel::setGlobalShortcut(showMenuAction, QList<QKeySequence>());
	connect(showMenuAction, &QAction::triggered, [this]() {
		if (!CLI::isArg("hide-ui")) {
			auto menu = std::make_unique<QMenu>();
			initFileMenu(menu.get(), false);

			Utils::execMenu(menu.get());
		}
	});
	#endif // KS_KF5
}

void MainWindow::readConfig() {
	//qDebug() << "MainWindow::readConfig()";

	setSelectedAction(Config::selectedAction.getString());
	setSelectedTrigger(Config::selectedTrigger.getString());

#ifdef KS_KF5
	m_actionCollection->readSettings();
#endif // KS_KF5
}

void MainWindow::replaceContainerWidget(Base *base) {
	bool isAction = (dynamic_cast<Action *>(base) != nullptr);

	auto *current = isAction ? m_currentActionWidget : m_currentTriggerWidget;
	auto *layout = isAction ? m_actionLayout : m_triggerLayout;

	if (current != nullptr) {
		//qDebug() << "REMOVE:" << current;
		current->hide();
		layout->removeWidget(current);
	}

	auto *w = base->getContainerWidget();
	//qDebug() << "INSERT:" << w;
	layout->insertWidget(2, w);
	w->show();

	if (isAction)
		m_currentActionWidget = w;
	else
		m_currentTriggerWidget = w;
}

void MainWindow::setTitle(const QString &_plain, const QString &_html) {
	const QString appName = QApplication::applicationDisplayName();
	QString htmlToolTip  = _html.isEmpty()  ? appName : _html;
	QString plainToolTip = _plain.isEmpty() ? appName : _plain;

	// convert multi line tool tip
	QString plainTitle = plainToolTip;
	plainTitle = plainTitle.replace("\n\n", "\n"); // avoid double "-"
	plainTitle = plainTitle.replace("\n", Utils::TITLE_SEPARATOR);
	plainTitle = plainTitle.remove(appName + Utils::TITLE_SEPARATOR); // HACK: avoid double "KShutdown" in title

	#ifdef Q_OS_WIN32
	setWindowTitle(Utils::makeTitle(plainTitle, appName)); // HACK: force Utils::TITLE_SEPARATOR instead if " - "
	#else
	setWindowTitle(plainTitle);
	#endif // Q_OS_WIN32

	#ifdef KS_PURE_QT
	m_systemTray->setToolTip(plainToolTip);
	#else
	m_systemTray->setToolTip(htmlToolTip);
	#endif // KS_PURE_QT

	// HACK: add "Progress Bar" prefix
	if (htmlToolTip.startsWith("<qt>"))
		htmlToolTip.insert(4, i18n("Progress Bar") + Utils::TITLE_SEPARATOR);

	m_progressBar->setToolTip(htmlToolTip);
}

void MainWindow::updateWidgets() {
	qDebug() << "MainWindow::updateWidgets()";

	bool enabled = !m_active;
	m_actions->setEnabled(enabled);

	if (m_currentActionWidget != nullptr)
		m_currentActionWidget->setEnabled(enabled);

	m_triggers->setEnabled(enabled);

	if (m_currentTriggerWidget != nullptr)
		m_currentTriggerWidget->setEnabled(enabled);

	m_bookmarksMenu->setEnabled(enabled);

	Mod::applyMainWindowColors(this);

	bool hasIcon = m_okCancelButton->style()->styleHint(QStyle::SH_DialogButtonBox_ButtonsHaveIcons);
	if (m_active) {
		if (hasIcon) {
			#ifdef KS_KF5
			m_okCancelButton->setIcon(KStandardGuiItem::cancel().icon());
			#else
			m_okCancelButton->setIcon(QIcon::fromTheme("dialog-cancel"));
			#endif // KS_KF5
		}
		m_okCancelButton->setText(i18n("Cancel"));
	}
	else {
		if (hasIcon) {
			#ifdef KS_KF5
			m_okCancelButton->setIcon(KStandardGuiItem::ok().icon());
			#else
			m_okCancelButton->setIcon(QIcon::fromTheme("dialog-ok"));
			#endif // KS_KF5
		}
		m_okCancelButton->setText(i18n("Activate"));
	}

	// update info widgets
	if (m_active) {
		m_actionInfoWidget->hide();
		m_triggerInfoWidget->hide();
	}

	// update "Cancel" action
	if (m_active) {
		Action *action = getSelectedAction();
		m_cancelAction->setEnabled(!Utils::isRestricted("kshutdown/action/cancel"));
		m_cancelAction->setText(i18n("Cancel: %0").arg(action->originalText()));
	}
	else {
		m_cancelAction->setEnabled(false);
		m_cancelAction->setText(i18n("Cancel"));
	}
}

void MainWindow::writeConfig() { // public
	//qDebug() << "MainWindow::writeConfig()";

	PluginManager::writeConfig();

	Config::selectedAction.setString(getSelectedAction()->id());
	Config::selectedAction.write();

	Config::selectedTrigger.setString(getSelectedTrigger()->id());
	Config::selectedTrigger.write();

#ifdef KS_KF5
	m_actionCollection->writeSettings();
#endif // KS_KF5

	Config::sync();
}

// public slots

void MainWindow::onQuit() {
	//qDebug() << "MainWindow::onQuit()";
	
	if (!PasswordDialog::authorizeQuit(this))
		return;

	m_forceQuit = true;
	m_systemTray->setVisible(false);
	close();
	qApp->quit();
}

// private slots

void MainWindow::onActionActivated([[maybe_unused]] int index) {
	Action *action = getSelectedAction();
	replaceContainerWidget(action);

	QString toolTip = action->uiAction()->toolTip();
	m_actions->setToolTip((toolTip == action->uiAction()->text()) ? "" : toolTip);

	action->updateStatusWidget(this);
}

void MainWindow::onCheckTrigger() { // private
	if (!m_active) {
		qDebug() << "MainWindow::onCheckTrigger(): INTERNAL ERROR";

		return;
	}

	Action *action = getSelectedAction();
	Trigger *trigger = getSelectedTrigger();

	//qDebug() << "MainWindow::onCheckTrigger():" << trigger->checkInterval().count();

	// activate action

	if (trigger->canActivateAction()) {
		// NOTE: 2nd "false" fixes https://sourceforge.net/p/kshutdown/bugs/33/
		setActive(false, false);

		if (action->isEnabled()) {
			action->activate();
		}
		
		// update date/time after resume from suspend, etc.
		action->updateStatusWidget(this);
		trigger->updateStatusWidget(this);
	}

	// update status

	else {
		QString plain = trigger->createDisplayStatus(action, Trigger::DISPLAY_STATUS_APP_NAME);
		QString html = trigger->createDisplayStatus(action, Trigger::DISPLAY_STATUS_HTML | Trigger::DISPLAY_STATUS_APP_NAME);
		setTitle(plain, html);
	}
}

#ifdef KS_KF5
void MainWindow::onConfigureNotifications() {
	if (!PasswordDialog::authorizeSettings(this))
		return;

	KNotifyConfigWidget::configure(this);
}

void MainWindow::onConfigureShortcuts() {
	if (!PasswordDialog::authorizeSettings(this))
		return;

// FIXME: local shortcuts column for actions with KGlobalAccel::setGlobalShortcut set are not visible #kf6
// BUG: works in KF5
// RELATED: https://bugs.kde.org/show_bug.cgi?id=474570
	KShortcutsDialog::showDialog(m_actionCollection, KShortcutsEditor::LetterShortcutsDisallowed, this);
}
#endif // KS_KF5

void MainWindow::onFocusChange([[maybe_unused]] QWidget *old, QWidget *now) {
// FIXME: invoked on Alt+Tab (?)

	// update trigger status info on focus gain
	if ((now != nullptr) && (now == m_currentTriggerWidget)) {
		//qDebug() << "MainWindow::onFocusChange()";
		//getSelectedAction()->updateStatusWidget(this);
		getSelectedTrigger()->updateStatusWidget(this);
	}
}

void MainWindow::onTriggerActivated([[maybe_unused]] int index) {
	Trigger *trigger = getSelectedTrigger();
	replaceContainerWidget(trigger);

	m_triggers->setToolTip(trigger->toolTip());

	trigger->updateStatusWidget(this);
}
