#include "td/protocol/packets/WorldDataPacket.h"

namespace td {
namespace protocol {

const int BITS_IN_BYTE = 8;
const int BITS_IN_LONG = BITS_IN_BYTE * sizeof(std::uint64_t);

static unsigned int countBits(unsigned int number) {
	// log function in base 2
	// take only integer part
	return static_cast<unsigned int>(std::log2(number) + 1);
}

typedef std::vector<uint64_t> ChunkPackedData;

DataBuffer WorldDataPacket::Serialize(bool packetID) const {
	DataBuffer data;

	WritePacketID(data, packetID);

	data << m_World->GetChunks().size();
	for (const auto& pair : m_World->GetChunks()) {
		game::ChunkCoord coords = pair.first;
		game::ChunkPtr chunk = pair.second;

		data << coords.x << coords.y << static_cast<std::uint64_t>(chunk->palette.size());

		std::size_t bufferSize = data.GetSize();
		data.Resize(data.GetSize() + chunk->palette.size() * sizeof(game::ChunkPalette::value_type));
		memcpy(reinterpret_cast<std::uint8_t*>(data.data()) + bufferSize, chunk->palette.data(), chunk->palette.size() * sizeof(game::ChunkPalette::value_type));

		std::uint8_t bitsPerTile = countBits(chunk->palette.size());

		game::Chunk::ChunkData::value_type individualValueMask = ((1 << bitsPerTile) - 1);

		ChunkPackedData chunkData(game::Chunk::ChunkSize / (BITS_IN_BYTE * sizeof(ChunkPackedData::value_type) / bitsPerTile), 0);

		for (unsigned int tileNumber = 0; tileNumber < game::Chunk::ChunkSize; tileNumber++) {
			std::size_t startLong = static_cast<std::size_t>((tileNumber * bitsPerTile) / BITS_IN_LONG);
			std::size_t startOffset = static_cast<std::size_t>((tileNumber * bitsPerTile) % BITS_IN_LONG);
			std::size_t endLong = static_cast<std::size_t>(((tileNumber + 1) * bitsPerTile - 1) / BITS_IN_LONG);

			std::uint64_t value = static_cast<std::uint64_t>(chunk->tiles[tileNumber]);

			value &= individualValueMask;

			chunkData[startLong] |= (value << startOffset);

			if (startLong != endLong) {
				chunkData[endLong] = (value >> (BITS_IN_LONG - startOffset));
			}
		}

		bufferSize = data.GetSize();
		data.Resize(data.GetSize() + chunkData.size() * sizeof(ChunkPackedData::value_type));
		memcpy(reinterpret_cast<std::uint8_t*>(data.data()) + bufferSize, chunkData.data(), chunkData.size() * sizeof(ChunkPackedData::value_type));
	}
	return data;
}

void WorldDataPacket::Deserialize(DataBuffer& data) {
	std::uint64_t chunkCount;
	data >> chunkCount;

	for (std::uint64_t chunkNumber = 0; chunkNumber < chunkCount; chunkNumber++) {
		game::ChunkPtr chunk = std::make_shared<game::Chunk>();

		decltype(game::ChunkCoord::x) chunkX, chunkY;
		data >> chunkX >> chunkY;

		std::uint64_t chunkPaletteSize;
		data >> chunkPaletteSize;

		game::ChunkPalette chunkPalette(chunkPaletteSize);

		memcpy(reinterpret_cast<void*>(chunkPalette.data()), data.data() + data.GetReadOffset(), chunkPaletteSize * sizeof(game::ChunkPalette::value_type));
		data.SetReadOffset(data.GetReadOffset() + chunkPaletteSize * sizeof(game::ChunkPalette::value_type));

		chunk->palette = chunkPalette;

		std::uint8_t bitsPerTile = countBits(chunkPaletteSize);

		// A bitmask that contains bitsPerTile set bits
		game::Chunk::ChunkData::value_type individualValueMask = ((1 << bitsPerTile) - 1);

		ChunkPackedData chunkData(game::Chunk::ChunkSize / (BITS_IN_BYTE * sizeof(ChunkPackedData::value_type) / bitsPerTile), 0);

		memcpy(reinterpret_cast<void*>(chunkData.data()), data.data() + data.GetReadOffset(), chunkData.size() * sizeof(ChunkPackedData::value_type));
		data.SetReadOffset(data.GetReadOffset() + chunkData.size() * sizeof(ChunkPackedData::value_type));

		for (unsigned int tileNumber = 0; tileNumber < game::Chunk::ChunkSize; tileNumber++) {
			std::size_t startLong = (tileNumber * bitsPerTile) / BITS_IN_LONG;
			std::size_t startOffset = (tileNumber * bitsPerTile) % BITS_IN_LONG;
			std::size_t endLong = ((tileNumber + 1) * bitsPerTile - 1) / BITS_IN_LONG;

			game::Chunk::ChunkData::value_type value;
			if (startLong == endLong) {
				value = (chunkData[startLong] >> startOffset);
			} else {
				int endOffset = BITS_IN_LONG - startOffset;
				value = (chunkData[startLong] >> startOffset | chunkData[endLong] << endOffset);
			}
			value &= individualValueMask;

			chunk->tiles[tileNumber] = value;
		}


		m_WorldData.m_Chunks.insert({ {chunkX, chunkY}, chunk });
	}
}

} // namespace protocol
} // namespace td
