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

#include "config.h"
#include "udialog.h"
#include "utils.h"
#include "uwidgets.h"
#include "version.h"

#include <QDate>
#include <QDebug>
#include <QDesktopServices>
#include <QStyle>
#include <QTextBrowser>
#include <QToolTip>
#include <QWidgetAction>

#ifdef KS_KF5
	#include <KAuthorized>
#endif // KS_KF5

// public:

ToolTipFilter::ToolTipFilter(QObject *parent) : QObject(parent) { }

// protected:

bool ToolTipFilter::eventFilter(QObject *object, QEvent *event) {
	// HACK: do not display tool tip if same as text
	if (event->type() == QEvent::ToolTip) {
		auto *menu = dynamic_cast<QMenu *>(object);
		if (menu != nullptr) {
			QAction *action = menu->activeAction();

			if (
				(action != nullptr) &&
				(action->toolTip() == action->text().remove('&'))
			)
				return true;
		}
	}

	return QObject::eventFilter(object, event);
}

// public

void Utils::addTitle(QMenu *menu, const QIcon &icon, const QString &text) {
	// NOTE: "QMenu::addSection" is fully implemented only in Oxygen style...

	auto *titleLabel = new ULabel(menu);
	titleLabel->setMarginAndSpacing(5_px, 5_px);
	setFont(titleLabel->textLabel(), -1, true);

	titleLabel->setIcon(icon, getSmallIconSize().width());
	titleLabel->setText(text);

	auto *widgetAction = new QWidgetAction(menu);
	widgetAction->setDefaultWidget(titleLabel);
	menu->addAction(widgetAction);
}

QAction *Utils::execMenu(QMenu *menu) {
	// do not show menu near the mouse cursor to avoid accidental "Shut Down" click
	const int safeOffset = 10_px;

// HEISENBUG: if the menu popups over a Konsole output window the mouse cursor *moves* itself twice #wtf
	return menu->exec(QCursor::pos() + QPoint(safeOffset, safeOffset));
}

QTime Utils::fromMinutes(const minutes &minutes) {
	return fromSeconds(minutes);
}

QTime Utils::fromSeconds(const seconds &seconds) {
	QTime zeroTime(0, 0);

	return zeroTime.addSecs(seconds.count());
}

QString Utils::getAntiqueMessage() {
	return
		i18n("NOTE: This KShutdown version is more than 2 years old.") + "\n" +
		"\n" +
		i18n("If it works correctly on your system, you can ignore this message.");
}

QDir Utils::getAppDir() {
	#ifdef Q_OS_LINUX
	QFileInfo appImage = getAppImageInfo();

	if (appImage.isFile())
		return appImage.dir();
	#endif // Q_OS_LINUX

	return QDir(QApplication::applicationDirPath());
}

#ifdef Q_OS_LINUX
QFileInfo Utils::getAppImageInfo() {
	auto APPDIR = qEnvironmentVariable("APPDIR", "");
	auto APPIMAGE = qEnvironmentVariable("APPIMAGE", "");

	if (APPDIR.isEmpty() || APPIMAGE.isEmpty())
		return QFileInfo();

	QDir dir(APPDIR);

	if (! dir.exists())
		return QFileInfo();

	QFileInfo bin(dir.path() + "/usr/bin/kshutdown");

	if ( ! bin.isFile()) {
		qWarning() << "Not our AppImage:" << APPDIR;

		return QFileInfo();
	}

	QFileInfo image(APPIMAGE);

	if (image.isFile())
		return image;

	return QFileInfo();
}
#endif // Q_OS_LINUX

QString Utils::getApplicationVersionInfo() {
	QString result = QApplication::applicationDisplayName() + " " + QApplication::applicationVersion() +
		" (" + KS_RELEASE_DATE + ")";

	if (Config::isPortable())
		result += " | Portable";

	return result;
}

UPath Utils::getAutostartDir(const bool mkdir) {
	QString xdgConfigHomeEnv = qEnvironmentVariable("XDG_CONFIG_HOME", "")
		.trimmed();

	UPath dir;
	if (! xdgConfigHomeEnv.isEmpty()) {
		dir = xdgConfigHomeEnv.toStdString();

		if (! std::filesystem::is_directory(dir) || ! Utils::isWritable(dir))
			dir = "";
	}

	if (dir.empty()) {
		UPath homeDir = QDir::home().path().toStdString();

		dir = homeDir / ".config/autostart";
	}
	else {
		dir = dir / "autostart";
	}

	if (mkdir)
		std::filesystem::create_directories(dir);

	return dir;
}

