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

#include "processmonitor.h"

#include "../config.h"
#include "../utils.h"
#include "../uwidgets.h"

#include <QAbstractItemView>
#include <QCryptographicHash>
#include <QDebug>
#include <QHBoxLayout>
#include <QPushButton>

#ifndef Q_OS_WIN32
	#include <csignal> // for ::kill
#endif // !Q_OS_WIN32

// public

Process::Process(QObject *parent, const QString &command, qint64 pid)
	: QObject(parent),
	m_pid(pid),
	m_command(command) {
}

QIcon Process::icon() const {
	#ifndef Q_OS_WIN32
	// show icons for own processes only (faster)
	if ( ! own())
		return QIcon();

	if (ProcessMonitor::m_noIconsLastThemeName != QIcon::themeName()) {
		//qDebug() << "Icon theme changed for icon:" << m_command;

		return QIcon::fromTheme(m_command);
	}

	// Use m_noIconsList to remember which icon is not available to skip its loading the next time

	QString hash = QCryptographicHash::hash(("kshutdown-" + m_command).toUtf8(), QCryptographicHash::Sha1)
		.toHex();

	if (ProcessMonitor::m_noIconsList.contains(hash)) {
		//qDebug() << "Known empty icon:" << hash << m_command;

		return QIcon();
	}

	QIcon icon = QIcon::fromTheme(m_command);

	if (icon.isNull()) {
		//qDebug() << "Caching empty icon:" << hash << m_command;

		ProcessMonitor::m_noIconsList += hash;
	}

	return icon;
	#else
/* FIXME: still crashy?
	if (!visible())
		return QIcon();

	ULONG_PTR iconHandle = ::GetClassLongPtr(windowHandle(), GCLP_HICONSM);

	if (iconHandle != 0)
		return QPixmap::fromWinHICON((HICON)iconHandle);
*/
	return QIcon();
	#endif // !Q_OS_WIN32
}

bool Process::important() const {
	#ifdef Q_OS_WIN32
	return m_visible;
	#else
	return m_own;
	#endif // Q_OS_WIN32
}

bool Process::isRunning() const {
	#ifndef Q_OS_WIN32
	if (::kill(m_pid, 0)) { // check if process exists
		switch (errno) {
			case EINVAL:
			case ESRCH:
				return false;
			case EPERM:
				return true;
		}
	}

	return true;
	#else
	return ::IsWindow(m_windowHandle) != 0;
	#endif // !Q_OS_WIN32
}

// private:

void Process::makeStringCache() {
	#ifndef Q_OS_WIN32
	m_stringCache = QString("%1 (pid %2, %3)")
		.arg(m_command, QString::number(m_pid), m_user);
	#else
	m_stringCache = QString("%1 (pid %2)")
		.arg(m_command, QString::number(m_pid));
	#endif // !Q_OS_WIN32
}

// public

ProcessMonitor::ProcessMonitor()
	: Trigger(i18n("When selected application exit"), "application-exit", "process-monitor"),
	m_processList(QList<Process*>())
{
	setCheckInterval(2s);
}

void ProcessMonitor::addProcess(Process *process) {
	process->makeStringCache();

	m_processList.append(process);
}

bool ProcessMonitor::canActivateAction() {
	if (m_processList.isEmpty())
		return false;

	int index = m_processesComboBox->currentIndex();
	Process *p = m_processList.value(index);
	updateStatus(p);

	return !p->isRunning();
}

void ProcessMonitor::initContainerWidget() {
	m_processesComboBox = new QComboBox();
	m_processesComboBox->view()->setAlternatingRowColors(true);
	m_processesComboBox->setFocusPolicy(Qt::StrongFocus);
	m_processesComboBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);

	connect(m_processesComboBox, QOverload<int>::of(&QComboBox::activated), [this](const int index) {
		updateStatus(m_processList.value(index));
	});

	auto *refreshButton = new QPushButton();
	refreshButton->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred));
	refreshButton->setText(i18n("Refresh"));
	connect(refreshButton, &QPushButton::clicked, [this] { onRefresh(); });

	auto *layout = makeHBoxLayout();
	layout->addWidget(m_processesComboBox);
	layout->addWidget(refreshButton);

	onRefresh();
}

