/**
 * @file multi.cpp
 *
 * Implementation of functions for keeping multiplaye games in sync.
 */

#include <cstdint>
#include <cstring>
#include <ctime>

#include <SDL.h>
#include <config.h>
#include <fmt/format.h>

#include "DiabloUI/diabloui.h"
#include "diablo.h"
#include "engine/demomode.h"
#include "engine/point.hpp"
#include "engine/random.hpp"
#include "engine/world_tile.hpp"
#include "menu.h"
#include "nthread.h"
#include "options.h"
#include "pfile.h"
#include "plrmsg.h"
#include "qol/chatlog.h"
#include "storm/storm_net.hpp"
#include "sync.h"
#include "tmsg.h"
#include "utils/endian_read.hpp"
#include "utils/language.h"
#include "utils/stdcompat/cstddef.hpp"
#include "utils/stdcompat/string_view.hpp"
#include "utils/str_cat.hpp"

namespace devilution {

bool gbSomebodyWonGameKludge;
TBuffer highPriorityBuffer;
uint16_t sgwPackPlrOffsetTbl[MAX_PLRS];
bool sgbPlayerTurnBitTbl[MAX_PLRS];
bool sgbPlayerLeftGameTbl[MAX_PLRS];
bool shareNextHighPriorityMessage;
uint8_t gbActivePlayers;
bool gbGameDestroyed;
bool sgbSendDeltaTbl[MAX_PLRS];
GameData sgGameInitInfo;
bool gbSelectProvider;
int sglTimeoutStart;
uint32_t sgdwPlayerLeftReasonTbl[MAX_PLRS];
TBuffer lowPriorityBuffer;
uint32_t sgdwGameLoops;
/**
 * Specifies the maximum number of players in a game, where 1
 * represents a single player game and 4 represents a multi player game.
 */
bool gbIsMultiplayer;
bool sgbTimeout;
std::string GameName;
std::string GamePassword;
bool PublicGame;
uint8_t gbDeltaSender;
bool sgbNetInited;
uint32_t player_state[MAX_PLRS];
Uint32 playerInfoTimers[MAX_PLRS];
bool IsLoopback;

/**
 * Contains the set of supported event types supported by the multiplayer
 * event handler.
 */
const event_type EventTypes[3] = {
	EVENT_TYPE_PLAYER_LEAVE_GAME,
	EVENT_TYPE_PLAYER_CREATE_GAME,
	EVENT_TYPE_PLAYER_MESSAGE
};

void GameData::swapLE()
{
	size = SDL_SwapLE32(size);
	dwSeed = SDL_SwapLE32(dwSeed);
	programid = SDL_SwapLE32(programid);
}

namespace {

constexpr uint16_t HeaderCheckVal =
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
    LoadBE16("ip");
#else
    LoadLE16("ip");
#endif

uint32_t sgbSentThisCycle;

void BufferInit(TBuffer *pBuf)
{
	pBuf->dwNextWriteOffset = 0;
	pBuf->bData[0] = byte { 0 };
}

void CopyPacket(TBuffer *buf, const byte *packet, size_t size)
{
	if (buf->dwNextWriteOffset + size + 2 > 0x1000) {
		return;
	}

	byte *p = &buf->bData[buf->dwNextWriteOffset];
	buf->dwNextWriteOffset += size + 1;
	*p = static_cast<byte>(size);
	p++;
	memcpy(p, packet, size);
	p[size] = byte { 0 };
}

byte *CopyBufferedPackets(byte *destination, TBuffer *source, size_t *size)
{
	if (source->dwNextWriteOffset != 0) {
		byte *srcPtr = source->bData;
		while (true) {
			auto chunkSize = static_cast<uint8_t>(*srcPtr);
			if (chunkSize == 0)
				break;
			if (chunkSize > *size)
				break;
			srcPtr++;
			memcpy(destination, srcPtr, chunkSize);
			destination += chunkSize;
			srcPtr += chunkSize;
			*size -= chunkSize;
		}
		memcpy(source->bData, srcPtr, (source->bData - srcPtr) + source->dwNextWriteOffset + 1);
		source->dwNextWriteOffset += static_cast<uint32_t>(source->bData - srcPtr);
		return destination;
	}
	return destination;
}

void NetReceivePlayerData(TPkt *pkt)
{
	const Player &myPlayer = *MyPlayer;
	Point target = myPlayer.GetTargetPosition();
	// Don't send desired target position when we will change our position soon.
	// This prevents a desync where the remote client starts a walking to the old target position when the teleport is finished but the the new position isn't received yet.
	if (myPlayer._pmode == PM_SPELL && IsAnyOf(myPlayer.executedSpell.spellId, SpellID::Teleport, SpellID::Phasing, SpellID::Warp))
		target = {};

	pkt->hdr.wCheck = HeaderCheckVal;
	pkt->hdr.px = myPlayer.position.tile.x;
	pkt->hdr.py = myPlayer.position.tile.y;
	pkt->hdr.targx = target.x;
	pkt->hdr.targy = target.y;
	pkt->hdr.php = SDL_SwapLE32(myPlayer._pHitPoints);
	pkt->hdr.pmhp = SDL_SwapLE32(myPlayer._pMaxHP);
	pkt->hdr.mana = SDL_SwapLE32(myPlayer._pMana);
	pkt->hdr.maxmana = SDL_SwapLE32(myPlayer._pMaxMana);
	pkt->hdr.bstr = myPlayer._pBaseStr;
	pkt->hdr.bmag = myPlayer._pBaseMag;
	pkt->hdr.bdex = myPlayer._pBaseDex;
}

bool IsNetPlayerValid(const Player &player)
{
	return player._pLevel >= 1
	    && player._pLevel <= MaxCharacterLevel
	    && static_cast<uint8_t>(player._pClass) < enum_size<HeroClass>::value
	    && player.plrlevel < NUMLEVELS
	    && InDungeonBounds(player.position.tile)
	    && !string_view(player._pName).empty();
}

void CheckPlayerInfoTimeouts()
{
	for (size_t i = 0; i < Players.size(); i++) {
		Player &player = Players[i];
		if (&player == MyPlayer) {
			continue;
		}

		Uint32 &timerStart = playerInfoTimers[i];
		bool isPlayerConnected = (player_state[i] & PS_CONNECTED) != 0;
		bool isPlayerValid = isPlayerConnected && IsNetPlayerValid(player);
		if (isPlayerConnected && !isPlayerValid && timerStart == 0) {
			timerStart = SDL_GetTicks();
		}
		if (!isPlayerConnected || isPlayerValid) {
			timerStart = 0;
		}

		if (timerStart == 0) {
			continue;
		}

		// Time the player out after 15 seconds
		// if we do not receive valid player info
		if (SDL_GetTicks() - timerStart >= 15000) {
			SNetDropPlayer(i, LEAVE_DROP);
			timerStart = 0;
		}
	}
}

void SendPacket(int playerId, const byte *packet, size_t size)
{
	TPkt pkt;

	NetReceivePlayerData(&pkt);
	const size_t sizeWithheader = size + sizeof(pkt.hdr);
	pkt.hdr.wLen = SDL_SwapLE16(static_cast<uint16_t>(sizeWithheader));
	memcpy(pkt.body, packet, size);
	if (!SNetSendMessage(playerId, &pkt.hdr, sizeWithheader))
		nthread_terminate_game("SNetSendMessage0");
}

void MonsterSeeds()
{
	sgdwGameLoops++;
	const uint32_t seed = (sgdwGameLoops >> 8) | (sgdwGameLoops << 24);
	for (size_t i = 0; i < MaxMonsters; i++)
		Monsters[i].aiSeed = seed + i;
}

void HandleTurnUpperBit(size_t pnum)
{
	size_t i;

	for (i = 0; i < Players.size(); i++) {
		if ((player_state[i] & PS_CONNECTED) != 0 && i != pnum)
			break;
	}

	if (MyPlayerId == i) {
		sgbSendDeltaTbl[pnum] = true;
	} else if (pnum == MyPlayerId) {
		gbDeltaSender = i;
	}
}

void ParseTurn(size_t pnum, uint32_t turn)
{
	if ((turn & 0x80000000) != 0)
		HandleTurnUpperBit(pnum);
	uint32_t absTurns = turn & 0x7FFFFFFF;
	if (sgbSentThisCycle < gdwTurnsInTransit + absTurns) {
		if (absTurns >= 0x7FFFFFFF)
			absTurns &= 0xFFFF;
		sgbSentThisCycle = absTurns + gdwTurnsInTransit;
		sgdwGameLoops = 4 * absTurns * sgbNetUpdateRate;
	}
}

void PlayerLeftMsg(int pnum, bool left)
{
	Player &player = Players[pnum];

	if (&player == InspectPlayer)
		InspectPlayer = MyPlayer;

	if (&player == MyPlayer)
		return;
	if (!player.plractive)
		return;

	FixPlrWalkTags(player);
	RemovePortalMissile(pnum);
	DeactivatePortal(pnum);
	delta_close_portal(pnum);
	RemovePlrMissiles(player);
	if (left) {
		string_view pszFmt = _("Player '{:s}' just left the game");
		switch (sgdwPlayerLeftReasonTbl[pnum]) {
		case LEAVE_ENDING:
			pszFmt = _("Player '{:s}' killed Diablo and left the game!");
			gbSomebodyWonGameKludge = true;
			break;
		case LEAVE_DROP:
			pszFmt = _("Player '{:s}' dropped due to timeout");
			break;
		}
		EventPlrMsg(fmt::format(fmt::runtime(pszFmt), player._pName));
	}
	player.plractive = false;
	player._pName[0] = '\0';
	ResetPlayerGFX(player);
	gbActivePlayers--;
}

void ClearPlayerLeftState()
{
	for (size_t i = 0; i < Players.size(); i++) {
		if (sgbPlayerLeftGameTbl[i]) {
			if (gbBufferMsgs == 1)
				msg_send_drop_pkt(i, sgdwPlayerLeftReasonTbl[i]);
			else
				PlayerLeftMsg(i, true);

			sgbPlayerLeftGameTbl[i] = false;
			sgdwPlayerLeftReasonTbl[i] = 0;
		}
	}
}

void CheckDropPlayer()
{
	for (size_t i = 0; i < Players.size(); i++) {
		if ((player_state[i] & PS_ACTIVE) == 0 && (player_state[i] & PS_CONNECTED) != 0) {
			SNetDropPlayer(i, LEAVE_DROP);
		}
	}
}

void BeginTimeout()
{
	if (!sgbTimeout) {
		return;
	}
#ifdef _DEBUG
	if (DebugDisableNetworkTimeout) {
		return;
	}
#endif

	uint32_t nTicks = SDL_GetTicks() - sglTimeoutStart;
	if (nTicks > 20000) {
		gbRunGame = false;
		return;
	}
	if (nTicks < 10000) {
		return;
	}

	CheckDropPlayer();
}

void HandleAllPackets(size_t pnum, const byte *data, size_t size)
{
	for (unsigned offset = 0; offset < size;) {
		size_t messageSize = ParseCmd(pnum, reinterpret_cast<const TCmd *>(&data[offset]));
		if (messageSize == 0) {
			break;
		}
		offset += messageSize;
	}
}

void ProcessTmsgs()
{
	while (true) {
		std::unique_ptr<byte[]> msg;
		uint8_t size = tmsg_get(&msg);
		if (size == 0)
			break;

		HandleAllPackets(MyPlayerId, msg.get(), size);
	}
}

void SendPlayerInfo(int pnum, _cmd_id cmd)
{
	PlayerNetPack packed;
	Player &myPlayer = *MyPlayer;
	PackNetPlayer(packed, myPlayer);
	multi_send_zero_packet(pnum, cmd, reinterpret_cast<byte *>(&packed), sizeof(PlayerNetPack));
}

void SetupLocalPositions()
{
	currlevel = 0;
	leveltype = DTYPE_TOWN;
	setlevel = false;

	const WorldTilePosition spawns[9] = { { 75, 68 }, { 77, 70 }, { 75, 70 }, { 77, 68 }, { 76, 69 }, { 75, 69 }, { 76, 68 }, { 77, 69 }, { 76, 70 } };

	Player &myPlayer = *MyPlayer;

	myPlayer.position.tile = spawns[MyPlayerId];
	myPlayer.position.future = myPlayer.position.tile;
	myPlayer.setLevel(currlevel);
	myPlayer._pLvlChanging = true;
	myPlayer.pLvlLoad = 0;
	myPlayer._pmode = PM_NEWLVL;
	myPlayer.destAction = ACTION_NONE;
}

void HandleEvents(_SNETEVENT *pEvt)
{
	const uint32_t playerId = pEvt->playerid;
	switch (pEvt->eventid) {
	case EVENT_TYPE_PLAYER_CREATE_GAME: {
		GameData gameData;
		if (pEvt->databytes < sizeof(GameData))
			app_fatal(StrCat("Invalid packet size (<sizeof(GameData)): ", pEvt->databytes));
		std::memcpy(&gameData, pEvt->data, sizeof(gameData));
		gameData.swapLE();
		if (gameData.size != sizeof(GameData))
			app_fatal(StrCat("Invalid size of game data: ", gameData.size));
		sgGameInitInfo = gameData;
		sgbPlayerTurnBitTbl[playerId] = true;
	} break;
	case EVENT_TYPE_PLAYER_LEAVE_GAME: {
		sgbPlayerLeftGameTbl[playerId] = true;
		sgbPlayerTurnBitTbl[playerId] = false;

		int32_t leftReason = 0;
		if (pEvt->data != nullptr && pEvt->databytes >= sizeof(leftReason)) {
			std::memcpy(&leftReason, pEvt->data, sizeof(leftReason));
			leftReason = SDL_SwapLE32(leftReason);
		}
		sgdwPlayerLeftReasonTbl[playerId] = leftReason;
		if (leftReason == LEAVE_ENDING)
			gbSomebodyWonGameKludge = true;

		sgbSendDeltaTbl[playerId] = false;

		if (gbDeltaSender == playerId)
			gbDeltaSender = MAX_PLRS;
	} break;
	case EVENT_TYPE_PLAYER_MESSAGE: {
		string_view data(static_cast<const char *>(pEvt->data), pEvt->databytes);
		if (const size_t nullPos = data.find('\0'); nullPos != string_view::npos) {
			data.remove_suffix(data.size() - nullPos);
		}
		EventPlrMsg(data);
	} break;
	}
}

void RegisterNetEventHandlers()
{
	for (auto eventType : EventTypes) {
		if (!SNetRegisterEventHandler(eventType, HandleEvents)) {
			app_fatal(StrCat("SNetRegisterEventHandler:\n", SDL_GetError()));
		}
	}
}

void UnregisterNetEventHandlers()
{
	for (auto eventType : EventTypes) {
		SNetUnregisterEventHandler(eventType);
	}
}

bool InitSingle(GameData *gameData)
{
	Players.resize(1);

	if (!SNetInitializeProvider(SELCONN_LOOPBACK, gameData)) {
		return false;
	}

	int unused = 0;
	GameData gameInitInfo = sgGameInitInfo;
	gameInitInfo.swapLE();
	if (!SNetCreateGame("local", "local", reinterpret_cast<char *>(&gameInitInfo), sizeof(gameInitInfo), &unused)) {
		app_fatal(StrCat("SNetCreateGame1:\n", SDL_GetError()));
	}

	MyPlayerId = 0;
	MyPlayer = &Players[MyPlayerId];
	InspectPlayer = MyPlayer;
	gbIsMultiplayer = false;

	pfile_read_player_from_save(gSaveNumber, *MyPlayer);

	return true;
}

bool InitMulti(GameData *gameData)
{
	Players.resize(MAX_PLRS);

	int playerId;

	while (true) {
		if (gbSelectProvider && !UiSelectProvider(gameData)) {
			return false;
		}

		RegisterNetEventHandlers();
		if (UiSelectGame(gameData, &playerId))
			break;

		gbSelectProvider = true;
	}

	if (static_cast<size_t>(playerId) >= Players.size()) {
		return false;
	}
	MyPlayerId = playerId;
	MyPlayer = &Players[MyPlayerId];
	InspectPlayer = MyPlayer;
	gbIsMultiplayer = true;

	pfile_read_player_from_save(gSaveNumber, *MyPlayer);

	return true;
}

} // namespace

void InitGameInfo()
{
	sgGameInitInfo.size = sizeof(sgGameInitInfo);
	sgGameInitInfo.dwSeed = static_cast<uint32_t>(time(nullptr));
	sgGameInitInfo.programid = GAME_ID;
	sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR;
	sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR;
	sgGameInitInfo.versionPatch = PROJECT_VERSION_PATCH;
	sgGameInitInfo.nTickRate = *sgOptions.Gameplay.tickRate;
	sgGameInitInfo.bRunInTown = *sgOptions.Gameplay.runInTown ? 1 : 0;
	sgGameInitInfo.bTheoQuest = *sgOptions.Gameplay.theoQuest ? 1 : 0;
	sgGameInitInfo.bCowQuest = *sgOptions.Gameplay.cowQuest ? 1 : 0;
	sgGameInitInfo.bFriendlyFire = *sgOptions.Gameplay.friendlyFire ? 1 : 0;
	sgGameInitInfo.fullQuests = (!gbIsMultiplayer || *sgOptions.Gameplay.multiplayerFullQuests) ? 1 : 0;
}

void NetSendLoPri(int playerId, const byte *data, size_t size)
{
	if (data != nullptr && size != 0) {
		CopyPacket(&lowPriorityBuffer, data, size);
		SendPacket(playerId, data, size);
	}
}

void NetSendHiPri(int playerId, const byte *data, size_t size)
{
	if (data != nullptr && size != 0) {
		CopyPacket(&highPriorityBuffer, data, size);
		SendPacket(playerId, data, size);
	}
	if (shareNextHighPriorityMessage) {
		shareNextHighPriorityMessage = false;
		TPkt pkt;
		NetReceivePlayerData(&pkt);
		byte *destination = pkt.body;
		size_t remainingSpace = gdwNormalMsgSize - sizeof(TPktHdr);
		destination = CopyBufferedPackets(destination, &highPriorityBuffer, &remainingSpace);
		destination = CopyBufferedPackets(destination, &lowPriorityBuffer, &remainingSpace);
		remainingSpace = sync_all_monsters(destination, remainingSpace);
		const size_t len = gdwNormalMsgSize - remainingSpace;
		pkt.hdr.wLen = SDL_SwapLE16(static_cast<uint16_t>(len));
		if (!SNetSendMessage(SNPLAYER_OTHERS, &pkt.hdr, static_cast<unsigned>(len)))
			nthread_terminate_game("SNetSendMessage");
	}
}

void multi_send_msg_packet(uint32_t pmask, const byte *data, size_t size)
{
	TPkt pkt;
	NetReceivePlayerData(&pkt);
	const size_t len = size + sizeof(pkt.hdr);
	pkt.hdr.wLen = SDL_SwapLE16(static_cast<uint16_t>(len));
	memcpy(pkt.body, data, size);
	size_t playerID = 0;
	for (size_t v = 1; playerID < Players.size(); playerID++, v <<= 1) {
		if ((v & pmask) != 0) {
			if (!SNetSendMessage(playerID, &pkt.hdr, len) && SErrGetLastError() != STORM_ERROR_INVALID_PLAYER) {
				nthread_terminate_game("SNetSendMessage");
				return;
			}
		}
	}
}

void multi_msg_countdown()
{
	for (size_t i = 0; i < Players.size(); i++) {
		if ((player_state[i] & PS_TURN_ARRIVED) != 0) {
			if (gdwMsgLenTbl[i] == sizeof(int32_t))
				ParseTurn(i, *(int32_t *)glpMsgTbl[i]);
		}
	}
}

void multi_player_left(int pnum, int reason)
{
	sgbPlayerLeftGameTbl[pnum] = true;
	sgdwPlayerLeftReasonTbl[pnum] = reason;
	ClearPlayerLeftState();
}

void multi_net_ping()
{
	sgbTimeout = true;
	sglTimeoutStart = SDL_GetTicks();
}

bool multi_handle_delta()
{
	if (gbGameDestroyed) {
		gbRunGame = false;
		return false;
	}

	for (size_t i = 0; i < Players.size(); i++) {
		if (sgbSendDeltaTbl[i]) {
			sgbSendDeltaTbl[i] = false;
			DeltaExportData(i);
		}
	}

	sgbSentThisCycle = nthread_send_and_recv_turn(sgbSentThisCycle, 1);
	bool received;
	if (!nthread_recv_turns(&received)) {
		BeginTimeout();
		return false;
	}

	sgbTimeout = false;
	if (received) {
		if (!shareNextHighPriorityMessage) {
			// If there are any high priority messages pending,
			// share them with other players now
			shareNextHighPriorityMessage = true;
			if (highPriorityBuffer.dwNextWriteOffset != 0)
				NetSendHiPri(MyPlayerId, nullptr, 0);
		} else {
			// If there were no high priority messages in at least two consecutive game
			// ticks, this shares the low priority messages and monster sync data
			NetSendHiPri(MyPlayerId, nullptr, 0);
			shareNextHighPriorityMessage = true;
		}
	}
	MonsterSeeds();

	return true;
}

void multi_process_network_packets()
{
	ClearPlayerLeftState();
	ProcessTmsgs();

	uint8_t playerId = std::numeric_limits<uint8_t>::max();
	TPktHdr *pkt;
	uint32_t dwMsgSize = 0;
	while (SNetReceiveMessage(&playerId, (void **)&pkt, &dwMsgSize)) {
		dwRecCount++;
		ClearPlayerLeftState();
		if (dwMsgSize < sizeof(TPktHdr))
			continue;
		if (playerId >= Players.size())
			continue;
		if (pkt->wCheck != HeaderCheckVal)
			continue;
		if (SDL_SwapLE16(pkt->wLen) != dwMsgSize)
			continue;
		Player &player = Players[playerId];
		if (!IsNetPlayerValid(player)) {
			_cmd_id cmd = *(const _cmd_id *)(pkt + 1);
			if (gbBufferMsgs == 0 && IsNoneOf(cmd, CMD_SEND_PLRINFO, CMD_ACK_PLRINFO)) {
				// Distrust all messages until
				// player info is received
				continue;
			}
		}
		Point syncPosition = { pkt->px, pkt->py };
		player.position.last = syncPosition;
		if (&player != MyPlayer) {
			assert(gbBufferMsgs != 2);
			player._pHitPoints = SDL_SwapLE32(pkt->php);
			player._pMaxHP = SDL_SwapLE32(pkt->pmhp);
			player._pMana = SDL_SwapLE32(pkt->mana);
			player._pMaxMana = SDL_SwapLE32(pkt->maxmana);
			bool cond = gbBufferMsgs == 1;
			player._pBaseStr = pkt->bstr;
			player._pBaseMag = pkt->bmag;
			player._pBaseDex = pkt->bdex;
			if (!cond && player.plractive && player._pHitPoints != 0) {
				if (player.isOnActiveLevel() && !player._pLvlChanging) {
					if (player.position.tile.WalkingDistance(syncPosition) > 3 && PosOkPlayer(player, syncPosition)) {
						// got out of sync, clear the tiles around where we last thought the player was located
						FixPlrWalkTags(player);

						player.position.old = player.position.tile;
						// then just in case clear the tiles around the current position (probably unnecessary)
						FixPlrWalkTags(player);
						player.position.tile = syncPosition;
						player.position.future = syncPosition;
						if (player.isWalking())
							player.position.temp = syncPosition;
						SetPlayerOld(player);
						dPlayer[player.position.tile.x][player.position.tile.y] = playerId + 1;
					}
					if (player.position.future.WalkingDistance(player.position.tile) > 1) {
						player.position.future = player.position.tile;
					}
					Point target = { pkt->targx, pkt->targy };
					if (target != Point {}) // does the client send a desired (future) position of remote player?
						MakePlrPath(player, target, true);
				} else {
					player.position.tile = syncPosition;
					player.position.future = syncPosition;
					SetPlayerOld(player);
				}
			}
		}
		HandleAllPackets(playerId, (const byte *)(pkt + 1), dwMsgSize - sizeof(TPktHdr));
	}
	if (SErrGetLastError() != STORM_ERROR_NO_MESSAGES_WAITING)
		nthread_terminate_game("SNetReceiveMsg");
	CheckPlayerInfoTimeouts();
}

void multi_send_zero_packet(size_t pnum, _cmd_id bCmd, const byte *data, size_t size)
{
	assert(pnum != MyPlayerId);
	assert(data != nullptr);
	assert(size <= 0x0ffff);

	for (size_t offset = 0; offset < size;) {
		TPkt pkt {};
		pkt.hdr.wCheck = HeaderCheckVal;
		auto &message = *reinterpret_cast<TCmdPlrInfoHdr *>(pkt.body);
		message.bCmd = bCmd;
		message.wOffset = SDL_SwapLE16(offset);

		size_t dwBody = gdwLargestMsgSize - sizeof(pkt.hdr) - sizeof(message);
		dwBody = std::min(dwBody, size - offset);
		assert(dwBody <= 0x0ffff);
		message.wBytes = SDL_SwapLE16(dwBody);

		memcpy(&pkt.body[sizeof(message)], &data[offset], dwBody);

		const size_t dwMsg = sizeof(pkt.hdr) + sizeof(message) + dwBody;
		pkt.hdr.wLen = SDL_SwapLE16(dwMsg);

		if (!SNetSendMessage(pnum, &pkt, dwMsg)) {
			nthread_terminate_game("SNetSendMessage2");
			return;
		}

		offset += dwBody;
	}
}

void NetClose()
{
	if (!sgbNetInited) {
		return;
	}

	sgbNetInited = false;
	nthread_cleanup();
	tmsg_cleanup();
	UnregisterNetEventHandlers();
	SNetLeaveGame(3);
	if (gbIsMultiplayer)
		SDL_Delay(2000);
	if (!demo::IsRunning()) {
		Players.clear();
		MyPlayer = nullptr;
	}
}

bool NetInit(bool bSinglePlayer)
{
	while (true) {
		SetRndSeed(0);
		InitGameInfo();
		memset(sgbPlayerTurnBitTbl, 0, sizeof(sgbPlayerTurnBitTbl));
		gbGameDestroyed = false;
		memset(sgbPlayerLeftGameTbl, 0, sizeof(sgbPlayerLeftGameTbl));
		memset(sgdwPlayerLeftReasonTbl, 0, sizeof(sgdwPlayerLeftReasonTbl));
		memset(sgbSendDeltaTbl, 0, sizeof(sgbSendDeltaTbl));
		Players.clear();
		MyPlayer = nullptr;
		memset(sgwPackPlrOffsetTbl, 0, sizeof(sgwPackPlrOffsetTbl));
		SNetSetBasePlayer(0);
		if (bSinglePlayer) {
			if (!InitSingle(&sgGameInitInfo))
				return false;
		} else {
			if (!InitMulti(&sgGameInitInfo))
				return false;
		}
		sgbNetInited = true;
		sgbTimeout = false;
		delta_init();
		InitPlrMsg();
		BufferInit(&highPriorityBuffer);
		BufferInit(&lowPriorityBuffer);
		shareNextHighPriorityMessage = true;
		sync_init();
		nthread_start(sgbPlayerTurnBitTbl[MyPlayerId]);
		tmsg_start();
		sgdwGameLoops = 0;
		sgbSentThisCycle = 0;
		gbDeltaSender = MyPlayerId;
		gbSomebodyWonGameKludge = false;
		nthread_send_and_recv_turn(0, 0);
		SetupLocalPositions();
		SendPlayerInfo(SNPLAYER_OTHERS, CMD_SEND_PLRINFO);

		Player &myPlayer = *MyPlayer;
		ResetPlayerGFX(myPlayer);
		myPlayer.plractive = true;
		gbActivePlayers = 1;

		if (!sgbPlayerTurnBitTbl[MyPlayerId] || msg_wait_resync())
			break;
		NetClose();
		gbSelectProvider = false;
	}
	SetRndSeed(sgGameInitInfo.dwSeed);
	gnTickDelay = 1000 / sgGameInitInfo.nTickRate;

	for (int i = 0; i < NUMLEVELS; i++) {
		glSeedTbl[i] = AdvanceRndSeed();
	}
	PublicGame = DvlNet_IsPublicGame();

	Player &myPlayer = *MyPlayer;
	// separator for marking messages from a different game
	AddMessageToChatLog(_("New Game"), nullptr, UiFlags::ColorRed);
	AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer._pLevel));

	return true;
}

