/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "Host/SystemInfo.hpp"
#include "Base/Logger.hpp"
#include "Base/Utils.hpp"
#include "Math/ByteSize.hpp"
#include <fstream>
#include <iostream>
#include <string>
#include <climits>
#include <sys/resource.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <thread>

namespace OpenVulkano
{
	namespace
	{
		size_t ReadMemInfo(std::string_view value)
		{
			std::ifstream meminfoFile("/proc/meminfo");
			if (!meminfoFile)
			{
				Logger::PERF->error("Failed to open /proc/meminfo");
				return 0;
			}
			std::string line;
			while (std::getline(meminfoFile, line))
			{
				if (Utils::StartsWith(line, value))
				{
					std::string n, t;
					size_t v;
					std::istringstream ss(line);
					ss >> n >> v >> t;
					return ByteSize(v, ByteSizeUnit::FromName(t));
				}
			}
			meminfoFile.close();
			return 0;
		}

		size_t ReadProcStatus(std::string_view value)
		{
			std::ifstream statusFile("/proc/self/status");
			if (statusFile)
			{
				size_t mem = 0;
				std::string line;
				while (std::getline(statusFile, line))
				{
					if (Utils::StartsWith(line, value))
					{
						std::string n, t;
						size_t v;
						std::istringstream ss(line);
						ss >> n >> v >> t;
						return ByteSize(v, ByteSizeUnit::FromName(t));
					}
				}
				statusFile.close();
				return mem;
			}
			Logger::PERF->error("Failed to open /proc/self/status");
			return 0;
		}
	}

	size_t SystemInfo::GetSystemRam()
	{
		return ReadMemInfo("MemTotal");
	}

	size_t SystemInfo::GetSystemRamAvailable()
	{
		return ReadMemInfo("MemAvailable");
	}

	size_t SystemInfo::GetAppRamUsed()
	{
		return ReadProcStatus("VmSize");
	}

	size_t SystemInfo::GetAppRamAvailable()
	{
		return std::min(GetSystemRamAvailable(), GetAppVirtualMemoryMax() - GetAppRamUsed());
	}

	size_t SystemInfo::GetAppRamMax()
	{
		return std::min(GetSystemRam(), GetAppVirtualMemoryMax());
	}

	size_t SystemInfo::GetAppVirtualMemoryMax()
	{
		rlimit limit;
		if (getrlimit(RLIMIT_AS, &limit) == 0)
		{
			return limit.rlim_cur;
		}
		Logger::PERF->error("Failed to query max application memory");
		return 0;
	}

	std::string SystemInfo::GetUserName()
	{
		char* name = getlogin();
		if (!name) return "unknown";
		return name;
	}

	std::string SystemInfo::GetHostName()
	{
		char hostname[HOST_NAME_MAX + 1];
		gethostname(hostname, HOST_NAME_MAX + 1);
		return hostname;
	}

	std::string SystemInfo::GetDeviceName()
	{
		return GetHostName();
	}

	std::string SystemInfo::GetDeviceVendorName()
	{
		std::ifstream dmiStream("/sys/class/dmi/id/board_vendor");
		if (!dmiStream)
		{
			Logger::PERF->error("Failed to read /sys/class/dmi/id/board_vendor");
			return "Unknown";
		}

		std::string motherboardName;
		std::getline(dmiStream, motherboardName);
		return motherboardName;
	}

	std::string SystemInfo::GetDeviceModelName()
	{
		std::ifstream dmiStream("/sys/class/dmi/id/board_name");
		if (!dmiStream)
		{
			Logger::PERF->error("Failed to read /sys/class/dmi/id/board_name");
			return "Unknown";
		}

		std::string motherboardName;
		std::getline(dmiStream, motherboardName);
		return motherboardName;
	}

	namespace
	{
		struct OsInfo
		{
			std::string osName, osNameFormatted;
			OsVersion version;

			static std::string TryRead(const std::string line, std::string_view prefix)
			{
				if (Utils::StartsWith(line, prefix))
				{
					size_t prefixLen = prefix.length();
					return line.substr(prefixLen, line.length() - prefixLen - 1);
				}
				return "";
			}

			static OsVersion ParseVersionString(char* str)
			{
				int ver[3];
				int i = 0;

				while (*str && i < 3)
				{
					if (isdigit(*str))
					{
						ver[i] = strtol(str, &str, 10);
						i++;
					}
					else
					{
						str++;
					}
				}
				return {ver[0], ver[1], ver[2], 0};
			}