QString Utils::getBuildTypeInfo() {
	QStringList result;

	#ifdef KS_KF5
	result += "KS_KF5";
	#else
	result += "KS_PURE_QT";
	#endif // KS_KF5

	#ifdef KS_PORTABLE
	result += "KS_PORTABLE";
	#endif // KS_PORTABLE

	result += "QT_VERSION_STR=" + QString(QT_VERSION_STR);

	#ifdef Q_OS_LINUX
	result += "Q_OS_LINUX";
	#elif defined(Q_OS_WIN32)
	result += "Q_OS_WIN32";
	#elif defined(Q_OS_HAIKU)
	result += "Q_OS_HAIKU";
	#else
	result += "Q_OS_?";
	#endif

	return result.join(" | ");
}

QSize Utils::getLargeIconSize() {
	int i = QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize);

	return { i, i };
}

QString Utils::getMonospaceFontName() {
	#ifdef Q_OS_WIN32
	return "Consolas";
	#else
	return "DejaVu Sans Mono";
	#endif // Q_OS_WIN32
}

QString Utils::getQtVersionInfo() {
	return "Qt " + QString(qVersion());
}

QSize Utils::getSmallIconSize() {
	int i = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize);

	return { i, i };
}

QString Utils::getSystemInfo() {
	return QSysInfo::prettyProductName() + " | " +
		QSysInfo::kernelType() + " " + QSysInfo::kernelVersion() + " | " +
		QSysInfo::currentCpuArchitecture();
}

QString Utils::getUser() {
	#ifdef Q_OS_WIN32
	QString USERNAME = m_env.value("USERNAME");
	
	if (! USERNAME.isEmpty())
		return USERNAME;
	#else
	QString LOGNAME = m_env.value("LOGNAME");
	
	if (!LOGNAME.isEmpty())
		return LOGNAME;

	QString USER = m_env.value("USER");
	
	if (!USER.isEmpty())
		return USER;
	#endif // Q_OS_WIN32
		
	return { };
}

#ifndef Q_OS_WIN32
static void printCond(const Cond &cond) {
	if (cond)
		qInfo("%s: FOUND", KS_S(cond.name()));
	//else
	//	qDebug("%s: NOT FOUND", KS_S(cond.name()));
}
#endif

