#include "../../include/dv-sdk/cross/portable_io.h"
#include "../../include/dv-sdk/cross/portable_threads.h"
#include "../../include/dv-sdk/cross/portable_time.h"
#include "../../include/dv-sdk/utils.h"

#include <boost/filesystem.hpp>
#include <boost/nowide/cstdlib.hpp>

#include <cstring>

#if defined(OS_UNIX)
#	include <pthread.h>
#	include <pwd.h>
#	include <sched.h>
#	include <sys/time.h>
#	include <sys/types.h>
#	include <unistd.h>

#	if defined(OS_LINUX)
#		include <sys/prctl.h>
#		include <sys/resource.h>
#	elif defined(OS_MACOS)
#		include <mach/clock.h>
#		include <mach/clock_types.h>
#		include <mach/mach.h>
#		include <mach/mach_host.h>
#		include <mach/mach_port.h>
#		include <mach/mach_time.h>
#		include <mach-o/dyld.h>
#	endif
#elif defined(OS_WINDOWS)
#	define WIN32_LEAN_AND_MEAN
#	include <errno.h>
#	include <io.h>
#	include <windows.h>
#	include <Lmcons.h>
#	include <boost/nowide/convert.hpp>
#endif

#define DV_UTF8_PATH_BUF_SIZE (PATH_MAX * 4)

static inline bool checkPath(const boost::filesystem::path &path) {
	return (!path.empty() && boost::filesystem::exists(path) && boost::filesystem::is_directory(path));
}

// Remember to free strings returned by this.
char *portable_get_user_home_directory(void) {
	boost::filesystem::path homeDir;

#if defined(OS_UNIX)
	// Unix: First check the environment for $HOME.
	const auto envVar = boost::nowide::getenv("HOME");

	if (envVar != nullptr) {
		const boost::filesystem::path envHome{envVar};

		if (checkPath(envHome)) {
			homeDir = envHome;
		}
	}

	// Else try to get it from the user data storage.
	if (homeDir.empty()) {
		struct passwd userPasswd;
		struct passwd *userPasswdResultPtr = nullptr;
		char userPasswdBuf[DV_UTF8_PATH_BUF_SIZE];

		if ((getpwuid_r(getuid(), &userPasswd, userPasswdBuf, DV_UTF8_PATH_BUF_SIZE, &userPasswdResultPtr) == 0)
			&& (userPasswdResultPtr != nullptr)) {
			const boost::filesystem::path passwdPwDir{userPasswd.pw_dir};

			if (checkPath(passwdPwDir)) {
				homeDir = passwdPwDir;
			}
		}
	}
#elif defined(OS_WINDOWS)
	// Windows: First check the environment for $USERPROFILE.
	auto envVar = boost::nowide::getenv("USERPROFILE");

	if (envVar != nullptr) {
		const boost::filesystem::path envUserProfile{envVar};

		if (checkPath(envUserProfile)) {
			homeDir = envUserProfile;
		}
	}

	// Else try the concatenation of $HOMEDRIVE and $HOMEPATH.
	if (homeDir.empty()) {
		envVar             = boost::nowide::getenv("HOMEDRIVE");
		const auto envVar2 = boost::nowide::getenv("HOMEPATH");

		if ((envVar != nullptr) && (envVar2 != nullptr)) {
			boost::filesystem::path envHomeDrive{envVar};
			const boost::filesystem::path envHomePath{envVar2};

			envHomeDrive /= envHomePath;

			if (checkPath(envHomeDrive)) {
				homeDir = envHomeDrive;
			}
		}
	}

	// And last try $HOME.
	if (homeDir.empty()) {
		envVar = boost::nowide::getenv("HOME");

		if (envVar != nullptr) {
			const boost::filesystem::path envHome{envVar};

			if (checkPath(envHome)) {
				homeDir = envHome;
			}
		}
	}
#else
#	error "No portable way to get user home directory found."
#endif

	// If nothing else worked, try getting a temporary path.
	if (homeDir.empty()) {
		const boost::filesystem::path tempDir = boost::filesystem::temp_directory_path();

		if (checkPath(tempDir)) {
			homeDir = tempDir;
		}
	}

	// Absolutely nothing worked: stop and return NULL.
	if (homeDir.empty()) {
		return (nullptr);
	}

	// Got something, fully resolve its path.
	homeDir = boost::filesystem::canonical(homeDir).make_preferred();

	// Output it as a C-style string.
	const auto homeDirString = homeDir.string();

	char *homeDirMem = static_cast<char *>(malloc(homeDirString.size() + 1));
	if (homeDirMem == nullptr) {
		return (nullptr);
	}

	memcpy(homeDirMem, homeDirString.data(), homeDirString.size());
	homeDirMem[homeDirString.size()] = '\0';

	return (homeDirMem);
}

char *portable_get_executable_location(void) {
	char buf[DV_UTF8_PATH_BUF_SIZE];

#if defined(OS_LINUX)
	const ssize_t res = readlink("/proc/self/exe", buf, DV_UTF8_PATH_BUF_SIZE);
	if ((res <= 0) || (res == DV_UTF8_PATH_BUF_SIZE)) {
		return (nullptr);
	}

	buf[res] = '\0';
#elif defined(OS_MACOS)
	uint32_t bufSize = DV_UTF8_PATH_BUF_SIZE;
	if (_NSGetExecutablePath(buf, &bufSize) != 0) {
		return (nullptr);
	}
#elif defined(OS_WINDOWS)
	wchar_t wideBuf[PATH_MAX];

	const uint32_t res = GetModuleFileNameW(nullptr, wideBuf, PATH_MAX);
	if ((res == 0) || (res == PATH_MAX)) {
		return (nullptr);
	}

	if (boost::nowide::narrow(buf, DV_UTF8_PATH_BUF_SIZE, wideBuf) == nullptr) {
		return (nullptr);
	}
#else
#	error "No portable way to get executable location found."
#endif

	boost::filesystem::path execLocation{buf};

	// Got something, fully resolve its path.
	execLocation = boost::filesystem::canonical(execLocation).make_preferred();

	// Output it as a C-style string.
	const auto execLocationString = execLocation.string();

	char *execLocationMem = static_cast<char *>(malloc(execLocationString.size() + 1));
	if (execLocationMem == nullptr) {
		return (nullptr);
	}

	memcpy(execLocationMem, execLocationString.data(), execLocationString.size());
	execLocationMem[execLocationString.size()] = '\0';

	return (execLocationMem);
}

