// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+

#include "FileSystem.h"
#include "Error.h"
#include "Path.h"
#include "Assertions.h"
#include "Console.h"
#include "StringUtil.h"
#include "Path.h"
#include "ProgressCallback.h"

#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <numeric>

#ifdef __APPLE__
#include <mach-o/dyld.h>
#include <stdlib.h>
#include <sys/param.h>
#endif

#ifdef __FreeBSD__
#include <sys/sysctl.h>
#endif

#if defined(_WIN32)
#include "RedtapeWindows.h"
#include <io.h>
#include <malloc.h>
#include <pathcch.h>
#include <winioctl.h>
#include <share.h>
#include <shlobj.h>
#else
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif

#ifdef _WIN32

static std::time_t ConvertFileTimeToUnixTime(const FILETIME& ft)
{
	// based off https://stackoverflow.com/a/6161842
	static constexpr s64 WINDOWS_TICK = 10000000;
	static constexpr s64 SEC_TO_UNIX_EPOCH = 11644473600LL;

	const s64 full = static_cast<s64>((static_cast<u64>(ft.dwHighDateTime) << 32) | static_cast<u64>(ft.dwLowDateTime));
	return static_cast<std::time_t>(full / WINDOWS_TICK - SEC_TO_UNIX_EPOCH);
}

template <class T>
static bool IsUNCPath(const T& path)
{
	return (path.length() >= 3 && path[0] == '\\' && path[1] == '\\');
}

#endif

static inline bool FileSystemCharacterIsSane(char32_t c, bool strip_slashes)
{
#ifdef _WIN32
	// https://docs.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions
	if ((c == U'/' || c == U'\\') && strip_slashes)
		return false;

	if (c == U'<' || c == U'>' || c == U':' || c == U'"' || c == U'|' || c == U'?' || c == U'*' || c == 0 ||
		c <= static_cast<char32_t>(31))
	{
		return false;
	}
#else
	if (c == '/' && strip_slashes)
		return false;

	// drop asterisks too, they make globbing annoying
	if (c == '*')
		return false;

		// macos doesn't allow colons, apparently
#ifdef __APPLE__
	if (c == U':')
		return false;
#endif
#endif

	return true;
}

template <typename T>
static inline void PathAppendString(std::string& dst, const T& src)
{
	if (dst.capacity() < (dst.length() + src.length()))
		dst.reserve(dst.length() + src.length());

	bool last_separator = (!dst.empty() && dst.back() == FS_OSPATH_SEPARATOR_CHARACTER);

	size_t index = 0;

#ifdef _WIN32
	// special case for UNC paths here
	if (dst.empty() && src.length() >= 3 && src[0] == '\\' && src[1] == '\\' && src[2] != '\\')
	{
		dst.append("\\\\");
		index = 2;
	}
#endif

	for (; index < src.length(); index++)
	{
		const char ch = src[index];

#ifdef _WIN32
		// convert forward slashes to backslashes
		if (ch == '\\' || ch == '/')
#else
		if (ch == '/')
#endif
		{
			if (last_separator)
				continue;
			last_separator = true;
			dst.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
		}
		else
		{
			last_separator = false;
			dst.push_back(ch);
		}
	}
}

std::string Path::SanitizeFileName(const std::string_view str, bool strip_slashes /* = true */)
{
	std::string ret;
	ret.reserve(str.length());

	size_t pos = 0;
	while (pos < str.length())
	{
		char32_t ch;
		pos += StringUtil::DecodeUTF8(str, pos, &ch);
		ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_';
		StringUtil::EncodeAndAppendUTF8(ret, ch);
	}

#ifdef _WIN32
	// Windows: Can't end filename with a period.
	if (ret.length() > 0 && ret.back() == '.')
		ret.back() = '_';
#endif

	return ret;
}

void Path::SanitizeFileName(std::string* str, bool strip_slashes /* = true */)
{
	const size_t len = str->length();

	char small_buf[128];
	std::unique_ptr<char[]> large_buf;
	char* str_copy = small_buf;
	if (len >= std::size(small_buf))
	{
		large_buf = std::make_unique<char[]>(len + 1);
		str_copy = large_buf.get();
	}
	std::memcpy(str_copy, str->c_str(), sizeof(char) * (len + 1));
	str->clear();

	size_t pos = 0;
	while (pos < len)
	{
		char32_t ch;
		pos += StringUtil::DecodeUTF8(str_copy + pos, pos - len, &ch);
		ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_';
		StringUtil::EncodeAndAppendUTF8(*str, ch);
	}

#ifdef _WIN32
	// Windows: Can't end filename with a period.
	if (str->length() > 0 && str->back() == '.')
		str->back() = '_';
#endif
}

bool Path::IsValidFileName(const std::string_view str, bool allow_slashes)
{
	const size_t len = str.length();
	size_t pos = 0;
	while (pos < len)
	{
		char32_t ch;
		pos += StringUtil::DecodeUTF8(str.data() + pos, pos - len, &ch);
		if (!FileSystemCharacterIsSane(ch, !allow_slashes))
			return false;
	}

#ifdef _WIN32
	// Windows: Can't end filename with a period.
	if (len > 0 && str.back() == '.')
		return false;
#endif

	return true;
}

#ifdef _WIN32

bool FileSystem::GetWin32Path(std::wstring* dest, std::string_view str)
{
	// Just convert to wide if it's a relative path, MAX_PATH still applies.
	if (!Path::IsAbsolute(str))
		return StringUtil::UTF8StringToWideString(*dest, str);

	// PathCchCanonicalizeEx() thankfully takes care of everything.
	// But need to widen the string first, avoid the stack allocation.
	int wlen = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), nullptr, 0);
	if (wlen <= 0) [[unlikely]]
		return false;

	// So copy it to a temp wide buffer first.
	wchar_t* wstr_buf = static_cast<wchar_t*>(_malloca(sizeof(wchar_t) * (static_cast<size_t>(wlen) + 1)));
	wlen = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), wstr_buf, wlen);
	if (wlen <= 0) [[unlikely]]
	{
		_freea(wstr_buf);
		return false;
	}

	// And use PathCchCanonicalizeEx() to fix up any non-direct elements.
	wstr_buf[wlen] = '\0';
	dest->resize(std::max<size_t>(static_cast<size_t>(wlen) + (IsUNCPath(str) ? 9 : 5), 16));
	for (;;)
	{
		const HRESULT hr =
			PathCchCanonicalizeEx(dest->data(), dest->size(), wstr_buf, PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH);
		if (SUCCEEDED(hr))
		{
			dest->resize(std::wcslen(dest->data()));
			_freea(wstr_buf);
			return true;
		}
		else if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER))
		{
			dest->resize(dest->size() * 2);
			continue;
		}
		else [[unlikely]]
		{
			Console.ErrorFmt("PathCchCanonicalizeEx() returned {:08X}", static_cast<unsigned>(hr));
			_freea(wstr_buf);
			return false;
		}
	}
}

std::wstring FileSystem::GetWin32Path(std::string_view str)
{
	std::wstring ret;
	if (!GetWin32Path(&ret, str))
		ret.clear();
	return ret;
}

#endif

bool Path::IsAbsolute(const std::string_view path)
{
#ifdef _WIN32
	return (path.length() >= 3 && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) &&
			   path[1] == ':' && (path[2] == '/' || path[2] == '\\')) ||
		   (path.length() >= 3 && path[0] == '\\' && path[1] == '\\');
#else
	return (path.length() >= 1 && path[0] == '/');
#endif
}