void Utils::init() {
	QDate releaseDate = QDate::fromString(KS_RELEASE_DATE, Qt::ISODate);
	// TEST: QDate releaseDate = QDate::fromString("2018-01-02", Qt::ISODate);
	// TEST: QDate releaseDate = QDate::fromString("2030-01-02", Qt::ISODate);
	// TEST: QDate releaseDate = QDate::fromString("INVALID" + KS_RELEASE_DATE, Qt::ISODate);
	//qDebug() << KS_RELEASE_DATE << releaseDate << releaseDate.isValid();
	if (releaseDate.isValid()) {
		auto age = releaseDate.daysTo(QDate::currentDate());
		m_antique = age > (365 * 2);
		//qDebug() << "AGE:" << age << ", ANTIQUE:" << m_antique;
	}

// TODO: disable such detection on Windows
	m_desktopSession = m_env.value("DESKTOP_SESSION");
	m_xdgCurrentDesktop = m_env.value("XDG_CURRENT_DESKTOP");
	QString xdgSessionType = m_env.value("XDG_SESSION_TYPE");
	m_desktopInfo = "DESKTOP_SESSION=\"" + m_desktopSession + "\" | XDG_CURRENT_DESKTOP=\"" + m_xdgCurrentDesktop + "\"" +
		" | XDG_SESSION_TYPE=\"" + xdgSessionType + "\"";

	m_kdeSessionVersion = m_env.value("KDE_SESSION_VERSION")
		.toInt();

	// DOC: https://specifications.freedesktop.org/menu-spec/latest/apb.html

// TODO: test
	haiku = Cond(
		"Haiku",
		#ifdef Q_OS_HAIKU
		true
		#else
		false
		#endif // Q_OS_HAIKU
	);

	linuxOS = Cond(
		"Linux",
		#ifdef Q_OS_LINUX
		true
		#else
		false
		#endif // Q_OS_LINUX
	);

	#ifdef Q_OS_WIN32
	unixLike = Cond("Unix", false);
	windows = Cond("Windows", true);
	#else
	unixLike = Cond("Unix", true);
	windows = Cond("Windows", false);
	#endif // Q_OS_WIN32

// TODO: test
	cinnamon = Cond("Cinnamon", (m_xdgCurrentDesktop == "Cinnamon") || dsContainsIgnoreCase("cinnamon"));

	enlightenment = Cond("Enlightenment", (m_xdgCurrentDesktop == "Enlightenment") || dsContainsIgnoreCase("enlightenment"));
	kde = Cond("KDE", isKDE());

// TODO: Utils::isDesktop(QString)
	lxde = Cond("LXDE", (m_xdgCurrentDesktop == "LXDE") || dsContainsIgnoreCase("LXDE"));

	lxqt = Cond("LXQt", (m_xdgCurrentDesktop == "LXQt") || dsContainsIgnoreCase("LXQT"));
	mate = Cond("MATE", (m_xdgCurrentDesktop == "MATE") || dsContainsIgnoreCase("mate"));
	xfce = Cond("Xfce", (m_xdgCurrentDesktop == "XFCE") || dsContainsIgnoreCase("xfce"));

	#ifndef Q_OS_WIN32
// TODO: cleanup log texts
	if (enlightenment) {
		qWarning("%s: Load System → DBus Extension module for Lock Screen action support", KS_S(enlightenment.name()));
		qWarning("%s: Load Utilities → Systray module for system tray support", KS_S(enlightenment.name()));
	}

	Cond kdeFullSession(QString("KDE Full Session (version %1)").arg(m_kdeSessionVersion), isKDEFullSession());
	printCond(kdeFullSession);

	Cond gnome("GNOME", isGNOME());
	Cond openbox("Openbox", isOpenbox());
	Cond trinity("Trinity", isTrinity());
	Cond unity("Unity", isUnity());

	int foundCount = 0;
	for (auto &i : { cinnamon, enlightenment, gnome, haiku, kde, lxde, lxqt, mate, openbox, trinity, unity, windows, xfce }) {
		printCond(i);

		if (i)
			foundCount++;
	}

	switch (foundCount) {
		case 0:
			qWarning("Unknown or unsupported Desktop Environment: %s", KS_S(m_desktopInfo));
			break;
		case 1:
			break;
		default:
			qWarning("More than one Desktop Environment detected from: %s", KS_S(m_desktopInfo));
			break;
	}
	#endif // !Q_OS_WIN32
}

bool Utils::isDark(const QColor &color) {
	int yiq = ((color.red() * 299) + (color.green() * 587) + (color.blue() * 114)) / 1'000;

	return (yiq < 128);
}

bool Utils::isDark(const QWidget *widget) {
	return
		(widget != nullptr) &&
		isDark(widget->palette().color(QPalette::Window));
}

bool Utils::isGNOME() {
	return
		m_desktopSession.contains("gnome", Qt::CaseInsensitive) ||
		m_xdgCurrentDesktop.contains("gnome", Qt::CaseInsensitive);
}

bool Utils::isGTKStyle() {
	#ifdef Q_OS_WIN32
	return false;
	#else
	return cinnamon || isGNOME() || haiku || lxde || mate || xfce || isUnity();
	#endif // Q_OS_WIN32
}

bool Utils::isKDEFullSession() {
	return m_env.value("KDE_FULL_SESSION") == "true";
}

bool Utils::isKDE() {
	return
// TODO: maybe remove full session detection
		isKDEFullSession() &&
		(
			(m_xdgCurrentDesktop == "KDE") ||
			m_desktopSession.contains("kde", Qt::CaseInsensitive) ||
			m_xdgCurrentDesktop.contains("kde", Qt::CaseInsensitive) ||
			(m_kdeSessionVersion >= 4)
		);
}

bool Utils::isOpenbox() {
	// NOTE: Use "contains" instead of "compare"
	// to correctly detect "DESKTOP_SESSION=/usr/share/xsessions/openbox".
	// BUG: https://sourceforge.net/p/kshutdown/bugs/31/
	return
		m_desktopSession.contains("openbox", Qt::CaseInsensitive) ||
		m_xdgCurrentDesktop.contains("openbox", Qt::CaseInsensitive);
}

bool Utils::isRestricted(const QString &action) {
#ifdef KS_KF5
	return !KAuthorized::authorize(action);
#else
	Q_UNUSED(action)

	return false;
#endif // KS_KF5
}