void ProcessMonitor::readConfig() {
	m_recentPid = Config::readVariant(configGroup(), "Recent Pid", -1)
		.toLongLong();

	m_recentProgram = Config::readString(configGroup(), "Recent Program", "");

	m_noIconsLastThemeName = QIcon::themeName();
	m_noIconsList = Config::readString(configGroup(), "No Icons " + m_noIconsLastThemeName, "")
		.split(' ', Qt::SkipEmptyParts);
}

void ProcessMonitor::writeConfig() {
	Config::writeVariant(configGroup(), "Recent Pid", m_recentPid);
	Config::writeString(configGroup(), "Recent Program", m_recentProgram);

	if (m_noIconsLastThemeName == QIcon::themeName())
		Config::writeString(configGroup(), "No Icons " + m_noIconsLastThemeName, m_noIconsList.join(' '));
}

void ProcessMonitor::setPID(const qint64 pid) {
	#ifndef Q_OS_WIN32
	clearAll();
	
	auto *p = new Process(this, "?", pid);
	p->m_own = false;
	p->m_user = '?';
	addProcess(p);

	m_processesComboBox->addItem(p->icon(), p->toString());
	#else
	Q_UNUSED(pid)
	#endif // !Q_OS_WIN32
}

// private

void ProcessMonitor::clearAll() {
	qDeleteAll(m_processList);
	m_processesComboBox->clear();
	m_processList.clear();
}

void ProcessMonitor::errorMessage() {
	clearAll();
	m_processesComboBox->setEnabled(false);

	m_processesComboBox->addItem(
		QIcon::fromTheme("dialog-error"),
		i18n("Error")
	);

	m_processesComboBox->setToolTip(m_errorMessage);
}

#ifdef Q_OS_WIN32
// CREDITS: http://stackoverflow.com/questions/7001222/enumwindows-pointer-error
BOOL CALLBACK EnumWindowsCallback(HWND windowHandle, LPARAM param) {
	// exclude KShutdown...
	DWORD pid = 0;
	::GetWindowThreadProcessId(windowHandle, &pid);

	if (pid == qApp->applicationPid())
		return TRUE;

	auto *processMonitor = reinterpret_cast<ProcessMonitor *>(param);

	int textLength = ::GetWindowTextLengthW(windowHandle) + 1;
	wchar_t *textBuf = new wchar_t[textLength];
	int result = ::GetWindowTextW(windowHandle, textBuf, textLength);
	if (result > 0) {
		QString title = QString::fromWCharArray(textBuf, result);

// TODO: show process name (*.exe)
		Process *p = new Process(processMonitor, Utils::trim(title, 30), pid);
		p->setVisible(::IsWindowVisible(windowHandle));
		p->setWindowHandle(windowHandle);
		processMonitor->addProcess(p);
	}

	delete[] textBuf;

	return TRUE;
}
#endif // Q_OS_WIN32

void ProcessMonitor::refreshProcessList() {
	#ifndef Q_OS_WIN32
	QStringList args;
	// show all processes
	args << "-A";
	// order: user pid command
// TODO: args << "-o" << "user=,pid=,command=";
// http://sourceforge.net/p/kshutdown/bugs/11/
	args << "-o" << "user=,pid=,comm=";

	QProcess process;
	process.start("ps", args);
	process.waitForStarted(-1);
	qint64 psPID = process.processId();

	m_errorMessage = "";
	bool ok = false;
	QString text = Utils::read(process, ok);

	if (!ok) {
		m_errorMessage = text;

		return;
	}

	qint64 appPID = QApplication::applicationPid();
	QString user = Utils::getUser();
	QStringList processLines = text.split('\n');

	for (const QString &line : processLines) {
		QStringList processInfo = line.simplified()
			.split(' ');

		if (processInfo.count() >= 3) {
			auto i = Utils::toInt64(processInfo[1]);

			if (! i) {
				qCritical() << "Invalid process ID:" << processInfo[1];

				continue; // for
			}

			qint64 processID = i.value();

			// exclude "ps" and self
			if (
				(processID == appPID) ||
				((processID == psPID) && (psPID != 0))
			)
				continue; // for

			QString command;
			if (processInfo.count() > 3) // HACK: fix a command name that contains spaces
				command = QStringList(processInfo.mid(2)).join(" ");
			else
				command = processInfo[2];

			auto *p = new Process(this, command, processID);
			p->m_user = processInfo[0];
			p->m_own = (p->m_user == user);
			addProcess(p);
		}
	}
	#else
// TODO: error message
	::EnumWindows(EnumWindowsCallback, (LPARAM)this);
/* TODO: also use tasklist.exe
	QStringList args;
	args << "/NH"; // no header
	args << "/FO" << "CSV"; // CSV output format

	QProcess process;
	process.start("tasklist.exe", args);
	process.waitForStarted(-1);

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

	if (!ok)
		return;

	qint64 appPID = QApplication::applicationPid();
	QStringList processLines = text.split('\n');

	for (const QString &i : processLines) {
		QStringList processInfo = i.simplified().split("\",\"");
		
		if (processInfo.count() >= 2) {
			ok = false;
			qint64 processID = processInfo[1].toLongLong(&ok);

			// exclude "tasklist.exe" and self
			if (
				(processID == appPID)
				//((processID == psPID) && (psPID != 0))
			)
				continue; // for

			QString command = processInfo[0].remove(0, 1); // remove first "
			Process *p = new Process(this, command, processID);
			p->setVisible(true);
			addProcess(p);
		}
	}
*/
	#endif // !Q_OS_WIN32
}