std::string Path::RealPath(const std::string_view path)
{
	// Resolve non-absolute paths first.
	std::vector<std::string_view> components;
	if (!IsAbsolute(path))
		components = Path::SplitNativePath(Path::Combine(FileSystem::GetWorkingDirectory(), path));
	else
		components = Path::SplitNativePath(path);

	std::string realpath;
	if (components.empty())
		return realpath;

	// Different to path because relative.
	realpath.reserve(std::accumulate(components.begin(), components.end(), static_cast<size_t>(0),
						 [](size_t l, const std::string_view& s) { return l + s.length(); }) +
					 components.size() + 1);

#ifdef _WIN32
	std::wstring wrealpath;
	std::vector<WCHAR> symlink_buf;
	wrealpath.reserve(realpath.size());
	symlink_buf.resize(path.size() + 1);

	// Check for any symbolic links throughout the path while adding components.
	const bool skip_first = IsUNCPath(path);
	bool test_symlink = true;
	for (const std::string_view& comp : components)
	{
		if (!realpath.empty())
		{
			realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
			realpath.append(comp);
		}
		else if (skip_first)
		{
			realpath.append(comp);
			continue;
		}
		else
		{
			realpath.append(comp);
		}
		if (test_symlink)
		{
			DWORD attribs;
			if (FileSystem::GetWin32Path(&wrealpath, realpath) &&
				(attribs = GetFileAttributesW(wrealpath.c_str())) != INVALID_FILE_ATTRIBUTES)
			{
				// if not a link, go to the next component
				if (attribs & FILE_ATTRIBUTE_REPARSE_POINT)
				{
					const HANDLE hFile =
						CreateFileW(wrealpath.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
							nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
					if (hFile != INVALID_HANDLE_VALUE)
					{
						// is a link! resolve it.
						DWORD ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast<DWORD>(symlink_buf.size()),
							FILE_NAME_NORMALIZED);
						if (ret > symlink_buf.size())
						{
							symlink_buf.resize(ret);
							ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast<DWORD>(symlink_buf.size()),
								FILE_NAME_NORMALIZED);
						}
						if (ret != 0)
							StringUtil::WideStringToUTF8String(realpath, std::wstring_view(symlink_buf.data(), ret));
						else
							test_symlink = false;

						CloseHandle(hFile);
					}
				}
			}
			else
			{
				// not a file or link
				test_symlink = false;
			}
		}
	}

	// GetFinalPathNameByHandleW() adds a \\?\ prefix, so remove it.
	if (realpath.starts_with("\\\\?\\") && IsAbsolute(std::string_view(realpath.data() + 4, realpath.size() - 4)))
	{
		realpath.erase(0, 4);
	}
	else if (realpath.starts_with("\\\\?\\UNC\\"))
	{
		realpath.erase(0, 7);
		realpath.insert(realpath.begin(), '\\');
	}

#else
	// Why this monstrosity instead of calling realpath()? realpath() only works on files that exist.
	std::string basepath;
	std::string symlink;

	basepath.reserve(realpath.capacity());
	symlink.resize(realpath.capacity());

	// Check for any symbolic links throughout the path while adding components.
	bool test_symlink = true;
	for (const std::string_view& comp : components)
	{
		if (!test_symlink)
		{
			realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
			realpath.append(comp);
			continue;
		}

		basepath = realpath;
		if (realpath.empty() || realpath.back() != FS_OSPATH_SEPARATOR_CHARACTER)
			realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
		realpath.append(comp);

		// Check if the last component added is a symlink
		struct stat sb;
		if (lstat(realpath.c_str(), &sb) != 0)
		{
			// Don't bother checking any further components once we error out.
			test_symlink = false;
			continue;
		}
		else if (!S_ISLNK(sb.st_mode))
		{
			// Nope, keep going.
			continue;
		}

		for (;;)
		{
			ssize_t sz = readlink(realpath.c_str(), symlink.data(), symlink.size());
			if (sz < 0)
			{
				// shouldn't happen, due to the S_ISLNK check above.
				test_symlink = false;
				break;
			}
			else if (static_cast<size_t>(sz) == symlink.size())
			{
				// need a larger buffer
				symlink.resize(symlink.size() * 2);
				continue;
			}
			else
			{
				// is a link, and we resolved it. gotta check if the symlink itself is relative :(
				symlink.resize(static_cast<size_t>(sz));
				if (!Path::IsAbsolute(symlink))
				{
					// symlink is relative to the directory of the symlink
					realpath = basepath;
					if (realpath.empty() || realpath.back() != FS_OSPATH_SEPARATOR_CHARACTER)
						realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER);
					realpath.append(symlink);
				}
				else
				{
					// Use the new, symlinked path.
					realpath = symlink;
				}

				break;
			}
		}
	}
#endif

	return realpath;
}

std::string Path::ToNativePath(const std::string_view path)
{
	std::string ret;
	PathAppendString(ret, path);

	// remove trailing slashes
	if (ret.length() > 1)
	{
		while (ret.back() == FS_OSPATH_SEPARATOR_CHARACTER)
			ret.pop_back();
	}

	return ret;
}

void Path::ToNativePath(std::string* path)
{
	*path = Path::ToNativePath(*path);
}

std::string Path::Canonicalize(const std::string_view path)
{
	std::vector<std::string_view> components = Path::SplitNativePath(path);
	std::vector<std::string_view> new_components;
	new_components.reserve(components.size());
	for (const std::string_view& component : components)
	{
		if (component == ".")
		{
			// current directory, so it can be skipped, unless it's the only component
			if (components.size() == 1)
				new_components.push_back(std::move(component));
		}
		else if (component == "..")
		{
			// parent directory, pop one off if we're not at the beginning, otherwise preserve.
			if (!new_components.empty())
				new_components.pop_back();
			else
				new_components.push_back(std::move(component));
		}
		else
		{
			// anything else, preserve
			new_components.push_back(std::move(component));
		}
	}

	return Path::JoinNativePath(new_components);
}

void Path::Canonicalize(std::string* path)
{
	*path = Canonicalize(*path);
}

std::string Path::MakeRelative(const std::string_view path, const std::string_view relative_to)
{
	// simple algorithm, we just work on the components. could probably be better, but it'll do for now.
	std::vector<std::string_view> path_components(SplitNativePath(path));
	std::vector<std::string_view> relative_components(SplitNativePath(relative_to));
	std::vector<std::string_view> new_components;

	// both must be absolute paths
	if (Path::IsAbsolute(path) && Path::IsAbsolute(relative_to))
	{
		// find the number of same components
		size_t num_same = 0;
		for (size_t i = 0; i < path_components.size() && i < relative_components.size(); i++)
		{
			if (path_components[i] == relative_components[i])
				num_same++;
			else
				break;
		}

		// we need at least one same component
		if (num_same > 0)
		{
			// from the relative_to directory, back up to the start of the common components
			const size_t num_ups = relative_components.size() - num_same;
			for (size_t i = 0; i < num_ups; i++)
				new_components.emplace_back("..");

			// and add the remainder of the path components
			for (size_t i = num_same; i < path_components.size(); i++)
				new_components.push_back(std::move(path_components[i]));
		}
		else
		{
			// no similarity
			new_components = std::move(path_components);
		}
	}
	else
	{
		// not absolute
		new_components = std::move(path_components);
	}

	return JoinNativePath(new_components);
}

std::string_view Path::GetExtension(const std::string_view path)
{
	const std::string_view::size_type pos = path.rfind('.');
	if (pos == std::string_view::npos)
		return std::string_view();
	else
		return path.substr(pos + 1);
}

std::string_view Path::StripExtension(const std::string_view path)
{
	const std::string_view::size_type pos = path.rfind('.');
	if (pos == std::string_view::npos)
		return path;

	return path.substr(0, pos);
}

std::string Path::ReplaceExtension(const std::string_view path, const std::string_view new_extension)
{
	const std::string_view::size_type pos = path.rfind('.');
	if (pos == std::string_view::npos)
		return std::string(path);

	std::string ret(path, 0, pos + 1);
	ret.append(new_extension);
	return ret;
}

static std::string_view::size_type GetLastSeperatorPosition(const std::string_view filename, bool include_separator)
{
	std::string_view::size_type last_separator = filename.rfind('/');
	if (include_separator && last_separator != std::string_view::npos)
		last_separator++;

#if defined(_WIN32)
	std::string_view::size_type other_last_separator = filename.rfind('\\');
	if (other_last_separator != std::string_view::npos)
	{
		if (include_separator)
			other_last_separator++;
		if (last_separator == std::string_view::npos || other_last_separator > last_separator)
			last_separator = other_last_separator;
	}
#endif

	return last_separator;
}

std::string_view Path::GetDirectory(const std::string_view path)
{
	const std::string::size_type pos = GetLastSeperatorPosition(path, false);
	if (pos == std::string_view::npos)
		return {};

	return path.substr(0, pos);
}

std::string_view Path::GetFileName(const std::string_view path)
{
	const std::string_view::size_type pos = GetLastSeperatorPosition(path, true);
	if (pos == std::string_view::npos)
		return path;

	return path.substr(pos);
}

std::string_view Path::GetFileTitle(const std::string_view path)
{
	const std::string_view filename(GetFileName(path));
	const std::string::size_type pos = filename.rfind('.');
	if (pos == std::string_view::npos)
		return filename;

	return filename.substr(0, pos);
}

std::string Path::ChangeFileName(const std::string_view path, const std::string_view new_filename)
{
	std::string ret;
	PathAppendString(ret, path);

	const std::string_view::size_type pos = GetLastSeperatorPosition(ret, true);
	if (pos == std::string_view::npos)
	{
		ret.clear();
		PathAppendString(ret, new_filename);
	}
	else
	{
		if (!new_filename.empty())
		{
			ret.erase(pos);
			PathAppendString(ret, new_filename);
		}
		else
		{
			ret.erase(pos - 1);
		}
	}

	return ret;
}

