/*
 * This file is part of OpenTTD.
 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
 */

/** @file network_coordinator.cpp Game Coordinator sending/receiving part of the network protocol. */

#include "../stdafx.h"
#include "../debug.h"
#include "../error.h"
#include "../rev.h"
#include "../settings_type.h"
#include "../strings_func.h"
#include "../window_func.h"
#include "../window_type.h"
#include "network.h"
#include "network_coordinator.h"
#include "network_gamelist.h"
#include "network_gui.h"
#include "network_internal.h"
#include "network_server.h"
#include "network_stun.h"
#include "table/strings.h"

#include "../safeguards.h"

static const auto NETWORK_COORDINATOR_DELAY_BETWEEN_UPDATES = std::chrono::seconds(30); ///< How many time between updates the server sends to the Game Coordinator.
ClientNetworkCoordinatorSocketHandler _network_coordinator_client; ///< The connection to the Game Coordinator.
ConnectionType _network_server_connection_type = CONNECTION_TYPE_UNKNOWN; ///< What type of connection the Game Coordinator detected we are on.
std::string _network_server_invite_code = ""; ///< Our invite code as indicated by the Game Coordinator.

/** Connect to a game server by IP:port. */
class NetworkDirectConnecter : public TCPConnecter {
private:
	std::string token;     ///< Token of this connection.
	uint8_t tracking_number; ///< Tracking number of this connection.

public:
	/**
	 * Try to establish a direct (hostname:port based) connection.
	 * @param hostname The hostname of the server.
	 * @param port The port of the server.
	 * @param token The token as given by the Game Coordinator to track this connection attempt.
	 * @param tracking_number The tracking number as given by the Game Coordinator to track this connection attempt.
	 */
	NetworkDirectConnecter(std::string_view hostname, uint16_t port, std::string &&token, uint8_t tracking_number) : TCPConnecter(hostname, port), token(std::move(token)), tracking_number(tracking_number) {}

	void OnFailure() override
	{
		_network_coordinator_client.ConnectFailure(this->token, this->tracking_number);
	}

	void OnConnect(SOCKET s) override
	{
		NetworkAddress address = NetworkAddress::GetPeerAddress(s);
		_network_coordinator_client.ConnectSuccess(this->token, s, address);
	}
};

/** Connecter used after STUN exchange to connect from both sides to each other. */
class NetworkReuseStunConnecter : public TCPConnecter {
private:
	std::string token;     ///< Token of this connection.
	uint8_t tracking_number; ///< Tracking number of this connection.
	uint8_t family;          ///< Family of this connection.

public:
	/**
	 * Try to establish a STUN-based connection.
	 * @param hostname The hostname of the peer.
	 * @param port The port of the peer.
	 * @param bind_address The local bind address used for this connection.
	 * @param token The connection token.
	 * @param tracking_number The tracking number of the connection.
	 * @param family The family this connection is using.
	 */
	NetworkReuseStunConnecter(std::string_view hostname, uint16_t port, const NetworkAddress &bind_address, std::string &&token, uint8_t tracking_number, uint8_t family) :
		TCPConnecter(hostname, port, bind_address),
		token(std::move(token)),
		tracking_number(tracking_number),
		family(family)
	{
	}

	void OnFailure() override
	{
		/* Close the STUN connection too, as it is no longer of use. */
		_network_coordinator_client.CloseStunHandler(this->token, this->family);

		_network_coordinator_client.ConnectFailure(this->token, this->tracking_number);
	}

	void OnConnect(SOCKET s) override
	{
		NetworkAddress address = NetworkAddress::GetPeerAddress(s);
		_network_coordinator_client.ConnectSuccess(this->token, s, address);
	}
};

/** Connect to the Game Coordinator server. */
class NetworkCoordinatorConnecter : public TCPConnecter {
public:
	/**
	 * Initiate the connecting.
	 * @param connection_string The address of the Game Coordinator server.
	 */
	NetworkCoordinatorConnecter(std::string_view connection_string) : TCPConnecter(connection_string, NETWORK_COORDINATOR_SERVER_PORT) {}

	void OnFailure() override
	{
		_network_coordinator_client.connecting = false;
		_network_coordinator_client.CloseConnection(true);
	}