			OsInfo() : version({0, 0, 0, 0})
			{
				std::ifstream osRel("/etc/os-release");
				std::string versionString;
				if (osRel)
				{
					std::string line, name, tmp;
					while (std::getline(osRel, line))
					{
						if (!(tmp = TryRead(line, "PRETTY_NAME=\"")).empty())
						{
							osName = std::move(tmp);
						}
						else if (!(tmp = TryRead(line, "NAME=\"")).empty())
						{
							name = std::move(tmp);
						}
						else if (name.empty() && Utils::StartsWith(line, "ID=\""))
						{
							name = line.substr(4, line.length() - 5);
						}
						else if (!(tmp = TryRead(line, "BUILD_ID=\"")).empty())
						{
							versionString = std::move(tmp);
						}
						else if (!(tmp = TryRead(line, "VERSION_ID=\"")).empty())
						{
							versionString = std::move(tmp);
						}
					}
					if (osName.empty()) osName = name;
					osNameFormatted = osName + " " + versionString;
					if (!versionString.empty()) version = ParseVersionString(versionString.data());
				}
				if (osName.empty() || versionString.empty())
				{ // fallback to uname
					struct utsname buffer;

					errno = 0;
					if (uname(&buffer) == 0)
					{
						osName = buffer.sysname;
						osNameFormatted = osName + " " + buffer.release;

						version = ParseVersionString(buffer.release);
						return;
					}
					perror("uname");
				}
				if (osName.empty()) osName = "Linux";
				if (osNameFormatted.empty()) osNameFormatted = "Linux";
			}
		};

		OsInfo osInfo;
	}

	std::string SystemInfo::GetOsName()
	{
		return osInfo.osName;
	}

	OsVersion SystemInfo::GetOsVersion()
	{
		return osInfo.version;
	}

	std::string SystemInfo::GetOsNameHumanReadable()
	{
		return osInfo.osNameFormatted;
	}

	DeviceType  SystemInfo::GetDeviceType()
	{
		return DeviceType::PC; // TODO?
	}

	size_t  SystemInfo::GetCpuCoreCount()
	{
		return 0; //TODO
	}

	size_t  SystemInfo::GetCpuThreadCount()
	{
		return std::thread::hardware_concurrency();
	}

	int32_t SystemInfo::GetCpuTemperature()
	{
		std::ifstream tempFile("/sys/class/thermal/thermal_zone0/temp");
		if (!tempFile)
		{
			Logger::PERF->error("Failed to open the temperature file");
			return 20;
		}

		std::string tempStr;
		std::getline(tempFile, tempStr);

		return static_cast<int>(std::stof(tempStr) / 1000.0f);
	}

	CpuThermalState SystemInfo::GetCpuThermalState()
	{
		auto temp = GetCpuTemperature();
		if (temp < 70) return CpuThermalState::Normal;
		if (temp < 80) return CpuThermalState::Fair;
		if (temp < 95) return CpuThermalState::Serious;
		return CpuThermalState::Critical;
	}

	bool SystemInfo::IsDeviceInLowPowerMode()
	{
		return false; //TODO?
	}

	BatteryState SystemInfo::GetDeviceBatteryState()
	{
		std::ifstream powerSupplyFile("/sys/class/power_supply/BAT0/status");
		if (powerSupplyFile)
		{
			std::string status;
			std::getline(powerSupplyFile, status);
			if (status == "Charging") return BatteryState::Charging;
			if (status == "Not charging") return BatteryState::NotCharging;
			if (status == "Full") return BatteryState::ChargingFull;
			if (status == "Discharging") return BatteryState::Unplugged;
		}
		return BatteryState::Unavailable;
	}

	float SystemInfo::GetDeviceBatteryLevel()
	{
		std::ifstream powerSupplyFile("/sys/class/power_supply/BAT0/capacity");
		if (powerSupplyFile)
		{
			float value;
			powerSupplyFile >> value;
			return value;
		}
		return 0;
	}

	void SystemInfo::EnableEnergyEvents()
	{
		//TODO
	}

	bool SystemInfo::IsMultitaskingSupported()
	{
		return true;
	}

	DeviceOrientation SystemInfo::GetDeviceOrientation()
	{
		return DeviceOrientation::LandscapeRight; // TODO?
	}

	void SystemInfo::EnableDeviceOrientationEvents()
	{
		//TODO?
	}

	InterfaceOrientation SystemInfo::GetInterfaceOrientation()
	{
		return InterfaceOrientation::Landscape; // TODO?
	}
}