void Path::ChangeFileName(std::string* path, const std::string_view new_filename)
{
	*path = ChangeFileName(*path, new_filename);
}

std::string Path::AppendDirectory(const std::string_view path, const std::string_view new_dir)
{
	std::string ret;
	if (!new_dir.empty())
	{
		const std::string_view::size_type pos = GetLastSeperatorPosition(path, true);

		ret.reserve(path.length() + new_dir.length() + 1);
		if (pos != std::string_view::npos)
			PathAppendString(ret, path.substr(0, pos));

		while (!ret.empty() && ret.back() == FS_OSPATH_SEPARATOR_CHARACTER)
			ret.pop_back();

		if (!ret.empty())
			ret += FS_OSPATH_SEPARATOR_CHARACTER;

		PathAppendString(ret, new_dir);

		if (pos != std::string_view::npos)
		{
			const std::string_view filepart(path.substr(pos));
			if (!filepart.empty())
			{
				ret += FS_OSPATH_SEPARATOR_CHARACTER;
				PathAppendString(ret, filepart);
			}
		}
		else if (!path.empty())
		{
			ret += FS_OSPATH_SEPARATOR_CHARACTER;
			PathAppendString(ret, path);
		}
	}
	else
	{
		PathAppendString(ret, path);
	}

	return ret;
}

void Path::AppendDirectory(std::string* path, const std::string_view new_dir)
{
	*path = AppendDirectory(*path, new_dir);
}

std::vector<std::string_view> Path::SplitWindowsPath(const std::string_view path)
{
	std::vector<std::string_view> parts;

	std::string::size_type start = 0;
	std::string::size_type pos = 0;

	// preserve unc paths
	if (path.size() > 2 && path[0] == '\\' && path[1] == '\\')
		pos = 2;

	while (pos < path.size())
	{
		if (path[pos] != '/' && path[pos] != '\\')
		{
			pos++;
			continue;
		}

		// skip consecutive separators
		if (pos != start)
			parts.push_back(path.substr(start, pos - start));

		pos++;
		start = pos;
	}

	if (start != pos)
		parts.push_back(path.substr(start));

	return parts;
}

std::string Path::JoinWindowsPath(const std::vector<std::string_view>& components)
{
	return StringUtil::JoinString(components.begin(), components.end(), '\\');
}

std::vector<std::string_view> Path::SplitNativePath(const std::string_view path)
{
#ifdef _WIN32
	return SplitWindowsPath(path);
#else
	std::vector<std::string_view> parts;

	std::string::size_type start = 0;
	std::string::size_type pos = 0;
	while (pos < path.size())
	{
		if (path[pos] != '/')
		{
			pos++;
			continue;
		}

		// skip consecutive separators
		// for unix, we create an empty element at the beginning when it's an absolute path
		// that way, when it's re-joined later, we preserve the starting slash.
		if (pos != start || pos == 0)
			parts.push_back(path.substr(start, pos - start));

		pos++;
		start = pos;
	}

	if (start != pos)
		parts.push_back(path.substr(start));

	return parts;
#endif
}

std::string Path::JoinNativePath(const std::vector<std::string_view>& components)
{
	return StringUtil::JoinString(components.begin(), components.end(), FS_OSPATH_SEPARATOR_CHARACTER);
}

std::vector<std::string> FileSystem::GetRootDirectoryList()
{
	std::vector<std::string> results;

#if defined(_WIN32)
	char buf[256];
	const DWORD size = GetLogicalDriveStringsA(sizeof(buf), buf);
	if (size != 0 && size < (sizeof(buf) - 1))
	{
		const char* ptr = buf;
		while (*ptr != '\0')
		{
			const std::size_t len = std::strlen(ptr);
			results.emplace_back(ptr, len);
			ptr += len + 1u;
		}
	}
#else
	const char* home_path = std::getenv("HOME");
	if (home_path)
		results.push_back(home_path);

	results.push_back("/");
#endif

	return results;
}

std::string Path::BuildRelativePath(const std::string_view filename, const std::string_view new_filename)
{
	std::string new_string;

	std::string_view::size_type pos = GetLastSeperatorPosition(filename, true);
	if (pos != std::string_view::npos)
		new_string.assign(filename, 0, pos);
	new_string.append(new_filename);
	return new_string;
}

std::string Path::Combine(const std::string_view base, const std::string_view next)
{
	std::string ret;
	ret.reserve(base.length() + next.length() + 1);

	PathAppendString(ret, base);
	while (!ret.empty() && ret.back() == FS_OSPATH_SEPARATOR_CHARACTER)
		ret.pop_back();

	ret += FS_OSPATH_SEPARATOR_CHARACTER;
	PathAppendString(ret, next);
	while (!ret.empty() && ret.back() == FS_OSPATH_SEPARATOR_CHARACTER)
		ret.pop_back();

	return ret;
}


std::string Path::URLEncode(std::string_view str)
{
	std::string ret;
	ret.reserve(str.length() + ((str.length() + 3) / 4) * 3);

	for (size_t i = 0, l = str.size(); i < l; i++)
	{
		const char c = str[i];
		if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_' ||
			c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')')
		{
			ret.push_back(c);
		}
		else
		{
			ret.push_back('%');

			const unsigned char n1 = static_cast<unsigned char>(c) >> 4;
			const unsigned char n2 = static_cast<unsigned char>(c) & 0x0F;
			ret.push_back((n1 >= 10) ? ('a' + (n1 - 10)) : ('0' + n1));
			ret.push_back((n2 >= 10) ? ('a' + (n2 - 10)) : ('0' + n2));
		}
	}

	return ret;
}

std::string Path::URLDecode(std::string_view str)
{
	std::string ret;
	ret.reserve(str.length());

	for (size_t i = 0, l = str.size(); i < l; i++)
	{
		const char c = str[i];
		if (c == '+')
		{
			ret.push_back(c);
		}
		else if (c == '%')
		{
			if ((i + 2) >= str.length())
				break;

			const char clower = str[i + 1];
			const char cupper = str[i + 2];
			const unsigned char lower =
				(clower >= '0' && clower <= '9') ?
					static_cast<unsigned char>(clower - '0') :
					((clower >= 'a' && clower <= 'f') ?
							static_cast<unsigned char>(clower - 'a') :
							((clower >= 'A' && clower <= 'F') ? static_cast<unsigned char>(clower - 'A') : 0));
			const unsigned char upper =
				(cupper >= '0' && cupper <= '9') ?
					static_cast<unsigned char>(cupper - '0') :
					((cupper >= 'a' && cupper <= 'f') ?
							static_cast<unsigned char>(cupper - 'a') :
							((cupper >= 'A' && cupper <= 'F') ? static_cast<unsigned char>(cupper - 'A') : 0));
			const char dch = static_cast<char>(lower | (upper << 4));
			ret.push_back(dch);
		}
		else
		{
			ret.push_back(c);
		}
	}

	return std::string(str);
}

std::string Path::CreateFileURL(std::string_view path)
{
	pxAssert(IsAbsolute(path));

	std::string ret;
	ret.reserve(path.length() + 10);
	ret.append("file://");

	const std::vector<std::string_view> components = SplitNativePath(path);
	pxAssertRel(!components.empty(), "Trying to create a URL from an empty path.");

	const std::string_view& first = components.front();
#ifdef _WIN32
	// Windows doesn't urlencode the drive letter.
	// UNC paths should be omit the leading slash.
	if (first.starts_with("\\\\"))
	{
		// file://hostname/...
		ret.append(first.substr(2));
	}
	else
	{
		// file:///c:/...
		fmt::format_to(std::back_inserter(ret), "/{}", first);
	}
#else
	// Don't append a leading slash for the first component.
	ret.append(first);
#endif

	for (size_t comp = 1; comp < components.size(); comp++)
	{
		fmt::format_to(std::back_inserter(ret), "/{}", URLEncode(components[comp]));
	}

	return ret;
}

std::FILE* FileSystem::OpenCFile(const char* filename, const char* mode, Error* error)
{
#ifdef _WIN32
	const std::wstring wfilename = GetWin32Path(filename);
	const std::wstring wmode = GetWin32Path(mode);
	if (!wfilename.empty() && !wmode.empty())
	{
		std::FILE* fp;
		const errno_t err = _wfopen_s(&fp, wfilename.c_str(), wmode.c_str());
		if (err != 0)
		{
			Error::SetErrno(error, err);
			return nullptr;
		}

		return fp;
	}

	std::FILE* fp;
	const errno_t err = fopen_s(&fp, filename, mode);
	if (err != 0)
	{
		Error::SetErrno(error, err);
		return nullptr;
	}

	return fp;
#else
	std::FILE* fp = std::fopen(filename, mode);
	if (!fp)
		Error::SetErrno(error, errno);
	return fp;
#endif
}