	void OnConnect(SOCKET s) override
	{
		assert(_network_coordinator_client.sock == INVALID_SOCKET);

		_network_coordinator_client.sock = s;
		_network_coordinator_client.last_activity = std::chrono::steady_clock::now();
		_network_coordinator_client.connecting = false;
	}
};

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_ERROR(Packet &p)
{
	NetworkCoordinatorErrorType error = (NetworkCoordinatorErrorType)p.Recv_uint8();
	std::string detail = p.Recv_string(NETWORK_ERROR_DETAIL_LENGTH);
	Debug(net, 9, "Coordinator::Receive_GC_ERROR({}, {})", error, detail);

	switch (error) {
		case NETWORK_COORDINATOR_ERROR_UNKNOWN:
			this->CloseConnection();
			return false;

		case NETWORK_COORDINATOR_ERROR_REGISTRATION_FAILED:
			ShowErrorMessage(GetEncodedString(STR_NETWORK_ERROR_COORDINATOR_REGISTRATION_FAILED), {}, WL_ERROR);

			/* To prevent that we constantly try to reconnect, switch to local game. */
			_settings_client.network.server_game_type = SERVER_GAME_TYPE_LOCAL;

			this->CloseConnection();
			return false;

		case NETWORK_COORDINATOR_ERROR_INVALID_INVITE_CODE: {
			auto connecter_pre_it = this->connecter_pre.find(detail);
			if (connecter_pre_it != this->connecter_pre.end()) {
				connecter_pre_it->second->SetFailure();
				this->connecter_pre.erase(connecter_pre_it);
			}

			/* Mark the server as offline. */
			NetworkGame *item = NetworkGameListAddItem(detail);
			item->status = NGLS_OFFLINE;

			UpdateNetworkGameWindow();
			return true;
		}

		case NETWORK_COORDINATOR_ERROR_REUSE_OF_INVITE_CODE:
			ShowErrorMessage(GetEncodedString(STR_NETWORK_ERROR_COORDINATOR_REUSE_OF_INVITE_CODE), {}, WL_ERROR);

			/* To prevent that we constantly battle for the same invite-code, switch to local game. */
			_settings_client.network.server_game_type = SERVER_GAME_TYPE_LOCAL;

			this->CloseConnection();
			return false;

		default:
			Debug(net, 0, "Invalid error type {} received from Game Coordinator", error);
			this->CloseConnection();
			return false;
	}
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_REGISTER_ACK(Packet &p)
{
	/* Schedule sending an update. */
	this->next_update = std::chrono::steady_clock::now();

	_settings_client.network.server_invite_code = p.Recv_string(NETWORK_INVITE_CODE_LENGTH);
	_settings_client.network.server_invite_code_secret = p.Recv_string(NETWORK_INVITE_CODE_SECRET_LENGTH);
	_network_server_connection_type = (ConnectionType)p.Recv_uint8();
	Debug(net, 9, "Coordinator::Receive_GC_REGISTER_ACK({}, {})", _settings_client.network.server_invite_code, _network_server_connection_type);

	if (_network_server_connection_type == CONNECTION_TYPE_ISOLATED) {
		ShowErrorMessage(
			GetEncodedString(STR_NETWORK_ERROR_COORDINATOR_ISOLATED),
			GetEncodedString(STR_NETWORK_ERROR_COORDINATOR_ISOLATED_DETAIL),
			WL_ERROR);
	}

	/* Users can change the invite code in the settings, but this has no effect
	 * on the invite code as assigned by the server. So
	 * _network_server_invite_code contains the current invite code,
	 * and _settings_client.network.server_invite_code contains the one we will
	 * attempt to re-use when registering again. */
	_network_server_invite_code = _settings_client.network.server_invite_code;

	SetWindowDirty(WC_CLIENT_LIST, 0);

	if (_network_dedicated) {
		std::string connection_type;
		switch (_network_server_connection_type) {
			case CONNECTION_TYPE_ISOLATED: connection_type = "Remote players can't connect"; break;
			case CONNECTION_TYPE_DIRECT:   connection_type = "Public"; break;
			case CONNECTION_TYPE_STUN:     connection_type = "Behind NAT"; break;
			case CONNECTION_TYPE_TURN:     connection_type = "Via relay"; break;

			case CONNECTION_TYPE_UNKNOWN: // Never returned from Game Coordinator.
			default: connection_type = "Unknown"; break; // Should never happen, but don't fail if it does.
		}

		std::string game_type;
		switch (_settings_client.network.server_game_type) {
			case SERVER_GAME_TYPE_INVITE_ONLY: game_type = "Invite only"; break;
			case SERVER_GAME_TYPE_PUBLIC: game_type = "Public"; break;

			case SERVER_GAME_TYPE_LOCAL: // Impossible to register local servers.
			default: game_type = "Unknown"; break; // Should never happen, but don't fail if it does.
		}

		Debug(net, 3, "----------------------------------------");
		Debug(net, 3, "Your server is now registered with the Game Coordinator:");
		Debug(net, 3, "  Game type:       {}", game_type);
		Debug(net, 3, "  Connection type: {}", connection_type);
		Debug(net, 3, "  Invite code:     {}", _network_server_invite_code);
		Debug(net, 3, "----------------------------------------");
	} else {
		Debug(net, 3, "Game Coordinator registered our server with invite code '{}'", _network_server_invite_code);
	}

	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_LISTING(Packet &p)
{
	uint8_t servers = p.Recv_uint16();
	Debug(net, 9, "Coordinator::Receive_GC_LISTING({})", servers);

	/* End of list; we can now remove all expired items from the list. */
	if (servers == 0) {
		NetworkGameListRemoveExpired();
		return true;
	}

	for (; servers > 0; servers--) {
		std::string connection_string = p.Recv_string(NETWORK_HOSTNAME_PORT_LENGTH);

		/* Now we know the connection string, we can add it to our list. */
		NetworkGame *item = NetworkGameListAddItem(connection_string);

		/* Clear any existing GRFConfig chain. */
		ClearGRFConfigList(item->info.grfconfig);
		/* Read the NetworkGameInfo from the packet. */
		DeserializeNetworkGameInfo(p, item->info, &this->newgrf_lookup_table);
		/* Check for compatibility with the client. */
		CheckGameCompatibility(item->info);
		/* Mark server as online. */
		item->status = NGLS_ONLINE;
		/* Mark the item as up-to-date. */
		item->version = _network_game_list_version;
	}

	UpdateNetworkGameWindow();
	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_CONNECTING(Packet &p)
{
	std::string token = p.Recv_string(NETWORK_TOKEN_LENGTH);
	std::string invite_code = p.Recv_string(NETWORK_INVITE_CODE_LENGTH);
	Debug(net, 9, "Coordinator::Receive_GC_CONNECTING({}, {})", token, invite_code);

	/* Find the connecter based on the invite code. */
	auto connecter_pre_it = this->connecter_pre.find(invite_code);
	if (connecter_pre_it == this->connecter_pre.end()) {
		this->CloseConnection();
		return false;
	}

	/* Now store it based on the token. */
	this->connecter[token] = {invite_code, connecter_pre_it->second};
	this->connecter_pre.erase(connecter_pre_it);

	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_CONNECT_FAILED(Packet &p)
{
	std::string token = p.Recv_string(NETWORK_TOKEN_LENGTH);
	Debug(net, 9, "Coordinator::Receive_GC_CONNECT_FAILED({})", token);
	this->CloseToken(token);

	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_DIRECT_CONNECT(Packet &p)
{
	std::string token = p.Recv_string(NETWORK_TOKEN_LENGTH);
	uint8_t tracking_number = p.Recv_uint8();
	std::string hostname = p.Recv_string(NETWORK_HOSTNAME_LENGTH);
	uint16_t port = p.Recv_uint16();
	Debug(net, 9, "Coordinator::Receive_GC_DIRECT_CONNECT({}, {}, {}, {})", token, tracking_number, hostname, port);

	/* Ensure all other pending connection attempts are killed. */
	if (this->game_connecter != nullptr) {
		this->game_connecter->Kill();
		this->game_connecter = nullptr;
	}

	this->game_connecter = TCPConnecter::Create<NetworkDirectConnecter>(hostname, port, std::move(token), tracking_number);
	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_STUN_REQUEST(Packet &p)
{
	std::string token = p.Recv_string(NETWORK_TOKEN_LENGTH);
	Debug(net, 9, "Coordinator::Receive_GC_STUN_REQUEST({})", token);

	this->stun_handlers[token][AF_INET6] = ClientNetworkStunSocketHandler::Stun(token, AF_INET6);
	this->stun_handlers[token][AF_INET] = ClientNetworkStunSocketHandler::Stun(token, AF_INET);
	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_STUN_CONNECT(Packet &p)
{
	std::string token = p.Recv_string(NETWORK_TOKEN_LENGTH);
	uint8_t tracking_number = p.Recv_uint8();
	uint8_t family = p.Recv_uint8();
	std::string host = p.Recv_string(NETWORK_HOSTNAME_PORT_LENGTH);
	uint16_t port = p.Recv_uint16();

	/* Check if we know this token. */
	auto stun_it = this->stun_handlers.find(token);
	if (stun_it == this->stun_handlers.end()) return true;
	auto family_it = stun_it->second.find(family);
	if (family_it == stun_it->second.end()) return true;

	/* Ensure all other pending connection attempts are killed. */
	if (this->game_connecter != nullptr) {
		this->game_connecter->Kill();
		this->game_connecter = nullptr;
	}

	/* We now mark the connection as closed, but we do not really close the
	 * socket yet. We do this when the NetworkReuseStunConnecter is connected.
	 * This prevents any NAT to already remove the route while we create the
	 * second connection on top of the first. */
	family_it->second->CloseConnection(false);

	/* Connect to our peer from the same local address as we use for the
	 * STUN server. This means that if there is any NAT in the local network,
	 * the public ip:port is still pointing to the local address, and as such
	 * a connection can be established. */
	this->game_connecter = TCPConnecter::Create<NetworkReuseStunConnecter>(host, port, family_it->second->local_addr, std::move(token), tracking_number, family);
	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_NEWGRF_LOOKUP(Packet &p)
{
	this->newgrf_lookup_table_cursor = p.Recv_uint32();

	uint16_t newgrfs = p.Recv_uint16();
	Debug(net, 9, "Coordinator::Receive_GC_NEWGRF_LOOKUP({}, {})", this->newgrf_lookup_table_cursor, newgrfs);
	for (; newgrfs> 0; newgrfs--) {
		uint32_t index = p.Recv_uint32();
		DeserializeGRFIdentifierWithName(p, this->newgrf_lookup_table[index]);
	}
	return true;
}

bool ClientNetworkCoordinatorSocketHandler::Receive_GC_TURN_CONNECT(Packet &p)
{
	std::string token = p.Recv_string(NETWORK_TOKEN_LENGTH);
	uint8_t tracking_number = p.Recv_uint8();
	std::string ticket = p.Recv_string(NETWORK_TOKEN_LENGTH);
	std::string connection_string = p.Recv_string(NETWORK_HOSTNAME_PORT_LENGTH);
	Debug(net, 9, "Coordinator::Receive_GC_TURN_CONNECT({}, {}, {}, {})", token, tracking_number, ticket, connection_string);

	/* Ensure all other pending connection attempts are killed. */
	if (this->game_connecter != nullptr) {
		this->game_connecter->Kill();
		this->game_connecter = nullptr;
	}

	this->turn_handlers[token] = ClientNetworkTurnSocketHandler::Turn(token, tracking_number, ticket, connection_string);

	if (!_network_server) {
		auto connecter_it = this->connecter.find(token);
		if (connecter_it == this->connecter.end()) {
			/* Make sure we are still interested in connecting to this server. */
			this->ConnectFailure(token, 0);
			return true;
		}

		switch (_settings_client.network.use_relay_service) {
			case URS_NEVER:
				this->ConnectFailure(token, 0);
				break;

			case URS_ASK:
				ShowNetworkAskRelay(connecter_it->second.first, std::move(connection_string), std::move(token));
				break;

			case URS_ALLOW:
				this->StartTurnConnection(token);
				break;
		}
	} else {
		this->StartTurnConnection(token);
	}

	return true;
}

void ClientNetworkCoordinatorSocketHandler::StartTurnConnection(std::string_view token)
{
	auto turn_it = this->turn_handlers.find(token);
	if (turn_it == this->turn_handlers.end()) return;

	turn_it->second->Connect();
}

void ClientNetworkCoordinatorSocketHandler::Connect()
{
	/* We are either already connected or are trying to connect. */
	if (this->sock != INVALID_SOCKET || this->connecting) return;

	this->Reopen();

	this->connecting = true;
	this->last_activity = std::chrono::steady_clock::now();

	TCPConnecter::Create<NetworkCoordinatorConnecter>(NetworkCoordinatorConnectionString());
}

NetworkRecvStatus ClientNetworkCoordinatorSocketHandler::CloseConnection(bool error)
{
	NetworkCoordinatorSocketHandler::CloseConnection(error);

	this->CloseSocket();
	this->connecting = false;

	_network_server_connection_type = CONNECTION_TYPE_UNKNOWN;
	this->next_update = {};

	this->CloseAllConnections();

	SetWindowDirty(WC_CLIENT_LIST, 0);

	return NETWORK_RECV_STATUS_OKAY;
}

/**
 * Register our server to receive our invite code.
 */
void ClientNetworkCoordinatorSocketHandler::Register()
{
	_network_server_connection_type = CONNECTION_TYPE_UNKNOWN;
	this->next_update = {};

	SetWindowDirty(WC_CLIENT_LIST, 0);

	this->Connect();

	auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_SERVER_REGISTER);
	p->Send_uint8(NETWORK_COORDINATOR_VERSION);
	p->Send_uint8(_settings_client.network.server_game_type);
	p->Send_uint16(_settings_client.network.server_port);
	if (_settings_client.network.server_invite_code.empty() || _settings_client.network.server_invite_code_secret.empty()) {
		p->Send_string("");
		p->Send_string("");
	} else {
		p->Send_string(_settings_client.network.server_invite_code);
		p->Send_string(_settings_client.network.server_invite_code_secret);
	}

	Debug(net, 9, "Coordinator::SendRegister({}, {}, {})", _settings_client.network.server_game_type, _settings_client.network.server_port, _settings_client.network.server_invite_code);
	this->SendPacket(std::move(p));
}

/**
 * Send an update of our server status to the Game Coordinator.
 */
void ClientNetworkCoordinatorSocketHandler::SendServerUpdate()
{
	Debug(net, 6, "Sending server update to Game Coordinator");

	auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_SERVER_UPDATE, TCP_MTU);
	p->Send_uint8(NETWORK_COORDINATOR_VERSION);
	SerializeNetworkGameInfo(*p, GetCurrentNetworkServerGameInfo(), this->next_update.time_since_epoch() != std::chrono::nanoseconds::zero());

	Debug(net, 9, "Coordinator::SendServerUpdate()");
	this->SendPacket(std::move(p));

	this->next_update = std::chrono::steady_clock::now() + NETWORK_COORDINATOR_DELAY_BETWEEN_UPDATES;
}

/**
 * Request a listing of all public servers.
 */
void ClientNetworkCoordinatorSocketHandler::GetListing()
{
	this->Connect();

	_network_game_list_version++;

	auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_CLIENT_LISTING);
	p->Send_uint8(NETWORK_COORDINATOR_VERSION);
	p->Send_uint8(NETWORK_GAME_INFO_VERSION);
	p->Send_string(_openttd_revision);
	p->Send_uint32(this->newgrf_lookup_table_cursor);

	Debug(net, 9, "Coordinator::SendGetListing({}, {})", _openttd_revision, this->newgrf_lookup_table_cursor);
	this->SendPacket(std::move(p));
}

/**
 * Join a server based on an invite code.
 * @param invite_code The invite code of the server to connect to.
 * @param connecter The connecter of the request.
 */
void ClientNetworkCoordinatorSocketHandler::ConnectToServer(std::string_view invite_code, TCPServerConnecter *connecter)
{
	assert(invite_code.starts_with("+"));

	if (this->connecter_pre.find(invite_code) != this->connecter_pre.end()) {
		/* If someone is hammering the refresh key, one can sent out two
		 * requests for the same invite code. There isn't really a great way
		 * of handling this, so just ignore this request. */
		connecter->SetFailure();
		return;
	}

	/* Initially we store based on invite code; on first reply we know the
	 * token, and will start using that key instead. */
	this->connecter_pre[std::string(invite_code)] = connecter;

	this->Connect();

	auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_CLIENT_CONNECT);
	p->Send_uint8(NETWORK_COORDINATOR_VERSION);
	p->Send_string(invite_code);

	Debug(net, 9, "Coordinator::SendConnectToClient({})", invite_code);
	this->SendPacket(std::move(p));
}

/**
 * Callback from a Connecter to let the Game Coordinator know the connection failed.
 * @param token Token of the connecter that failed.
 * @param tracking_number Tracking number of the connecter that failed.
 */
void ClientNetworkCoordinatorSocketHandler::ConnectFailure(std::string_view token, uint8_t tracking_number)
{
	/* Connecter will destroy itself. */
	this->game_connecter = nullptr;

	auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_SERCLI_CONNECT_FAILED);
	p->Send_uint8(NETWORK_COORDINATOR_VERSION);
	p->Send_string(token);
	p->Send_uint8(tracking_number);

	Debug(net, 9, "Coordinator::SendConnectFailure({}, {})", token, tracking_number);
	this->SendPacket(std::move(p));

	/* We do not close the associated connecter here yet, as the
	 * Game Coordinator might have other methods of connecting available. */
}

/**
 * Callback from a Connecter to let the Game Coordinator know the connection
 * to the game server is established.
 * @param token Token of the connecter that succeeded.
 * @param sock The socket that the connecter can now use.
 */
void ClientNetworkCoordinatorSocketHandler::ConnectSuccess(std::string_view token, SOCKET sock, NetworkAddress &address)
{
	assert(sock != INVALID_SOCKET);

	/* Connecter will destroy itself. */
	this->game_connecter = nullptr;

	if (_network_server) {
		if (!ServerNetworkGameSocketHandler::ValidateClient(sock, address)) return;
		Debug(net, 3, "[{}] Client connected from {} on frame {}", ServerNetworkGameSocketHandler::GetName(), address.GetHostname(), _frame_counter);
		ServerNetworkGameSocketHandler::AcceptConnection(sock, address);
	} else {
		/* The client informs the Game Coordinator about the success. The server
		 * doesn't have to, as it is implied by the client telling. */
		auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_CLIENT_CONNECTED);
		p->Send_uint8(NETWORK_COORDINATOR_VERSION);
		p->Send_string(token);
		Debug(net, 9, "Coordinator::SendConnectSuccess({})", token);
		this->SendPacket(std::move(p));

		/* Find the connecter; it can happen it no longer exist, in cases where
		 * we aborted the connect but the Game Coordinator was already in the
		 * processes of connecting us. */
		auto connecter_it = this->connecter.find(token);
		if (connecter_it != this->connecter.end()) {
			connecter_it->second.second->SetConnected(sock);
			this->connecter.erase(connecter_it);
		}
	}

	/* Close all remaining connections. */
	this->CloseToken(token);
}

/**
 * Callback from the STUN connecter to inform the Game Coordinator about the
 * result of the STUN.
 *
 * This helps the Game Coordinator not to wait for a timeout on its end, but
 * rather react as soon as the client/server knows the result.
 */
void ClientNetworkCoordinatorSocketHandler::StunResult(std::string_view token, uint8_t family, bool result)
{
	auto p = std::make_unique<Packet>(this, PACKET_COORDINATOR_SERCLI_STUN_RESULT);
	p->Send_uint8(NETWORK_COORDINATOR_VERSION);
	p->Send_string(token);
	p->Send_uint8(family);
	p->Send_bool(result);
	Debug(net, 9, "Coordinator::SendStunResult({}, {}, {})", token, family, result);
	this->SendPacket(std::move(p));
}

/**
 * Close the STUN handler.
 * @param token The token used for the STUN handlers.
 * @param family The family of STUN handlers to close. AF_UNSPEC to close all STUN handlers for this token.
 */
void ClientNetworkCoordinatorSocketHandler::CloseStunHandler(std::string_view token, uint8_t family)
{
	auto stun_it = this->stun_handlers.find(token);
	if (stun_it == this->stun_handlers.end()) return;

	if (family == AF_UNSPEC) {
		for (auto &[family, stun_handler] : stun_it->second) {
			stun_handler->CloseConnection();
			stun_handler->CloseSocket();
		}

		this->stun_handlers.erase(stun_it);
	} else {
		auto family_it = stun_it->second.find(family);
		if (family_it == stun_it->second.end()) return;

		family_it->second->CloseConnection();
		family_it->second->CloseSocket();

		stun_it->second.erase(family_it);
	}
}

/**
 * Close the TURN handler.
 * @param token The token used for the TURN handler.
 */
void ClientNetworkCoordinatorSocketHandler::CloseTurnHandler(std::string_view token)
{
	CloseWindowByClass(WC_NETWORK_ASK_RELAY, NRWCD_HANDLED);

	auto turn_it = this->turn_handlers.find(token);
	if (turn_it == this->turn_handlers.end()) return;

	turn_it->second->CloseConnection();
	turn_it->second->CloseSocket();

	/* We don't remove turn_handler here, as we can be called from within that
	 * turn_handler instance, so our object cannot be freed yet. Instead, we
	 * check later if the connection is closed, and free the object then. */
}

/**
 * Close everything related to this connection token.
 * @param token The connection token to close.
 */
void ClientNetworkCoordinatorSocketHandler::CloseToken(std::string_view token)
{
	/* Close all remaining STUN / TURN connections. */
	this->CloseStunHandler(token);
	this->CloseTurnHandler(token);

	/* Close the caller of the connection attempt. */
	auto connecter_it = this->connecter.find(token);
	if (connecter_it != this->connecter.end()) {
		connecter_it->second.second->SetFailure();
		this->connecter.erase(connecter_it);
	}
}

/**
 * Close all pending connection tokens.
 */
void ClientNetworkCoordinatorSocketHandler::CloseAllConnections()
{
	/* Ensure all other pending connection attempts are also killed. */
	if (this->game_connecter != nullptr) {
		this->game_connecter->Kill();
		this->game_connecter = nullptr;
	}

	/* Mark any pending connecters as failed. */
	for (auto &[token, it] : this->connecter) {
		this->CloseStunHandler(token);
		this->CloseTurnHandler(token);
		it.second->SetFailure();

		/* Inform the Game Coordinator he can stop trying to connect us to the server. */
		this->ConnectFailure(token, 0);
	}
	this->stun_handlers.clear();
	this->turn_handlers.clear();
	this->connecter.clear();

	/* Also close any pending invite-code requests. */
	for (auto &[invite_code, it] : this->connecter_pre) {
		it->SetFailure();
	}
	this->connecter_pre.clear();
}

/**
 * Check whether we received/can send some data from/to the Game Coordinator server and
 * when that's the case handle it appropriately.
 */
void ClientNetworkCoordinatorSocketHandler::SendReceive()
{
	/* Private games are not listed via the Game Coordinator. */
	if (_network_server && _settings_client.network.server_game_type == SERVER_GAME_TYPE_LOCAL) {
		if (this->sock != INVALID_SOCKET) {
			this->CloseConnection();
		}
		return;
	}

	static int last_attempt_backoff = 1;
	static bool first_reconnect = true;

	if (this->sock == INVALID_SOCKET) {
		static std::chrono::steady_clock::time_point last_attempt = {};

		/* Don't auto-reconnect when we are not a server. */
		if (!_network_server) return;
		/* Don't reconnect if we are connecting. */
		if (this->connecting) return;
		/* Throttle how often we try to reconnect. */
		if (std::chrono::steady_clock::now() < last_attempt + std::chrono::seconds(1) * last_attempt_backoff) return;

		last_attempt = std::chrono::steady_clock::now();
		/* Delay reconnecting with up to 32 seconds. */
		if (last_attempt_backoff < 32) {
			last_attempt_backoff *= 2;
		}

		/* Do not reconnect on the first attempt, but only initialize the
		 * last_attempt variables.  Otherwise after an outage all servers
		 * reconnect at the same time, potentially overwhelming the
		 * Game Coordinator. */
		if (first_reconnect) {
			first_reconnect = false;
			return;
		}

		Debug(net, 1, "Connection with Game Coordinator lost; reconnecting...");
		this->Register();
		return;
	}

	last_attempt_backoff = 1;
	first_reconnect = true;

	if (_network_server && _network_server_connection_type != CONNECTION_TYPE_UNKNOWN && std::chrono::steady_clock::now() > this->next_update) {
		this->SendServerUpdate();
	}

	if (!_network_server && std::chrono::steady_clock::now() > this->last_activity + IDLE_TIMEOUT) {
		this->CloseConnection();
		return;
	}

	if (this->CanSendReceive()) {
		if (this->ReceivePackets()) {
			this->last_activity = std::chrono::steady_clock::now();
		}
	}

	this->SendPackets();

	for (const auto &[token, families] : this->stun_handlers) {
		for (const auto &[family, stun_handler] : families) {
			stun_handler->SendReceive();
		}
	}

	/* Check for handlers that are not connecting nor connected. Destroy those objects. */
	for (auto turn_it = this->turn_handlers.begin(); turn_it != this->turn_handlers.end(); /* nothing */) {
		if (turn_it->second->connect_started && turn_it->second->connecter == nullptr && !turn_it->second->IsConnected()) {
			turn_it = this->turn_handlers.erase(turn_it);
		} else {
			turn_it++;
		}
	}

	for (const auto &[token, turn_handler] : this->turn_handlers) {
		turn_handler->SendReceive();
	}
}
