/* +------------------------------------------------------------------------+
   |                     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          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/config.h>  // MRPT_WORD_SIZE
#include <mrpt/io/CStream.h>
#include <mrpt/system/os.h>

#include <cstdint>
#include <cstring>	// strlen()
#include <string>

namespace mrpt
{
/** Serial and networking devices and utilities */
namespace comms
{
class CServerTCPSocket;

/** A TCP socket that can be connected to a TCP server, implementing MRPT's
 * CStream interface for passing objects as well as generic read/write methods.
 *  Unless otherwise noticed, operations are blocking.
 *
 *  Note that for convenience, DNS lookup is performed with a timeout
 * (default=3000ms), which can be changed by the static member
 * CClientTCPSocket::DNS_LOOKUP_TIMEOUT_MS
 * \ingroup mrpt_comms_grp
 */
class CClientTCPSocket : public mrpt::io::CStream
{
	friend class CServerTCPSocket;

   public:
	/** See description of CClientTCPSocket */
	static unsigned int DNS_LOOKUP_TIMEOUT_MS;

   protected:
#ifdef _WIN32
/** The handle for the connected TCP socket, or INVALID_SOCKET
 */
#if MRPT_WORD_SIZE == 64
	uint64_t m_hSock;
#else
	uint32_t m_hSock;
#endif
#else
	/** The handle for the connected TCP socket, or -1 */
	int m_hSock = -1;
#if defined(MRPT_OS_LINUX)	// These dont work in OSX
	int m_epoll4read_fd = -1;
	int m_epoll4write_fd = -1;
	void internal_attach_epoll_to_hsock();
#endif
#endif
	/** The IP address of the remote part of the connection. */
	std::string m_remotePartIP;
	/** The TCP port of the remote part of the connection. */
	unsigned short m_remotePartPort = 0;

	/** Introduces a virtual method responsible for reading from the stream
	 * (This method BLOCKS)
	 * This method is implemented as a call to "readAsync" with infinite
	 * timeouts.
	 * \sa readAsync */
	size_t Read(void* Buffer, size_t Count) override;

	/** Introduces a virtual method responsible for writing to the stream.
	 *  Write attempts to write up to Count bytes to Buffer, and returns the
	 * number of bytes actually written.
	 *  This method is implemented as a call to "writeAsync" with infinite
	 * timeouts.
	 * \sa writeAsync */
	size_t Write(const void* Buffer, size_t Count) override;

	/** Returns a description of the last Sockets error */
	std::string getLastErrorStr();

   public:
	/** Default constructor \sa connect  */
	CClientTCPSocket();

	/** Destructor */
	~CClientTCPSocket() override;

	/** Establishes a connection with a remote part.
	 * \param remotePartAddress This string can be a host name, like "server"
	 * or "www.mydomain.org", or an IP address "11.22.33.44".
	 * \param remotePartTCPPort The port on the remote machine to connect to.
	 * \param timeout_ms  The timeout to wait for the connection (0: NO
	 * TIMEOUT)
	 * \exception This method raises an exception if an error is found with a
	 * textual description of the error.
	 */
	void connect(
		const std::string& remotePartAddress, unsigned short remotePartTCPPort,
		unsigned int timeout_ms = 0);

	/** Returns true if this objects represents a successfully connected socket
	 */
	bool isConnected();

	/** Closes the connection */
	void close();

	/** Writes a string to the socket.
	 * \exception std::exception On communication errors
	 */
	void sendString(const std::string& str);

	/** This virtual method has no effect in this implementation over a TCP
	 * socket, and its use raises an exception */
	uint64_t Seek(
		int64_t off, CStream::TSeekOrigin org = sFromBeginning) override;

	/** This virtual method has no effect in this implementation over a TCP
	 * socket, and its use raises an exception */
	uint64_t getTotalBytesCount() const override;

	/** This virtual method has no effect in this implementation over a TCP
	 * socket, and its use raises an exception */
	uint64_t getPosition() const override;

	/** A method for reading from the socket with an optional timeout.
	 * \param Buffer The destination of data.
	 * \param Cound The number of bytes to read.
	 * \param timeoutStart_ms The maximum timeout (in milliseconds) to wait for
	 * the starting of data from the other side.
	 * \param timeoutBetween_ms The maximum timeout (in milliseconds) to wait
	 * for a chunk of data after a previous one.
	 *  Set timeout's to -1 to block until the desired number of bytes are
	 * read, or an error happens.
	 *  \return The number of actually read bytes.
	 */
	size_t readAsync(
		void* Buffer, size_t Count, const int timeoutStart_ms = -1,
		const int timeoutBetween_ms = -1);

