// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Network module"
// For conditions of distribution and use, see copyright notice in Config.hpp

#include <Nazara/Network/UdpSocket.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/StringExt.hpp>
#include <Nazara/Network/NetPacket.hpp>

#if defined(NAZARA_PLATFORM_WINDOWS)
#include <Nazara/Network/Win32/SocketImpl.hpp>
#elif defined(NAZARA_PLATFORM_POSIX)
#include <Nazara/Network/Posix/SocketImpl.hpp>
#else
#error Missing implementation: Socket
#endif

#include <Nazara/Network/Debug.hpp>

namespace Nz
{
	/*!
	* \ingroup network
	* \class Nz::UdpSocket
	* \brief Network class that represents a UDP socket, allowing for sending/receiving datagrams.
	*/

	/*!
	* \brief Binds a specific IpAddress
	* \return State of the socket
	*
	* \param address Address to bind
	*
	* \remark Produces a NazaraAssert if socket is invalid
	* \remark Produces a NazaraAssert if address is invalid
	*/

	SocketState UdpSocket::Bind(const IpAddress& address)
	{
		NazaraAssert(m_handle != SocketImpl::InvalidHandle, "Socket hasn't been created");
		NazaraAssert(address.IsValid(), "Invalid address");

		SocketState state = SocketImpl::Bind(m_handle, address, &m_lastError);
		if (state == SocketState::Bound)
			m_boundAddress = SocketImpl::QuerySocketAddress(m_handle);

		UpdateState(state);
		return state;
	}

	/*!
	* \brief Enables broadcasting
	*
	* \param broadcasting Should the UDP broadcast
	*
	* \remark Produces a NazaraAssert if socket is invalid
	*/

	void UdpSocket::EnableBroadcasting(bool broadcasting)
	{
		NazaraAssert(m_handle != SocketImpl::InvalidHandle, "Invalid handle");

		if (m_isBroadCastingEnabled != broadcasting)
		{
			SocketImpl::SetBroadcasting(m_handle, broadcasting, &m_lastError);
			m_isBroadCastingEnabled = broadcasting;
		}
	}

	/*!
	* \brief Gets the maximum datagram size allowed
	* \return Number of bytes
	*
	* \remark Produces a NazaraAssert if socket is invalid
	*/

	std::size_t UdpSocket::QueryMaxDatagramSize()
	{
		NazaraAssert(m_handle != SocketImpl::InvalidHandle, "Socket hasn't been created");

		return SocketImpl::QueryMaxDatagramSize(m_handle, &m_lastError);
	}

	/*!
	* \brief Receives the data available
	* \return true If data received
	*
	* \param buffer Raw memory to write
	* \param size Size of the buffer
	* \param from IpAddress of the peer
	* \param received Optional argument to get the number of bytes received
	*
	* \remark Produces a NazaraAssert if socket is invalid
	* \remark Produces a NazaraAssert if buffer and its size is invalid
	*/

	bool UdpSocket::Receive(void* buffer, std::size_t size, IpAddress* from, std::size_t* received)
	{
		NazaraAssert(m_handle != SocketImpl::InvalidHandle, "Socket hasn't been created");
		NazaraAssert(buffer && size > 0, "Invalid buffer");

		int read;
		if (!SocketImpl::ReceiveFrom(m_handle, buffer, static_cast<int>(size), from, &read, &m_lastError))
		{
			switch (m_lastError)
			{
				case SocketError::ConnectionClosed:
					m_lastError = SocketError::NoError;
					read = 0;
					break;

				default:
					return false;
			}
		}

		if (received)
			*received = read;

		return true;
	}

	/*!
	* \brief Receive multiple datagram from one peer
	* \return true If data were sent
	*
	* \param to Destination IpAddress (must match socket protocol)
	* \param buffers A pointer to an array of NetBuffer containing buffers and size data
	* \param bufferCount Number of buffers available
	* \param from IpAddress of the peer
	* \param received Optional argument to get the number of bytes received
	*/
	bool UdpSocket::ReceiveMultiple(NetBuffer* buffers, std::size_t bufferCount, IpAddress* from, std::size_t* received)
	{
		NazaraAssert(m_handle != SocketImpl::InvalidHandle, "Socket hasn't been created");
		NazaraAssert(buffers && bufferCount > 0, "Invalid buffer");

		int read;
		if (!SocketImpl::ReceiveMultiple(m_handle, buffers, bufferCount, from, &read, &m_lastError))
		{
			switch (m_lastError)
			{
				case SocketError::ConnectionClosed:
					m_lastError = SocketError::NoError;
					read = 0;
					break;

				default:
					return false;
			}
		}

		if (received)
			*received = read;

		return true;
	}

