// SPDX-License-Identifier: BSD-2-Clause
// author: Max Kellermann <max.kellermann@gmail.com>

#include "FileOutputStream.hxx"
#include "lib/fmt/PathFormatter.hxx"
#include "lib/fmt/SystemError.hxx"

#ifdef _WIN32
#include <tchar.h>
#endif

#ifdef __linux__
#include "io/linux/ProcPath.hxx"
#include <fcntl.h>
#endif

#ifdef __linux__
FileOutputStream::FileOutputStream(FileDescriptor _directory_fd,
				   Path _path, Mode _mode)
	:path(_path),
	 directory_fd(_directory_fd),
	 mode(_mode)
{
	Open();
}
#endif

FileOutputStream::FileOutputStream(Path _path, Mode _mode)
	:path(_path),
#ifdef __linux__
	 directory_fd(AT_FDCWD),
#endif
	 mode(_mode)
{
	Open();
}

inline void
FileOutputStream::Open()
{
	switch (mode) {
	case Mode::CREATE:
		OpenCreate(false);
		break;

	case Mode::CREATE_VISIBLE:
		OpenCreate(true);
		break;

	case Mode::APPEND_EXISTING:
		OpenAppend(false);
		break;

	case Mode::APPEND_OR_CREATE:
		OpenAppend(true);
		break;
	}
}

#ifdef _WIN32

inline void
FileOutputStream::OpenCreate(bool visible)
{
	if (!visible) {
		/* attempt to create a temporary file */
		tmp_path = path.WithSuffix(_T(".tmp"));
		Delete(tmp_path);

		handle = CreateFile(tmp_path.c_str(), GENERIC_WRITE, 0, nullptr,
				    CREATE_NEW,
				    FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
				    nullptr);
		if (handle != INVALID_HANDLE_VALUE)
			return;

	}

	handle = CreateFile(path.c_str(), GENERIC_WRITE, 0, nullptr,
			    CREATE_ALWAYS,
			    FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
			    nullptr);
	if (!IsDefined())
		throw FmtLastError("Failed to create {}", path);
}

inline void
FileOutputStream::OpenAppend(bool create)
{
	handle = CreateFile(path.c_str(), GENERIC_WRITE, 0, nullptr,
			    create ? OPEN_ALWAYS : OPEN_EXISTING,
			    FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
			    nullptr);
	if (!IsDefined())
		throw FmtLastError("Failed to append to {}", path);

	if (!SeekEOF()) {
		auto code = GetLastError();
		Close();
		throw FmtLastError(code, "Failed seek end-of-file of {}", path);
	}

}

uint64_t
FileOutputStream::Tell() const noexcept
{
	LONG high = 0;
	DWORD low = SetFilePointer(handle, 0, &high, FILE_CURRENT);
	if (low == 0xffffffff)
		return 0;

	return uint64_t(high) << 32 | uint64_t(low);
}

void
FileOutputStream::Write(std::span<const std::byte> src)
{
	assert(IsDefined());

	DWORD nbytes;
	if (!WriteFile(handle, src.data(), src.size(), &nbytes, nullptr))
		throw FmtLastError("Failed to write to {}", GetPath());

	if (size_t(nbytes) != src.size())
		throw FmtLastError(DWORD{ERROR_DISK_FULL},
				   "Failed to write to {}",
				   GetPath());
}

void
FileOutputStream::Sync()
{
	assert(IsDefined());

	if (!FlushFileBuffers(handle))
		throw FmtLastError("Failed to sync {}", GetPath());
}

void
FileOutputStream::Commit()
try {
	assert(IsDefined());

	Close();

	if (tmp_path != nullptr)
		RenameOrThrow(tmp_path, path);
} catch (...) {
	if (tmp_path != nullptr)
		Delete(tmp_path);
	throw;
}

#else

#include <cerrno>

#include <fcntl.h>
#include <unistd.h>

#ifdef HAVE_O_TMPFILE
#ifndef O_TMPFILE
/* supported since Linux 3.11 */
#define __O_TMPFILE 020000000
#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
#include <stdio.h>
#endif

/**
 * Open a file using Linux's O_TMPFILE for writing the given file.
 */
static bool
OpenTempFile(FileDescriptor directory_fd,
	     FileDescriptor &fd, Path path) noexcept
{
	if (directory_fd != FileDescriptor(AT_FDCWD))
		return fd.Open(directory_fd, ".", O_TMPFILE|O_WRONLY, 0666);

	const auto directory = path.GetDirectoryName();
	if (directory.IsNull())
		return false;

	return fd.Open(directory.c_str(), O_TMPFILE|O_WRONLY, 0666);
}