bool Utils::isTrinity() {
	return
		m_desktopSession.contains("TRINITY", Qt::CaseInsensitive) ||
		m_xdgCurrentDesktop.contains("TRINITY", Qt::CaseInsensitive);
}

bool Utils::isUnity() {
	return
		m_desktopSession.contains("UBUNTU", Qt::CaseInsensitive) ||
		m_xdgCurrentDesktop.contains("UNITY", Qt::CaseInsensitive);
}

bool Utils::isWritable(const UPath &path) {
	return QFileInfo(QString::fromStdString(path.string()))
		.isWritable();
}

QString Utils::makeHTML(const QString &html) {
	return HTML_START + html + HTML_END;
}

QString Utils::makeLink(QWidget *widget, const QString &href, const QString &text) {
	auto fg = isDark(widget) ? QColorConstants::Svg::lightskyblue : QColorConstants::Svg::royalblue;

	return QString("<a href=\"%1\" style=\"color: %2; text-decoration: none\">%3</a>")
		.arg(href.toHtmlEscaped())
		.arg(fg.name())
		.arg(text.toHtmlEscaped());
}

QString Utils::makeTitle(const QString &s1, const QString &s2) {
	if (s1 == s2)
		return s1;

	QStringList result;

	if (! s1.isEmpty())
		result += s1;

	if (! s2.isEmpty())
		result += s2;

	return result.join(TITLE_SEPARATOR);
}

QFormLayout *Utils::newFormLayout(QWidget *parent) {
	auto *layout = new QFormLayout(parent);
	layout->setLabelAlignment(Qt::AlignRight); // HACK: force "correct" alignment
	layout->setSpacing(10_px);

	return layout;
}

QHBoxLayout *Utils::newHeader(const QString &text, QWidget *buddy, const int relativeSize) {
	auto *layout = UWidgets::newHBoxLayout({ }, 15_px);

	if (!text.isEmpty()) {
		auto *label = newLabel(text, buddy);
		setFont(label, relativeSize, true);
		layout->addWidget(label);
	}

	auto *hLine = new QFrame();
// FIXME: actually none of the QFrame styles visually matches QStyle
	#ifdef Q_OS_WIN32
	hLine->setFrameStyle(QFrame::HLine | QFrame::Sunken);
	#else
	hLine->setFrameStyle(QFrame::HLine | QFrame::Plain);
	#endif // Q_OS_WIN32

	if (!text.isEmpty())
		hLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);

	layout->addWidget(hLine);

	return layout;
}

QTextEdit *Utils::newHTMLView() {
	auto *textEdit = new QTextBrowser();
	textEdit->setWordWrapMode(QTextOption::NoWrap);

	// open "file:/" links in external app

	textEdit->setOpenLinks(false);
	QObject::connect(textEdit, &QTextBrowser::anchorClicked, [](const QUrl &link) {
		openInExternalApp(link);
	});

	return textEdit;
}

QLabel *Utils::newLabel(const QString &text, QWidget *buddy) {
	auto *l = new QLabel(text);
	l->setBuddy(buddy);

	return l;
}

QPlainTextEdit *Utils::newTextEdit(const QString &text, const QPlainTextEdit::LineWrapMode lineWrapMode) {
	auto *textEdit = new QPlainTextEdit(text);
	textEdit->setStyleSheet("QPlainTextEdit { font-family: \"" + Utils::getMonospaceFontName() + "\"; }");

// TODO: line wrap option (?)
	textEdit->setLineWrapMode(lineWrapMode);

	return textEdit;
}

bool Utils::openInExternalApp(const QUrl &url, const bool showErrorMessage) {
	bool ok = QDesktopServices::openUrl(url);

	if (! ok && showErrorMessage)
		qCritical() << i18n("Could not open") << url;

	return ok;
}

bool Utils::openInExternalApp(const UPath &path, const bool showErrorMessage) {
	QString s = QString::fromStdString(path.string());

	if (! std::filesystem::exists(path) && showErrorMessage) {
		UDialog::error(
			nullptr,
			i18n("Could not open") + "\n" +
			"\n" +
			s
		);

		return false;
	}

	return QDesktopServices::openUrl(QUrl::fromLocalFile(s));
}

void Utils::println(const QString &text) {
	QTextStream out(stdout);
	out << text << Qt::endl;
}

