/*
 * 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 tcp_listen.h Basic functions to listen for TCP connections. */

#ifndef NETWORK_CORE_TCP_LISTEN_H
#define NETWORK_CORE_TCP_LISTEN_H

#include "tcp.h"
#include "../network.h"
#include "../network_func.h"
#include "../network_internal.h"
#include "../../debug.h"

#include "table/strings.h"

/**
 * Template for TCP listeners.
 * @param Tsocket The class we create sockets for.
 * @param EnumPacketType The enumeration type of the packet type.
 * @param Tfull_packet The packet type to return when we don't allow more sockets.
 * @param Tban_packet The packet type to return when the client is banned.
 */
template <class Tsocket, typename EnumPacketType, EnumPacketType Tfull_packet, EnumPacketType Tban_packet>
class TCPListenHandler {
	/** List of sockets we listen on. */
	static SocketList sockets;

public:
	/**
	 * Check whether this client is allowed to connect, by both the banlist and maximum client count.
	 * @param s The socket/network connection to the client.
	 * @param address The address of the client.
	 * @return \c true iff the client is allowed in.
	 */
	static bool ValidateClient(SOCKET s, NetworkAddress &address)
	{
		/* Check if the client is banned. */
		for (const auto &entry : _network_ban_list) {
			if (address.IsInNetmask(entry)) {
				Packet p(nullptr, Tban_packet);
				p.PrepareToSend();

				Debug(net, 2, "[{}] Banned ip tried to join ({}), refused", Tsocket::GetName(), entry);

				if (p.TransferOut(SocketSender{s}) < 0) {
					Debug(net, 0, "[{}] send failed: {}", Tsocket::GetName(), NetworkError::GetLast().AsString());
				}
				closesocket(s);
				return false;
			}
		}

		/* Can we handle a new client? */
		if (!Tsocket::AllowConnection()) {
			/* No more clients allowed?
			 * Send to the client that we are full! */
			Packet p(nullptr, Tfull_packet);
			p.PrepareToSend();

			if (p.TransferOut(SocketSender{s}) < 0) {
				Debug(net, 0, "[{}] send failed: {}", Tsocket::GetName(), NetworkError::GetLast().AsString());
			}
			closesocket(s);

			return false;
		}

		return true;
	}

	/**
	 * Accepts clients from the sockets.
	 * @param ls Socket to accept clients from.
	 */
	static void AcceptClient(SOCKET ls)
	{
		for (;;) {
			struct sockaddr_storage sin{};
			socklen_t sin_len = sizeof(sin);
			SOCKET s = accept(ls, (struct sockaddr*)&sin, &sin_len);
			if (s == INVALID_SOCKET) return;
#ifdef __EMSCRIPTEN__
			sin_len = FixAddrLenForEmscripten(sin);
#endif

			SetNonBlocking(s); // XXX error handling?

			NetworkAddress address(sin, sin_len);
			Debug(net, 3, "[{}] Client connected from {} on frame {}", Tsocket::GetName(), address.GetHostname(), _frame_counter);

			SetNoDelay(s); // XXX error handling?

			if (!Tsocket::ValidateClient(s, address)) continue;
			Tsocket::AcceptConnection(s, address);
		}
	}

	/**
	 * Handle the receiving of packets.
	 * @return true if everything went okay.
	 */
	static bool Receive()
	{
		fd_set read_fd, write_fd;
		struct timeval tv;

		FD_ZERO(&read_fd);
		FD_ZERO(&write_fd);


		for (Tsocket *cs : Tsocket::Iterate()) {
			FD_SET(cs->sock, &read_fd);
			FD_SET(cs->sock, &write_fd);
		}

		/* take care of listener port */
		for (auto &s : sockets) {
			FD_SET(s.first, &read_fd);
		}

		tv.tv_sec = tv.tv_usec = 0; // don't block at all.
		if (select(FD_SETSIZE, &read_fd, &write_fd, nullptr, &tv) < 0) return false;

		/* accept clients.. */
		for (auto &s : sockets) {
			if (FD_ISSET(s.first, &read_fd)) AcceptClient(s.first);
		}

		/* read stuff from clients */
		for (Tsocket *cs : Tsocket::Iterate()) {
			cs->writable = !!FD_ISSET(cs->sock, &write_fd);
			if (FD_ISSET(cs->sock, &read_fd)) {
				cs->ReceivePackets();
			}
		}
		return _networking;
	}

	/**
	 * Listen on a particular port.
	 * @param port The port to listen on.
	 * @return true if listening succeeded.
	 */
	static bool Listen(uint16_t port)
	{
		assert(sockets.empty());

		NetworkAddressList addresses;
		GetBindAddresses(&addresses, port);

		for (NetworkAddress &address : addresses) {
			address.Listen(SOCK_STREAM, &sockets);
		}

		if (sockets.empty()) {
			Debug(net, 0, "Could not start network: could not create listening socket");
			ShowNetworkError(STR_NETWORK_ERROR_SERVER_START);
			return false;
		}

		return true;
	}

	/** Close the sockets we're listening on. */
	static void CloseListeners()
	{
		for (auto &s : sockets) {
			closesocket(s.first);
		}
		sockets.clear();
		Debug(net, 5, "[{}] Closed listeners", Tsocket::GetName());
	}
};

/** Instantiate the sockets. */
template <class Tsocket, typename EnumPacketType, EnumPacketType Tfull_packet, EnumPacketType Tban_packet> SocketList TCPListenHandler<Tsocket, EnumPacketType, Tfull_packet, Tban_packet>::sockets;

#endif /* NETWORK_CORE_TCP_LISTEN_H */
