#include <cstdlib>
#include <memory>
#include <iostream>
#include <iomanip>
#include <cstring>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <sstream>
#include <limits>
#include <string>
#include <cctype>
#include <cmath>
#include <ctime>
#include <vector>
#include <map>
#include <sys/stat.h>
#include <fcntl.h>
#include <pwd.h>
#include <unistd.h>
#include "common.h"

namespace fs = std::filesystem;
#define VA_STR(x) dynamic_cast<std::ostringstream const&>(std::ostringstream().flush() << x).str()

auto constexpr MB_i = 1000000LL;
auto constexpr MB_f = 1000000.0;
auto constexpr GB_i = 1000000000LL;
auto constexpr GB_f = 1000000000.0;
auto constexpr TB_i = 1000000000000LL;
auto constexpr TB_f = 1000000000000.0; // only C++14 has a readable alternative

static std::string stateFilePath;

void abortApp(const char* reason)
{
	std::cout << "ERROR " << errno << ":\n" << (reason? reason : "") << ":" << strerror(errno) << std::endl;
	exit(2);
}

std::string ENV(const char *name)
{
	if (char *val = std::getenv(name))
		return val;
	return "";
}

bool readFile(const char* inputFile, std::vector<char>& buffer, bool mustExist = true)
{
	int fd = open(inputFile, O_RDONLY);
	if (fd < 0) {
		if (mustExist) abortApp(inputFile);
		return false;
	} else {
		buffer.resize(4000);
		for (std::size_t offset = 0;;) {
			int bytes = ::read(fd, &buffer[offset], buffer.size() - offset - 1);
			if (bytes < 0) abortApp(inputFile);
			offset += bytes;
			if (offset + 1 == buffer.size()) buffer.resize(buffer.size() * 2);
			else if (bytes == 0) {
				close(fd);
				buffer[offset] = 0;
				buffer.resize(offset);
				return true;
			}
		}
	}
}

bool writeFile(const char* outputFile, const std::ostringstream& data, int flag = O_CREAT | O_WRONLY | O_TRUNC)
{
	bool success = false;
	int fd = open(outputFile, flag, 0640);
	if (fd >= 0) {
		const std::string& buffer = data.str();
		if (write(fd, buffer.c_str(), buffer.length()) == (ssize_t) buffer.length()) success = true;
		close(fd);
	}
	return success;
}

template <typename K, typename V> std::ostream& operator<<(std::ostream& out, const std::map<K,V>& container)
{
	for (const auto& item : container) out << item.first << '|' << item.second << '\t'; // write into storage
	return out << '\n';
}

template <typename T> bool fromString(const std::string& from, T& to)
{
	return bool(std::istringstream(from) >> to);
}

template <> bool fromString(const std::string& from, std::string& to)
{
	to = from;
	return true;
}

template <typename K, typename V> std::istream& operator>>(std::istream& in, std::map<K,V>& container)
{
	while (!in.eof() && !in.fail() && (in.peek() != '\n')) { // read from storage
		std::string skey;
		if (std::getline(in, skey, '|')) {
			K key;
			V value;
			if (fromString(skey, key) && (in >> value)) {
				container.insert( { key, value } );
				in.ignore(); // tab
			}
		}
	}
	return in.ignore();
}

struct CPU {
	typedef int16_t Number; // -1 for all cores summary and 0,1,2,... for each core

	struct Core {
		int64_t user, nice, system, idle, iowait, irq, softirq, steal, guest, guestnice;
		uint64_t freq_hz;
		int64_t cpuUsed()  const
		{
			return user + nice + system + irq + softirq + guest + guestnice;
		}
		int64_t cpuTotal() const
		{
			return cpuUsed() + idle + iowait + steal;
		}
		Core operator-(const Core& that) const
		{
			int64_t roundUserFix = user == that.user-1? 1 : 0; // compensate for physical host & virtual
			int64_t roundNiceFix = nice == that.nice-1? 1 : 0; // guests counters evil rounding
			return {
				user - that.user + roundUserFix, nice - that.nice + roundNiceFix, system - that.system,
				idle - that.idle, iowait - that.iowait, irq - that.irq, softirq - that.softirq,
				steal - that.steal, guest - that.guest + roundUserFix, guestnice - that.guestnice + roundNiceFix,
				(freq_hz + that.freq_hz) / 2 // <-- a more useful abstraction
			};
		}
	};

