/**
 * Number of milliseconds to display network warnings.
 */
var g_NetworkWarningTimeout = 3000;

/**
 * Currently displayed network warnings. At most one message per user.
 */
var g_NetworkWarnings = {};

/**
 * Message-types to be displayed.
 */
var g_NetworkWarningTexts = {

	"server-timeout": (msg, username) =>
		sprintf(translate("Losing connection to server (%(seconds)ss)"), {
			"seconds": Math.ceil(msg.lastReceivedTime / 1000)
		}),

	"client-timeout": (msg, username) =>
		sprintf(translate("%(player)s losing connection (%(seconds)ss)"), {
			"player": username,
			"seconds": Math.ceil(msg.lastReceivedTime / 1000)
		}),

	"server-latency": (msg, username) =>
		sprintf(translate("Bad connection to server (%(milliseconds)sms)"), {
			"milliseconds": msg.meanRTT
		}),

	"client-latency": (msg, username) =>
		sprintf(translate("Bad connection to %(player)s (%(milliseconds)sms)"), {
			"player": username,
			"milliseconds": msg.meanRTT
		})
};

var g_NetworkCommands = {
	"/kick": argument => kickPlayer(argument, false),
	"/ban": argument => kickPlayer(argument, true),
	"/kickspecs": argument => kickObservers(false),
	"/banspecs": argument => kickObservers(true),
	"/list": argument => addChatMessage({ "type": "clientlist" }),
	"/clear": argument => clearChatMessages()
};

var g_ValidPorts = { "min": 1024, "max": 65535 };
// TODO: This check should be performed exclusively on the C++ side, currently this is sort of redundant.
function getValidPort(port)
{
	if (isNaN(+port) || +port < g_ValidPorts.min || +port > g_ValidPorts.max)
		return Engine.GetDefaultPort();

	return +port;
}

/**
 * Must be kept in sync with source/network/NetHost.h
 */
function getDisconnectReason(id)
{
	switch (id)
	{
	case 0: return translate("An unknown error occurred.");
	case 1: return translate("The connection request has timed out.");
	case 2: return translate("The host has ended the game.");
	case 3: return translate("Incorrect network protocol version.");
	case 4: return translate("Game is loading, please try again later.");
	case 5: return translate("Game has already started, no observers allowed.");
	case 6: return translate("You have been kicked.");
	case 7: return translate("You have been banned.");
	case 8: return translate("Player name in use. If you were disconnected, retry in few seconds.");
	case 9: return translate("Server full.");
	case 10: return translate("Secure lobby authentication failed. Join via lobby.");
	case 11: return translate("Error: Server failed to allocate a unique client identifier.");
	case 12: return translate("Error: Client commands were ready for an unexpected game turn.");
	case 13: return translate("Error: Client simulated an unexpected game turn.");
	case 14: return translate("Password is invalid.");
	case 15: return translate("Could not find an unused port for the enet STUN client.");
	case 16: return translate("Could not find the STUN endpoint.");
	case 17: return translate("Different game engine versions or different mods loaded.");

	default:
		warn("Unknown disconnect-reason ID received: " + id);
		return sprintf(translate("\\[Invalid value %(id)s]"), { "id": id });
	}
}

function getRequestTimeOutMessage()
{
	return (Engine.HasXmppClient() ? "" : translate("Please ensure that you entered the correct server name or IP address, and port number.\n\n")) +
		translate("If you haven't yet, try to connect again and to different hosts." +
		"\nIf the issue persists or occurs regularly, visit the official FAQ for detailed guidance and troubleshooting steps.");
}

function getMismatchMessage(mismatchType, clientMismatchInfo, serverMismatchInfo)
{
	switch (mismatchType)
	{
	case "engine": return sprintf(translate("Different engine versions detected client version: %(clientMismatch)s server version: %(serverMismatch)s"), {
		"clientMismatch": clientMismatchInfo,
		"serverMismatch": serverMismatchInfo
	});
	case "mod": return sprintf(translate("Different mods enabled, or mods enabled in a different order. Client mod: '%(clientMismatch)s' Server mod: '%(serverMismatch)s'"), {
		"clientMismatch": clientMismatchInfo,
		"serverMismatch": serverMismatchInfo
	});
	default: return sprintf(translate("Unrecognized handshake mismatch type: %(mismatchType)s"), {
		"mismatchType": mismatchType
	});
	}
}

/**
 * Show the disconnect reason in a message box.
 *
 * @param {Object} message
 * @param {boolean} wasConnected
 */
function reportDisconnect(message, wasConnected)
{
	if (message.reason === 0)
		reportConnectionRequestTimeOut();
	else if (message.reason == 16)
		reportMismatchingSoftwareVersions(message.mismatch_type, message.client_mismatch, message.server_mismatch);
	else
		messageBox(
			400, 200,
			(wasConnected ?
				translate("Lost connection to the server.") :
				translate("Failed to connect to the server.")
			) + "\n\n" + getDisconnectReason(message.reason),
			translate("Disconnected")
		);
}

