#include "mungal4.h"
#pragma hdrstop

#include "l4nettransport.h"
#include <Ws2tcpip.h>

//########################################################################
// WinsockNetTransport - the TCP wire the arcade always used, moved
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
// notes preserved from the original:
//  - Connect retries while the remote refuses (WSAECONNREFUSED): the
//    console feeds eggs in [pilots] order with ACKs, so earlier pods
//    are listening before later pods open - the retry covers the race.
//  - Sockets go nonblocking once connected; listeners are nonblocking
//    from creation.
//########################################################################

namespace
{
	class WinsockNetTransport:
		public NetTransport
	{
	public:
		WinsockNetTransport():
			started(False)
		{
		}

		Logical
			Startup()
		{
			if (started)
			{
				return True;
			}
			int result = WSAStartup(MAKEWORD(2, 2), &winsockData);
			if (result != NO_ERROR)
			{
				DEBUG_STREAM << "ERROR: WSAStartup() failed with " << result
					<< "!\n" << std::flush;
				return False;
			}
			started = True;
			return True;
		}

		void
			Cleanup()
		{
			if (started)
			{
				WSACleanup();
				started = False;
			}
		}

		int
			GetLocalAddresses(
				unsigned long *addresses,
				int max_count
			)
		{
			char name[255];
			PHOSTENT hostinfo;

			if (gethostname(name, sizeof(name)) != 0)
			{
				DEBUG_STREAM << "ERROR: gethostname() failed!" << std::endl << std::flush;
				return 0;
			}
			if ((hostinfo = gethostbyname(name)) == NULL)
			{
				DEBUG_STREAM << "ERROR: gethostbyname() failed!" << std::endl << std::flush;
				return 0;
			}

			int count = 0;
			for (int i = 0; hostinfo->h_addr_list[i] != NULL && count < max_count; ++i)
			{
				addresses[count++] = *((unsigned long *) hostinfo->h_addr_list[i]);
			}

			// loopback rounds out the list (single-machine testing)
			if (count < max_count)
			{
				addresses[count++] = 0x0100007F;
			}
			return count;
		}

		Logical
			Resolve(
				const char *host_name,
				SOCKADDR_IN *address
			)
		{
			// numeric ip[:port] only - the egg carries addresses, not
			// names; port stays 0 when absent (caller applies default)
			int buffer_size = sizeof(SOCKADDR_IN);
			return WSAStringToAddressA(
				(LPSTR) host_name, AF_INET, NULL,
				(LPSOCKADDR) address, &buffer_size) == 0;
		}

		Connection
			Connect(
				const SOCKADDR_IN *remote,
				int local_port
			)
		{
			DEBUG_STREAM << "Opening connection to "
				<< inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port)
				<< "...\n" << std::flush;

			//
			// Retry-while-refused, bounded: the egg-ACK ordering means
			// the peer may not be listening yet. A refused TCP socket
			// is dead - every attempt needs a fresh one.
			//
			DWORD deadline = GetTickCount() + 120 * 1000;
			for (;;)
			{
				SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
				if (sock == INVALID_SOCKET)
				{
					DEBUG_STREAM << "ERROR: socket() failed with "
						<< WSAGetLastError() << "!\n";
					return InvalidConnection;
				}

				bool reuse_address = true;
				if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
					(char *) &reuse_address, sizeof(bool)))
				{
					DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with "
						<< WSAGetLastError() << "!\n" << std::flush;
					closesocket(sock);
					return InvalidConnection;
				}

				// bind the game port: the mesh identifies peers by
				// address AND port, so the source port must be ours
				// (port 0 = ephemeral, for console channels)
				sockaddr_in local_endpoint;
				memset(&local_endpoint, 0, sizeof(local_endpoint));
				local_endpoint.sin_family = AF_INET;
				local_endpoint.sin_port = htons((unsigned short) local_port);
				local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY;
				if (bind(sock, (sockaddr *) &local_endpoint, sizeof(local_endpoint)))
				{
					DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with "
						<< WSAGetLastError() << "!\n" << std::flush;
					closesocket(sock);
					return InvalidConnection;
				}

