/*
  Maelstrom: Open Source version of the classic game by Ambrosia Software
  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/

#include "Maelstrom_Globals.h"
#include "../screenlib/UIElement.h"
#include "../screenlib/UIElementCheckbox.h"
#include "../screenlib/UIElementRadio.h"

#include "gameinfo.h"


GameInfo::GameInfo()
{
	Reset();
}

void
GameInfo::Reset()
{
	localID = SDL_rand(SDL_MAX_SINT32);
	gameID = 0;
	seed = 0;
	wave = 0;
	lives = 0;
	turbo = 0;
	gameMode = 0;
	numNodes = 0;
	SDL_zero(nodes);
	SDL_zero(players);
	replayVersion = REPLAY_VERSION;
	spriteCRC = gSpriteCRC;
}

void
GameInfo::SetHost(Uint8 wave, Uint8 lives, Uint8 turbo, bool deathmatch, bool cheat)
{
	Reset();

	this->gameID = localID;
	this->seed = GetRandSeed();
	this->wave = wave;
	this->lives = lives;
	this->turbo = turbo;
	this->gameMode = 0;
	if (cheat) {
		this->gameMode |= GAME_MODE_CHEAT;
	}
	if (deathmatch) {
		this->gameMode |= GAME_MODE_DEATHMATCH;
	}
	if (prefs->GetBool(PREFERENCES_KIDMODE)) {
        this->gameMode |= GAME_MODE_KIDS;
	}
	if (gControlBrakes) {
		this->gameMode |= GAME_MODE_CONTROL_BRAKES;
	}

	// We are the host node
	assert(HOST_NODE == 0);
	nodes[HOST_NODE].nodeID = localID;
	numNodes = 1;
}

void
GameInfo::SetPlayerSlot(int slot, const char *name, Uint8 controlMask)
{
	GameInfoPlayer *player = &players[slot];

	SDL_strlcpy(player->name, name ? name : "", sizeof(player->name));

	if (IS_LOCAL_CONTROL(controlMask) || IS_REMOTE_CONTROL(controlMask)) {
		player->nodeID = localID;
	} else {
		player->nodeID = 0;
	}
	player->controlMask = controlMask;

	if (controlMask == CONTROL_NETWORK) {
		player->available = true;
	} else {
		player->available = false;
	}

	UpdateUI(player);
}

void
GameInfo::SetPlayerName(int slot, const char *name)
{
	GameInfoPlayer *player = &players[slot];

	SDL_strlcpy(player->name, name ? name : "", sizeof(player->name));

	UpdateUI(player);
}

bool
GameInfo::AddNetworkPlayer(Uint32 nodeID, const IPaddress &address, const char *name)
{
	int slot;

	if (IsFull()) {
		return false;
	}

	slot = numNodes;
	GameInfoNode *node = &nodes[slot];
	node->nodeID = nodeID;
	node->address = address;
	InitializePing(slot);
	++numNodes;

	for (slot = 0; slot < MAX_PLAYERS; ++slot) {
		if (players[slot].available) {
			break;
		}
	}
	assert(slot < MAX_PLAYERS);

	GameInfoPlayer *player = &players[slot];
	player->nodeID = nodeID;
	SDL_strlcpy(player->name, name, sizeof(player->name));
	player->available = false;

	UpdateUI(player);

	return true;
}

void
GameInfo::CopyFrom(const GameInfo &rhs)
{
	int i, j;

	gameID = rhs.gameID;
	seed = rhs.seed;
	wave = rhs.wave;
	lives = rhs.lives;
	turbo = rhs.turbo;
	gameMode = rhs.gameMode;
	replayVersion = rhs.replayVersion;
	spriteCRC = rhs.spriteCRC;

	for (i = 0; i < MAX_NODES; ++i) {
		const GameInfoNode *node = rhs.GetNode(i);
		if (nodes[i].nodeID != node->nodeID ||
		    nodes[i].address != node->address) {
			// See if this node was just slid down
			for (j = i+1; j < MAX_NODES; ++j) {
				if (nodes[j].nodeID == node->nodeID &&
				    nodes[j].address == node->address) {
					nodes[i].ping = nodes[j].ping;
					break;
				}
			}
			if (j == MAX_NODES) {
				// Reset the ping info
				InitializePing(i);
			}
		}
		nodes[i].nodeID = node->nodeID;
		nodes[i].address = node->address;
	}
	numNodes = rhs.numNodes;

	for (i = 0; i < MAX_PLAYERS; ++i) {
		const GameInfoPlayer *player = rhs.GetPlayer(i);
		players[i].nodeID = player->nodeID;
		SDL_memcpy(players[i].name, player->name,
			sizeof(players[i].name));
		if (players[i].nodeID == localID) {
			if (players[i].controlMask == CONTROL_NONE) {
				players[i].controlMask = CONTROL_LOCAL;
			}
		} else if (players[i].nodeID != 0) {
			players[i].controlMask = CONTROL_NETWORK;
		} else {
			players[i].controlMask = CONTROL_NONE;
		}
		players[i].available = player->available;
	}

	UpdateUI();
}

bool
GameInfo::ReadFromPacket(DynamicPacket &packet)
{
	int i;

	if (!packet.Read(gameID)) {
		return false;
	}
	if (!packet.Read(seed)) {
		return false;
	}
	if (!packet.Read(wave)) {
		return false;
	}
	if (!packet.Read(lives)) {
		return false;
	}
	if (!packet.Read(turbo)) {
		return false;
	}
	if (!packet.Read(gameMode)) {
		return false;
	}

	if (!packet.Read(numNodes)) {
		return false;
	}
	for (i = 0; i < MAX_NODES; ++i) {
		if (!packet.Read(nodes[i].nodeID)) {
			return false;
		}
		if (!packet.Read(nodes[i].address.host)) {
			return false;
		}
		if (!packet.Read(nodes[i].address.port)) {
			return false;
		}
	}

	for (i = 0; i < MAX_PLAYERS; ++i) {
		if (!packet.Read(players[i].nodeID)) {
			return false;
		}
		if (!packet.Read(players[i].name, sizeof(players[i].name))) {
			return false;
		}
		if (players[i].nodeID) {
			players[i].controlMask = CONTROL_REPLAY;
		} else {
			players[i].controlMask = CONTROL_NONE;
		}
	}

	// We want to get the public address of the server
	// If we already have one, we assume that's the fastest interface
	if (!nodes[HOST_NODE].address.host) {
		nodes[HOST_NODE].address = packet.address;
	}

	if (!packet.Read(replayVersion)) {
		// Older version
		replayVersion = 0;
	}

	if (!packet.Read(spriteCRC)) {
		// Older version
		spriteCRC = 0;
	}

	for (i = 0; i < MAX_PLAYERS; ++i) {
		if (!packet.Read(players[i].available)) {
			// Older version
			players[i].available = false;
		}
	}

	return true;
}

void
GameInfo::WriteToPacket(DynamicPacket &packet)
{
	int i;

	packet.Write(gameID);
	packet.Write(seed);
	packet.Write(wave);
	packet.Write(lives);
	packet.Write(turbo);
	packet.Write(gameMode);

	packet.Write(numNodes);
	for (i = 0; i < MAX_NODES; ++i) {
		packet.Write(nodes[i].nodeID);
		packet.Write(nodes[i].address.host);
		packet.Write(nodes[i].address.port);
	}

	for (i = 0; i < MAX_PLAYERS; ++i) {
		packet.Write(players[i].nodeID);
		packet.Write(players[i].name);
	}

	packet.Write(replayVersion);
	packet.Write(spriteCRC);

	for (i = 0; i < MAX_PLAYERS; ++i) {
		packet.Write(players[i].available);
	}
}

void
GameInfo::PrepareForReplay()
{
	// Clean up the game info for privacy and so replay works
	gameID = localID;

	SDL_zero(nodes);
	nodes[HOST_NODE].nodeID = localID;
	numNodes = 1;

	for (int i = 0; i < MAX_PLAYERS; ++i) {
		if (IsValidPlayer(i)) {
			players[i].nodeID = localID;
			players[i].controlMask = CONTROL_REPLAY;
		}
	}
}

bool
GameInfo::HasNode(Uint32 nodeID) const
{
	for (int i = 0; i < MAX_NODES; ++i) {
		if (nodes[i].nodeID == nodeID) {
			return true;
		}
	}
	return false;
}

bool
GameInfo::HasNode(const IPaddress &address) const
{
	for (int i = 0; i < MAX_NODES; ++i) {
		if (nodes[i].address == address) {
			return true;
		}
	}
	return false;
}

void
GameInfo::RemoveNode(Uint32 nodeID)
{
	int i;

	i = 0;
	while (i < GetNumNodes()) {
		if (nodeID == nodes[i].nodeID) {
			for (int j = i; j < (GetNumNodes() - 1); ++j) {
				nodes[j] = nodes[j + 1];
			}
			nodes[GetNumNodes() - 1].Reset();
			--numNodes;
		} else {
			++i;
		}
	}

	for (i = 0; i < MAX_PLAYERS; ++i) {
		if (nodeID == players[i].nodeID) {
			RemovePlayer(i);
		}
	}
}

void
GameInfo::RemovePlayer(int index)
{
	GameInfoPlayer *player = &players[index];
	player->nodeID = 0;
	SDL_zero(player->name);
	player->controlMask = CONTROL_NETWORK;
	player->available = true;

	UpdateUI(player);
}

bool
GameInfo::IsHosting() const
{
	return localID == gameID;
}

bool
GameInfo::IsMultiplayer() const
{
	return (GetNumPlayers() > 1);
}

bool
GameInfo::IsLocalNode(int index) const
{
	if (index >= GetNumNodes()) {
		return false;
	}
	return (nodes[index].nodeID == localID);
}

bool
GameInfo::IsNetworkNode(int index) const
{
	if (index >= GetNumNodes()) {
		return false;
	}
	return (nodes[index].nodeID != localID);
}

bool
GameInfo::IsValidPlayer(int index) const
{
	if (!players[index].nodeID) {
		return false;
	}
	return true;
}

bool
GameInfo::IsLocalPlayer(int index) const
{
	if (!players[index].nodeID) {
		return false;
	}
	return (players[index].nodeID == localID);
}

bool
GameInfo::IsNetworkPlayer(int index) const
{
	if (!players[index].nodeID) {
		return false;
	}
	return (players[index].nodeID != localID);
}

bool
GameInfo::HasLocalControl() const
{
	for (int i = 0; i < MAX_PLAYERS; ++i) {
		if (!IsLocalPlayer(i)) {
			continue;
		}
		if (IS_LOCAL_CONTROL(players[i].controlMask)) {
			return true;
		}
	}
	return false;
}

int
GameInfo::GetNumPlayers() const
{
	int numPlayers = 0;
	for (int i = 0; i < MAX_PLAYERS; ++i) {
		if (players[i].nodeID) {
			++numPlayers;
		}
	}
	return numPlayers;
}

bool
GameInfo::IsFull() const
{
	for (int i = 0; i < MAX_PLAYERS; ++i) {
		if (players[i].available) {
			return false;
		}
	}
	return true;
}

void
GameInfo::TogglePauseRequest()
{
	if (paused & (PAUSE_REQUEST|PAUSE_CONTROLLER)) {
		SetPauseReason((PAUSE_REQUEST|PAUSE_CONTROLLER), false);
	} else {
		SetPauseReason(PAUSE_REQUEST, true);
	}
}

void
GameInfo::SetPauseReason(Uint8 reason, bool enabled)
{
	if (enabled) {
		paused |= reason;
	} else {
		paused &= ~reason;
	}
	SetLocalState(STATE_PAUSE, paused ? true : false);
}

void
GameInfo::SetLocalState(Uint8 state, bool enabled)
{
	if (enabled) {
		nodes[GetLocalIndex()].state |= state;
	} else {
		nodes[GetLocalIndex()].state &= ~state;
	}
}

void
GameInfo::ToggleLocalState(Uint8 state)
{
	if (GetLocalState() & state) {
		SetLocalState(state, false);
	} else {
		SetLocalState(state, true);
	}
}

void
GameInfo::SetNodeState(int index, Uint8 state)
{
	nodes[index].state = state;
}

Uint8
GameInfo::GetNodeState(int index) const
{
	return nodes[index].state;
}

void
GameInfo::BindPlayerToUI(int index, UIElement *element)
{
	char name[32];
	GameInfoPlayer *player = &players[index];

	if (player->UI.element == element) {
		return;
	}

	player->UI.element = element;
	player->UI.join = element->GetElement<UIElement>("join");
	player->UI.desc = element->GetElement<UIElement>("desc");
	player->UI.name = element->GetElement<UIElement>("name");
	player->UI.host = element->GetElement<UIElement>("host");
	player->UI.control = element->GetElement<UIElement>("control_image");

	for (int i = 0; i < NUM_PING_STATES; ++i) {
		SDL_snprintf(name, sizeof(name), "ping%d", i);
		player->UI.ping_states[i] = element->GetElement<UIElement>(name);
	}

	UpdateUI(player);
}

void
GameInfo::BindHostPlayerToUI(UIElement *element)
{
	Uint32 hostNodeID = nodes[HOST_NODE].nodeID;
	for (int index = 0; index < MAX_PLAYERS; ++index) {
		if (players[index].nodeID == hostNodeID) {
			BindPlayerToUI(index, element);
			break;
		}
	}
}

void
GameInfo::UpdateUI()
{
	for (int i = 0; i < MAX_PLAYERS; ++i) {
		UpdateUI(&players[i]);
	}
}

static const char *GetGamepadName(int index)
{
	const char *name = nullptr;
	int count = 0;
	SDL_JoystickID *gamepads = SDL_GetGamepads(&count);
	if (gamepads) {
		if (index < count) {
			name = SDL_GetGamepadNameForID(gamepads[index]);
		}
		SDL_free(gamepads);
	}
	return name;
}

void
GameInfo::UpdateUI(GameInfoPlayer *player)
{
	if (!player->UI.element) {
		return;
	}

	bool enableJoin = true;

	if (player->UI.name && player->UI.host) {
		const GameInfoNode *node = GetNodeByID(player->nodeID);
		if (!node || node->nodeID == localID) {
			player->UI.name->Hide();
			player->UI.host->Hide();
		} else {
			player->UI.name->Show();
			player->UI.name->SetText(player->name);
			player->UI.host->Show();
			if (replayVersion != REPLAY_VERSION) {
				player->UI.host->SetText("(different version)");
				enableJoin = false;
			} else if (spriteCRC != gSpriteCRC) {
				player->UI.host->SetText("(different sprites)");
				enableJoin = false;
			} else if (IsFull() && !HasLocalControl()) {
				player->UI.host->SetText("(no ships available)");
				enableJoin = false;
			} else {
				player->UI.host->SetText(NET_GetAddressString(node->address.host));
			}
		}
	}

	if (player->UI.join) {
		if (enableJoin) {
			player->UI.join->SetDisabled(false);
		} else {
			player->UI.join->SetDisabled(true);
		}
	}

	if (player->UI.control) {
		SDL_Surface *surface = nullptr;
		if (IS_REMOTE_CONTROL(player->controlMask)) {
			surface = GetRemotePlayerAvatar(player->controlMask);
		}
		if (surface) {
			player->UI.control->SetImage(surface);
		} else {
			char name[128];
			SDL_snprintf(name, sizeof(name), "control%d", player->controlMask);
			player->UI.control->SetImage(name);
		}
	}

	if (player->UI.desc) {
		const char *desc = NULL;

		if (player->controlMask == CONTROL_JOYSTICK1) {
			desc = GetGamepadName(0);
		} else if (player->controlMask == CONTROL_JOYSTICK2) {
			desc = GetGamepadName(1);
		} else if (player->controlMask == CONTROL_JOYSTICK3) {
			desc = GetGamepadName(2);
		} else if (IS_REMOTE_CONTROL(player->controlMask)) {
			desc = player->name;
		} else if (player->controlMask == CONTROL_NETWORK) {
			if (!GetNodeByID(player->nodeID)) {
				desc = "Ship available";
			}
		} else if (player->controlMask == CONTROL_LOCAL) {
			desc = "You're locked in!";
		} else if (player->controlMask == CONTROL_NONE) {
			desc = "Ship disabled";
		}
		player->UI.desc->SetText(desc);
	}

	for (int i = 0; i < NUM_PING_STATES; ++i) {
		UIElement *element = player->UI.ping_states[i];
		if (element) {
			const GameInfoNode *node = GetNodeByID(player->nodeID);
			if (node && node->ping.status == i) {
				element->Show();
			} else {
				element->Hide();
			}
		}
	}
}

void
GameInfo::InitializePing()
{
	for (int i = 0; i < MAX_NODES; ++i) {
		InitializePing(i);
	}
}

void
GameInfo::InitializePing(int index)
{
	GameInfoNode *node = &nodes[index];

	if (node->nodeID != localID) {
		node->ping.lastPing = (Uint32)SDL_GetTicks();
		node->ping.roundTripTime = 0;
		node->ping.status = PING_GOOD;
	}
}

void
GameInfo::UpdatePingTime(int index, Uint32 timestamp)
{
	Uint32 now;
	Uint32 elapsed;
	GameInfoNode *node;

	now = (Uint32)SDL_GetTicks();
	elapsed = (now - timestamp);

	node = &nodes[index];
	node->ping.lastPing = now;
	if (!node->ping.roundTripTime) {
		node->ping.roundTripTime = elapsed;
	} else {
		// Use a weighted average 2/3 previous value, 1/3 new value
		node->ping.roundTripTime = (2*node->ping.roundTripTime + 1*elapsed) / 3;
	}
}

void
GameInfo::UpdatePingStatus()
{
	for (int i = 0; i < GetNumNodes(); ++i) {
		UpdatePingStatus(i);
	}
}

void
GameInfo::UpdatePingStatus(int index)
{
	GameInfoNode *node = &nodes[index];

	if (!IsNetworkNode(index)) {
		node->ping.status = PING_LOCAL;
	} else {
		Uint32 sinceLastPing;

		sinceLastPing = int(SDL_GetTicks() - node->ping.lastPing);
		if (sinceLastPing < 2*PING_INTERVAL) {
			if (node->ping.roundTripTime <= 2*FRAME_DELAY_MS) {
#ifdef DEBUG_NETWORK
printf("Game 0x%8.8x: node 0x%8.8x round trip time %d (GOOD)\n",
	gameID, node->nodeID, node->ping.roundTripTime);
#endif
				node->ping.status = PING_GOOD;
			} else if (node->ping.roundTripTime <= 3*FRAME_DELAY_MS) {
#ifdef DEBUG_NETWORK
printf("Game 0x%8.8x: node 0x%8.8x round trip time %d (OKAY)\n",
	gameID, node->nodeID, node->ping.roundTripTime);
#endif
				node->ping.status = PING_OKAY;
			} else {
#ifdef DEBUG_NETWORK
printf("Game 0x%8.8x: node 0x%8.8x round trip time %d (BAD)\n",
	gameID, node->nodeID, node->ping.roundTripTime);
#endif
				node->ping.status = PING_BAD;
			}
		} else if (sinceLastPing < PING_TIMEOUT) {
#ifdef DEBUG_NETWORK
printf("Game 0x%8.8x: node 0x%8.8x since last ping %d (BAD)\n",
	gameID, node->nodeID, sinceLastPing);
#endif
			node->ping.status = PING_BAD;
		} else {
#ifdef DEBUG_NETWORK
printf("Game 0x%8.8x: node 0x%8.8x since last ping %d (TIMEDOUT)\n",
	gameID, node->nodeID, sinceLastPing);
#endif
			node->ping.status = PING_TIMEDOUT;
		}

		// Update the UI for matching players
		for (int i = 0; i < MAX_PLAYERS; ++i) {
			if (players[i].nodeID == node->nodeID) {
				UpdateUI(&players[i]);
			}
		}
	}
}

PING_STATUS
GameInfo::GetPingStatus(int index)
{
	if (IsNetworkNode(index)) {
		return nodes[index].ping.status;
	} else {
		return PING_LOCAL;
	}
}