int FileSystem::OpenFDFile(const char* filename, int flags, int mode, Error* error)
{
#ifdef _WIN32
	const std::wstring wfilename = GetWin32Path(filename);
	if (!wfilename.empty())
		return _wopen(wfilename.c_str(), flags, mode);

	return -1;
#else
	const int fd = open(filename, flags, mode);
	if (fd < 0)
		Error::SetErrno(error, errno);
	return fd;
#endif
}

FileSystem::ManagedCFilePtr FileSystem::OpenManagedCFile(const char* filename, const char* mode, Error* error)
{
	return ManagedCFilePtr(OpenCFile(filename, mode, error));
}

std::FILE* FileSystem::OpenSharedCFile(const char* filename, const char* mode, FileShareMode share_mode, Error* error)
{
#ifdef _WIN32
	const std::wstring wfilename = GetWin32Path(filename);
	const std::wstring wmode = GetWin32Path(mode);
	if (wfilename.empty() || wmode.empty())
		return nullptr;

	int share_flags = 0;
	switch (share_mode)
	{
		case FileShareMode::DenyNone:
			share_flags = _SH_DENYNO;
			break;
		case FileShareMode::DenyRead:
			share_flags = _SH_DENYRD;
			break;
		case FileShareMode::DenyWrite:
			share_flags = _SH_DENYWR;
			break;
		case FileShareMode::DenyReadWrite:
		default:
			share_flags = _SH_DENYRW;
			break;
	}

	std::FILE* fp = _wfsopen(wfilename.c_str(), wmode.c_str(), share_flags);
	if (fp)
		return fp;

	Error::SetErrno(error, errno);
	return nullptr;
#else
	std::FILE* fp = std::fopen(filename, mode);
	if (!fp)
		Error::SetErrno(error, errno);
	return fp;
#endif
}

FileSystem::ManagedCFilePtr FileSystem::OpenManagedSharedCFile(const char* filename, const char* mode, FileShareMode share_mode, Error* error)
{
	return ManagedCFilePtr(OpenSharedCFile(filename, mode, share_mode, error));
}

int FileSystem::FSeek64(std::FILE* fp, s64 offset, int whence)
{
#ifdef _WIN32
	return _fseeki64(fp, offset, whence);
#else
	return fseeko(fp, static_cast<off_t>(offset), whence);
#endif
}

s64 FileSystem::FTell64(std::FILE* fp)
{
#ifdef _WIN32
	return static_cast<s64>(_ftelli64(fp));
#else
	return static_cast<s64>(ftello(fp));
#endif
}

s64 FileSystem::FSize64(std::FILE* fp)
{
	const s64 pos = FTell64(fp);
	if (pos >= 0)
	{
		if (FSeek64(fp, 0, SEEK_END) == 0)
		{
			const s64 size = FTell64(fp);
			if (FSeek64(fp, pos, SEEK_SET) == 0)
				return size;
		}
	}

	return -1;
}

s64 FileSystem::GetPathFileSize(const char* Path)
{
	FILESYSTEM_STAT_DATA sd;
	if (!StatFile(Path, &sd))
		return -1;

	return sd.Size;
}

std::optional<std::time_t> FileSystem::GetFileTimestamp(const char* path)
{
	FILESYSTEM_STAT_DATA sd;
	if (!StatFile(path, &sd))
		return std::nullopt;

	return sd.ModificationTime;
}

std::optional<std::vector<u8>> FileSystem::ReadBinaryFile(const char* filename)
{
	ManagedCFilePtr fp = OpenManagedCFile(filename, "rb");
	if (!fp)
		return std::nullopt;

	return ReadBinaryFile(fp.get());
}

std::optional<std::vector<u8>> FileSystem::ReadBinaryFile(std::FILE* fp)
{
	const s64 size = FSize64(fp);
	if (size < 0)
		return std::nullopt;

	std::fseek(fp, 0, SEEK_SET);
	std::vector<u8> res(static_cast<size_t>(size));
	if (size > 0 && std::fread(res.data(), 1u, static_cast<size_t>(size), fp) != static_cast<size_t>(size))
		return std::nullopt;

	return res;
}

std::optional<std::string> FileSystem::ReadFileToString(const char* filename)
{
	ManagedCFilePtr fp = OpenManagedCFile(filename, "rb");
	if (!fp)
		return std::nullopt;

	return ReadFileToString(fp.get());
}

std::optional<std::string> FileSystem::ReadFileToString(std::FILE* fp)
{
	const s64 size = FSize64(fp);
	if (size < 0)
		return std::nullopt;

	std::fseek(fp, 0, SEEK_SET);
	std::string res;
	res.resize(static_cast<size_t>(size));
	// NOTE - assumes mode 'rb', for example, this will fail over missing Windows carriage return bytes
	if (size > 0 && std::fread(res.data(), 1u, static_cast<size_t>(size), fp) != static_cast<size_t>(size))
		return std::nullopt;

	return res;
}

bool FileSystem::WriteBinaryFile(const char* filename, const void* data, size_t data_length)
{
	ManagedCFilePtr fp = OpenManagedCFile(filename, "wb");
	if (!fp)
		return false;

	if (data_length > 0 && std::fwrite(data, 1u, data_length, fp.get()) != data_length)
		return false;

	return true;
}

bool FileSystem::WriteStringToFile(const char* filename, const std::string_view sv)
{
	ManagedCFilePtr fp = OpenManagedCFile(filename, "wb");
	if (!fp)
		return false;

	if (sv.length() > 0 && std::fwrite(sv.data(), 1u, sv.length(), fp.get()) != sv.length())
		return false;

	return true;
}

size_t FileSystem::ReadFileWithProgress(std::FILE* fp, void* dst, size_t length,
	ProgressCallback* progress, Error* error, size_t chunk_size)
{
	progress->SetProgressRange(100);

	size_t done = 0;
	while (done < length)
	{
		if (progress->IsCancelled())
			break;

		const size_t read_size = std::min(length - done, chunk_size);
		if (std::fread(static_cast<u8*>(dst) + done, read_size, 1, fp) != 1)
		{
			Error::SetErrno(error, "fread() failed: ", errno);
			break;
		}

		progress->SetProgressValue((done * 100) / length);
		done += read_size;
	}

	return done;
}

bool FileSystem::EnsureDirectoryExists(const char* path, bool recursive, Error* error)
{
	if (FileSystem::DirectoryExists(path))
		return true;

	// if it fails to create, we're not going to be able to use it anyway
	return FileSystem::CreateDirectoryPath(path, recursive, error);
}

bool FileSystem::RecursiveDeleteDirectory(const char* path)
{
	FindResultsArray results;
	if (FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, &results))
	{
		for (const FILESYSTEM_FIND_DATA& fd : results)
		{
			if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
			{
				if (!RecursiveDeleteDirectory(fd.FileName.c_str()))
					return false;
			}
			else
			{
				if (!DeleteFilePath(fd.FileName.c_str()))
					return false;
			}
		}
	}

	return DeleteDirectory(path);
}

bool FileSystem::CopyFilePath(const char* source, const char* destination, bool replace)
{
#ifndef _WIN32
	// TODO: There's technically a race here between checking and opening the file..
	// But fopen doesn't specify any way to say "don't create if it exists"...
	if (!replace && FileExists(destination))
		return false;

	auto in_fp = OpenManagedCFile(source, "rb");
	if (!in_fp)
		return false;

	auto out_fp = OpenManagedCFile(destination, "wb");
	if (!out_fp)
		return false;

	u8 buf[4096];
	while (!std::feof(in_fp.get()))
	{
		size_t bytes_in = std::fread(buf, 1, sizeof(buf), in_fp.get());
		if ((bytes_in == 0 && !std::feof(in_fp.get())) ||
			(bytes_in > 0 && std::fwrite(buf, 1, bytes_in, out_fp.get()) != bytes_in))
		{
			out_fp.reset();
			DeleteFilePath(destination);
			return false;
		}
	}

	if (std::fflush(out_fp.get()) != 0)
	{
		out_fp.reset();
		DeleteFilePath(destination);
		return false;
	}

	return true;
#else
	return CopyFileW(GetWin32Path(source).c_str(), GetWin32Path(destination).c_str(), !replace);
#endif
}