	std::map<Number, Core> cores;

	void readProc()
	{
		std::vector<char> buffer;
		readFile("/proc/stat", buffer);
		std::istringstream cpustat(&buffer[0]);
		std::string name;
		Number number;
		while (cpustat >> name) {
			if (name.find("cpu") == 0) {
				name.erase(0,3);
				if (name.empty()) number = -1;
				else if (!fromString(name, number)) continue;
				Core& core = cores[number];
				cpustat >> core.user >> core.nice >> core.system >> core.idle
				        >> core.iowait >> core.irq >> core.softirq;
				if (cpustat.peek() != '\n') cpustat >> core.steal;
				else core.steal = 0; // caution with old kernels
				if (cpustat.peek() != '\n') cpustat >> core.guest;
				else core.guest = 0;
				if (cpustat.peek() != '\n') cpustat >> core.guestnice;
				else core.guestnice = 0;
				core.freq_hz = 0;
				core.user -= core.guest;	 // adjust for the already accounted values (may cause evil rounding
				core.nice -= core.guestnice; // effects, though)
			}
			cpustat.ignore(buffer.size(), '\n');
		}
		readFile("/proc/cpuinfo", buffer);
		std::istringstream cpuinfo(&buffer[0]);
		std::string key;
		std::string skip;
		uint64_t sum_freq = 0;
		number = -1;
		while (cpuinfo >> key) {
			if (key == "processor") cpuinfo >> skip >> number;
			else if ((key == "cpu") && (number >= 0)) {
				if ((cpuinfo >> skip) && (skip == "MHz")) {
					double mhz;
					cpuinfo >> skip >> mhz;
					cores[number].freq_hz = uint64_t(mhz * MB_i);
					sum_freq += cores[number].freq_hz;
					number = -1;
				}
			}
			cpuinfo.ignore(buffer.size(), '\n');
		}
		auto allcpu = cores.find(-1);
		if (allcpu != cores.end()) allcpu->second.freq_hz = sum_freq;
	}
};

std::ostream& operator<<(std::ostream& out, const CPU::Core& core) // write into storage
{
	return out << core.user << ' ' << core.nice << ' ' << core.system << ' ' << core.idle << ' '
	       << core.iowait << ' ' << core.irq << ' ' << core.softirq << ' '
	       << core.steal << ' ' << core.guest << ' ' << core.guestnice << ' ' << core.freq_hz;
}

std::istream& operator>>(std::istream& in, CPU::Core& core) // read from storage
{
	return in >> core.user >> core.nice >> core.system >> core.idle >> core.iowait >> core.irq
	       >> core.softirq >> core.steal >> core.guest >> core.guestnice >> core.freq_hz;
}

struct Memory {
	struct RAM {
		uint64_t total;
		uint64_t available;
		uint64_t free;
		uint64_t shared;
		uint64_t buffers;
		uint64_t cached;
		uint64_t swapTotal;
		uint64_t swapFree;
	};

	RAM ram;

	void readProc()
	{
		std::vector<char> buffer;
		readFile("/proc/meminfo", buffer);
		std::istringstream meminfo(&buffer[0]);
		bool hasAvailable = false;
		std::string key;
		for (int count = 0; meminfo >> key;) {
			if	  (key == "MemTotal:") {
				count++;
				meminfo >> ram.total;
			} else if (key == "MemFree:") {
				count++;
				meminfo >> ram.free;
			} else if (key == "MemAvailable:") {
				count++;
				meminfo >> ram.available;
				hasAvailable = true;
			} else if (key == "Buffers:") {
				count++;
				meminfo >> ram.buffers;
			} else if (key == "Cached:") {
				count++;
				meminfo >> ram.cached;
			} else if (key == "SwapTotal:") {
				count++;
				meminfo >> ram.swapTotal;
			} else if (key == "SwapFree:") {
				count++;
				meminfo >> ram.swapFree;
			} else if (key == "Shmem:") {
				count++;
				meminfo >> ram.shared;
			}
			if (count == 7 + (hasAvailable? 1:0)) break;
			meminfo.ignore(buffer.size(), '\n');
		}
		if (!hasAvailable) ram.available = ram.free + ram.buffers + ram.cached; // pre-2014 kernels
	}
};