QString Utils::read(QProcess &process, bool &ok) {
	ok = false;

	if (!process.waitForStarted(-1)) {
		QString message = i18n("Error: %0").arg(
			process.program() + " " + process.arguments().join(" ") + "\n" +
			process.errorString()
		);

		return message;
	}

	QString err = "";
	QString out = "";

	while (process.waitForReadyRead(-1)) {
		out += QString::fromUtf8(process.readAllStandardOutput());
		err += QString::fromUtf8(process.readAllStandardError());
	}

	if (!err.isEmpty())
		return err;

	ok = true;

	return out;
}

bool Utils::run(const QString &program, const QStringList &args, const QString &workingDirectory, const bool waitFor) {
// TODO: remove APPDIR/APPIMAGE from new process environment (?)

	qDebug() << "Running" << program << args << "with working directory" << workingDirectory << "detached" << !waitFor;

	bool ok;

// TODO: workingDirectory
	if (waitFor)
		ok = QProcess::execute(program, args) >= 0;
	else
		ok = QProcess::startDetached(program, args, workingDirectory);

	if (!ok)
		qCritical() << "Process failed to start:" << program;

	return ok;
}

bool Utils::runSplitCommand(const QString &programAndArgs, const QString &workingDirectory, const bool waitFor) {
	QStringList list = QProcess::splitCommand(programAndArgs);

	if (list.isEmpty())
		return false;

	QString program = list.takeFirst();

	return run(program, list, workingDirectory, waitFor);
}

void Utils::setFont(QWidget *widget, const int relativeSize, const bool bold) {
	QFont newFont(widget->font());
	if (bold)
		newFont.setBold(bold);
	int size = newFont.pointSize();
	if (size != -1) {
		newFont.setPointSize(qMax(8, size + relativeSize));
	}
	else {
		size = newFont.pixelSize();
		newFont.setPixelSize(qMax(8_px, size + relativeSize));
	}
	widget->setFont(newFont);
}

void Utils::setMargin(QLayout *layout, const int margin) {
	// RANT: WTF? #qt6
	layout->setContentsMargins(margin, margin, margin, margin);
}

void Utils::showToolTips(QMenu *menu) {
	// HACK: hide previous tool tip window if the new tool tip is empty...
	QObject::connect(menu, &QMenu::hovered, [](QAction *action) {
		if (QToolTip::isVisible()) {
			#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
			auto list = action->associatedWidgets();
			#else
			auto list = action->associatedObjects();
			#endif // QT_VERSION
			if (list.count() > 0) {
				QString toolTip = action->toolTip();

				// HACK: '&' = Alt shortcut auto added by KF5
				if (toolTip == action->text().remove('&'))
					QToolTip::hideText();
			}
		}
	});

	ToolTipFilter *filter = new ToolTipFilter(menu);
	menu->installEventFilter(filter);

	menu->setToolTipsVisible(true);
}

QStringList Utils::splitPair(const QString &s, const char separator, const bool trim) {
	int i = s.indexOf(separator);

	if (i == -1)
		return { };

	QString first = s.left(i);
	QString second = s.mid(i + 1);

	if (trim) {
		first = first.trimmed();
		second = second.trimmed();
	}

	return { first, second };
}

QStringList Utils::splitPair(const QString &s, const QString &separator, const bool trim) {
	int i = s.indexOf(separator);

	if (i == -1)
		return { };

	QString first = s.left(i);
	QString second = s.mid(i + 1);

	if (trim) {
		first = first.trimmed();
		second = second.trimmed();
	}

	return { first, second };
}

std::optional<double> Utils::toDouble(const QString &s) {
	bool ok = false;
	double d = s.toDouble(&ok);

	return ok ? std::optional<double>{d} : std::nullopt;
}

std::optional<int> Utils::toInt(const QString &s) {
	bool ok = false;
	int i = s.toInt(&ok);

	return ok ? std::optional<int>{i} : std::nullopt;
}

std::optional<qint64> Utils::toInt64(const QString &s) {
	bool ok = false;
	qint64 i = s.toLongLong(&ok);

	return ok ? std::optional<qint64>{i} : std::nullopt;
}

seconds Utils::toSeconds(const QTime &time) {
	auto s = 0s;
	s += hours(time.hour());
	s += minutes(time.minute());
	s += seconds(time.second());

	return s;
}

QString Utils::trim(QString &text, const int maxLength) {
	if (text.length() > maxLength) {
		text.truncate(maxLength);
		text = text.trimmed();
		text.append("...");
	}
	
	return text;
}

// private:

bool Utils::dsContainsIgnoreCase(const QString &de) {
	return m_desktopSession.contains(de, Qt::CaseInsensitive);
}