void recv_plrinfo(int pnum, const TCmdPlrInfoHdr &header, bool recv)
{
	static PlayerNetPack PackedPlayerBuffer[MAX_PLRS];

	assert(pnum >= 0 && pnum < MAX_PLRS);
	Player &player = Players[pnum];
	if (&player == MyPlayer) {
		return;
	}
	auto &packedPlayer = PackedPlayerBuffer[pnum];

	if (sgwPackPlrOffsetTbl[pnum] != SDL_SwapLE16(header.wOffset)) {
		sgwPackPlrOffsetTbl[pnum] = 0;
		if (header.wOffset != 0) {
			return;
		}
	}
	if (!recv && sgwPackPlrOffsetTbl[pnum] == 0) {
		SendPlayerInfo(pnum, CMD_ACK_PLRINFO);
	}

	memcpy(reinterpret_cast<uint8_t *>(&packedPlayer) + SDL_SwapLE16(header.wOffset), reinterpret_cast<const uint8_t *>(&header) + sizeof(header), SDL_SwapLE16(header.wBytes));

	sgwPackPlrOffsetTbl[pnum] += SDL_SwapLE16(header.wBytes);
	if (sgwPackPlrOffsetTbl[pnum] != sizeof(packedPlayer)) {
		return;
	}
	sgwPackPlrOffsetTbl[pnum] = 0;

	PlayerLeftMsg(pnum, false);
	if (!UnPackNetPlayer(packedPlayer, player)) {
		player = {};
		SNetDropPlayer(pnum, LEAVE_DROP);
		return;
	}

	if (!recv) {
		return;
	}

	ResetPlayerGFX(player);
	player.plractive = true;
	gbActivePlayers++;

	string_view szEvent;
	if (sgbPlayerTurnBitTbl[pnum]) {
		szEvent = _("Player '{:s}' (level {:d}) just joined the game");
	} else {
		szEvent = _("Player '{:s}' (level {:d}) is already in the game");
	}
	EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player._pLevel));

	SyncInitPlr(player);

	if (!player.isOnActiveLevel()) {
		return;
	}

	if (player._pHitPoints >> 6 > 0) {
		StartStand(player, Direction::South);
		return;
	}

	player._pgfxnum &= ~0xFU;
	player._pmode = PM_DEATH;
	NewPlrAnim(player, player_graphic::Death, Direction::South);
	player.AnimInfo.currentFrame = player.AnimInfo.numberOfFrames - 2;
	dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer;
}

} // namespace devilution
