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

#include "commandline.h"

#include "config.h"
#include "mainwindow.h"
#include "plugins.h"
#include "udialog.h"
#include "utils.h"
#include "uwidgets.h"

#include <QDebug>
#include <QTextEdit>

// public

// CLI

bool CLI::check() {
	if (isArg("help")) {
		showHelp(nullptr);

		return true;
	}

	TimeOption::init();
	
	Action *actionToActivate = nullptr;
	bool confirm = isConfirm();
	for (Action *action : PluginManager::actionList()) {
		if (action->isCommandLineOptionSet()) {
			if (confirm && !action->showConfirmationMessage())
				return false; // user cancel
			
			actionToActivate = action;

			break; // for
		}
	}

	if (actionToActivate != nullptr) {
		if (!actionToActivate->onCommandLineOption())
			return false;

		// setup main window and execute action later
		if (TimeOption::isValid()) {
			TimeOption::setAction(actionToActivate);
			
			return false;
		}
		else {
			if (TimeOption::isError()) {
// TODO: show valid example
				UDialog::error(nullptr, i18n("Invalid time: %0").arg(TimeOption::value()));
				
				return false;
			}

			// execute action and quit now
			if (actionToActivate->authorize(nullptr))
				actionToActivate->activate();

			return true;
		}
	}
	
	return false;
}

QString CLI::formatName(const QCommandLineOption &option, const QString &value) {
	QString longName = "";
	QString shortName = "";

	for (auto &i : option.names()) {
		if (longName.isEmpty() && (i.size() > 1))
			longName = i;
		else if (shortName.isEmpty() && (i.size() == 1))
			shortName = i;
	}

	QString result;

	if (! longName.isEmpty()) // prefer long name
		result = "--" + longName;
	else if (! shortName.isEmpty())
		result = "-" + shortName;

	if (! value.isNull())
		result += " " + value;

	return result;
}

QString CLI::getOption(const QString &name) {
	//qDebug() << "CLI::getOption:" << name;

	QString option = m_args->value(name);

	return option.isEmpty() ? QString() : option;
}

QString CLI::getTimeOption() {
	QStringList pa = m_args->positionalArguments();

	return pa.isEmpty() ? "" : pa.front();
}

QStringList CLI::getUILayoutOption(const QString &name) {
	QString layout = getOption(name);

	if (layout.isEmpty()) {
		//qDebug() << "Empty layout option:" << name;

		return { };
	}

	//qDebug() << "Layout option:" << name << "=" << layout;

	return layout.split(':');
}

void CLI::init(const QString &appDescription) {
	m_args = new QCommandLineParser();
	m_args->setApplicationDescription(appDescription);
	m_args->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
}

void CLI::initBeforeApp(int argc, char **argv) {
	if (argc < 2)
		return;

	for (int i = 1; i < argc; i++) {
		if ((i < argc - 1) && matches(argv[i], SCALE_NAME)) {
			i++;

			if (auto scaleOptional = Utils::toDouble(argv[i])) {
				auto scale = std::clamp(scaleOptional.value(), 1.0, 3.0);
				qputenv("QT_SCALE_FACTOR", QByteArray::number(scale));
			}
		}
	}
}

void CLI::initOptions() {
// TODO: plain text? options.add(":", ki18n("Other Options:"));

	m_confirmOption = { "confirm",  i18n("Show confirmation message") };
	m_hideUIOption  = { "hide-ui",  i18n("Hide main window and system tray icon") };
	m_initOption    = { "init",     i18n("Do not show main window on startup") };
	m_scaleOption   = { SCALE_NAME, i18n("User Interface scale (examples: 2, 1.5)"), "value" };

	m_args->addOptions({
		{
			{ "i", "idle", "inactivity" },
			i18n(
				"Detect user inactivity. Example:\n"
				"--logout --inactivity 90 - automatically logout after 90 minutes of user inactivity"
			)
		},

		{ "help", i18n("Show this help") },
		#ifdef KS_KF5
		{ "cancel", i18n("Cancel an active action") },
		#endif // KS_KF5
		m_confirmOption,
		{ "confirm-auto", i18n("Show confirmation message only if the \"Confirm Action\" option is enabled") },
		m_hideUIOption,
		m_initOption,
		{ "mod", i18n("A list of modifications"), "value" },

// TODO: docs
		m_scaleOption,
		{ "style", i18n("User Interface style"), "value" },

		#if defined(KS_PURE_QT) && !defined(KS_PORTABLE)
		{ "portable", i18n("Run in \"portable\" mode") },
		#endif

		{ "ui-dialog", Utils::makeTitle(i18n("Experimental"), i18n("Show custom dialog instead of main window")), "value" },
		{ "ui-menu", Utils::makeTitle(i18n("Experimental"), i18n("Show custom popup menu instead of main window")), "value" },
	});

	m_args->addPositionalArgument(
		"time",
		i18n(
			"Activate countdown. Examples:\n"
			"13:37 (HH:MM) or \"1:37 PM\" - absolute time\n"
			"10 or 10m - number of minutes from now\n"
			"2h - two hours"
		)
	);
}