#ifdef _WIN32

static u32 TranslateWin32Attributes(u32 Win32Attributes)
{
	u32 r = 0;

	if (Win32Attributes & FILE_ATTRIBUTE_DIRECTORY)
		r |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY;
	if (Win32Attributes & FILE_ATTRIBUTE_READONLY)
		r |= FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY;
	if (Win32Attributes & FILE_ATTRIBUTE_COMPRESSED)
		r |= FILESYSTEM_FILE_ATTRIBUTE_COMPRESSED;

	return r;
}

static u32 RecursiveFindFiles(const char* origin_path, const char* parent_path, const char* path, const char* pattern,
	u32 flags, FileSystem::FindResultsArray* results, std::vector<std::string>& visited)
{
	std::string search_dir;
	if (path)
	{
		if (parent_path)
			search_dir = fmt::format("{}\\{}\\{}\\*", origin_path, parent_path, path);
		else
			search_dir = fmt::format("{}\\{}\\*", origin_path, path);
	}
	else
	{
		search_dir = fmt::format("{}\\*", origin_path);
	}

	// holder for utf-8 conversion
	WIN32_FIND_DATAW wfd;
	std::string utf8_filename;
	utf8_filename.reserve((sizeof(wfd.cFileName) / sizeof(wfd.cFileName[0])) * 2);

	const HANDLE hFind = FindFirstFileW(FileSystem::GetWin32Path(search_dir).c_str(), &wfd);
	if (hFind == INVALID_HANDLE_VALUE)
		return 0;

	// small speed optimization for '*' case
	bool hasWildCards = false;
	bool wildCardMatchAll = false;
	u32 nFiles = 0;
	if (std::strpbrk(pattern, "*?"))
	{
		hasWildCards = true;
		wildCardMatchAll = !(std::strcmp(pattern, "*"));
	}

	// iterate results
	do
	{
		if (wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN && !(flags & FILESYSTEM_FIND_HIDDEN_FILES))
			continue;

		if (wfd.cFileName[0] == L'.')
		{
			if (wfd.cFileName[1] == L'\0' || (wfd.cFileName[1] == L'.' && wfd.cFileName[2] == L'\0'))
				continue;
		}

		if (!StringUtil::WideStringToUTF8String(utf8_filename, wfd.cFileName))
			continue;

		FILESYSTEM_FIND_DATA outData;
		outData.Attributes = 0;

		if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
		{
			if (flags & FILESYSTEM_FIND_RECURSIVE)
			{
				// check that we're not following an infinite symbolic link loop
				std::string real_recurse_dir;
				if (parent_path)
					real_recurse_dir = Path::RealPath(fmt::format("{}\\{}\\{}\\{}", origin_path, parent_path, path, utf8_filename));
				else if (path)
					real_recurse_dir = Path::RealPath(fmt::format("{}\\{}\\{}", origin_path, path, utf8_filename));
				else
					real_recurse_dir = Path::RealPath(fmt::format("{}\\{}", origin_path, utf8_filename));
				if (real_recurse_dir.empty() || std::find(visited.begin(), visited.end(), real_recurse_dir) == visited.end())
				{
					if (!real_recurse_dir.empty())
						visited.push_back(std::move(real_recurse_dir));

					// recurse into this directory
					if (parent_path)
					{
						const std::string recurse_dir = fmt::format("{}\\{}", parent_path, path);
						nFiles += RecursiveFindFiles(origin_path, recurse_dir.c_str(), utf8_filename.c_str(), pattern, flags, results, visited);
					}
					else
					{
						nFiles += RecursiveFindFiles(origin_path, path, utf8_filename.c_str(), pattern, flags, results, visited);
					}
				}
			}

			if (!(flags & FILESYSTEM_FIND_FOLDERS))
				continue;

			outData.Attributes |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY;
		}
		else
		{
			if (!(flags & FILESYSTEM_FIND_FILES))
				continue;
		}

		if (wfd.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
			outData.Attributes |= FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY;

		// match the filename
		if (hasWildCards)
		{
			if (!wildCardMatchAll && !StringUtil::WildcardMatch(utf8_filename.c_str(), pattern))
				continue;
		}
		else
		{
			if (std::strcmp(utf8_filename.c_str(), pattern) != 0)
				continue;
		}

		// add file to list
		if (!(flags & FILESYSTEM_FIND_RELATIVE_PATHS))
		{
			if (parent_path)
				outData.FileName = fmt::format("{}\\{}\\{}\\{}", origin_path, parent_path, path, utf8_filename);
			else if (path)
				outData.FileName = fmt::format("{}\\{}\\{}", origin_path, path, utf8_filename);
			else
				outData.FileName = fmt::format("{}\\{}", origin_path, utf8_filename);
		}
		else
		{
			if (parent_path)
				outData.FileName = fmt::format("{}\\{}\\{}", parent_path, path, utf8_filename);
			else if (path)
				outData.FileName = fmt::format("{}\\{}", path, utf8_filename);
			else
				outData.FileName = utf8_filename;
		}

		outData.CreationTime = ConvertFileTimeToUnixTime(wfd.ftCreationTime);
		outData.ModificationTime = ConvertFileTimeToUnixTime(wfd.ftLastWriteTime);
		outData.Size = (static_cast<u64>(wfd.nFileSizeHigh) << 32) | static_cast<u64>(wfd.nFileSizeLow);

		nFiles++;
		results->push_back(std::move(outData));
	} while (FindNextFileW(hFind, &wfd) == TRUE);
	FindClose(hFind);

	return nFiles;
}

bool FileSystem::FindFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* results)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// clear result array
	if (!(flags & FILESYSTEM_FIND_KEEP_ARRAY))
		results->clear();

	// add self if recursive, we don't want to visit it twice
	std::vector<std::string> visited;
	if (flags & FILESYSTEM_FIND_RECURSIVE)
	{
		std::string real_path = Path::RealPath(path);
		if (!real_path.empty())
			visited.push_back(std::move(real_path));
	}

	// enter the recursive function
	if (RecursiveFindFiles(path, nullptr, nullptr, pattern, flags, results, visited) == 0)
		return false;

	if (flags & FILESYSTEM_FIND_SORT_BY_NAME)
	{
		std::sort(results->begin(), results->end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) {
			// directories first
			if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) !=
				(rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY))
			{
				return ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0);
			}

			return (StringUtil::Strcasecmp(lhs.FileName.c_str(), rhs.FileName.c_str()) < 0);
		});
	}

	return true;
}

static void TranslateStat64(struct stat* st, const struct _stat64& st64)
{
	static constexpr __int64 MAX_SIZE = static_cast<__int64>(std::numeric_limits<decltype(st->st_size)>::max());
	st->st_dev = st64.st_dev;
	st->st_ino = st64.st_ino;
	st->st_mode = st64.st_mode;
	st->st_nlink = st64.st_nlink;
	st->st_uid = st64.st_uid;
	st->st_rdev = st64.st_rdev;
	st->st_size = static_cast<decltype(st->st_size)>((st64.st_size > MAX_SIZE) ? MAX_SIZE : st64.st_size);
	st->st_atime = static_cast<time_t>(st64.st_atime);
	st->st_mtime = static_cast<time_t>(st64.st_mtime);
	st->st_ctime = static_cast<time_t>(st64.st_ctime);
}

bool FileSystem::StatFile(const char* path, struct stat* st)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// convert to wide string
	const std::wstring wpath = GetWin32Path(path);
	if (wpath.empty())
		return false;

	struct _stat64 st64;
	if (_wstat64(wpath.c_str(), &st64) != 0)
		return false;

	TranslateStat64(st, st64);
	return true;
}

bool FileSystem::StatFile(std::FILE* fp, struct stat* st)
{
	const int fd = _fileno(fp);
	if (fd < 0)
		return false;

	struct _stat64 st64;
	if (_fstat64(fd, &st64) != 0)
		return false;

	TranslateStat64(st, st64);
	return true;
}