	/** A method for writing to the socket with optional timeouts.
	 *  The method supports writing block by block as the socket allows us to
	 * write more data.
	 * \param Buffer The data.
	 * \param Cound The number of bytes to write.
	 * \param timeout_ms The maximum timeout (in milliseconds) to wait for the
	 * socket to be available for writing (for each block).
	 *  Set timeout's to -1 to block until the desired number of bytes are
	 * written, or an error happens.
	 *  \return The number of actually written bytes.
	 */
	size_t writeAsync(
		const void* Buffer, size_t Count, const int timeout_ms = -1);

	/** Send a message through the TCP stream.
	 * \param outMsg The message to be shown.
	 * \param timeout_ms The maximum timeout (in milliseconds) to wait for the
	 * socket in each write operation.
	 * \return Returns false on any error, or true if everything goes fine.
	 * \tparam MESSAGE can be mrpt::serialization::CMessage
	 */
	template <class MESSAGE>
	bool sendMessage(const MESSAGE& outMsg, const int timeout_ms = -1)
	{
		// (1) Send a "magic word":
		const char* magic = "MRPTMessage";
		uint32_t toWrite = strlen(magic);
		uint32_t written = writeAsync(magic, toWrite, timeout_ms);
		if (written != toWrite) return false;  // Error!
		// (2) Send the message type:
		toWrite = sizeof(outMsg.type);
		written = writeAsync(&outMsg.type, toWrite, timeout_ms);
		if (written != toWrite) return false;  // Error!
		// (3) Send the message's content length:
		uint32_t contentLen = outMsg.content.size();
		toWrite = sizeof(contentLen);
		written = writeAsync(&contentLen, toWrite, timeout_ms);
		if (written != toWrite) return false;  // Error!
		// (4) Send the message's contents:
		toWrite = contentLen;
		written = writeAsync(&outMsg.content[0], toWrite, timeout_ms);
		if (written != toWrite) return false;  // Error!
		return true;
	}

	/** Waits for an incoming message through the TCP stream.
	 * \param inMsg The received message is placed here.
	 * \param timeoutStart_ms The maximum timeout (in milliseconds) to wait for
	 * the starting of data from the other side.
	 * \param timeoutBetween_ms The maximum timeout (in milliseconds) to wait
	 * for a chunk of data after a previous one.
	 * \return Returns false on any error (or timeout), or true if everything
	 * goes fine.
	 * \tparam MESSAGE can be mrpt::serialization::CMessage
	 */
	template <class MESSAGE>
	bool receiveMessage(
		MESSAGE& inMsg, const unsigned int timeoutStart_ms = 100,
		const unsigned int timeoutBetween_ms = 1000)
	{
		// (1) Read the "magic word":
		char magic[20];
		uint32_t toRead = 11;
		uint32_t actRead =
			readAsync(magic, toRead, timeoutStart_ms, timeoutBetween_ms);
		if (actRead != toRead) return false;  // Error!
		magic[actRead] = 0;	 // Null-term string
		// Check magic:
		if (0 != ::strcmp("MRPTMessage", magic)) return false;
		// (2) Read the message type:
		toRead = sizeof(inMsg.type);
		actRead = readAsync(
			&inMsg.type, toRead, timeoutBetween_ms, timeoutBetween_ms);
		if (actRead != toRead) return false;  // Error!
		// (3) Read the message's content length:
		uint32_t contentLen;
		toRead = sizeof(contentLen);
		actRead = readAsync(
			&contentLen, toRead, timeoutBetween_ms, timeoutBetween_ms);
		if (actRead != toRead) return false;  // Error!
		inMsg.content.resize(contentLen);
		// (4) Read the message's contents:
		toRead = contentLen;
		actRead = readAsync(
			&inMsg.content[0], toRead, timeoutBetween_ms, timeoutBetween_ms);
		if (actRead != toRead) return false;  // Error!
		return true;
	}

	/** Return the number of bytes already in the receive queue (they can be
	 * read without waiting) */
	size_t getReadPendingBytes();

	/** Set the TCP no delay option of the protocol (Nagle algorithm).
	 * \param newValue New value (0 enable Nagle algorithm, 1 disable).
	 * \return Return a number lower than 0 if any error occurred.
	 */
	int setTCPNoDelay(int newValue);

	/** Return the value of the TCPNoDelay option. */
	int getTCPNoDelay();

	/** Set the size of the SO send buffer. This buffer is used to store data,
	 * and is sended when is full.
	 * \param newValue New size of the SO send buffer.
	 * \return Return a number lower than 0 if any error occurred.
	 */
	int setSOSendBufffer(int newValue);

	/** Return the current size of the SO send buffer. */
	int getSOSendBufffer();

};	// End of class def.

}  // namespace comms
}  // namespace mrpt