bool CLI::isArg(const QString &name) {
	//qDebug() << "CLI::isArg:" << name;

	return m_args->isSet(name);
}

bool CLI::isConfirm() {
	if (m_args->isSet(m_confirmOption))
		return true;

	return isArg("confirm-auto") && Config::confirmAction;
}

bool CLI::matches(const QString &arg, const QString &name) {
	return (arg == '-' + name) || (arg == "--" + name);
}

#ifdef QT_DBUS_LIB
// TODO: help.cpp
void CLI::showDBusHelp(QWidget *parent) {
	QList<QStringList> rows;

	const QString SERVICE = "net.sf.kshutdown";
	const QString PATH = "/kshutdown";

	rows += { QString("D-Bus Service:"), SERVICE };//!!!auto
	rows += { QString("D-Bus Path:"), "/kshutdown" };

	rows += { UWidgets::CELL_HEADER + "D-Bus API Function" };

	auto *mainWindow = MainWindow::self();

	QList<QStringList> apiList {
		{ "MainWindow.actionList(bool showDescription)", "Lists all action IDs", "actionList false", mainWindow->actionList(false).join(" ") },
		{ "MainWindow.triggerList(bool showDescription)", "Lists all trigger IDs", "triggerList false", mainWindow->triggerList(false).join(" ") },
		{ "MainWindow.setSelectedAction(string id)", "Select an action by its ID (see above)", "setSelectedAction test" },
		{ "MainWindow.setSelectedTrigger(string id)", "Select a trigger by its ID (see above)", "setSelectedTrigger time-from-now" },
		//!!!{ "MainWindow.setTime(string trigger, string time)", "", "" },
		//!!!{ "MainWindow.setWaitForProcess(pid)", "", "" },
		{ "MainWindow.setActive(bool yes)", "Activate the selected action (same as pressing the \"Activate/Cancel\" button)" },
		{ "MainWindow.active()", "Returns \"true\" if KShutdown is active (e.g. the countdown is active)" },
		//!!!{ "MainWindow.setExtrasCommand(string command)", "", "" }
	};

	for (const QStringList &api : apiList) {
		rows += { "Function:" , api[0] };
		rows += { "Description:", api[1] };

		if (api.count() >= 3)
			rows += { "Example:", api[2] };

		if (api.count() == 4)
			rows += { "Output:", api[3] };

		rows += QStringList();
	}

	rows += { UWidgets::CELL_HEADER + "Examples" }; // no i18n

	QList<QStringList> exampleList {
		{
			"qdbus command example:",
			"qdbus " + SERVICE + " " + PATH + " setSelectedAction test"
		},
		{
			"dbus-send command example:",
			"dbus-send --print-reply --dest=" + SERVICE + " " + PATH + " \\\n " + SERVICE + ".MainWindow.setSelectedAction string:test"
		},
		{
			"Lock Screen at the specified time:",
			"qdbus net.sf.kshutdown /kshutdown setSelectedAction lock\n"
			"qdbus net.sf.kshutdown /kshutdown setTime date-time 21:00"//!!!!
		},
		{
			"Cancel an active action:",
			"qdbus net.sf.kshutdown /kshutdown setActive false"
		},
/* FIXME:
		{
			"Quit KShutdown:",
			"qdbus net.sf.kshutdown /MainApplication quit"
		}
*/
	};

	for (const QStringList &example : exampleList) {
		rows += { UWidgets::CELL_HTML + "<b>" + example[0].toHtmlEscaped() + "</b>" };
		rows += { UWidgets::CELL_HTML + example[1].toHtmlEscaped().replace("\n", "<br>") };
		rows += QStringList();
	}

	auto *htmlWidget = UWidgets::newHTMLTableView(rows);

	auto *wikiButton = UWidgets::newLinkButton(
		"Wiki",
// TODO: update/remove https://sourceforge.net/p/kshutdown/wiki/D-Bus/
		"https://sourceforge.net/p/kshutdown/wiki/D-Bus/"
	);

	UDialog::largeWidget(parent, htmlWidget, "D-Bus", { wikiButton });
}
#endif // QT_DBUS_LIB

