/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QSettings>
#include <QSysInfo>

#include "src/datovka_shared/log/log.h"
#include "src/settings/system_detection.h"

const QString SysDetect::pkgTypeName(enum PkgType pkgType)
{
	switch (pkgType) {
	case PKG_UNKNOWN: return QString("PKG_UNKNOWN"); break;
	case PKG_WIN32_PORTABLE: return QString("PKG_WIN32_PORTABLE"); break;
	case PKG_WIN32_INST_EXE: return QString("PKG_WIN32_INST_EXE"); break;
	case PKG_WIN32_INST_MSI: return QString("PKG_WIN32_INST_MSI"); break;
	case PKG_WIN64_PORTABLE: return QString("PKG_WIN64_PORTABLE"); break;
	case PKG_WIN64_INST_EXE: return QString("PKG_WIN64_INST_EXE"); break;
	case PKG_WIN64_INST_MSI: return QString("PKG_WIN64_INST_MSI"); break;
	case PKG_MACOS32_DMG: return QString("PKG_MACOS32_DMG"); break;
	case PKG_MACOS64_DMG: return QString("PKG_MACOS64_DMG"); break;
	case PKG_MACOS_ARM64_DMG: return QString("PKG_MACOS_ARM64_DMG"); break;
	default:
		Q_ASSERT(0);
		return QString();
		break;
	}
}

#if defined(Q_OS_WIN) && !defined(PORTABLE_APPLICATION)
/*!
 * @brief Check whether installation used the NSIS installer.
 *
 * @return True if installation was performed using the NSIS installer.
 */
static
bool isNsisInstallation(void)
{
	/*
	 * Detect whether the NSIS (exe) installer was used.
	 * The NSIS installer sets the DisplayName parameter in the HKLM registers.
	 * Check whether the DisplayName parameter contains a value.
	 *
	 * The NSIS installer is currently always a 32-bit application. The
	 * installer therefore writes 32-bit entries into 64-bit registry.
	 * Therefore we cannot use isWow64() here because it's related to the
	 * running application which may be 64-bit. Therefore the following
	 * procedure is used:
	 * First read the DisplayName value in the standard registry path for
	 * native versions (which, from the application's view, corresponds to
	 * app32 on Win32 or Win64, app64 on Win64) then explicitly try the WOW64Node
	 * in order to detect the 32-bit installer entry from a 64-bit app on Win64.
	 */

	/*
	 * 32-bit installer entry detected from a 32-bit application on Win32 and Win64,
	 * 64-bit installer entry detected from a 64-bit application on Win64.
	 */
	{
#define REG "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
		QSettings reg(REG, QSettings::NativeFormat);
		reg.beginGroup("Datovka");
		QString displayName = reg.value("DisplayName").toString();
		reg.endGroup();
		if (!displayName.isEmpty()) {
			logDebugLv0NL("Detected NSIS installer entry in registry '%s'.",
			    REG);
			return true;
		}
		logDebugLv0NL("Didn't detect NSIS installer entry in registry '%s'.",
		    REG);
#undef REG
	}

	/*
	 * 32-bit installer entry detected from a 64-bit application on Win64.
	 */
#define REG "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
	QSettings reg(REG, QSettings::NativeFormat);
	reg.beginGroup("Datovka");
	QString displayName = reg.value("DisplayName").toString();
	reg.endGroup();
	if (!displayName.isEmpty()) {
		logDebugLv0NL("Detected NSIS installer entry in registry '%s'.",
		    REG);
		return true;
	}
	logDebugLv0NL("Didn't detect NSIS installer entry in registry '%s'.",
	    REG);
#undef REG

	return false;
}

/*!
 * @brief Check whether installation used the MSI installer.
 *
 * @return True if installation was performed using MSI installer.
 */
