#include "StdAfx.h"

#include "fs/FileSink.h"

//#include "fs/FileIncludes.h"
#include "fs/NativeUtils.h"
#include "tl/crash.h"
#include "tl/enum.h"

#include <climits>
#include "fast_io.h"

#include "tl/narrow_cast.h"

//////////////////////////////////////////////////////////////////////////

using FH = fast_io::native_file;

template<typename Arena>
FH& toFH(Arena& arena)
{
	static_assert(sizeof(arena) >= sizeof(FH));
	return *reinterpret_cast<FH*>(arena.data());
}
template<typename Arena>
const FH& toFH(const Arena& arena)
{
	static_assert(sizeof(arena) >= sizeof(FH));
	return *reinterpret_cast<const FH*>(arena.data());
}
template<typename Arena>
FH& constructFH(Arena& arena, FH&& f)
{
	static_assert(sizeof(arena) >= sizeof(FH));
	return *new (arena.data()) FH(std::move(f));
}
template<typename Arena>
void destructFH(Arena& arena)
{
	static_assert(sizeof(arena) >= sizeof(FH));
	toFH(arena).~FH();
}

namespace fs
{
//////////////////////////////////////////////////////////////////////////

FileSink::FileSink(AbsPathView filepath, Mode mode, Flags flags) noexcept
{
	fast_io::open_mode fmode = fast_io::open_mode::out;

	switch (mode)
	{
	case Mode::CreateOrOpen:
		fmode |= fast_io::open_mode::creat;
		// fall through
	case Mode::Open:
		break;
	case Mode::CreateOrOpenAndClear:
		fmode |= fast_io::open_mode::creat;
		// fall through
	case Mode::OpenAndClear:
		fmode |= fast_io::open_mode::trunc;
		break;
	case Mode::CreateIfNew:
		fmode |= fast_io::open_mode::creat;
		fmode |= fast_io::open_mode::excl;
		break;
	default:
		TL_CRASH("Unknown sink Mode: {}", (int)mode);
	}

	auto& fh = constructFH(m_arena, fast_io::native_file(filepath.std_str(), fmode, fast_io::perms::owner_read | fast_io::perms::owner_write));
	m_isConstructed = true;
}

//////////////////////////////////////////////////////////////////////////

FileSink::FileSink(FileSink&& other) noexcept
	: m_isConstructed(other.m_isConstructed)
	, m_arena(other.m_arena)
	, m_offset(other.m_offset)
	, m_optLastError(std::move(other.m_optLastError))
{
	other.m_isConstructed = false;
	other.m_offset = 0;
}

//////////////////////////////////////////////////////////////////////////

FileSink& FileSink::operator=(FileSink&& other) noexcept
{
	close();
	tl::swap(m_isConstructed, other.m_isConstructed);
	tl::swap(m_arena, other.m_arena);
	tl::swap(m_offset, other.m_offset);
	m_optLastError = std::move(other.m_optLastError);

	return *this;
}

//////////////////////////////////////////////////////////////////////////

bool FileSink::isValid() const noexcept
{
	if (m_isConstructed)
	{
		auto& fh = toFH(m_arena);
		return fh.handle != nullptr;
	}
	return false;
}

//////////////////////////////////////////////////////////////////////////

FileSink::~FileSink() noexcept
{
	close();
	if (m_isConstructed)
		destructFH(m_arena);
}

//////////////////////////////////////////////////////////////////////////

void FileSink::close() noexcept
{
	m_offset = 0;
	m_optLastError = tl::nullopt;

	if (isValid())
	{
		auto& fh = toFH(m_arena);
		fh.close();
	}
}

//////////////////////////////////////////////////////////////////////////

size_t FileSink::write(tl::span<const uint8_t> data)
{
	if (m_optLastError.has_value() || !isValid())
	{
		TL_FAIL();
		return 0;
	}

	if (data.empty())
		return 0;

	const auto it = fast_io::write(toFH(m_arena), data.begin(), data.end());
	const int64_t sizeWritten = std::distance(data.begin(), it);

	if (sizeWritten < 0)
	{
		int err = errno;
		m_optLastError = tl::make_error<Error>(ErrorCode::SystemError, "Failed to write to file: ({}): {}", err, strerror(err));
		TL_FAIL("{}", m_optLastError);
		return 0;
	}
	m_offset += static_cast<uint64_t>(sizeWritten);

	return sizeWritten;
}

//////////////////////////////////////////////////////////////////////////

void FileSink::seekBeg(uint64_t offset)
{
	if (m_optLastError.has_value() || !isValid())
	{
		TL_FAIL();
		return;
	}

	if (m_offset != offset)
		m_offset = fast_io::seek(toFH(m_arena), offset, fast_io::seekdir::beg);
}

//////////////////////////////////////////////////////////////////////////

void FileSink::seekRel(int64_t offset)
{
	if (m_optLastError.has_value() || !isValid())
	{
		TL_FAIL();
		return;
	}

	if (offset != 0)
	{
		int64_t newOffset = static_cast<int64_t>(tell());
		newOffset += offset;
		newOffset = tl::max<int64_t>(newOffset, 0);
		seekBeg(static_cast<uint64_t>(tl::min(newOffset, (int64_t)getSize())));
	}
}

//////////////////////////////////////////////////////////////////////////

uint64_t FileSink::tell() const
{
	if (m_optLastError.has_value() || !isValid())
	{
		TL_FAIL();
		return 0;
	}

	return m_offset;
}

//////////////////////////////////////////////////////////////////////////

uint64_t FileSink::getSize() const
{
	if (m_optLastError.has_value() || !isValid())
	{
		TL_FAIL();
		return 0;
	}

	return fast_io::status(toFH(m_arena)).size;
}

//////////////////////////////////////////////////////////////////////////

tl::optional<Error> FileSink::getLastError() const
{
	tl::optional<Error> error = m_optLastError;
	m_optLastError = tl::nullopt;
	return error;
}

//////////////////////////////////////////////////////////////////////////

size_t FileSink::getPreferredBufferSize() const
{
	return 0;
}

//////////////////////////////////////////////////////////////////////////

bool FileSink::resize(uint64_t newSize) noexcept
{
	if (m_optLastError.has_value() || !isValid())
	{
		TL_FAIL();
		return false;
	}

	fast_io::truncate(toFH(m_arena), newSize);
	fast_io::seek(toFH(m_arena), newSize, fast_io::seekdir::beg);
	m_offset = newSize;

	return true;
}

//////////////////////////////////////////////////////////////////////////

}