#endif /* HAVE_O_TMPFILE */

inline void
FileOutputStream::OpenCreate(bool visible)
{
#ifdef HAVE_O_TMPFILE
	/* try Linux's O_TMPFILE first */
	if (!visible && OpenTempFile(directory_fd, fd, GetPath())) {
		is_tmpfile = true;
		return;
	}
#endif

	if (!visible) {
		/* attempt to create a temporary file */
		tmp_path = path + ".tmp";
		Delete(tmp_path);

		if (fd.Open(
#ifdef __linux__
			    directory_fd,
#endif
			    tmp_path.c_str(),
			    O_WRONLY|O_CREAT|O_EXCL,
			    0666))
			return;

	}

	/* fall back to plain POSIX */
	if (!fd.Open(
#ifdef __linux__
		    directory_fd,
#endif
		    GetPath().c_str(),
		    O_WRONLY|O_CREAT|O_TRUNC,
		    0666))
		throw FmtErrno("Failed to create {}", GetPath());
}

inline void
FileOutputStream::OpenAppend(bool create)
{
	int flags = O_WRONLY|O_APPEND;
	if (create)
		flags |= O_CREAT;

	if (!fd.Open(
#ifdef __linux__
		     directory_fd,
#endif
		     path.c_str(), flags))
		throw FmtErrno("Failed to append to {}", path);
}

uint64_t
FileOutputStream::Tell() const noexcept
{
	return fd.Tell();
}

void
FileOutputStream::Write(std::span<const std::byte> src)
{
	assert(IsDefined());

	ssize_t nbytes = fd.Write(src);
	if (nbytes < 0)
		throw FmtErrno("Failed to write to {}", GetPath());
	else if ((size_t)nbytes < src.size())
		throw FmtErrno(ENOSPC, "Failed to write to {}", GetPath());
}

void
FileOutputStream::Sync()
{
	assert(IsDefined());

#ifdef __linux__
	const bool success = fdatasync(fd.Get()) == 0;
#else
	const bool success = fsync(fd.Get()) == 0;
#endif
	if (!success)
		throw FmtErrno("Failed to sync {}", GetPath());
}

void
FileOutputStream::Commit()
try {
	assert(IsDefined());

#ifdef HAVE_O_TMPFILE
	if (is_tmpfile) {
		unlinkat(directory_fd.Get(), GetPath().c_str(), 0);

		/* hard-link the temporary file to the final path */
		if (linkat(-1, ProcFdPath(fd),
			   directory_fd.Get(), path.c_str(),
			   AT_SYMLINK_FOLLOW) < 0)
			throw FmtErrno("Failed to commit {}", path);
	}
#endif

	if (!Close()) {
		throw FmtErrno("Failed to commit {}", path);
	}

	if (tmp_path != nullptr)
		RenameOrThrow(tmp_path, path);
} catch (...) {
	if (tmp_path != nullptr)
		Delete(tmp_path);
	throw;
}

#endif

void
FileOutputStream::Cancel() noexcept
{
	assert(IsDefined());

	Close();

	if (tmp_path != nullptr) {
		Delete(tmp_path);
		return;
	}

	switch (mode) {
	case Mode::CREATE:
#ifdef HAVE_O_TMPFILE
		if (!is_tmpfile)
#endif
			Delete(GetPath());
		break;

	case Mode::CREATE_VISIBLE:
	case Mode::APPEND_EXISTING:
	case Mode::APPEND_OR_CREATE:
		/* can't roll this back */
		break;
	}
}

inline void
FileOutputStream::RenameOrThrow([[maybe_unused]] Path old_path,
				[[maybe_unused]] Path new_path) const
{
	assert(old_path != nullptr);
	assert(new_path != nullptr);

#ifdef _WIN32
	if (!MoveFileEx(old_path.c_str(), new_path.c_str(),
			MOVEFILE_REPLACE_EXISTING))
		throw MakeLastError("Failed to rename file");
#elif defined(__linux__)
	if (renameat(directory_fd.Get(), tmp_path.c_str(),
		     directory_fd.Get(), path.c_str()) < 0)
		throw MakeErrno("Failed to rename file");
#else
	if (rename(tmp_path.c_str(), path.c_str()))
		throw MakeErrno("Failed to rename file");
#endif
}

inline void
FileOutputStream::Delete(Path delete_path) const noexcept
{
	assert(delete_path != nullptr);

#ifdef _WIN32
	DeleteFile(delete_path.c_str());
#elif defined(__linux__)
	unlinkat(directory_fd.Get(), delete_path.c_str(), 0);
#else
	unlink(delete_path.c_str());
#endif
}
