/*
 * fd/win32.cc: support for linux's /proc filesystem
 *
 * plan 9 caused this monstrocity...
 */
#include "animone/fd/proc.h"
#include "animone.h"
#include "animone/util.h"

#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <algorithm>

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

static constexpr std::string_view PROC_LOCATION = "/proc";

namespace animone::internal::proc {

struct Fdinfo {
	bool Parse(std::istream& istr);
	std::unordered_map<std::string, std::string> data;
};

bool Fdinfo::Parse(std::istream& istr) {
	/* shift to the start of the stream */
	istr.seekg(0);

	for (std::string line; std::getline(istr, line); ) {
		if (line.empty()) /* huh? */
			continue;

		std::size_t colon = line.find(':');
		if (colon == std::string::npos)
			return false;

		std::string key = line.substr(0, colon);
		std::string value = line.substr(colon + 1);
		util::TrimLeft(value, " ");

		data[key] = value;
	}

	return true;
}

static bool IsRegularFile(const std::string& link) {
	struct stat sb;
	if (stat(link.c_str(), &sb) == -1)
		return false;

	return S_ISREG(sb.st_mode);
}

static bool AreFlagsOk(pid_t pid, int fd) {
	const std::filesystem::path path =
	    std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "fdinfo" / std::to_string(fd);

	std::ifstream file(path);
	if (!file)
		return false;

	Fdinfo fdinfo;
	if (!fdinfo.Parse(file))
		return false;

	if (fdinfo.data.find("flags") == fdinfo.data.end())
		return false;

	/* check if the file was opened in a write mode */
	try {
		const long long accflags = std::stoll(fdinfo.data["flags"]) & O_ACCMODE;

		if (accflags == O_WRONLY || accflags == O_RDWR)
			return false;
	} catch (const std::exception& ex) {
		return false; /* huh ? */
	}

	return true;
}

static bool GetFilenameFromFd(std::string link, std::string& out) {
	/* /proc is a "virtual filesystem", so we have to guess the path size. yippee! */
	constexpr size_t OUT_MAX = (1ul << 15); // 32KiB
	out.resize(32);

	ssize_t exe_used = 0;
	do {
		out.resize(out.length() * 2);

		exe_used = readlink(link.c_str(), &out.front(), out.length());
		if (exe_used == (ssize_t)-1 || exe_used < (ssize_t)1)
			return false; // we got a bad result. SAD!
	} while (out.length() < OUT_MAX && exe_used >= static_cast<ssize_t>(out.length()));

	out.resize(out.find('\0'));

	return true;
}

static bool IsSystemFile(const std::string& path) {
	static constexpr std::array<std::string_view, 9> invalid_paths = {"/boot", "/dev", "/bin", "/usr", "/opt",
	                                                                  "/proc", "/var", "/etc", "/dev"};

	for (const auto& invalid_path : invalid_paths)
		if (!path.rfind(invalid_path, 0))
			return true;

	return false;
}

bool GetProcessName(pid_t pid, std::string& result) {
	const std::filesystem::path path = std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "comm";

	if (!util::ReadFile(path, result))
		return false;

	result.erase(std::remove(result.begin(), result.end(), '\n'), result.cend());
	return true;
}

bool EnumerateOpenProcesses(process_proc_t process_proc) {
	bool success = false;

	for (const auto& dir : std::filesystem::directory_iterator{PROC_LOCATION}) {
		Process proc;
		proc.platform = ExecutablePlatform::Posix;

		try {
			proc.pid = util::StringToInt(dir.path().stem());
			success = true;
		} catch (std::invalid_argument const& ex) {
			continue;
		}

		if (!GetProcessName(proc.pid, proc.comm))
			continue;

		if (!process_proc(proc))
			return false;
	}

	return success;
}

bool EnumerateOpenFiles(const std::set<pid_t>& pids, open_file_proc_t open_file_proc) {
	if (!open_file_proc)
		return false;

	for (const auto& pid : pids) {
		const std::filesystem::path path = std::filesystem::path(PROC_LOCATION) / std::to_string(pid) / "fd";

		try {
			for (const auto& dir : std::filesystem::directory_iterator{path}) {
				if (!AreFlagsOk(pid, util::StringToInt(dir.path().stem())))
					continue;

				std::string name;
				if (!GetFilenameFromFd(dir.path(), name))
					continue;

				if (!IsRegularFile(name))
					continue;

				if (IsSystemFile(name))
					continue;

				if (!open_file_proc({pid, name}))
					return false;
			}
		} catch (std::filesystem::filesystem_error &fs_error) {
			/* I hate C++ */
			continue;
		}
	}
	return true;
}

} // namespace animia::internal::proc