// Remember to free strings returned by this.
char *portable_get_user_name(void) {
#if defined(OS_UNIX)
	struct passwd userPasswd;
	struct passwd *userPasswdResultPtr = nullptr;
	char userPasswdBuf[DV_UTF8_PATH_BUF_SIZE];

	if ((getpwuid_r(getuid(), &userPasswd, userPasswdBuf, DV_UTF8_PATH_BUF_SIZE, &userPasswdResultPtr) != 0)
		|| (userPasswdResultPtr == nullptr)) {
		return (nullptr);
	}

	const size_t strSize = strlen(userPasswd.pw_name);
	char *const buf      = static_cast<char *>(malloc(strSize + 1));
	if (buf == nullptr) {
		return (nullptr);
	}

	memcpy(buf, userPasswd.pw_name, strSize);
	buf[strSize] = '\0';

	return (buf);
#elif defined(OS_WINDOWS)
	DWORD bufSize = UNLEN + 1;
	wchar_t wideBuf[UNLEN + 1];

	if (!GetUserNameW(wideBuf, &bufSize)) {
		return (nullptr);
	}

	const std::string username = boost::nowide::narrow(wideBuf);

	const size_t strSize = username.size();
	char *const buf      = static_cast<char *>(malloc(strSize + 1));
	if (buf == nullptr) {
		return (nullptr);
	}

	memcpy(buf, username.data(), strSize);
	buf[strSize] = '\0';

	return (buf);
#else
#	error "No portable way to get user name found."
#endif
}

#if defined(OS_MACOS)
bool portable_clock_gettime_monotonic(struct timespec *monoTime) {
	kern_return_t kRet;
	clock_serv_t clockRef;
	mach_timespec_t machTime;

	mach_port_t host = mach_host_self();

	kRet = host_get_clock_service(host, SYSTEM_CLOCK, &clockRef);
	mach_port_deallocate(mach_task_self(), host);

	if (kRet != KERN_SUCCESS) {
		errno = EINVAL;
		return (false);
	}

	kRet = clock_get_time(clockRef, &machTime);
	mach_port_deallocate(mach_task_self(), clockRef);

	if (kRet != KERN_SUCCESS) {
		errno = EINVAL;
		return (false);
	}

	monoTime->tv_sec  = machTime.tv_sec;
	monoTime->tv_nsec = machTime.tv_nsec;

	return (true);
}

bool portable_clock_gettime_realtime(struct timespec *realTime) {
	kern_return_t kRet;
	clock_serv_t clockRef;
	mach_timespec_t machTime;

	mach_port_t host = mach_host_self();

	kRet = host_get_clock_service(host, CALENDAR_CLOCK, &clockRef);
	mach_port_deallocate(mach_task_self(), host);

	if (kRet != KERN_SUCCESS) {
		errno = EINVAL;
		return (false);
	}

	kRet = clock_get_time(clockRef, &machTime);
	mach_port_deallocate(mach_task_self(), clockRef);

	if (kRet != KERN_SUCCESS) {
		errno = EINVAL;
		return (false);
	}

	realTime->tv_sec  = machTime.tv_sec;
	realTime->tv_nsec = machTime.tv_nsec;

	return (true);
}
#elif ((defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 200112L) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 600) \
	   || (defined(_WIN32) && defined(__MINGW32__)) || (defined(_WIN64) && defined(__MINGW64__)))
bool portable_clock_gettime_monotonic(struct timespec *monoTime) {
	return (clock_gettime(CLOCK_MONOTONIC, monoTime) == 0);
}

bool portable_clock_gettime_realtime(struct timespec *realTime) {
	return (clock_gettime(CLOCK_REALTIME, realTime) == 0);
}
#else
#	error "No portable way of getting absolute monotonic time found."
#endif

bool portable_thread_set_name(const char *name) {
#if defined(OS_LINUX)
	if (prctl(PR_SET_NAME, name) != 0) {
		return (false);
	}

	return (true);
#elif defined(OS_MACOS)
	if (pthread_setname_np(name) != 0) {
		return (false);
	}

	return (true);
#elif defined(OS_WINDOWS)
	// Windows: this is not possible, only for debugging.
	UNUSED_ARGUMENT(name);
	return (false);
#else
#	error "No portable way of setting thread name found."
#endif
}

bool portable_thread_set_priority_highest(void) {
#if defined(OS_UNIX)
	int sched_policy = 0;
	struct sched_param sched_priority;
	memset(&sched_priority, 0, sizeof(struct sched_param));

	if (pthread_getschedparam(pthread_self(), &sched_policy, &sched_priority) != 0) {
		return (false);
	}

	sched_priority.sched_priority = sched_get_priority_max(sched_policy);

	if (pthread_setschedparam(pthread_self(), sched_policy, &sched_priority) != 0) {
		return (false);
	}

	return (true);
#elif defined(OS_WINDOWS)
	if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST) == 0) {
		return (false);
	}

	return (true);
#else
#	error "No portable way of raising thread priority found."
#endif
}