void ProcessMonitor::selectRecent() {
	#ifndef Q_OS_WIN32

	if (m_recentProgram.isEmpty())
		return;

	// 1. try to match both PID and program

	if (m_recentPid != -1) {
		for (int i = 0; i < m_processList.count(); i++) {
			Process *p = m_processList.at(i);
			if ((p->m_pid == m_recentPid) && (p->m_command == m_recentProgram)) {
				m_processesComboBox->setCurrentIndex(i);

				return;
			}
		}
	}

	// 2. try to match program only

	for (int i = 0; i < m_processList.count(); i++) {
		if (m_processList.at(i)->m_command == m_recentProgram) {
			m_processesComboBox->setCurrentIndex(i);

			return;
		}
	}
	#endif // !Q_OS_WIN32
}

void ProcessMonitor::updateStatus(const Process *process) {
	m_recentPid = -1;
	m_recentProgram = "";

	if (process != nullptr) {
		#ifndef Q_OS_WIN32
		m_recentPid = process->m_pid;
		m_recentProgram = process->m_command;
		#endif // !Q_OS_WIN32

		if (process->isRunning()) {
			setInfoStatus(
				i18n("Waiting for \"%0\"")
					.arg(process->toString())
			);
		}
		else {
			setWarningStatus(
				i18n("Process or Window does not exist: %0")
					.arg(process->toString())
			);
		}
	}
	else {
		setInfoStatus("");
	}
}

// event handlers:

void ProcessMonitor::onRefresh() {
	clearAll();
	m_processesComboBox->setEnabled(true);

	qApp->setOverrideCursor(Qt::WaitCursor);

	refreshProcessList();

	if (m_processList.isEmpty()) {
		errorMessage();
	}
	else {
		m_processesComboBox->setToolTip(i18n("List of the running processes"));

		// sort alphabetically, important first
		std::sort(m_processList.begin(), m_processList.end(), [](const Process *p1, const Process *p2) {
			bool i1 = p1->important();
			bool i2 = p2->important();

			if (i1 && !i2)
				return true;

			if (!i1 && i2)
				return false;

			QString s1 = p1->toString();
			QString s2 = p2->toString();

			return s1.compare(s2, Qt::CaseInsensitive) < 0;
		});

		bool separatorAdded = false;
		int dummySeparatorProcessIndex = -1;

		//QElapsedTimer benchmark;
		//benchmark.start();

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

		for (const Process *i : m_processList) {
			// separate non-important processes
			if (!i->important() && !separatorAdded) {
				separatorAdded = true;
				dummySeparatorProcessIndex = m_processesComboBox->count();
				m_processesComboBox->insertSeparator(m_processesComboBox->count());
			}

			QIcon icon = i->icon();
			m_processesComboBox->addItem(icon.isNull() ? alignFallbackIcon : icon, i->toString(), i->m_command);
		}
		//qDebug() << "Elapsed time:" << benchmark.elapsed();

		// NOTE: insert dummy list entry to match combo box indexes
		if (dummySeparatorProcessIndex != -1) {
			// call it outside of the above "for" loop
			m_processList.insert(dummySeparatorProcessIndex, new Process(this, "", 0));
		}

		selectRecent();
	}

	qApp->restoreOverrideCursor();

	int index = m_processesComboBox->currentIndex();
	updateStatus((index == -1) ? nullptr : m_processList.value(index));
}