static
bool isMsiInstallation(void)
{
	/*
	 * Detect whether the MSI installer was used.
	 * The MSI installer has a unique UpgradeCode generated from the apps Guid.
	 * (https://stackoverflow.com/questions/17936064/how-can-i-find-the-upgrade-code-for-an-installed-application-in-c)
	 * The identifier "98EC3B1C37743FF4FB7F2141D4FEF251" is derived from
	 * the ProductUpgradeCode "C1B3CE89-4773-4FF3-BFF7-12144DEF2F15".
	 * Check whether UpgradeCodes exists and ProductName parameter contains a value.
	 */

	/* Detect
	 * HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes\98EC3B1C37743FF4FB7F2141D4FEF251
	 * Check whether it holds a value entry. Let's call it VALUE.
	 * Detect whether there is a registry entry
	 * HKEY_CLASSES_ROOT\Installer\Products\VALUE\ProductName
	 * which should hold the application name.
	 */

	QString value;

	{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
		enum QSettings::Format format = QSettings::Registry64Format;
#else /* < Qt-5.7 */
		enum QSettings::Format format = QSettings::NativeFormat;
#  warning "Compiling against version < Qt-5.7 which does not have QSettings::Registry64Format."
#  warning "It is likely that the MSI installer detection won't work properly."
#endif /* >= Qt-5.7 */

#define REG "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UpgradeCodes\\98EC3B1C37743FF4FB7F2141D4FEF251"
		QSettings regUpCode(REG, format);
		QStringList keys = regUpCode.allKeys();
		if (keys.isEmpty()) {
			logDebugLv0NL("Didn't detect MSI installer entry in registry '%s'.",
			    REG);
			return false;
		}
		logDebugLv0NL("Detected MSI installer entry in registry '%s'.",
		    REG);
#undef REG

		value = keys.first();
		if (value.isEmpty()) {
			return false;
		}
	}

	QSettings reg("HKEY_CLASSES_ROOT\\Installer\\Products\\" + value, QSettings::NativeFormat);
	value = reg.value("ProductName").toString();
	return !value.isEmpty();
}
#endif /* Q_OS_WIN && !PORTABLE_APPLICATION */

enum SysDetect::PkgType SysDetect::detectPackageType(void)
{
	static const QString cpu_i386("i386");
	static const QString cpu_x86_64("x86_64");
	static const QString cpu_arm64("arm64");

	enum PkgType pkgType = PKG_UNKNOWN;

	/* Detect build architecture of running application. */
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
	const QString cpu = QSysInfo::buildCpuArchitecture();
#else /* < Qt-5.4 */
#  warning "Compiling against version < Qt-5.4 which does not have QSysInfo::buildCpuArchitecture()."
#  warning "Installation package detection won't work."
	const QString cpu;
#endif /* >= Qt-5.4 */

#if defined(Q_OS_WIN)
#  if defined(PORTABLE_APPLICATION)
	if (cpu_x86_64 == cpu) {
		pkgType = PKG_WIN64_PORTABLE;
	} else {
		pkgType = PKG_WIN32_PORTABLE;
	}
#  else /* !PORTABLE_APPLICATION */
	if (isNsisInstallation()) {
		if (cpu_x86_64 == cpu) {
			pkgType = PKG_WIN64_INST_EXE;
		} else {
			pkgType = PKG_WIN32_INST_EXE;
		}
	} else if (isMsiInstallation()) {
		if (cpu_x86_64 == cpu) {
			pkgType = PKG_WIN64_INST_MSI;
		} else {
			pkgType = PKG_WIN32_INST_MSI;
		}
	}
#  endif /* PORTABLE_APPLICATION */
#endif /* Q_OS_WIN */

#if defined(Q_OS_MACOS)
	if (cpu_i386 == cpu) {
		pkgType = PKG_MACOS32_DMG;
	} else if (cpu_x86_64 == cpu) {
		pkgType = PKG_MACOS64_DMG;
	} else if (cpu_arm64 == cpu) {
		pkgType = PKG_MACOS_ARM64_DMG;
	}
#endif /* Q_OS_MACOS */

	logDebugLv0NL("Detected package type '%s'.",
	    pkgTypeName(pkgType).toUtf8().constData());
	return pkgType;
}

bool SysDetect::canInstallPortableVersion(void)
{
#if defined(Q_OS_WIN) && defined(PORTABLE_APPLICATION)
	QString pv = QSysInfo::productVersion();
	return (pv.contains("11") || pv.contains("10") || pv.contains("8.1") ||
	    pv.toLower().contains("server 2016"));
#else /* !defined(Q_OS_WIN) || !defined(PORTABLE_APPLICATION) */
	return false;
#endif /* defined(Q_OS_WIN) && defined(PORTABLE_APPLICATION) */
}
