// Copyright (C) 2015 Jérôme Leclercq
// This file is part of the "Nazara Engine - Utility module"
// For conditions of distribution and use, see copyright notice in Config.hpp

#include <Nazara/Network/RUdpConnection.hpp>
#include <Nazara/Core/CallOnExit.hpp>
#include <Nazara/Core/Log.hpp>
#include <Nazara/Network/NetPacket.hpp>
#include <Nazara/Network/Debug.hpp>

namespace Nz
{
	/*!
	* \ingroup network
	* \class Nz::RUdpConnection
	* \brief Network class that represents a reliable UDP connection
	*/

	/*!
	* \brief Constructs a RUdpConnection object by default
	*/

	RUdpConnection::RUdpConnection() :
	m_peerIterator(0),
	m_forceAckSendTime(10'000), //< 10ms
	m_pingInterval(1'000'000), //< 1s
	m_protocol(0x4E4E6574), //< "NNet"
	m_timeBeforePing(500'000), //< 0.5s
	m_timeBeforeTimeOut(10'000'000), //< 10s
	m_currentTime(0),
	m_isSimulationEnabled(false),
	m_shouldAcceptConnections(true)
	{
	}

	/*!
	* \brief Connects to the IpAddress
	* \return true
	*
	* \param remoteAddress Address to connect to
	*
	* \remark Produces a NazaraAssert if socket is not bound
	* \remark Produces a NazaraAssert if remote is invalid
	* \remark Produces a NazaraAssert if port is not specified
	*/

	bool RUdpConnection::Connect(const IpAddress& remoteAddress)
	{
		NazaraAssert(m_socket.GetState() == SocketState_Bound, "Socket must be bound first");
		NazaraAssert(remoteAddress.IsValid(), "Invalid remote address");
		NazaraAssert(remoteAddress.GetPort() != 0, "Remote address has no port");

		PeerData& client = RegisterPeer(remoteAddress, PeerState_Connecting);
		client.stateData1 = s_randomGenerator();

		NetPacket connectionRequestPacket(NetCode_RequestConnection);
		connectionRequestPacket << client.stateData1;

		EnqueuePacket(client, PacketPriority_Immediate, PacketReliability_Reliable, connectionRequestPacket);
		return true;
	}

	/*!
	* \brief Connects to the hostname
	* \return true If successful
	*
	* \param hostName Hostname of the remote
	* \param protocol Net protocol to use
	* \param service Specify the protocol used
	* \param error Optional argument to get the error
	*/

	bool RUdpConnection::Connect(const String& hostName, NetProtocol protocol, const String& service, ResolveError* error)
	{
		std::vector<HostnameInfo> results = IpAddress::ResolveHostname(protocol, hostName, service, error);
		if (results.empty())
		{
			m_lastError = SocketError_ResolveError;
			return false;
		}

		IpAddress hostnameAddress;
		for (const HostnameInfo& result : results)
		{
			if (!result.address)
				continue;

			if (result.socketType != SocketType_UDP)
				continue;

			hostnameAddress = result.address;
			break; //< Take first valid address
		}

		return Connect(hostnameAddress);
	}

	/*!
	* \brief Listens to a socket
	* \return true If successfully bound
	*
	* \param remoteAddress Address to listen to
	*/

	bool RUdpConnection::Listen(const IpAddress& address)
	{
		if (!InitSocket(address.GetProtocol()))
			return false;

		return m_socket.Bind(address) == SocketState_Bound;
	}

	/*!
	* \brief Polls the message
	* \return true If there is a message
	*
	* \param message Message to poll
	*
	* \remark Produces a NazaraAssert if message is invalid
	*/

	bool RUdpConnection::PollMessage(RUdpMessage* message)
	{
		NazaraAssert(message, "Invalid message");

		if (m_receivedMessages.empty())
			return false;

		*message = std::move(m_receivedMessages.front());
		m_receivedMessages.pop();
		return true;
	}

	/*!
	* \brief Sends the packet to a peer
	* \return true If peer exists (false may result from disconnected client)
	*
	* \param peerIp IpAddress of the peer
	* \param priority Priority of the packet
	* \param reliability Policy of reliability of the packet
	* \param packet Packet to send
	*/

	bool RUdpConnection::Send(const IpAddress& peerIp, PacketPriority priority, PacketReliability reliability, const NetPacket& packet)
	{
		auto it = m_peerByIP.find(peerIp);
		if (it == m_peerByIP.end())
			return false; /// Silently fail (probably a disconnected client)

		EnqueuePacket(m_peers[it->second], priority, reliability, packet);
		return true;
	}

	/*!
	* \brief Updates the reliable connection
	*/

	void RUdpConnection::Update()
	{
		m_currentTime = m_clock.GetMicroseconds();

		NetPacket receivedPacket;
		IpAddress senderIp;
		while (m_socket.ReceivePacket(&receivedPacket, &senderIp))
			OnPacketReceived(senderIp, std::move(receivedPacket));

		//for (unsigned int i = m_activeClients.FindFirst(); i != m_activeClients.npos; i = m_activeClients.FindNext(i))
		//{
		//	PeerData& clientData = m_peers[i];

		CallOnExit resetIterator([this] () { m_peerIterator = m_peers.size(); });

		for (m_peerIterator = 0; m_peerIterator < m_peers.size(); ++m_peerIterator)
		{
			PeerData& peer = m_peers[m_peerIterator];

			UInt32 timeSinceLastPacket = static_cast<UInt32>(m_currentTime - peer.lastPacketTime);
			if (timeSinceLastPacket > m_timeBeforeTimeOut)
			{
				DisconnectPeer(peer.index);
				continue;
			}
			else if (timeSinceLastPacket > m_timeBeforePing)
			{
				if (m_currentTime - peer.lastPingTime > m_pingInterval)
				{
					NetPacket pingPacket(NetCode_Ping);
					EnqueuePacket(peer, PacketPriority_Low, PacketReliability_Unreliable, pingPacket);
				}
			}

			if (peer.state == PeerState_WillAck && m_currentTime - peer.stateData1 > m_forceAckSendTime)
			{
				NetPacket acknowledgePacket(NetCode_Acknowledge);
				EnqueuePacket(peer, PacketPriority_Low, PacketReliability_Reliable, acknowledgePacket);
			}

			for (unsigned int priority = PacketPriority_Highest; priority <= PacketPriority_Lowest; ++priority)
			{
				std::vector<PendingPacket>& pendingPackets = peer.pendingPackets[priority];
				for (PendingPacket& packetData : pendingPackets)
					SendPacket(peer, std::move(packetData));

				pendingPackets.clear();
			}

			auto it = peer.pendingAckQueue.begin();
			while (it != peer.pendingAckQueue.end())
			{
				if (m_currentTime - it->timeSent > 2 * peer.roundTripTime)
				{
					OnPacketLost(peer, std::move(*it));
					it = peer.pendingAckQueue.erase(it);
				}
				else
					++it;
			}
		}
		//m_activeClients.Reset();
	}

	/*!
	* \brief Disconnects a peer
	*
	* \param peerIndex Index of the peer
	*
	* \remark Produces a NazaraNotice
	*/

	void RUdpConnection::DisconnectPeer(std::size_t peerIndex)
	{
		PeerData& peer = m_peers[peerIndex];
		NazaraNotice(m_socket.GetBoundAddress().ToString() + ": " + peer.address.ToString() + " has been disconnected due to time-out");

		OnPeerDisconnected(this, peer.address);

		// Remove from IP lookup table
		m_peerByIP.erase(peer.address);

		// Can we safely "remove" this slot?
		if (m_peerIterator >= m_peers.size() - 1 || peerIndex > m_peerIterator)
		{
			// Yes we can
			PeerData& newSlot = m_peers[peerIndex];
			newSlot = std::move(m_peers.back());
			newSlot.index = peerIndex; //< Update the moved slot index before resizing (in case it's the last one)
		}
		else
		{
			// Nope, let's be tricky
			PeerData& current = m_peers[m_peerIterator];
			PeerData& newSlot = m_peers[peerIndex];

			newSlot = std::move(current);
			newSlot.index = peerIndex; //< Update the moved slot index

			current = std::move(m_peers.back());
			current.index = m_peerIterator; //< Update the moved slot index

			--m_peerIterator;
		}

		// Pop the last entry (from where we moved our slot)
		m_peers.pop_back();
	}

	/*!
	* \brief Enqueues a packet in the sending list
	*
	* \param peer Data relative to the peer
	* \param priority Priority of the packet
	* \param reliability Policy of reliability of the packet
	* \param packet Packet to send
	*/

	void RUdpConnection::EnqueuePacket(PeerData& peer, PacketPriority priority, PacketReliability reliability, const NetPacket& packet)
	{
		UInt16 protocolBegin = static_cast<UInt16>(m_protocol & 0xFFFF);
		UInt16 protocolEnd = static_cast<UInt16>((m_protocol & 0xFFFF0000) >> 16);

		NetPacket data(packet.GetNetCode(), MessageHeader + packet.GetDataSize() + MessageFooter);
		data << protocolBegin;

		data.GetStream()->SetCursorPos(NetPacket::HeaderSize + MessageHeader);
		data.Write(packet.GetConstData() + NetPacket::HeaderSize, packet.GetDataSize());

		data << protocolEnd;
		EnqueuePacketInternal(peer, priority, reliability, std::move(data));
	}

	/*!
	* \brief Enqueues internally a packet in the sending list
	*
	* \param peer Data relative to the peer
	* \param priority Priority of the packet
	* \param reliability Policy of reliability of the packet
	* \param packet Packet to send
	*/

	void RUdpConnection::EnqueuePacketInternal(PeerData& peer, PacketPriority priority, PacketReliability reliability, NetPacket&& data)
	{
		PendingPacket pendingPacket;
		pendingPacket.data = std::move(data);
		pendingPacket.priority = priority;
		pendingPacket.reliability = reliability;

		peer.pendingPackets[priority].emplace_back(std::move(pendingPacket));
		m_activeClients.UnboundedSet(peer.index);
	}

	/*!
	* \brief Inits the internal socket
	* \return true If successful
	*
	* \param protocol Net protocol to use
	*/

	bool RUdpConnection::InitSocket(NetProtocol protocol)
	{
		CallOnExit updateLastError([this]
		{
			m_lastError = m_socket.GetLastError();
		});

		if (!m_socket.Create(protocol))
			return false;

		m_socket.EnableBlocking(false);
		return true;
	}

	/*!
	* \brief Processes the acks
	*
	* \param peer Data relative to the peer
	* \param lastAck Last index of the ack
	* \param ackBits Bits for acking
	*/

	void RUdpConnection::ProcessAcks(PeerData& peer, SequenceIndex lastAck, UInt32 ackBits)
	{
		auto it = peer.pendingAckQueue.begin();
		while (it != peer.pendingAckQueue.end())
		{
			bool acked = false;
			if (lastAck == it->sequenceId)
				acked = true;
			else if (!IsAckMoreRecent(it->sequenceId, lastAck))
			{
				unsigned int difference = ComputeSequenceDifference(lastAck, it->sequenceId);
				if (difference <= 32)
					acked = (ackBits >> (difference - 1)) & 1;
			}

			if (acked)
			{
				it = peer.pendingAckQueue.erase(it);
			}
			else
				++it;
		}
	}

	/*!
	* \brief Registers a peer
	* \return Data relative to the peer
	*
	* \param address Address of the peer
	* \param state Status of the peer
	*/

	RUdpConnection::PeerData& RUdpConnection::RegisterPeer(const IpAddress& address, PeerState state)
	{
		PeerData data;
		data.address = address;
		data.localSequence = 0;
		data.remoteSequence = 0;
		data.index = m_peers.size();
		data.lastPacketTime = m_currentTime;
		data.lastPingTime = m_currentTime;
		data.roundTripTime = 1'000'000; ///< Okay that's quite a lot
		data.state = state;

		m_activeClients.UnboundedSet(data.index);
		m_peerByIP[address] = data.index;

		m_peers.emplace_back(std::move(data));
		return m_peers.back();
	}

	/*!
	* \brief Operation to do when client requests a connection
	*
	* \param address Address of the peer
	* \param sequenceId Sequence index for the ack
	* \param token Token for connection
	*/

	void RUdpConnection::OnClientRequestingConnection(const IpAddress& address, SequenceIndex sequenceId, UInt64 token)
	{
		// Call hook to check if client should be accepted or not
		OnPeerConnection(this, address);

		PeerData& client = RegisterPeer(address, PeerState_Aknowledged);
		client.remoteSequence = sequenceId;

		/// Acknowledge connection
		NetPacket connectionAcceptedPacket(NetCode_AcknowledgeConnection);
		//connectionAcceptedPacket << address;
		connectionAcceptedPacket << ~token;

		EnqueuePacket(client, PacketPriority_Immediate, PacketReliability_Reliable, connectionAcceptedPacket);
	}

	/*!
	* \brief Operation to do when a packet is lost
	*
	* \param peer Data relative to the peer
	* \param packet Pending packet
	*/

	void RUdpConnection::OnPacketLost(PeerData& peer, PendingAckPacket&& packet)
	{
		//NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Lost packet " + String::Number(packet.sequenceId));

		if (IsReliable(packet.reliability))
			EnqueuePacketInternal(peer, packet.priority, packet.reliability, std::move(packet.data));
	}

	/*!
	* \brief Operation to do when receiving a packet
	*
	* \param peerIndex Index of the peer
	*
	* \remark Produces a NazaraNotice
	*/

	void RUdpConnection::OnPacketReceived(const IpAddress& peerIp, NetPacket&& packet)
	{
		UInt16 protocolBegin;
		UInt16 protocolEnd;
		SequenceIndex sequenceId;
		SequenceIndex lastAck;
		UInt32 ackBits;

		packet.GetStream()->SetCursorPos(packet.GetSize() - MessageFooter);
		packet >> protocolEnd;

		packet.GetStream()->SetCursorPos(NetPacket::HeaderSize);
		packet >> protocolBegin;

		UInt32 protocolId = static_cast<UInt32>(protocolEnd) << 16 | protocolBegin;
		if (protocolId != m_protocol)
			return; ///< Ignore

		packet >> sequenceId >> lastAck >> ackBits;

		auto it = m_peerByIP.find(peerIp);
		if (it == m_peerByIP.end())
		{
			switch (packet.GetNetCode())
			{
				case NetCode_RequestConnection:
				{
					UInt64 token;
					packet >> token;

					NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Received NetCode_RequestConnection from " + peerIp.ToString() + ": " + String::Number(token));
					if (!m_shouldAcceptConnections)
						return; //< Ignore

					OnClientRequestingConnection(peerIp, sequenceId, token);
					break;
				}

				default:
					return; //< Ignore
			}
		}
		else
		{
			PeerData& peer = m_peers[it->second];
			peer.lastPacketTime = m_currentTime;

			if (peer.receivedQueue.find(sequenceId) != peer.receivedQueue.end())
				return; //< Ignore

			if (m_isSimulationEnabled && m_packetLossProbability(s_randomGenerator))
			{
				NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Lost packet " + String::Number(sequenceId) + " from " + peerIp.ToString() + " for simulation purpose");
				return;
			}

			///< Receiving a packet from an acknowledged client means the connection works in both ways
			if (peer.state == PeerState_Aknowledged && packet.GetNetCode() != NetCode_RequestConnection)
			{
				peer.state = PeerState_Connected;
				OnPeerAcknowledged(this, peerIp);
			}

			if (IsAckMoreRecent(sequenceId, peer.remoteSequence))
				peer.remoteSequence = sequenceId;

			ProcessAcks(peer, lastAck, ackBits);

			peer.receivedQueue.insert(sequenceId);

			switch (packet.GetNetCode())
			{
				case NetCode_Acknowledge:
					return; //< Do not switch to will ack mode (to prevent infinite replies, just let's ping/pong do that)

				case NetCode_AcknowledgeConnection:
				{
					if (peer.state == PeerState_Connected)
						break;

					IpAddress externalAddress;
					UInt64 token;
					packet /*>> externalAddress*/ >> token;

					NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Received NetCode_AcknowledgeConnection from " + peerIp.ToString() + ": " + String::Number(token));
					if (token == ~peer.stateData1)
					{
						peer.state = PeerState_Connected;
						OnConnectedToPeer(this);
					}
					else
					{
						NazaraNotice("Received wrong token (" + String::Number(token) + " instead of " + String::Number(~peer.stateData1) + ") from client " + peer.address);
						return; //< Ignore
					}

					break;
				}

				case NetCode_RequestConnection:
					NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Received NetCode_RequestConnection from " + peerIp.ToString());
					return; //< Ignore

				case NetCode_Ping:
				{
					NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Received NetCode_Ping from " + peerIp.ToString());

					NetPacket pongPacket(NetCode_Pong);
					EnqueuePacket(peer, PacketPriority_Low, PacketReliability_Unreliable, pongPacket);
					break;
				}

				case NetCode_Pong:
					NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Received NetCode_Pong from " + peerIp.ToString());
					break;

				default:
				{
					NazaraNotice(m_socket.GetBoundAddress().ToString() + ": Received 0x" + String::Number(packet.GetNetCode(), 16) + " from " + peerIp.ToString());
					RUdpMessage receivedMessage;
					receivedMessage.from = peerIp;
					receivedMessage.data = std::move(packet);

					m_receivedMessages.emplace(std::move(receivedMessage));
					break;
				}
			}

			if (!HasPendingPackets(peer))
			{
				peer.state      = PeerState_WillAck;
				peer.stateData1 = m_currentTime;
			}
		}
	}

	/*!
	* \brief Sends a packet to a peer
	*
	* \param peer Data relative to the peer
	* \param packet Pending packet
	*/

	void RUdpConnection::SendPacket(PeerData& peer, PendingPacket&& packet)
	{
		if (peer.state == PeerState_WillAck)
			peer.state = PeerState_Connected;

		SequenceIndex remoteSequence = peer.remoteSequence;

		UInt32 previousAcks = 0;
		for (SequenceIndex ack : peer.receivedQueue)
		{
			if (ack == remoteSequence)
				continue;

			unsigned int difference = ComputeSequenceDifference(remoteSequence, ack);
			if (difference <= 32U)
				previousAcks |= (1U << (difference - 1));
		}

		SequenceIndex sequenceId = ++peer.localSequence;

		packet.data.GetStream()->SetCursorPos(NetPacket::HeaderSize + sizeof(UInt16)); ///< Protocol begin has already been filled
		packet.data << sequenceId;
		packet.data << remoteSequence;
		packet.data << previousAcks;

		m_socket.SendPacket(peer.address, packet.data);

		PendingAckPacket pendingAckPacket;
		pendingAckPacket.data = std::move(packet.data);
		pendingAckPacket.priority = packet.priority;
		pendingAckPacket.reliability = packet.reliability;
		pendingAckPacket.sequenceId = sequenceId;
		pendingAckPacket.timeSent = m_currentTime;

		peer.pendingAckQueue.emplace_back(std::move(pendingAckPacket));
	}

	/*!
	* \brief Initializes the RUdpConnection class
	* \return true
	*/

	bool RUdpConnection::Initialize()
	{
		std::random_device device;
		s_randomGenerator.seed(device());

		return true;
	}

	/*!
	* \brief Uninitializes the RUdpConnection class
	*/

	void RUdpConnection::Uninitialize()
	{
	}

	std::mt19937_64 RUdpConnection::s_randomGenerator;
}