bool FileSystem::StatFile(const char* path, FILESYSTEM_STAT_DATA* sd)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// convert to wide string
	const std::wstring wpath = GetWin32Path(path);
	if (wpath.empty())
		return false;

	// determine attributes for the path. if it's a directory, things have to be handled differently..
	DWORD fileAttributes = GetFileAttributesW(wpath.c_str());
	if (fileAttributes == INVALID_FILE_ATTRIBUTES)
		return false;

	// test if it is a directory
	HANDLE hFile;
	if (fileAttributes & FILE_ATTRIBUTE_DIRECTORY)
	{
		hFile = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
			OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
	}
	else
	{
		hFile = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
			OPEN_EXISTING, 0, nullptr);
	}

	// createfile succeded?
	if (hFile == INVALID_HANDLE_VALUE)
		return false;

	// use GetFileInformationByHandle
	BY_HANDLE_FILE_INFORMATION bhfi;
	if (GetFileInformationByHandle(hFile, &bhfi) == FALSE)
	{
		CloseHandle(hFile);
		return false;
	}

	// close handle
	CloseHandle(hFile);

	// fill in the stat data
	sd->Attributes = TranslateWin32Attributes(bhfi.dwFileAttributes);
	sd->CreationTime = ConvertFileTimeToUnixTime(bhfi.ftCreationTime);
	sd->ModificationTime = ConvertFileTimeToUnixTime(bhfi.ftLastWriteTime);
	sd->Size = static_cast<s64>(((u64)bhfi.nFileSizeHigh) << 32 | (u64)bhfi.nFileSizeLow);
	return true;
}

bool FileSystem::StatFile(std::FILE* fp, FILESYSTEM_STAT_DATA* sd)
{
	const int fd = _fileno(fp);
	if (fd < 0)
		return false;

	struct _stat64 st;
	if (_fstat64(fd, &st) != 0)
		return false;

	// parse attributes
	sd->CreationTime = st.st_ctime;
	sd->ModificationTime = st.st_mtime;
	sd->Attributes = 0;
	if ((st.st_mode & _S_IFMT) == _S_IFDIR)
		sd->Attributes |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY;

	// parse size
	if ((st.st_mode & _S_IFMT) == _S_IFREG)
		sd->Size = st.st_size;
	else
		sd->Size = 0;

	return true;
}

bool FileSystem::FileExists(const char* path)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// convert to wide string
	const std::wstring wpath = GetWin32Path(path);
	if (wpath.empty())
		return false;

	// determine attributes for the path. if it's a directory, things have to be handled differently..
	DWORD fileAttributes = GetFileAttributesW(wpath.c_str());
	if (fileAttributes == INVALID_FILE_ATTRIBUTES)
		return false;

	if (fileAttributes & FILE_ATTRIBUTE_DIRECTORY)
		return false;
	else
		return true;
}

bool FileSystem::DirectoryExists(const char* path)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// convert to wide string
	const std::wstring wpath = GetWin32Path(path);
	if (wpath.empty())
		return false;

	// determine attributes for the path. if it's a directory, things have to be handled differently..
	DWORD fileAttributes = GetFileAttributesW(wpath.c_str());
	if (fileAttributes == INVALID_FILE_ATTRIBUTES)
		return false;

	if (fileAttributes & FILE_ATTRIBUTE_DIRECTORY)
		return true;
	else
		return false;
}

bool FileSystem::IsRealDirectory(const char* path)
{
	// convert to wide string
	const std::wstring wpath = GetWin32Path(path);
	if (wpath.empty())
		return false;

	// determine attributes for the path. if it's a directory, things have to be handled differently..
	const DWORD fileAttributes = GetFileAttributesW(wpath.c_str());
	if (fileAttributes == INVALID_FILE_ATTRIBUTES)
		return false;

	return ((fileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != FILE_ATTRIBUTE_DIRECTORY);
}

bool FileSystem::DirectoryIsEmpty(const char* path)
{
	std::wstring wpath = GetWin32Path(path);
	wpath += L"\\*";

	WIN32_FIND_DATAW wfd;
	HANDLE hFind = FindFirstFileW(wpath.c_str(), &wfd);

	if (hFind == INVALID_HANDLE_VALUE)
		return true;

	do
	{
		if (wfd.cFileName[0] == L'.')
		{
			if (wfd.cFileName[1] == L'\0' || (wfd.cFileName[1] == L'.' && wfd.cFileName[2] == L'\0'))
				continue;
		}

		FindClose(hFind);
		return false;
	} while (FindNextFileW(hFind, &wfd));

	FindClose(hFind);
	return true;
}

bool FileSystem::CreateDirectoryPath(const char* Path, bool Recursive, Error* error)
{
	const std::wstring wpath = GetWin32Path(Path);

	// has a path
	if (wpath.empty()) [[unlikely]]
	{
		Error::SetStringView(error, "Path is empty.");
		return false;
	}

	// try just flat-out, might work if there's no other segments that have to be made
	if (CreateDirectoryW(wpath.c_str(), nullptr))
		return true;

	// check error
	DWORD lastError = GetLastError();
	if (lastError == ERROR_ALREADY_EXISTS)
	{
		// check the attributes
		const u32 Attributes = GetFileAttributesW(wpath.c_str());
		if (Attributes != INVALID_FILE_ATTRIBUTES && Attributes & FILE_ATTRIBUTE_DIRECTORY)
			return true;
	}

	if (!Recursive)
	{
		Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError);
		return false;
	}

	// check error
	if (lastError == ERROR_PATH_NOT_FOUND)
	{
		// part of the path does not exist, so we'll create the parent folders, then
		// the full path again.
		const size_t pathLength = wpath.size();
		std::wstring tempPath;
		tempPath.reserve(pathLength);

		// for absolute paths, we need to skip over the path root
		size_t rootLength = 0;
		if (Path::IsAbsolute(Path))
		{
			const wchar_t* root_start = wpath.c_str();
			wchar_t* root_end;
			const HRESULT hr = PathCchSkipRoot(const_cast<wchar_t*>(root_start), &root_end);
			if (FAILED(hr))
			{
				Error::SetHResult(error, "PathCchSkipRoot() failed: ", hr);
				return false;
			}
			rootLength = static_cast<size_t>(root_end - root_start);

			// copy path root
			tempPath.append(wpath, 0, rootLength);
		}

		// create directories along the path
		for (size_t i = rootLength; i < pathLength; i++)
		{
			if (wpath[i] == L'\\' || wpath[i] == L'/')
			{
				const BOOL result = CreateDirectoryW(tempPath.c_str(), nullptr);
				if (!result)
				{
					lastError = GetLastError();
					if (lastError != ERROR_ALREADY_EXISTS) // fine, continue to next path segment
					{
						Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError);
						return false;
					}
				}

				// replace / with \.
				tempPath.push_back('\\');
			}
			else
			{
				tempPath.push_back(wpath[i]);
			}
		}

		// re-create the end if it's not a separator, check / as well because windows can interpret them
		if (wpath[pathLength - 1] != L'\\' && wpath[pathLength - 1] != L'/')
		{
			const BOOL result = CreateDirectoryW(wpath.c_str(), nullptr);
			if (!result)
			{
				lastError = GetLastError();
				if (lastError != ERROR_ALREADY_EXISTS)
				{
					Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError);
					return false;
				}
			}
		}

		// ok
		return true;
	}
	else
	{
		// unhandled error
		Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError);
		return false;
	}
}

bool FileSystem::DeleteFilePath(const char* path, Error* error)
{
	if (path[0] == '\0')
	{
		Error::SetStringView(error, "Path is empty.");
		return false;
	}

	const std::wstring wpath = GetWin32Path(path);
	const DWORD fileAttributes = GetFileAttributesW(wpath.c_str());
	if (fileAttributes == INVALID_FILE_ATTRIBUTES || fileAttributes & FILE_ATTRIBUTE_DIRECTORY)
	{
		Error::SetStringView(error, "File does not exist.");
		return false;
	}

	if (!DeleteFileW(wpath.c_str()))
	{
		Error::SetWin32(error, "DeleteFileW() failed: ", GetLastError());
		return false;
	}

	return true;
}

bool FileSystem::RenamePath(const char* old_path, const char* new_path, Error* error)
{
	const std::wstring old_wpath = GetWin32Path(old_path);
	const std::wstring new_wpath = GetWin32Path(new_path);

	if (!MoveFileExW(old_wpath.c_str(), new_wpath.c_str(), MOVEFILE_REPLACE_EXISTING))
	{
		const DWORD err = GetLastError();
		Error::SetWin32(error, "MoveFileExW() failed: ", err);
		Console.Error("MoveFileEx('%s', '%s') failed: %08X", old_path, new_path, err);
		return false;
	}

	return true;
}

bool FileSystem::DeleteDirectory(const char* path)
{
	const std::wstring wpath = GetWin32Path(path);
	return RemoveDirectoryW(wpath.c_str());
}

