// 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 <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 scale = Utils::toDouble(argv[i])) {
				qputenv("QT_SCALE_FACTOR", QByteArray::number(qBound(1.0, scale.value(), 3.0)));
			}
		}
	}
}

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);
}

void CLI::showHelp(QWidget *parent) {
	auto *htmlWidget = Utils::newHTMLView();

	auto palette = htmlWidget->palette();

	const QColor &bg = palette.color(QPalette::Base);
	const QColor &bg2 = palette.color(QPalette::AlternateBase);
	const QColor &fg = palette.color(QPalette::Text);

	QString plainTextTrimmed = m_args->helpText()
		.trimmed();

	QString html = "";

	// BUG: Bold font weight is ignored in some monospaced fonts #linux
	//      until you manually remove ",Book" (or similar) from ~/.config/kdeglobals
	//      https://bugs.kde.org/show_bug.cgi?id=378523

	// HACK: CSS "monospace" font is undefined in Qt...
	QString fontFamily = Utils::getMonospaceFontName();
	html += "<table cellspacing=\"0\" cellpadding=\"1\" style=\"background-color: " + bg.name() + "; color: " + fg.name() + "; font-family: '" + fontFamily + "'; font-size: large\">\n";
	//qDebug() << html;

	int rowNum = 0;
	QString rowStyle = "";

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

		html += "<tr>\n";

		auto pair = Utils::splitPair(trimmedLine, "  "/* 2 spaces */, true);
		if (!pair.isEmpty()) {
			QString name = pair.first();
			QString desc = pair.last();

			rowStyle = ((rowNum % 2) == 0)
				? "background-color: " + bg2.name()
				: "";
			rowNum++;

			html += "\t<td style=\"padding-right: 20px; " + rowStyle + "\"><b>" + name.toHtmlEscaped() + "</b></td>\n";
			html += "\t<td style=\"" + rowStyle + "\">" + desc.toHtmlEscaped() + "</td>\n";
		}
		else {
			if (rawLine.startsWith("    "/* 4 spaces; assume wrapped text continuation */)) {
				html += "\t<td style=\"" + rowStyle + "\"></td>\n";
				html += "\t<td style=\"" + rowStyle + "\">" + trimmedLine.toHtmlEscaped() + "</td>\n";
			}
			else if (trimmedLine.isEmpty()) {
				html += "\t<td colspan=\"2\"><hr /></td>\n";
			}
			else if (trimmedLine.endsWith(':')) {
				html += "\t<td colspan=\"2\" style=\"padding-top: 0px; padding-bottom: 1em\"><h2>" + trimmedLine.toHtmlEscaped() + "</h2></td>\n";
			}
			else {
				html += "\t<td colspan=\"2\">" + trimmedLine.toHtmlEscaped() + "</td>\n";
			}
		}

		html += "</tr>\n";
	}

	html += "</table>";

	htmlWidget->setText(Utils::makeHTML(html));

// DEBUG: Utils::println(html);

	std::unique_ptr<QAction> wikiAction(Utils::newLinkAction(
		"Wiki",
		"https://sourceforge.net/p/kshutdown/wiki/Command%20Line/"
	));

	auto *wikiButton = new QPushButton(wikiAction->text());
	wikiButton->setToolTip(wikiAction->toolTip());

	QObject::connect(wikiButton, &QPushButton::clicked, [&wikiAction]() {
		wikiAction->trigger();
	});

	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);
}
