#include "file.h"

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

class File::Impl
{
public:
	inline Impl()
		: handle_(INVALID_HANDLE_VALUE)
	{
	}

	inline Impl(const char* const file_path, const OpenMode::Flags mode)
		: handle_(INVALID_HANDLE_VALUE)
	{
		open(file_path, mode);
	}

	inline ~Impl()
	{
		close();
	}

	bool open(const char* const file_path, const OpenMode::Flags mode) 
	{
		DWORD access_mode = 0;
		if ((mode & OpenMode::kREAD) == OpenMode::kREAD)
			access_mode |= FILE_READ_DATA;
		else if ((mode & OpenMode::kWRITE) == OpenMode::kWRITE)
			access_mode |= FILE_WRITE_DATA;
		else if ((mode & OpenMode::kAPPEND) == OpenMode::kAPPEND)
			access_mode |= FILE_WRITE_DATA | FILE_APPEND_DATA;
		assert(access_mode != 0 && "No valid access mode specified.");

		DWORD share_mode = FILE_SHARE_READ;
		if ((mode & OpenMode::kSHARE) == OpenMode::kSHARE)
			share_mode |= FILE_SHARE_WRITE;

		DWORD creation_mode = 0;
		if ((mode & OpenMode::kRECREATE) == OpenMode::kRECREATE)
			creation_mode = CREATE_ALWAYS;
		else if ((mode & OpenMode::kREAD) == OpenMode::kREAD)
			creation_mode = OPEN_EXISTING;
		else if ((mode & OpenMode::kWRITE) == OpenMode::kWRITE)
			creation_mode = CREATE_NEW;
		else if ((mode & OpenMode::kAPPEND) == OpenMode::kAPPEND)
			creation_mode = OPEN_EXISTING;
		assert(creation_mode != 0 && "No valid creation mode specified.");

		DWORD flags = 0;
		if ((mode & OpenMode::kRANDOM_ACCESS) == OpenMode::kRANDOM_ACCESS)
			flags |= FILE_FLAG_RANDOM_ACCESS;
		else
			flags |= FILE_FLAG_SEQUENTIAL_SCAN;
		if ((mode & OpenMode::kWRITE_THROUGH) == OpenMode::kWRITE_THROUGH)
			flags |= FILE_FLAG_WRITE_THROUGH;
		if ((mode & OpenMode::kASYNC) == OpenMode::kASYNC)
			flags |= FILE_FLAG_OVERLAPPED;

		handle_ = CreateFileA(file_path, access_mode, share_mode, nullptr, creation_mode, flags, nullptr);

		return handle_ != INVALID_HANDLE_VALUE;
	}

	inline void close()
	{
		if (handle_ != INVALID_HANDLE_VALUE) {
			CloseHandle(handle_);
			handle_ = INVALID_HANDLE_VALUE;
		}
	}

	inline bool read(void* const data, const uint32_t size, uint32_t* const num_read)
	{
		assert(data != nullptr);
		DWORD count;
		if (!ReadFile(handle_, data, size, &count, nullptr)) {
			if (num_read)
				*num_read = count;
			return false;
		} else {
			if (num_read)
				*num_read = count;
		}
		return true;
	}

	inline bool write(const void* const data, const uint32_t size, uint32_t* const num_written)
	{
		assert(data != nullptr);
		DWORD count;
		if (!WriteFile(handle_, data, size, &count, nullptr)) {
			if (num_written)
				*num_written = count;
			return false;
		} else {
			if (num_written)
				*num_written = count;
		}
		return true;
	}

	inline bool seek(const uint64_t offset, const uint32_t mode, uint64_t* const position)
	{
		DWORD method;
		switch (mode) {
			case SeekMode::kBEGIN: method = FILE_BEGIN; break;
			case SeekMode::kCURRENT: method = FILE_CURRENT; break;
			case SeekMode::kEND: method = FILE_END; break;
			default:
				assert(0 && "Invalid seek mode");
				break;
		}
		LARGE_INTEGER distance, current;
		distance.QuadPart = offset;
		if (!SetFilePointerEx(handle_, distance, &current, method))
			return false;
		if (position)
			*position = current.QuadPart;
		return true;
	}

	inline uint64_t file_size() const
	{
		LARGE_INTEGER size;
		if (!GetFileSizeEx(handle_, &size))
			return -1;
		return size.QuadPart;
	}

	inline bool is_valid() const
	{
		return handle_ != INVALID_HANDLE_VALUE;
	}

private:
	HANDLE handle_;
};

File::File()
	: impl_(new Impl())
{
}

File::File(const char* const file_path, const OpenMode::Flags mode)
	: impl_(new Impl(file_path, mode))
{
}

File::~File()
{
}

bool File::open(const char* const file_path, const OpenMode::Flags mode)
{
	return impl_->open(file_path, mode);
}

void File::close()
{
	impl_->close();
}

bool File::read(void* const data, const uint32_t size, uint32_t* const num_read)
{
	return impl_->read(data, size, num_read);
}

bool File::write(const void* const data, const uint32_t size, uint32_t* const num_written)
{
	return impl_->write(data, size, num_written);
}

bool File::seek(const uint64_t offset, const SeekMode::Enum mode, uint64_t* const pos)
{
	return impl_->seek(offset, mode, pos);
}

uint64_t File::file_size() const
{
	return impl_->file_size();
}

bool File::is_valid() const
{
	return impl_->is_valid();
}

bool read_file(const char* const file_path, char** const data, uint32_t* const size, const uint64_t offset)
{
	assert(file_path != nullptr);
	assert(data != nullptr);
	assert(size != nullptr);

	File file(file_path, OpenMode::kREAD | OpenMode::kSHARE);
	if (!file.is_valid())
		return false;

	uint32_t buffer_size;
	if (*size == -1) {
		const uint64_t file_size = file.file_size();
		if (file_size - offset > UINT32_MAX)
			return false;
		buffer_size = static_cast<uint32_t>(file_size - offset);
	} else {
		buffer_size = *size;
	}

	std::unique_ptr<char[]> buffer(new char[buffer_size]);
	if (!file.read_at(buffer.get(), offset, buffer_size))
		return false;
	file.close();

	*data = buffer.release();
	*size = buffer_size;

	return true;
}

bool write_file(const char* const file_path, const char* const data, const uint32_t size, const uint64_t offset)
{
	assert(file_path != nullptr);
	assert(data != nullptr);

	File file(file_path, OpenMode::kWRITE | OpenMode::kRECREATE);
	if (!file.is_valid())
		return false;

	if (!file.write_at(data, offset, size))
		return false;
	file.close();

	return true;
}