std::ostream& operator<<(std::ostream& out, const Memory::RAM& ram)
{
	return out << ram.total << ' ' << ram.available << ' ' << ram.free << ' ' << ram.shared << ' '
	       << ram.buffers << ' ' << ram.cached << ' ' << ram.swapTotal << ' ' << ram.swapFree << '\n';
}

std::istream& operator>>(std::istream& in, Memory::RAM& ram)
{
	return in >> ram.total >> ram.available >> ram.free >> ram.shared
	       >> ram.buffers >> ram.cached >> ram.swapTotal >> ram.swapFree;
}

struct Health {
	typedef std::string Name;

	struct Thermometer {
		int32_t tempMilliCelsius;
	};

	std::map<Name, Thermometer> thermometers;

	void readProc()
	{
		std::string coretemp;
		for (int hwmon = 0; hwmon < 256; hwmon++) {
			std::string base = VA_STR("/sys/class/hwmon/hwmon" << hwmon);
			std::vector<char> buffer;
			if (!readFile(VA_STR(base << "/name").c_str(), buffer, false)) { // pre-3.15 kernel?
				base.append("/device");
				if (!readFile(VA_STR(base << "/name").c_str(), buffer, false)) break;
			}
			std::istringstream sname(&buffer[0]);
			std::string name;
			if ((sname >> name) && (name == "coretemp")) {
				coretemp = base;
				break;
			}
		}

		if (!coretemp.empty()) for (int ic = 1; ic < 64; ic++) {
				std::vector<char> buffer;
				if (!readFile(VA_STR(coretemp << "/temp" << ic << "_label").c_str(), buffer, false)) {
					if (thermometers.empty()) continue;
					else break; // Atom CPU may start at 2 (!?)
				}
				std::istringstream sname(&buffer[0]);
				std::string name;
				if (!std::getline(sname, name) || name.empty()) break;
				if (!readFile(VA_STR(coretemp << "/temp" << ic << "_input").c_str(), buffer, false)) break;
				std::istringstream temp(&buffer[0]);
				int32_t tempMilliCelsius;
				if (!(temp >> tempMilliCelsius)) break;
				thermometers[name].tempMilliCelsius = tempMilliCelsius;
			}
	}
};

std::ostream& operator<<(std::ostream& out, const Health::Thermometer& thm)
{
	return out << thm.tempMilliCelsius;
}

std::istream& operator>>(std::istream& in, Health::Thermometer& thm)
{
	return in >> thm.tempMilliCelsius;
}

template <typename T> struct Padded {
	uint64_t max;
	T value;
};

template <typename T> std::ostream& operator<<(std::ostream& out, const Padded<T>& data)
{
	if (!std::isnan(data.value)) for (T div = data.max;; div /= 10) { // avoid infinite loop with NaN numbers
			if ((data.value >= div) && (data.value >= 1)) break;
			if ((data.value < 1) && (div <= 1)) break;
			out << "  "; // two spaces in place of each missing digit (same width in XFCE Generic Monitor applet)
		}
	return out << data.value;
}

