/*
 *  This file is part of nzbget. See <https://nzbget.com>.
 *
 *  Copyright (C) 2007-2017 Andrey Prygunkov <hugbug@users.sourceforge.net>
 *  Copyright (C) 2024-2026 Denis <denis@nzbget.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */


#ifndef FILESYSTEM_H
#define FILESYSTEM_H

#include <optional>
#include <chrono>
#include "NString.h"

#ifdef _WIN32
#include "Utf8.h"
#endif

#ifdef HAVE_STD_FILESYSTEM
#include <filesystem>

namespace fs
{
using namespace std::filesystem;
using error_code = std::error_code;
using errc = std::errc;

inline std::string u8string(const path& p)
{
	return p.u8string(); 
}
}

#else
#include <boost/filesystem.hpp>

namespace fs
{
using namespace boost::filesystem;
using error_code = boost::system::error_code;
namespace errc = boost::system::errc;

inline fs::path u8path(std::string_view pathStr)
{
#ifdef _WIN32
	if (auto wstr = Utf8::Utf8ToWide(pathStr))
		return fs::path(*wstr);
	return fs::path(pathStr); 
#else
	return fs::path(pathStr);
#endif
}

inline std::string u8string(const path& p)
{
#ifdef _WIN32
	return Utf8::WideToUtf8(p.native()).value_or("");
#else
	return p.string();
#endif
}

}

#endif

namespace fs
{
inline path make_unique_filename()
{
	return std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
}

inline fs::path make_unique_filename(const fs::path& targetPath)
{
	fs::error_code ec;
	if (!fs::exists(targetPath, ec) && !ec)
	{
		return targetPath;
	}

	const fs::path baseDir = targetPath.parent_path();
	const std::string stem = fs::u8string(targetPath.stem());
	const std::string ext = fs::u8string(targetPath.extension());

	int counter = 1;
	fs::path uniquePath;
	do
	{
		uniquePath = baseDir / (stem + " (" + std::to_string(counter++) + ")" + ext);
	} while (fs::exists(uniquePath, ec) && !ec);

	return uniquePath;
}

inline void move_file(const fs::path& src, const fs::path& dest, fs::error_code& ec) noexcept
{
	fs::rename(src, dest, ec);
	if (ec == std::errc::cross_device_link)
	{
		ec.clear();
		fs::copy_file(src, dest, fs::copy_options::overwrite_existing, ec);
		if (!ec) fs::remove(src, ec);
	}
}

inline void move_file(const fs::path& src, const fs::path& dest)
{
	error_code ec;
	move_file(src, dest, ec);
	if (ec) throw std::runtime_error(ec.message());
}
}

class FileSystem
{
	struct DiskState
	{
		int64 available;
		int64 total;
	};

public:
	static CString GetLastErrorMessage();
	static char* BaseFileName(const char* filename);
	static std::pair<std::string, std::string> SplitPathAndFilename(const std::string& fullPath);
	static bool SameFilename(const char* filename1, const char* filename2);
	static void NormalizePathSeparators(char* path);
	static std::optional<std::string> GetRealPath(const std::string& path);
	static bool LoadFileIntoBuffer(const char* filename, CharBuffer& buffer, bool addTrailingNull);
	static bool SaveBufferIntoFile(const char* filename, const char* buffer, int bufLen);
	static bool AllocateFile(const char* filename, int64 size, bool sparse, CString& errmsg);
	static bool TruncateFile(const char* filename, int size);
	static CString MakeValidFilename(const char* filename, bool allowSlashes = false);
	static bool ReservedChar(char ch);
	static CString MakeUniqueFilename(const char* destDir, const char* basename);
	static bool MoveFile(const char* srcFilename, const char* dstFilename);
	static bool CopyFile(const char* srcFilename, const char* dstFilename);
	static bool DeleteFile(const char* filename);
	static bool FileExists(const char* filename);
	static bool DirectoryExists(const char* dirFilename);
	static bool CreateDirectory(const char* dirFilename);
	static std::string ExtractFilePathFromCmd(const std::string& path);
	static std::string EscapePathForShell(const std::string& path);
	static std::optional<std::string> GetFileExtension(const std::string& filename);