std::string FileSystem::GetProgramPath()
{
	std::wstring buffer;
	buffer.resize(MAX_PATH);

	// Fall back to the main module if this fails.
	HMODULE module = nullptr;
	GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
		reinterpret_cast<LPCWSTR>(&GetProgramPath), &module);

	for (;;)
	{
		DWORD nChars = GetModuleFileNameW(module, buffer.data(), static_cast<DWORD>(buffer.size()));
		if (nChars == static_cast<DWORD>(buffer.size()) && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
		{
			buffer.resize(buffer.size() * 2);
			continue;
		}

		buffer.resize(nChars);
		break;
	}

	// Windows symlinks don't behave silly like Linux, so no need to RealPath() it.
	return StringUtil::WideStringToUTF8String(buffer);
}

std::string FileSystem::GetWorkingDirectory()
{
	DWORD required_size = GetCurrentDirectoryW(0, nullptr);
	if (!required_size)
		return {};

	std::wstring buffer;
	buffer.resize(required_size - 1);

	if (!GetCurrentDirectoryW(static_cast<DWORD>(buffer.size() + 1), buffer.data()))
		return {};

	return StringUtil::WideStringToUTF8String(buffer);
}

bool FileSystem::SetWorkingDirectory(const char* path)
{
	const std::wstring wpath = GetWin32Path(path);
	return (SetCurrentDirectoryW(wpath.c_str()) == TRUE);
}

bool FileSystem::SetPathCompression(const char* path, bool enable)
{
	const std::wstring wpath = GetWin32Path(path);
	const DWORD attrs = GetFileAttributesW(wpath.c_str());
	if (attrs == INVALID_FILE_ATTRIBUTES)
		return false;

	const bool isCompressed = (attrs & FILE_ATTRIBUTE_COMPRESSED) != 0;
	if (enable == isCompressed)
	{
		// already compressed/not compressed
		return true;
	}

	const bool isFile = !(attrs & FILE_ATTRIBUTE_DIRECTORY);
	const DWORD flags = isFile ? FILE_ATTRIBUTE_NORMAL : (FILE_FLAG_BACKUP_SEMANTICS | FILE_ATTRIBUTE_DIRECTORY);

	const HANDLE handle = CreateFileW(wpath.c_str(),
		FILE_GENERIC_WRITE | FILE_GENERIC_READ,
		FILE_SHARE_READ | FILE_SHARE_DELETE,
		nullptr,
		OPEN_EXISTING,
		flags,
		nullptr);
	if (handle == INVALID_HANDLE_VALUE)
		return false;

	DWORD bytesReturned = 0;
	DWORD compressMode = enable ? COMPRESSION_FORMAT_DEFAULT : COMPRESSION_FORMAT_NONE;

	bool result = DeviceIoControl(
		handle, FSCTL_SET_COMPRESSION,
		&compressMode, 2, nullptr, 0,
		&bytesReturned, nullptr);

	CloseHandle(handle);
	return result;
}

#else

// No 32-bit file offsets breaking stuff please.
static_assert(sizeof(off_t) == sizeof(s64));

static u32 RecursiveFindFiles(const char* OriginPath, const char* ParentPath, const char* Path, const char* Pattern,
	u32 Flags, FileSystem::FindResultsArray* pResults, std::vector<std::string>& visited)
{
	std::string tempStr;
	if (Path)
	{
		if (ParentPath)
			tempStr = fmt::format("{}/{}/{}", OriginPath, ParentPath, Path);
		else
			tempStr = fmt::format("{}/{}", OriginPath, Path);
	}
	else
	{
		tempStr = fmt::format("{}", OriginPath);
	}

	DIR* pDir = opendir(tempStr.c_str());
	if (!pDir)
		return 0;

	// small speed optimization for '*' case
	bool hasWildCards = false;
	bool wildCardMatchAll = false;
	u32 nFiles = 0;
	if (std::strpbrk(Pattern, "*?"))
	{
		hasWildCards = true;
		wildCardMatchAll = (std::strcmp(Pattern, "*") == 0);
	}

	// iterate results
	struct dirent* pDirEnt;
	while ((pDirEnt = readdir(pDir)) != nullptr)
	{
		if (pDirEnt->d_name[0] == '.')
		{
			if (pDirEnt->d_name[1] == '\0' || (pDirEnt->d_name[1] == '.' && pDirEnt->d_name[2] == '\0'))
				continue;

			if (!(Flags & FILESYSTEM_FIND_HIDDEN_FILES))
				continue;
		}

		std::string full_path;
		if (ParentPath)
			full_path = fmt::format("{}/{}/{}/{}", OriginPath, ParentPath, Path, pDirEnt->d_name);
		else if (Path)
			full_path = fmt::format("{}/{}/{}", OriginPath, Path, pDirEnt->d_name);
		else
			full_path = fmt::format("{}/{}", OriginPath, pDirEnt->d_name);

		FILESYSTEM_FIND_DATA outData;
		outData.Attributes = 0;

		struct stat sDir;
		if (stat(full_path.c_str(), &sDir) < 0)
			continue;

		if (S_ISDIR(sDir.st_mode))
		{
			if (Flags & FILESYSTEM_FIND_RECURSIVE)
			{
				// check that we're not following an infinite symbolic link loop
				if (std::string real_recurse_dir = Path::RealPath(full_path);
					real_recurse_dir.empty() || std::find(visited.begin(), visited.end(), real_recurse_dir) == visited.end())
				{
					if (!real_recurse_dir.empty())
						visited.push_back(std::move(real_recurse_dir));

					// recurse into this directory
					if (ParentPath)
					{
						const std::string recursive_dir = fmt::format("{}/{}", ParentPath, Path);
						nFiles += RecursiveFindFiles(OriginPath, recursive_dir.c_str(), pDirEnt->d_name, Pattern, Flags, pResults, visited);
					}
					else
					{
						nFiles += RecursiveFindFiles(OriginPath, Path, pDirEnt->d_name, Pattern, Flags, pResults, visited);
					}
				}
			}

			if (!(Flags & FILESYSTEM_FIND_FOLDERS))
				continue;

			outData.Attributes |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY;
		}
		else
		{
			if (!(Flags & FILESYSTEM_FIND_FILES))
				continue;
		}

		outData.Size = static_cast<u64>(sDir.st_size);
		outData.CreationTime = sDir.st_ctime;
		outData.ModificationTime = sDir.st_mtime;

		// match the filename
		if (hasWildCards)
		{
			if (!wildCardMatchAll && !StringUtil::WildcardMatch(pDirEnt->d_name, Pattern))
				continue;
		}
		else
		{
			if (std::strcmp(pDirEnt->d_name, Pattern) != 0)
				continue;
		}

		// add file to list
		if (!(Flags & FILESYSTEM_FIND_RELATIVE_PATHS))
		{
			outData.FileName = std::move(full_path);
		}
		else
		{
			if (ParentPath)
				outData.FileName = fmt::format("{}/{}/{}", ParentPath, Path, pDirEnt->d_name);
			else if (Path)
				outData.FileName = fmt::format("{}/{}", Path, pDirEnt->d_name);
			else
				outData.FileName = pDirEnt->d_name;
		}

		nFiles++;
		pResults->push_back(std::move(outData));
	}

	closedir(pDir);
	return nFiles;
}

bool FileSystem::FindFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* results)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// clear result array
	if (!(flags & FILESYSTEM_FIND_KEEP_ARRAY))
		results->clear();

	// add self if recursive, we don't want to visit it twice
	std::vector<std::string> visited;
	if (flags & FILESYSTEM_FIND_RECURSIVE)
	{
		std::string real_path = Path::RealPath(path);
		if (!real_path.empty())
			visited.push_back(std::move(real_path));
	}

	// enter the recursive function
	if (RecursiveFindFiles(path, nullptr, nullptr, pattern, flags, results, visited) == 0)
		return false;

	if (flags & FILESYSTEM_FIND_SORT_BY_NAME)
	{
		std::sort(results->begin(), results->end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) {
			// directories first
			if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) !=
				(rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY))
			{
				return ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0);
			}

			return (StringUtil::Strcasecmp(lhs.FileName.c_str(), rhs.FileName.c_str()) < 0);
		});
	}

	return true;
}

bool FileSystem::StatFile(const char* path, struct stat* st)
{
	return stat(path, st) == 0;
}

bool FileSystem::StatFile(std::FILE* fp, struct stat* st)
{
	const int fd = fileno(fp);
	if (fd < 0)
		return false;

	return fstat(fd, st) == 0;
}

bool FileSystem::StatFile(const char* path, FILESYSTEM_STAT_DATA* sd)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// stat file
	struct stat sysStatData;
	if (stat(path, &sysStatData) < 0)
		return false;

	// parse attributes
	sd->CreationTime = sysStatData.st_ctime;
	sd->ModificationTime = sysStatData.st_mtime;
	sd->Attributes = 0;
	if (S_ISDIR(sysStatData.st_mode))
		sd->Attributes |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY;

	// parse size
	if (S_ISREG(sysStatData.st_mode))
		sd->Size = sysStatData.st_size;
	else
		sd->Size = 0;

	// ok
	return true;
}