std::string dumpReport()
{
	std::shared_ptr<CPU>	 new_CPU, old_CPU;
	std::shared_ptr<Memory>  new_Memory;
	std::shared_ptr<Health>  new_Health;

	new_CPU.reset(new CPU());
	new_Memory.reset(new Memory());
	new_Health.reset(new Health());

	timespec tp;
	if (clock_gettime(CLOCK_MONOTONIC, &tp) != 0) abortApp("clock_gettime");
	uint64_t nowIs = tp.tv_sec * GB_i + tp.tv_nsec;

	std::ostringstream newState;
	newState << APP_VERSION << " " << nowIs << "\n";
	if (new_CPU) {
		new_CPU->readProc();
		newState << "CPU|"	 << new_CPU->cores;
	}
	if (new_Memory) {
		new_Memory->readProc();
	}
	if (new_Health) {
		new_Health->readProc();
	}

	for (int locTry = 0;; locTry++) { // read the previous state from disk and store the new state
		std::vector<char> oldStateData;
		std::ostringstream stateFileName;

		if (stateFilePath.length())
			stateFileName << stateFilePath;
		else if (locTry == 0)
			stateFileName << "/run/" APP_NAME "/" APP_NAME << ".dat";
		else if (locTry == 1)
			stateFileName << "/tmp/" APP_NAME "-" << getuid() << "/" APP_NAME << ".dat";
		else
			abortApp("can't write tmpfile");

		auto stateDir = fs::path(stateFileName.str()).parent_path();

		if(not fs::exists(stateDir)) {
			try {
				fs::create_directories(stateDir);
			} catch (const std::exception& e) {
				std::cerr << e.what() << std::endl;
			}
		}

		if (readFile(stateFileName.str().c_str(), oldStateData, false)) {
			std::istringstream oldState(&oldStateData[0]);
			std::string version;
			uint64_t previouslyWas;
			oldState >> version >> previouslyWas;
			oldState.ignore(oldStateData.size(), '\n');
			if (version == APP_VERSION) {
				std::string category;
				while (std::getline(oldState, category, '|')) {
					if (category == "CPU") {
						old_CPU.reset(new CPU());
						oldState >> old_CPU->cores;
					} else oldState.ignore(oldStateData.size(), '\n');
				}
			}
		}

		if (writeFile(stateFileName.str().c_str(), newState)) {
			if(not stateFilePath.length()) stateFilePath = stateFileName.str();
			break;
		}
	}

	std::ostringstream report;

	if (new_CPU && old_CPU) { // CPU report
		struct CpuStat {
			CPU::Number number;
			double percent;
			double ghz;
		};
		std::multimap<double, CpuStat> rankByGhzUsage;
		double cum_weighted_ghz = 0;
		for (auto itn = new_CPU->cores.cbegin(); itn != new_CPU->cores.cend(); ++itn) {
			if (itn->first < 0) continue;
			auto ito = old_CPU->cores.find(itn->first);
			if (ito == old_CPU->cores.end()) continue;
			CPU::Core diff = itn->second - ito->second;
			if (diff.cpuTotal() == 0) continue;
			double unityUsage = 1.0 * diff.cpuUsed() / diff.cpuTotal();
			double ghz = diff.freq_hz / GB_f;
			double ghzUsage = ghz * unityUsage;
			cum_weighted_ghz += ghzUsage;
			rankByGhzUsage.insert({ ghzUsage, CpuStat { itn->first, 100.0 * unityUsage, ghz } });
		}

		auto allnew = new_CPU->cores.find(-1);
		auto allold = old_CPU->cores.find(-1);
		if ((allnew != new_CPU->cores.end()) && (allold != old_CPU->cores.end())) {
			const CPU::Core& ncpu = allnew->second;
			const CPU::Core& ocpu = allold->second;
			CPU::Core diff = ncpu - ocpu;
			auto cpuTotal = diff.cpuTotal();
			if (cpuTotal > 0) {
				auto cpuTotalSinceBoot = ncpu.cpuTotal();
				double usagePercent = 100.0 * diff.cpuUsed() / cpuTotal;

				auto dumpPercent = [&](const char* title, int64_t user_hz, int64_t user_hz__sinceBoot) {
					report	<< "cpu\t" << title << "%\t"
					        << 100.0 * user_hz / cpuTotal << "\t"
					        << 100.0 * user_hz__sinceBoot / cpuTotalSinceBoot << std::endl;
				};

				report	<< "cpu\tbase\t" << std::fixed
				        << std::setprecision(2) << usagePercent
				        << "\t" << uint64_t(cum_weighted_ghz * 1000)
				        << std::setprecision(2) << std::endl;

				dumpPercent("user",   diff.user,   ncpu.user);
				dumpPercent("nice",   diff.nice,   ncpu.nice);
				dumpPercent("system", diff.system, ncpu.system);
				dumpPercent("idle",   diff.idle,   ncpu.idle);
				dumpPercent("iowait", diff.iowait, ncpu.iowait);
				if (ncpu.irq)	   dumpPercent("irq",		diff.irq,	   ncpu.irq);
				if (ncpu.softirq)   dumpPercent("softirq",	diff.softirq,   ncpu.softirq);
				if (ncpu.steal)	 dumpPercent("steal",	  diff.steal,	 ncpu.steal);
				if (ncpu.guest)	 dumpPercent("guest",	  diff.guest,	 ncpu.guest);
				if (ncpu.guestnice) dumpPercent("guestnice", diff.guestnice, ncpu.guestnice);

				int maxCpu = 8;
				for (auto itc = rankByGhzUsage.crbegin(); maxCpu-- && (itc != rankByGhzUsage.crend()); ++itc) {
					report	<< "cpu\tcore:" << itc->second.number << "\t"
					        << std::fixed << std::setprecision(2)
					        << itc->second.percent << "\t"
					        << std::setprecision(3) << uint64_t(itc->second.ghz * 1000)
					        << std::endl;
				}
			}
		}
	}

	if (new_Memory) { // RAM report
		if (new_CPU)
			report	<< "mem\t" << new_Memory->ram.total
			        << "\tavail:" << new_Memory->ram.available
			        << "\tcache:" << new_Memory->ram.cached + new_Memory->ram.buffers;

		if (new_Memory->ram.shared)
			report << "\tshare:" << new_Memory->ram.shared;

		if (new_Memory->ram.swapTotal)
			report	<< "\tswap:" << new_Memory->ram.swapTotal - new_Memory->ram.swapFree << "/"
			        << new_Memory->ram.swapTotal;
		report << std::endl;
	}

	if (new_Health) { // TEMP report
		struct ThermalStat {
			ThermalStat() : min(std::numeric_limits<int32_t>::max()),
				max(std::numeric_limits<int32_t>::min()),
				avg(0), count(0)
			{}
			int32_t min;
			int32_t max;
			int32_t avg;
			std::size_t count;
			Health::Name firstName;
		};

		std::map<std::string, ThermalStat> statByCategory;
		int32_t maxAbsTemp = std::numeric_limits<int32_t>::min();
		for (const auto& itt : new_Health->thermometers) {
			std::string key = itt.first;
			auto catEnd = key.find(" ");
			if (catEnd != std::string::npos) key.erase(catEnd);
			auto its = statByCategory.find(key);
			if (its == statByCategory.end()) {
				its = statByCategory.insert( { key, ThermalStat() } ).first;
				its->second.firstName = itt.first;
			}
			if (itt.second.tempMilliCelsius < its->second.min) its->second.min = itt.second.tempMilliCelsius;
			if (itt.second.tempMilliCelsius > its->second.max) its->second.max = itt.second.tempMilliCelsius;
			if (itt.second.tempMilliCelsius > maxAbsTemp) maxAbsTemp = itt.second.tempMilliCelsius;
			auto prevTotal = its->second.avg * its->second.count;
			its->second.count++;
			its->second.avg = (prevTotal + itt.second.tempMilliCelsius) / its->second.count;
		}

		for (auto its = statByCategory.crbegin(); its != statByCategory.crend(); ++its) {
			if (its->second.count == 1)
				report << "tmp\t" << its->second.firstName << "\tcur:" << its->second.max / 1000 << "\t";
			else
				report << "max:" << its->second.max / 1000
				       << "\tmin:" << its->second.min / 1000
				       << "\tavg:" << its->second.avg / 1000
				       << std::endl;
		}
	}
	return report.str();
}