	/* Delete empty directory */
	static bool RemoveDirectory(const char* dirFilename);

	/* Delete directory which is empty or contains only hidden files or directories */
	static bool DeleteDirectory(const char* dirFilename);

	static bool DeleteDirectoryWithContent(const char* dirFilename, CString& errmsg);
	static bool ForceDirectories(const char* path, CString& errmsg);
	static bool CreateHardLink(const char *from, const char *to, CString& errmsg);
	static CString GetCurrentDirectory();
	static bool SetCurrentDirectory(const char* dirFilename);
	static int64 FileSize(const char* filename);
	static std::optional<DiskState> GetDiskState(const char* path);
	static bool DirEmpty(const char* dirFilename);
	static bool RenameBak(const char* filename, const char* bakPart, bool removeOldExtension, CString& newName);
#ifndef WIN32
	static CString ExpandHomePath(const char* filename);
	static void FixExecPermission(const char* filename);
	static bool RestoreFileOrDirPermissions(const char* filename);
	static bool RestoreFilePermissions(const char* filename);
	static bool RestoreDirPermissions(const char* filename);
	static bool RestorePermissions(const char* filename, mode_t mode);
	static mode_t uMask;
#endif
	static CString ExpandFileName(const char* filename);
	static CString GetExeFileName(const char* argv0);

	/* Flush disk buffers for file with given descriptor */
	static bool FlushFileBuffers(int fileDescriptor, CString& errmsg);

	/* Flush disk buffers for file metadata (after file renaming) */
	static bool FlushDirBuffers(const char* filename, CString& errmsg);

	static CString MakeExtendedPath(const char* path, bool force);

#ifdef WIN32
	static WString UtfPathToWidePath(const char* utfpath);
	static CString WidePathToUtfPath(const wchar_t* wpath);
	static CString MakeCanonicalPath(const char* filename);
	static bool NeedLongPath(const char* path);

	static const size_t MAX_DIR_PATH;
#endif
};

class DirBrowser
{
public:
	DirBrowser(const char* path, bool snapshot = true);
	~DirBrowser();
	const char* Next();

private:
#ifdef WIN32
	WIN32_FIND_DATAW m_findData;
	HANDLE m_file = INVALID_HANDLE_VALUE;
	bool m_first;
	CString m_filename;
#else
	DIR* m_dir = nullptr;
	struct dirent* m_findData;
#endif

	bool m_snapshot;
	typedef std::deque<CString> FileList;
	FileList m_snapshotFiles;
	FileList::iterator m_snapshotIter;

	const char* InternNext();
};

class DiskFile
{
public:
	enum EOpenMode
	{
		omRead, // file must exist
		omReadWrite, // file must exist
		omWrite, // create new or overwrite existing
		omAppend // create new or append to existing
	};

	enum ESeekOrigin
	{
		soSet,
		soCur,
		soEnd
	};

	DiskFile() = default;
	DiskFile(const DiskFile&) = delete;
	~DiskFile();
	bool Open(const char* filename, EOpenMode mode);
	bool Close();
	bool Active() { return m_file != nullptr; }
	int64 Read(void* buffer, int64 size);
	int64 Write(const void* buffer, int64 size);
	int64 Position();
	bool Seek(int64 position, ESeekOrigin origin = soSet);
	bool Eof();
	bool Error();
	int64 Print(const char* format, ...) PRINTF_SYNTAX(2);
	char* ReadLine(char* buffer, int64 size);
	bool SetWriteBuffer(int size);
	bool Flush();
	bool Sync(CString& errmsg);

private:
	FILE* m_file = nullptr;
};

#endif
