#include "mpq/mpq_sdl_rwops.hpp"

#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>

namespace devilution {

namespace {

struct Data {
	// File information:
	std::optional<MpqArchive> ownedArchive;
	MpqArchive *mpqArchive;
	uint32_t fileNumber;
	uint32_t blockSize;
	uint32_t lastBlockSize;
	uint32_t numBlocks;
	uint32_t size;

	// State:
	uint32_t position;
	bool blockRead;
	std::unique_ptr<uint8_t[]> blockData;
};

Data *GetData(struct SDL_RWops *context)
{
	return reinterpret_cast<Data *>(context->hidden.unknown.data1);
}

void SetData(struct SDL_RWops *context, Data *data)
{
	context->hidden.unknown.data1 = data;
}

#ifndef USE_SDL1
using OffsetType = Sint64;
using SizeType = size_t;
#else
using OffsetType = int;
using SizeType = int;
#endif

extern "C" {

#ifndef USE_SDL1
static Sint64 MpqFileRwSize(struct SDL_RWops *context)
{
	return GetData(context)->size;
}
#endif

static OffsetType MpqFileRwSeek(struct SDL_RWops *context, OffsetType offset, int whence)
{
	Data &data = *GetData(context);
	OffsetType newPosition;
	switch (whence) {
	case RW_SEEK_SET:
		newPosition = offset;
		break;
	case RW_SEEK_CUR:
		newPosition = data.position + offset;
		break;
	case RW_SEEK_END:
		newPosition = data.size + offset;
		break;
	default:
		return -1;
	}

	if (newPosition == static_cast<OffsetType>(data.position))
		return newPosition;

	if (newPosition > static_cast<OffsetType>(data.size)) {
		SDL_SetError("MpqFileRwSeek beyond EOF (%d > %u)", static_cast<int>(newPosition), data.size);
		return -1;
	}

	if (newPosition < 0) {
		SDL_SetError("MpqFileRwSeek beyond BOF (%d < 0)", static_cast<int>(newPosition));
		return -1;
	}

	if (data.position / data.blockSize != newPosition / data.blockSize)
		data.blockRead = false;

	data.position = newPosition;

	return newPosition;
}

static SizeType MpqFileRwRead(struct SDL_RWops *context, void *ptr, SizeType size, SizeType maxnum)
{
	Data &data = *GetData(context);
	const SizeType totalSize = size * maxnum;
	uint32_t remainingSize = totalSize;

	auto *out = static_cast<uint8_t *>(ptr);

	if (data.blockData == nullptr) {
		data.blockData = std::unique_ptr<uint8_t[]> { new uint8_t[data.blockSize] };
	}

	uint32_t blockNumber = data.position / data.blockSize;
	while (remainingSize > 0) {
		if (data.position == data.size) {
			break;
		}

		const uint32_t currentBlockSize = blockNumber + 1 == data.numBlocks ? data.lastBlockSize : data.blockSize;

		if (!data.blockRead) {
			const int32_t error = data.mpqArchive->ReadBlock(data.fileNumber, blockNumber, data.blockData.get(), currentBlockSize);
			if (error != 0) {
				SDL_SetError("MpqFileRwRead ReadBlock: %s", MpqArchive::ErrorMessage(error));
				return 0;
			}
			data.blockRead = true;
		}

		const uint32_t blockPosition = data.position - blockNumber * data.blockSize;
		const uint32_t remainingBlockSize = currentBlockSize - blockPosition;

		if (remainingSize < remainingBlockSize) {
			std::memcpy(out, data.blockData.get() + blockPosition, remainingSize);
			data.position += remainingSize;
			return maxnum;
		}

		std::memcpy(out, data.blockData.get() + blockPosition, remainingBlockSize);
		out += remainingBlockSize;
		data.position += remainingBlockSize;
		remainingSize -= remainingBlockSize;
		++blockNumber;
		data.blockRead = false;
	}

	return (totalSize - remainingSize) / size;
}

static int MpqFileRwClose(struct SDL_RWops *context)
{
	Data *data = GetData(context);
	data->mpqArchive->CloseBlockOffsetTable(data->fileNumber);
	delete data;
	delete context;
	return 0;
}

} // extern "C"

} // namespace

SDL_RWops *SDL_RWops_FromMpqFile(MpqArchive &mpqArchive, uint32_t fileNumber, const char *filename, bool threadsafe)
{
	auto result = std::make_unique<SDL_RWops>();
	std::memset(result.get(), 0, sizeof(*result));

#ifndef USE_SDL1
	result->size = &MpqFileRwSize;
	result->type = SDL_RWOPS_UNKNOWN;
#else
	result->type = 0;
#endif

	result->seek = &MpqFileRwSeek;
	result->read = &MpqFileRwRead;
	result->write = nullptr;
	result->close = &MpqFileRwClose;

	auto data = std::make_unique<Data>();
	int32_t error = 0;

	if (threadsafe) {
		data->ownedArchive = mpqArchive.Clone(error);
		if (error != 0) {
			SDL_SetError("MpqFileRwRead Clone: %s", MpqArchive::ErrorMessage(error));
			return nullptr;
		}
		data->mpqArchive = &*data->ownedArchive;
	} else {
		data->mpqArchive = &mpqArchive;
	}
	data->fileNumber = fileNumber;
	MpqArchive &archive = *data->mpqArchive;

	error = archive.OpenBlockOffsetTable(fileNumber, filename);
	if (error != 0) {
		SDL_SetError("MpqFileRwRead OpenBlockOffsetTable: %s", MpqArchive::ErrorMessage(error));
		return nullptr;
	}

	data->size = archive.GetUnpackedFileSize(fileNumber, error);
	if (error != 0) {
		SDL_SetError("MpqFileRwRead GetUnpackedFileSize: %s", MpqArchive::ErrorMessage(error));
		return nullptr;
	}

	const std::uint32_t numBlocks = archive.GetNumBlocks(fileNumber, error);
	if (error != 0) {
		SDL_SetError("MpqFileRwRead GetNumBlocks: %s", MpqArchive::ErrorMessage(error));
		return nullptr;
	}
	data->numBlocks = numBlocks;

	const std::uint32_t blockSize = archive.GetBlockSize(fileNumber, 0, error);
	if (error != 0) {
		SDL_SetError("MpqFileRwRead GetBlockSize: %s", MpqArchive::ErrorMessage(error));
		return nullptr;
	}
	data->blockSize = blockSize;

	if (numBlocks > 1) {
		data->lastBlockSize = archive.GetBlockSize(fileNumber, numBlocks - 1, error);
		if (error != 0) {
			SDL_SetError("MpqFileRwRead GetBlockSize: %s", MpqArchive::ErrorMessage(error));
			return nullptr;
		}
	} else {
		data->lastBlockSize = blockSize;
	}

	data->position = 0;
	data->blockRead = false;

	SetData(result.get(), data.release());
	return result.release();
}

} // namespace devilution