void CLI::showHelp(QWidget *parent) {
	QString plainTextTrimmed = m_args->helpText()
		.trimmed();

	QList<QStringList> rows;

	for (const QString &rawLine : plainTextTrimmed.split('\n')) {
		QString trimmedLine = rawLine.trimmed();

		auto pair = Utils::splitPair(trimmedLine, "  "/* 2 spaces */, true);
		if ( ! pair.isEmpty()) {
			QString name = pair.first() + "    "/* 4 non-breaking spaces as a right margin */;
			QString desc = pair.last();
			rows += { name, desc };
		}
		else {
			if (rawLine.startsWith("    "/* 4 spaces; assume wrapped text continuation */))
				rows += { "", trimmedLine };
			else if (trimmedLine.endsWith(':'))
				rows += { UWidgets::CELL_HEADER + trimmedLine };
			else if ( ! trimmedLine.isEmpty())
				rows += { trimmedLine };
		}
	}

	auto *htmlWidget = UWidgets::newHTMLTableView(rows);

	auto *wikiButton = UWidgets::newLinkButton(
		"Wiki",
		"https://sourceforge.net/p/kshutdown/wiki/Command%20Line/"
	);

	UDialog::largeWidget(parent, htmlWidget, i18n("Command Line Options"), { wikiButton });
}

// TimeOption

void TimeOption::init() {
	m_absolute = false;
	m_relative = false;
	m_option = CLI::getTimeOption();
	m_time = QTime();
	
	if (m_option.isEmpty())
		return;
	
	qDebug() << "Time option: " << m_option;
	if ((m_option == "0") || (m_option.compare("NOW", Qt::CaseInsensitive) == 0)) {
		m_time = QTime(0, 0);
		m_relative = true;
	}
	else if (m_option.count(":") == 1) {
		m_time = parseTime(m_option);
		if (m_time.isValid())
			m_absolute = true;
	}
	else {
		auto minutes = 0min;
		int size = m_option.size();
		std::optional<int> duration;

		if ((size > 1) && m_option.endsWith('H', Qt::CaseInsensitive)) {
			duration = Utils::toInt(m_option.mid(0, size - 1));

			if (duration) {
				auto hours = std::chrono::hours(duration.value());
				minutes = hours;

				if (hours == Utils::DAY_HOURS)
					minutes--;
			}
		}
		else if ((size > 1) && m_option.endsWith('M', Qt::CaseInsensitive)) {
			duration = Utils::toInt(m_option.mid(0, size - 1));

			if (duration)
				minutes = std::chrono::minutes(duration.value());
		}
		else {
			duration = Utils::toInt(m_option);

			if (duration)
				minutes = std::chrono::minutes(duration.value());
		}

		if (duration && (minutes > 0min) && (minutes < Utils::DAY_HOURS)) {
			m_time = Utils::fromMinutes(minutes);
			m_relative = true;
		}
	}
	//qDebug() << "Absolute: " << m_absolute;
	//qDebug() << "Relative: " << m_relative;
	//qDebug() << "QTime: " << m_time;
	//qDebug() << "QTime.isNull(): " << m_time.isNull();
	//qDebug() << "QTime.isValid(): " << m_time.isValid();
	//qDebug() << "TimeOption::isError(): " << isError();
	//qDebug() << "TimeOption::isValid(): " << isValid();
}

bool TimeOption::isError() {
	return !isValid() && !m_option.isEmpty();
}

bool TimeOption::isValid() {
	return m_time.isValid() && (m_absolute || m_relative);
}

QTime TimeOption::parseTime(const QString &time) {
	auto cLocale = QLocale::c();

	QTime result = cLocale.toTime(time, Trigger::TIME_PARSE_FORMAT);

	// try alternate AM/PM format
	if (!result.isValid())
		result = cLocale.toTime(time, "h:mm AP");

	return result;
}

QString TimeOption::formatDateTime(const QDateTime &dateTime) {
	auto cLocale = QLocale::c();

	return cLocale.toString(dateTime.time(), Trigger::TIME_PARSE_FORMAT);
}

QString TimeOption::formatTime(const QTime &time) {
	seconds s = Utils::toSeconds(time);
	minutes m = duration_cast<minutes>(s);

	if ((m.count() % 60) == 0) {
		hours h = duration_cast<hours>(m);

		return QString::number(h.count()) + "h";
	}

	return QString::number(m.count()) + "m";
}

void TimeOption::setupMainWindow() {
	//qDebug() << "TimeOption::setupMainWindow(): " << m_action->text();
	
	MainWindow *mainWindow = MainWindow::self();
	mainWindow->setActive(false);
	
	mainWindow->setSelectedAction(m_action->id());
	
	QString trigger;
	if (CLI::isArg("inactivity")) {
// TODO: better error message if invalid time
		// set error mode
		if (!m_relative) {
			m_absolute = false;
			
			return;
		}
			
		trigger = "idle-monitor";
	}
	else {
		trigger = m_absolute ? "date-time" : "time-from-now";
	}

	mainWindow->setTime(trigger, m_time);
}