async function reportConnectionRequestTimeOut()
{
	const buttonIndex = await messageBox(
		600, 230,
		translate("Failed to connect to the server.") + " " + getDisconnectReason(1) + "\n\n" + getRequestTimeOutMessage(),
		translate("Connection Error"),
		[translate("Close"), translate("Open FAQ")]
	);
	if (buttonIndex === 1)
		openURL("https://gitea.wildfiregames.com/0ad/0ad/wiki/FAQ#what-shall-i-do-when-joining-multiplayer-matches-fails-with-an-error-message");

	return;
}

function reportMismatchingSoftwareVersions(mismatchType, clientMismatch, serverMismatch)
{
	messageBox(
		400, 200,
		translate("Failed to connect to the server.") +
		"\n\n" + getMismatchMessage(mismatchType, clientMismatch, serverMismatch),
		translate("Disconnected")
	);
}

function kickError()
{
	addChatMessage({
		"type": "system",
		"text": translate("Only the host can kick clients!")
	});
}

function kickPlayer(username, ban)
{
	if (!g_IsController)
	{
		kickError();
		return;
	}
	if (!g_IsNetworked)
	{
		addChatMessage({
			"type": "system",
			"text": translate("Offline game! Can not kick or ban.")
		});
		return;
	}
	Engine.KickPlayer(username, ban);
}

function kickObservers(ban)
{
	if (!g_IsController)
	{
		kickError();
		return;
	}
	if (!g_IsNetworked)
	{
		addChatMessage({
			"type": "system",
			"text": translate("Offline game! No spectators to kick or ban.")
		});
		return;
	}
	for (const guid in g_PlayerAssignments)
		if (g_PlayerAssignments[guid].player == -1)
			Engine.KickPlayer(g_PlayerAssignments[guid].name, ban);
}

/**
 * Sort GUIDs of connected users sorted by playerindex, observers last.
 * Requires g_PlayerAssignments.
 */
function sortGUIDsByPlayerID()
{
	return Object.keys(g_PlayerAssignments).sort((guidA, guidB) => {

		const playerIdA = g_PlayerAssignments[guidA].player;
		const playerIdB = g_PlayerAssignments[guidB].player;

		if (playerIdA == -1) return +1;
		if (playerIdB == -1) return -1;

		return playerIdA - playerIdB;
	});
}

/**
 * Get a colorized list of usernames sorted by player slot, observers last.
 * Requires g_PlayerAssignments and colorizePlayernameByGUID.
 *
 * @returns {string}
 */
function getUsernameList()
{
	const usernames = sortGUIDsByPlayerID().map(guid => colorizePlayernameByGUID(guid));

	// Translation: Number of currently connected players/observers and their names
	return sprintf(translate("Users (%(num)s): %(users)s"), {
		"users": usernames.join(translate(", ")),
		"num": usernames.length
	});
}

/**
 * Execute a command locally. Requires addChatMessage.
 *
 * @param {string} input
 * @returns {boolean} whether a command was executed
 */
function executeNetworkCommand(input)
{
	if (input.indexOf("/") != 0)
		return false;

	const command = input.split(" ", 1)[0];
	const argument = input.substr(command.length + 1);

	if (g_NetworkCommands[command])
		g_NetworkCommands[command](argument);

	return !!g_NetworkCommands[command];
}

/**
 * Remember this warning for a few seconds.
 * Overwrite previous warnings for this user.
 *
 * @param msg - GUI message sent by NetServer or NetClient
 */
function addNetworkWarning(msg)
{
	if (!g_NetworkWarningTexts[msg.warntype])
	{
		warn("Unknown network warning type received: " + uneval(msg));
		return;
	}

	if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") != "true")
		return;

	g_NetworkWarnings[msg.guid || "server"] = {
		"added": Date.now(),
		"msg": msg
	};
}

/**
 * Colorizes and concatenates all network warnings.
 * Returns text and textWidth.
 */
function getNetworkWarnings()
{
	// Remove outdated messages
	for (const guid in g_NetworkWarnings)
		if (Date.now() > g_NetworkWarnings[guid].added + g_NetworkWarningTimeout ||
		    guid != "server" && !g_PlayerAssignments[guid])
			delete g_NetworkWarnings[guid];

	// Show local messages first
	const guids = Object.keys(g_NetworkWarnings).sort(guid => guid != "server");

	const messages = [];

	for (const guid of guids)
	{
		const msg = g_NetworkWarnings[guid].msg;

		// Add formatted text
		messages.push(g_NetworkWarningTexts[msg.warntype](msg, colorizePlayernameByGUID(guid)));
	}

	return messages;
}