bool FileSystem::StatFile(std::FILE* fp, FILESYSTEM_STAT_DATA* sd)
{
	const int fd = fileno(fp);
	if (fd < 0)
		return false;

	// stat file
	struct stat sysStatData;
	if (fstat(fd, &sysStatData) < 0)
		return false;

	// parse attributes
	sd->CreationTime = sysStatData.st_ctime;
	sd->ModificationTime = sysStatData.st_mtime;
	sd->Attributes = 0;
	if (S_ISDIR(sysStatData.st_mode))
		sd->Attributes |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY;

	// parse size
	if (S_ISREG(sysStatData.st_mode))
		sd->Size = sysStatData.st_size;
	else
		sd->Size = 0;

	// ok
	return true;
}

bool FileSystem::FileExists(const char* path)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// stat file
	struct stat sysStatData;
	if (stat(path, &sysStatData) < 0)
		return false;

	if (S_ISDIR(sysStatData.st_mode))
		return false;
	else
		return true;
}

bool FileSystem::DirectoryExists(const char* path)
{
	// has a path
	if (path[0] == '\0')
		return false;

	// stat file
	struct stat sysStatData;
	if (stat(path, &sysStatData) < 0)
		return false;

	if (S_ISDIR(sysStatData.st_mode))
		return true;
	else
		return false;
}

bool FileSystem::IsRealDirectory(const char* path)
{
	struct stat sysStatData;
	if (lstat(path, &sysStatData) < 0)
		return false;

	return (S_ISDIR(sysStatData.st_mode) && !S_ISLNK(sysStatData.st_mode));
}

bool FileSystem::DirectoryIsEmpty(const char* path)
{
	DIR* pDir = opendir(path);
	if (pDir == nullptr)
		return true;

	// iterate results
	struct dirent* pDirEnt;
	while ((pDirEnt = readdir(pDir)) != nullptr)
	{
		if (pDirEnt->d_name[0] == '.')
		{
			if (pDirEnt->d_name[1] == '\0' || (pDirEnt->d_name[1] == '.' && pDirEnt->d_name[2] == '\0'))
				continue;
		}

		closedir(pDir);
		return false;
	}

	closedir(pDir);
	return true;
}

bool FileSystem::CreateDirectoryPath(const char* path, bool recursive, Error* error)
{
	// has a path
	const size_t pathLength = std::strlen(path);
	if (pathLength == 0)
		return false;

	// try just flat-out, might work if there's no other segments that have to be made
	if (mkdir(path, 0777) == 0)
		return true;

	// check error
	int lastError = errno;
	if (lastError == EEXIST)
	{
		// check the attributes
		struct stat sysStatData;
		if (stat(path, &sysStatData) == 0 && S_ISDIR(sysStatData.st_mode))
			return true;
	}

	if (!recursive)
	{
		Error::SetErrno(error, "mkdir() failed: ", lastError);
		return false;
	}

	if (lastError == ENOENT)
	{
		// part of the path does not exist, so we'll create the parent folders, then
		// the full path again.
		std::string tempPath;
		tempPath.reserve(pathLength);

		// create directories along the path
		for (size_t i = 0; i < pathLength; i++)
		{
			if (i > 0 && path[i] == '/')
			{
				if (mkdir(tempPath.c_str(), 0777) < 0)
				{
					lastError = errno;
					if (lastError != EEXIST) // fine, continue to next path segment
					{
						Error::SetErrno(error, "mkdir() failed: ", lastError);
						return false;
					}
				}
			}

			tempPath.push_back(path[i]);
		}

		// re-create the end if it's not a separator, check / as well because windows can interpret them
		if (path[pathLength - 1] != '/')
		{
			if (mkdir(path, 0777) < 0)
			{
				lastError = errno;
				if (lastError != EEXIST)
				{
					Error::SetErrno(error, "mkdir() failed: ", lastError);
					return false;
				}
			}
		}

		// ok
		return true;
	}
	else
	{
		// unhandled error
		Error::SetErrno(error, "mkdir() failed: ", lastError);
		return false;
	}
}

bool FileSystem::DeleteFilePath(const char* path, Error* error)
{
	if (path[0] == '\0')
	{
		Error::SetStringView(error, "Path is empty.");
		return false;
	}

	struct stat sysStatData;
	if (stat(path, &sysStatData) != 0 || S_ISDIR(sysStatData.st_mode))
	{
		Error::SetStringView(error, "File does not exist.");
		return false;
	}

	if (unlink(path) != 0)
	{
		Error::SetErrno(error, "unlink() failed: ", errno);
		return false;
	}

	return true;
}

bool FileSystem::RenamePath(const char* old_path, const char* new_path, Error* error)
{
	if (old_path[0] == '\0' || new_path[0] == '\0')
	{
		Error::SetStringView(error, "Path is empty.");
		return false;
	}

	if (rename(old_path, new_path) != 0)
	{
		const int err = errno;
		Error::SetErrno(error, "rename() failed: ", err);
		Console.Error("rename('%s', '%s') failed: %d", old_path, new_path, err);
		return false;
	}

	return true;
}

bool FileSystem::DeleteDirectory(const char* path)
{
	if (path[0] == '\0')
		return false;

	struct stat sysStatData;
	if (stat(path, &sysStatData) != 0 || !S_ISDIR(sysStatData.st_mode))
		return false;

	return (rmdir(path) == 0);
}

std::string FileSystem::GetProgramPath()
{
#if defined(__linux__)
	static const char* exeFileName = "/proc/self/exe";

	int curSize = PATH_MAX;
	char* buffer = static_cast<char*>(std::realloc(nullptr, curSize));
	for (;;)
	{
		int len = readlink(exeFileName, buffer, curSize);
		if (len < 0)
		{
			std::free(buffer);
			return {};
		}
		else if (len < curSize)
		{
			buffer[len] = '\0';
			std::string ret(buffer, len);
			std::free(buffer);
			return ret;
		}

		curSize *= 2;
		buffer = static_cast<char*>(std::realloc(buffer, curSize));
	}

#elif defined(__APPLE__)

	int curSize = PATH_MAX;
	char* buffer = static_cast<char*>(std::realloc(nullptr, curSize));
	for (;;)
	{
		u32 nChars = curSize - 1;
		int res = _NSGetExecutablePath(buffer, &nChars);
		if (res == 0)
		{
			buffer[nChars] = 0;

			char* resolvedBuffer = realpath(buffer, nullptr);
			if (resolvedBuffer == nullptr)
			{
				std::free(buffer);
				return {};
			}

			std::string ret(buffer);
			std::free(buffer);
			return ret;
		}

		curSize *= 2;
		buffer = static_cast<char*>(std::realloc(buffer, curSize + 1));
	}

#elif defined(__FreeBSD__)
	int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
	char buffer[PATH_MAX];
	size_t cb = sizeof(buffer) - 1;
	int res = sysctl(mib, std::size(mib), buffer, &cb, nullptr, 0);
	if (res != 0)
		return {};

	buffer[cb] = '\0';
	return buffer;
#else
	return {};
#endif
}

std::string FileSystem::GetWorkingDirectory()
{
	std::string buffer;
	buffer.resize(PATH_MAX);
	while (!getcwd(buffer.data(), buffer.size()))
	{
		if (errno != ERANGE)
			return {};

		buffer.resize(buffer.size() * 2);
	}

	buffer.resize(std::strlen(buffer.c_str())); // Remove excess nulls
	return buffer;
}

bool FileSystem::SetWorkingDirectory(const char* path)
{
	return (chdir(path) == 0);
}

bool FileSystem::SetPathCompression(const char* path, bool enable)
{
	return false;
}

FileSystem::POSIXLock::POSIXLock(int fd)
{
	if (lockf(fd, F_LOCK, 0) == 0)
	{
		m_fd = fd;
	}
	else
	{
		Console.Error("lockf() failed: %d", errno);
		m_fd = -1;
	}
}

FileSystem::POSIXLock::POSIXLock(std::FILE* fp)
{
	m_fd = fileno(fp);
	if (m_fd >= 0)
	{
		if (lockf(m_fd, F_LOCK, 0) != 0)
		{
			Console.Error("lockf() failed: %d", errno);
			m_fd = -1;
		}
	}
}

FileSystem::POSIXLock::~POSIXLock()
{
	if (m_fd >= 0)
		lockf(m_fd, F_ULOCK, m_fd);
}

#endif