/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "comms-precomp.h"	// Precompiled headers
//
#include <mrpt/comms/CClientTCPSocket.h>
#include <mrpt/comms/CServerTCPSocket.h>
#include <mrpt/comms/net_utils.h>
#include <mrpt/core/exceptions.h>
#include <mrpt/system/os.h>

#include <cstdio>  // stderr

using namespace mrpt::comms;

#if defined(MRPT_OS_LINUX) || defined(__APPLE__)
#define INVALID_SOCKET (-1)

#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <cerrno>
#endif

#ifdef _WIN32
#include <winsock.h>
using socklen_t = int;
#endif

std::string CServerTCPSocket::getLastErrorStr()
{
	return mrpt::comms::net::getLastSocketErrorStr();
}

void CServerTCPSocket::setupSocket(
	unsigned short listenPort, const std::string& IPaddress,
	int maxConnectionsWaiting)
{
	MRPT_START

	// Create the socket:
	// ----------------------------
	m_serverSock = socket(AF_INET, SOCK_STREAM, 0);
	if (INVALID_SOCKET == m_serverSock) THROW_EXCEPTION(getLastErrorStr());

	// Bind it:
	// ----------------------------
	sockaddr_in desiredIP;

	desiredIP.sin_family = AF_INET;
	desiredIP.sin_addr.s_addr = inet_addr(IPaddress.c_str());
	desiredIP.sin_port = htons((unsigned short)listenPort);

	if (INVALID_SOCKET ==
		::bind(m_serverSock, (struct sockaddr*)(&desiredIP), sizeof(desiredIP)))
		THROW_EXCEPTION(getLastErrorStr());

	// Put in listen mode:
	// ----------------------------
	if (INVALID_SOCKET == listen(m_serverSock, maxConnectionsWaiting))
		THROW_EXCEPTION(getLastErrorStr());

	MRPT_LOG_DEBUG(format(
		"[CServerTCPSocket] Listening at %s:%i\n", IPaddress.c_str(),
		listenPort));

	MRPT_END
}

/*---------------------------------------------------------------
					isListening
 ---------------------------------------------------------------*/
bool CServerTCPSocket::isListening() { return INVALID_SOCKET != m_serverSock; }
/*---------------------------------------------------------------
					accept
 ---------------------------------------------------------------*/
std::unique_ptr<CClientTCPSocket> CServerTCPSocket::accept(int timeout_ms)
{
	MRPT_START

	if (m_serverSock == INVALID_SOCKET) return nullptr;

	struct timeval timeoutSelect;
	struct timeval* ptrTimeout;
	fd_set sockArr;

	// Init fd_set structure & add our socket to it:
	FD_ZERO(&sockArr);
	FD_SET(m_serverSock, &sockArr);

	// The timeout:
	if (timeout_ms < 0) { ptrTimeout = nullptr; }
	else
	{
		timeoutSelect.tv_sec = timeout_ms / 1000;
		timeoutSelect.tv_usec = 1000 * (timeout_ms % 1000);
		ptrTimeout = &timeoutSelect;
	}

	// Wait for READ flag (meaning incoming connections):
	MRPT_LOG_DEBUG("[CServerTCPSocket::accept] Waiting incoming connections");

	int selRet = ::select(
		m_serverSock + 1,  // __nfds
		&sockArr,  // Wait for read
		nullptr,  // Wait for write
		nullptr,  // Wait for except.
		ptrTimeout);  // Timeout

	if (selRet == INVALID_SOCKET)
	{
		fprintf(stderr, "%s\n", getLastErrorStr().c_str());
		return nullptr;
	}

	if (selRet == 0)
	{
		MRPT_LOG_WARN(
			"[CServerTCPSocket::accept] Timeout waiting incoming "
			"connections\n");

		// Timeout:
		return nullptr;
	}
	else
	{
		MRPT_LOG_DEBUG(
			"[CServerTCPSocket::accept] Incoming connection accepted\n");

		// We have a new connection:

		sockaddr_in otherPart;
		socklen_t otherPartSize = sizeof(otherPart);

		int aceptdSock = ::accept(
			m_serverSock, (struct sockaddr*)&otherPart, &otherPartSize);

		if (aceptdSock == INVALID_SOCKET)
		{
			MRPT_LOG_ERROR_FMT("%s\n", getLastErrorStr().c_str());
			return {};
		}

		auto ret = std::make_unique<CClientTCPSocket>();

		ret->m_hSock = aceptdSock;
#if defined(MRPT_OS_LINUX)
		ret->internal_attach_epoll_to_hsock();
#endif

		ret->m_remotePartIP = std::string(inet_ntoa(otherPart.sin_addr));
		ret->m_remotePartPort = ntohs(otherPart.sin_port);

		MRPT_LOG_DEBUG_FMT(
			"[CServerTCPSocket::accept] Connection accepted from %s:%u\n",
			ret->m_remotePartIP.c_str(), ret->m_remotePartPort);

		return ret;
	}

	MRPT_END
}
