// DWBS ------------------------------------------------------------------ DWBS
//
// PLATFORM		: WIN32 PS2 PS3 PSP-INFRA UNIX OSX
// PRODUCT		: BIT_DEMON
// VISIBILITY	: PUBLIC
//
// DWBS ------------------------------------------------------------------ DWBS

// PURPOSE: UPnP Nat traversal module

#ifndef BD_UPNP_H
#define BD_UPNP_H

#include <bdCore/bdCore.h>
#include <bdCore/bdSocket/bdStreamSocket.h>

class bdUPnPListener;

/// Demonware's UPnP Nat traversal module.
/// This class is a singleton with the sole purpose of traversing UPnP capable
/// network address translators.
class bdUPnP
{
	public:
		BD_DECLARE_NEW_AND_DELETE_OPERATORS

		/// enumeration to describe the UPnP module's current state
		enum bdUPnPState
		{
			BD_INITIAL = 0, ///< initial state of the module; ready to discover.
			BD_DISCOVERING, ///< discovery is in progress
			BD_NONE_DISCOVERED, ///< discovery is complete and no UPnP NATs were found
			BD_READY, ///< devices are ready to be queried or port mapped
			BD_MAPPED, ///< port mapping was successful or usable prior mapping found
			BD_QUERYING, ///< querying registered devices for existing portmappings
			BD_COLLISION_FOUND, ///< the port mapping queried already exists on at least one registered device
			BD_OPENING, ///< opening port on UPnP NAT
			BD_CLOSING, ///< closing all ports opened by bdUPnP
			BD_FAILED ///< UPnP mapping has failed somewhere along the way
		};

		/// Default constructor. 
		bdUPnP();

		/// virtual destructor
		~bdUPnP();

		/// starts the discovery of UPnP devices present on the network
		void startDiscovery();

		/// queries whether a mapping already exists for the specified port
		void queryMapping(bdPort externalPort, const bdArray<bdInetAddr> *localAddrs);

		/// maps an arbitrary public port to the specified local port on the current machine
		/// \param localPort[in] : the local port to which traffic should be routed
		/// \param localAddr[in] : the local address of the client; this is ignored for Windows
		void openPort(bdPort localPort, 
					  const bdInAddr localAddr);

		/// clears all port mappings done by the UPnP module on the specified device
		void clearPortMap();

		/// returns the current internal state of the bDUPnP class
		bdUPnPState getState() const;

		/// returns the number of ports mapped by bdUPnP
		bdUInt getNumMappings() const;

		/// determines whether given port is an external port mapped by bdUPnP
		bdBool isMapped(bdPort port);

		/// returns the socket error encountered to trigger a state change to BD_FAILED
		bdSocketStatusCode getError() const;

		/// allows the bdUPnP class to continue any pending asynchronous tasks
		/// This should be called each frame during the main game loop
		void pump();

		void setDiscoveryTimeOut(bdUInt seconds);

	protected:

		/// internal representation of a UPnP capable NAT
		struct bdUPnPDevice
		{
			/// enumeration to represent the relevant states of a UPnP NAT
			enum bdDeviceState
			{
				BD_CONNECT_DESC, ///< connecting to the TCP description socket of this device
				BD_FETCHING_DESC, ///< in process of retrieving the UPnP device description
				BD_READY, ///< all relevant device info has been retrieved; ready for port mapping
				BD_CONNECT_QUERY, ///< connecting to the TCP control socket to query the port mappings
				BD_PORT_QUERYING, ///< querying existing port mappings
				BD_CONNECT_MAP, ///< connecting to the TCP control socket to create a new port map
				BD_MAPPING, ///< in process of mapping an external port to an internal one
				BD_MAPPED, ///< port mapping was successful
				BD_CONNECT_UNMAP, ///< connecting to TCP control socket to delete portmappings
				BD_UNMAPPING, ///< in process of removing port mappings
				BD_UNMAPPED, ///< all port mappings were successfully deleted
				BD_FAILED ///< port mapping / unmapping failed
			};

			/// default constructor
			bdUPnPDevice();
			
			/// copy constructor to properly handle deep copying strings
			bdUPnPDevice(const bdUPnPDevice& toCopy);
			
			/// destructor to properly handle string deletion
			~bdUPnPDevice();

			/// current state of this UPnP NAT
			bdDeviceState m_state;

			bdAddr m_connectionAddr; // address connected to the stream socket

			bdByte8* m_controlURL;

			/// TCP socket connection to NAT
			/// This connection will transition from the description URL to the
			/// control URL as UPnP traversal progresses.
			bdStreamSocket m_connection;

			/// indicates whether forwarding mechanism is IP or PPP
			bdBool m_isIp;

			/// string used to send http commands to UPnP devices
			bdByte8* m_httpRequest;

#if defined(BD_PLATFORM_WIN32)
			/// address associated with the TCP connection to this device.
			/// this address is required to generate port mapping requests
			/// on systems that are capable of multiple NICs with multiple IPs
			bdInAddr m_localAddr;
#endif
		};

		/// current internal state of the UPnP class
		bdUPnPState m_UPnPState;

		/// last socket error encountered by bdUPnP
		bdSocketStatusCode m_errorCode;

		/// list of UPnP capable devices discovered on the network
		bdLinkedList<bdUPnPDevice> m_deviceList;

		/// list of the the external ports opened by bdUPnP on registered UPnP devices
		bdLinkedList<bdPort> m_externalPortMap;

		/// UDP socket used for UPnP NAT discovery
		bdSocket m_discoverySocket;

		/// buffer to hold TCP data returned from UPnP NAT
		bdByte8* m_readBuffer;
		bdUInt m_readBufferOffset; ///< current offset into m_readBuffer buffer

		bdFloat32 m_discoveryDuration; ///< total time allocated for discovery by bdNetStartParams
		bdStopwatch m_timer; ///< timer to track discovery duration

		/// register discovered UPnP devices with bdUPnP
		void registerDevice(bdByte8* const discoveryResponse);

		/// retrieve the UPnP device description of the specified device
		void readDescription(bdUPnPDevice& currDevice);

		/// parse the mapping query reply
		void readQueryReply(bdUPnPDevice& currDevice);

		/// parse the port mapping/unmapping reply from the given device
		/// \param currDevice[in] device to receive from
		/// \param unMapping[in] true if port is being unmapped, false if port is being mapped
		void readMapReply(bdUPnPDevice& currDevice, bdBool unMapping);

		/// generate a full http post string for the given UPnP command
		bdByte8* genUPnPCommand(const bdUPnPDevice& device, const bdByte8* command, 
								const bdByte8* arguments) const;

		/// sends http requests to registered UPnP devices to remove the next mapping in the list
		void removeNextPortMap();

		/// listeners for UPnP errors
		bdFastArray<bdUPnPListener*> m_listeners;

		/// array of local addresses assigned to this peer
		const bdArray<bdInetAddr> *m_localAddrs;

		/// port number last queried for existing UPnP mappings
		bdPort lastPortQueried;
};

#endif