				if (connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)) == 0)
				{
					unsigned long enable = 1;
					if (ioctlsocket(sock, FIONBIO, &enable))
					{
						DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with "
							<< WSAGetLastError() << "!\n" << std::flush;
						closesocket(sock);
						return InvalidConnection;
					}
					return (Connection) sock;
				}

				int wsa_error = WSAGetLastError();
				closesocket(sock);
				if (wsa_error != WSAECONNREFUSED ||
					(LONG)(GetTickCount() - deadline) >= 0)
				{
					DEBUG_STREAM << "ERROR: connect() failed with "
						<< wsa_error << "!\n" << std::flush;
					return InvalidConnection;
				}
				Sleep(250);		// peer not up yet - redial
			}
		}

		Connection
			Listen(
				int local_port,
				int backlog
			)
		{
			DEBUG_STREAM << "Starting to listen on port " << local_port
				<< "...\n" << std::flush;

			SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
			if (listener == INVALID_SOCKET)
			{
				DEBUG_STREAM << "ERROR: Could not create listener socket; socket() failed with "
					<< WSAGetLastError() << "!\n" << std::flush;
				return InvalidConnection;
			}

			bool reuse_address = true;
			if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR,
				(char *) &reuse_address, sizeof(bool)))
			{
				DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on listener socket; setsockopt() failed with "
					<< WSAGetLastError() << "!\n" << std::flush;
				closesocket(listener);
				return InvalidConnection;
			}

			sockaddr_in local_endpoint;
			memset(&local_endpoint, 0, sizeof(local_endpoint));
			local_endpoint.sin_family = AF_INET;
			local_endpoint.sin_port = htons((unsigned short) local_port);
			local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY;
			if (bind(listener, (sockaddr *) &local_endpoint, sizeof(local_endpoint)))
			{
				DEBUG_STREAM << "ERROR: Could not bind listener socket; bind() failed with "
					<< WSAGetLastError() << "!\n" << std::flush;
				closesocket(listener);
				return InvalidConnection;
			}
			if (listen(listener, backlog))
			{
				DEBUG_STREAM << "ERROR: Could not listen on listener socket; listen() failed with "
					<< WSAGetLastError() << "!\n" << std::flush;
				closesocket(listener);
				return InvalidConnection;
			}

			unsigned long enable = 1;
			if (ioctlsocket(listener, FIONBIO, &enable))
			{
				DEBUG_STREAM << "ERROR: Could not set listener socket to nonblocking; ioctlsocket() failed with "
					<< WSAGetLastError() << "!\n" << std::flush;
				closesocket(listener);
				return InvalidConnection;
			}
			return (Connection) listener;
		}

		Connection
			Accept(Connection listener)
		{
			SOCKET accepted = accept((SOCKET) listener, NULL, 0);
			if (accepted == INVALID_SOCKET)
			{
				return InvalidConnection;
			}
			return (Connection) accepted;
		}

		void
			Close(Connection connection)
		{
			shutdown((SOCKET) connection, SD_BOTH);
			closesocket((SOCKET) connection);
		}

		int
			Send(
				Connection connection,
				const void *data,
				int size
			)
		{
			return send((SOCKET) connection, (const char *) data, size, 0);
		}

		int
			Receive(
				Connection connection,
				void *buffer,
				int size
			)
		{
			int received = recv((SOCKET) connection, (char *) buffer, size, 0);
			if (received > 0)
			{
				return received;
			}
			if (received == 0)
			{
				return ReceiveDisconnected;
			}

			DWORD error = WSAGetLastError();
			switch (error)
			{
			case WSAECONNRESET:
				// hard drop reads the same as an orderly close upstairs
				return ReceiveDisconnected;
			case WSAEWOULDBLOCK:
				return ReceiveNoData;
			default:
				DEBUG_STREAM << "WinsockNetTransport::Receive: recv returned an unexpected error: WSAGetLastError = "
					<< error << std::endl << std::flush;
				return ReceiveNoData;
			}
		}

		Logical
			GetRemoteAddress(
				Connection connection,
				SOCKADDR_IN *address
			)
		{
			int size = sizeof(SOCKADDR_IN);
			memset(address, 0, size);
			return getpeername((SOCKET) connection, (sockaddr *) address, &size) == 0;
		}

	private:
		Logical	started;
		WSADATA	winsockData;
	};

	WinsockNetTransport gWinsockTransport;
	NetTransport *gTransport = &gWinsockTransport;
}

NetTransport *
	NetTransport_Get()
{
	return gTransport;
}

void
	NetTransport_Set(NetTransport *transport)
{
	gTransport = (transport != NULL) ? transport : &gWinsockTransport;
}
