#pragma once

#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>

#include "utils/endian.hpp"
#include "utils/log.hpp"

namespace devilution {

inline void LoggedFread(void *buffer, size_t size, FILE *stream)
{
	if (std::fread(buffer, size, 1, stream) != 1) {
		LogError("fread failed: {}", std::strerror(errno));
	}
}

inline void LoggedFwrite(const void *buffer, size_t size, FILE *stream)
{
	if (std::fwrite(buffer, size, 1, stream) != 1) {
		LogError("fwrite failed: {}", std::strerror(errno));
	}
}

template <typename T = uint8_t>
T ReadByte(FILE *stream)
{
	static_assert(sizeof(T) == 1, "invalid argument");
	char buf;
	LoggedFread(&buf, sizeof(buf), stream);
	return static_cast<T>(buf);
}

template <typename T = uint16_t>
T ReadLE16(FILE *stream)
{
	static_assert(sizeof(T) == 2, "invalid argument");
	char buf[2];
	LoggedFread(&buf, sizeof(buf), stream);
	return static_cast<T>(LoadLE16(buf));
}

template <typename T = uint32_t>
T ReadLE32(FILE *stream)
{
	static_assert(sizeof(T) == 4, "invalid argument");
	char buf[4];
	LoggedFread(&buf, sizeof(buf), stream);
	return static_cast<T>(LoadLE32(buf));
}

inline float ReadLEFloat(FILE *stream)
{
	static_assert(sizeof(float) == sizeof(uint32_t), "invalid float size");
	const uint32_t val = ReadLE32(stream);
	float result;
	memcpy(&result, &val, sizeof(float));
	return result;
}

inline void WriteByte(FILE *out, uint8_t val)
{
	LoggedFwrite(&val, sizeof(val), out);
}

inline void WriteLE16(FILE *out, uint16_t val)
{
	char data[2];
	WriteLE16(data, val);
	LoggedFwrite(data, sizeof(data), out);
}

inline void WriteLE32(FILE *out, uint32_t val)
{
	char data[4];
	WriteLE32(data, val);
	LoggedFwrite(data, sizeof(data), out);
}

inline void WriteLEFloat(FILE *out, float val)
{
	static_assert(sizeof(float) == sizeof(uint32_t), "invalid float size");
	uint32_t intVal;
	memcpy(&intVal, &val, sizeof(uint32_t));
	char data[4];
	WriteLE32(data, intVal);
	LoggedFwrite(data, sizeof(data), out);
}

} // namespace devilution