	/*!
	* \brief Receives the packet available
	* \return true If packet received
	*
	* \param packet Packet to receive
	* \param from IpAddress of the peer
	*
	* \remark Produces a NazaraAssert if packet is invalid
	* \remark Produces a NazaraWarning if packet's header is invalid
	*/
	bool UdpSocket::ReceivePacket(NetPacket* packet, IpAddress* from)
	{
		NazaraAssert(packet, "Invalid packet");

		// I'm not sure what's the best between having a 65k bytes buffer ready for any datagram size
		// or querying the next datagram size every time, for now I'll leave it as is
		packet->Reset(0, std::numeric_limits<UInt16>::max());
		packet->Resize(std::numeric_limits<UInt16>::max());

		std::size_t received;
		if (!Receive(packet->GetData(), static_cast<std::size_t>(packet->GetSize()), from, &received))
			return false;

		if (received == 0)
			return false; //< No datagram received

		Nz::UInt16 netCode;
		Nz::UInt32 packetSize;
		if (!NetPacket::DecodeHeader(packet->GetConstData(), &packetSize, &netCode))
		{
			m_lastError = SocketError::Packet;
			NazaraWarning("Invalid header data");
			return false;
		}

		if (packetSize != received)
		{
			m_lastError = SocketError::Packet;
			NazaraWarning("Invalid packet size (packet size is " + NumberToString(packetSize) + " bytes, received " + NumberToString(received) + " bytes)");
			return false;
		}

		packet->Resize(received);
		packet->SetNetCode(netCode);
		return true;
	}

	/*!
	* \brief Sends the data available
	* \return true If data sended
	*
	* \param to IpAddress of the peer
	* \param buffer Raw memory to read
	* \param size Size of the buffer
	* \param sent Optional argument to get the number of bytes sent
	*
	* \remark Produces a NazaraAssert if peer address is invalid
	* \remark Produces a NazaraAssert if protocol of communication is not the same than the peer
	* \remark Produces a NazaraAssert if buffer and its size is invalid
	*/

	bool UdpSocket::Send(const IpAddress& to, const void* buffer, std::size_t size, std::size_t* sent)
	{
		NazaraAssert(to.IsValid(), "Invalid ip address");
		NazaraAssert(to.GetProtocol() == m_protocol, "IP Address has a different protocol than the socket");
		NazaraAssert(buffer && size > 0, "Invalid buffer");

		int byteSent;
		if (!SocketImpl::SendTo(m_handle, buffer, static_cast<int>(size), to, &byteSent, &m_lastError))
			return false;

		if (sent)
			*sent = byteSent;

		return true;
	}

	/*!
	* \brief Sends multiple buffers as one datagram
	* \return true If data were sent
	*
	* \param to Destination IpAddress (must match socket protocol)
	* \param buffers A pointer to an array of NetBuffer containing buffers and size data
	* \param size Number of NetBuffer to send
	* \param sent Optional argument to get the number of bytes sent
	*/
	bool UdpSocket::SendMultiple(const IpAddress& to, const NetBuffer* buffers, std::size_t bufferCount, std::size_t* sent)
	{
		NazaraAssert(to.IsValid(), "Invalid ip address");
		NazaraAssert(to.GetProtocol() == m_protocol, "IP Address has a different protocol than the socket");
		NazaraAssert(buffers && bufferCount > 0, "Invalid buffer");

		int byteSent;
		if (!SocketImpl::SendMultiple(m_handle, buffers, bufferCount, to, &byteSent, &m_lastError))
			return false;

		if (sent)
			*sent = byteSent;

		return true;
	}

	/*!
	* \brief Sends the packet available
	* \return true If packet sent
	*
	* \param to IpAddress of the peer
	* \param packet Packet to send
	*
	* \remark Produces a NazaraError if packet could not be prepared for sending
	*/

	bool UdpSocket::SendPacket(const IpAddress& to, const NetPacket& packet)
	{
		std::size_t size = 0;
		const UInt8* ptr = static_cast<const UInt8*>(packet.OnSend(&size));
		if (!ptr)
		{
			m_lastError = SocketError::Packet;
			NazaraError("failed to prepare packet");
			return false;
		}

		return Send(to, ptr, size, nullptr);
	}

	/*!
	* \brief Operation to do when closing socket
	*/

	void UdpSocket::OnClose()
	{
		AbstractSocket::OnClose();

		m_boundAddress = IpAddress::Invalid;
	}

	/*!
	* \brief Operation to do when opening socket
	*/

	void UdpSocket::OnOpened()
	{
		AbstractSocket::OnOpened();

		m_boundAddress = IpAddress::Invalid;
		m_isBroadCastingEnabled = false;
	}
}
