//----------------------------------------------------------------------------
//
// TSDuck - The MPEG Transport Stream Toolkit
// Copyright (c) 2005-2026, Thierry Lelegard
// BSD-2-Clause license, see LICENSE.txt file or https://tsduck.io/license
//
//----------------------------------------------------------------------------
//!
//!  @file
//!  UDP Socket
//!
//----------------------------------------------------------------------------

#pragma once
#include "tsSocket.h"
#include "tsIPSocketAddress.h"
#include "tsAbortInterface.h"
#include "tsReport.h"
#include "tsInitZero.h"

#if defined(DOXYGEN) || defined(TS_OPENBSD) || defined(TS_NETBSD) || defined(TS_DRAGONFLYBSD)
    //!
    //! Defined when the operating system does not support UDP/IP source-specific multicast (SSM).
    //!
    #define TS_NO_SSM 1
#endif

namespace ts {
    //!
    //! UDP Socket.
    //! @ingroup libtscore net
    //!
    class TSCOREDLL UDPSocket: public Socket
    {
        TS_NOCOPY(UDPSocket);
    public:
        //!
        //! Reference to the superclass.
        //!
        using SuperClass = Socket;

        //!
        //! Constructor.
        //! @param [in] report Where to report errors. The @a report object must remain valid as long as this object
        //! exists or setReport() is used with another Report object. If @a report is null, log messages are discarded.
        //! @param [in] auto_open If true, call open() immediately.
        //! @param [in] gen IP generation, IPv4 or IPv6.
        //! If set to IP::Any, this socket can receive IPv4 and IPv6 datagrams.
        //! If @a gen is IP::v6, the socket is created with option IPV6_V6ONLY set.
        //! @param [in] non_blocking It true, the device is initially set in non-blocking mode.
        //! @param [in] owner Optional address of an "owner" object, typically an instance of class containing this object.
        //!
        explicit UDPSocket(Report* report = nullptr, bool auto_open = false, IP gen = IP::Any, bool non_blocking = false, Object* owner = nullptr);

        //!
        //! Constructor.
        //! @param [in] delegate Use the report of another ReporterBase. If @a delegate is null, log messages are discarded.
        //! @param [in] auto_open If true, call open() immediately.
        //! @param [in] gen IP generation, IPv4 or IPv6.
        //! If set to IP::Any, this socket can receive IPv4 and IPv6 datagrams.
        //! If @a gen is IP::v6, the socket is created with option IPV6_V6ONLY set.
        //! @param [in] non_blocking It true, the device is initially set in non-blocking mode.
        //! @param [in] owner Optional address of an "owner" object, typically an instance of class containing this object.
        //!
        explicit UDPSocket(ReporterBase* delegate, bool auto_open = false, IP gen = IP::Any, bool non_blocking = false, Object* owner = nullptr);

        //!
        //! Destructor.
        //!
        virtual ~UDPSocket() override;

        //!
        //! Set a default destination address and port for outgoing messages.
        //!
        //! There are two versions of the send() method. One of them explicitly
        //! specifies the destination of the packet to send. The second version
        //! does not specify a destination; the packet is sent to the <i>default
        //! destination</i>.
        //!
        //! @param [in] addr Socket address of the destination.
        //! Both address and port are mandatory in the socket address, they cannot
        //! be set to IPAddress::AnyAddress4 or IPSocketAddress::AnyPort.
        //! @return True on success, false on error.
        //!
        bool setDefaultDestination(const IPSocketAddress& addr);

        //!
        //! Set a default destination address and port for outgoing messages.
        //!
        //! There are two versions of the send() method. One of them explicitly
        //! specifies the destination of the packet to send. The second version
        //! does not specify a destination; the packet is sent to the <i>default
        //! destination</i>.
        //!
        //! @param [in] name A string describing the socket address of the destination.
        //! See IPSocketAddress::resolve() for a description of the expected string format.
        //! @return True on success, false on error.
        //!
        bool setDefaultDestination(const UString& name);

        //!
        //! Get the default destination address and port for outgoing messages.
        //! @return The default destination address and port for outgoing messages.
        //!
        IPSocketAddress getDefaultDestination() const { return _default_destination; }

        //!
        //! Set the outgoing local interface for multicast messages.
        //!
        //! @param [in] addr IP address of a local interface.
        //! @return True on success, false on error.
        //!
        bool setOutgoingMulticast(const IPAddress& addr);

        //!
        //! Set the outgoing local interface for multicast messages.
        //!
        //! @param [in] name A string describing the IP address of a local interface.
        //! See IPAddress::resolve() for a description of the expected string format.
        //! @return True on success, false on error.
        //!
        bool setOutgoingMulticast(const UString& name);

        //!
        //! Set the Time To Live (TTL) option.
        //!
        //! @param [in] ttl The TTL value, ie. the maximum number of "hops" between
        //! routers before an IP packet is dropped.
        //! @param [in] multicast When true, set the <i>multicast TTL</i> option.
        //! When false, set the <i>unicast TTL</i> option.
        //! @return True on success, false on error.
        //!
        bool setTTL(int ttl, bool multicast);

        //!
        //! Set the Time To Live (TTL) option.
        //!
        //! If the <i>default destination</i> is a multicast address, set the
        //! <i>multicast TTL</i> option. Otherwise, set the <i>unicast TTL</i> option.
        //!
        //! @param [in] ttl The TTL value, ie. the maximum number of "hops" between
        //! routers before an IP packet is dropped.
        //! @return True on success, false on error.
        //!
        bool setTTL(int ttl) { return setTTL(ttl, _default_destination.isMulticast()); }

        //!
        //! Set the Type Of Service (TOS) or Traffic Class (IPv6) option.
        //!
        //! The interpretation of the @a tos parameter depends in the IP generation.
        //! With IPv4, this is a "type of service" value.
        //! With IPv6, this is a "traffic class" value.
        //!
        //! Note that correct support for this option depends on the operating
        //! system. Typically, it never worked correctly on Windows.
        //!
        //! @param [in] tos The type of service (IPv4) or traffic class (IPv6) value.
        //! @return True on success, false on error.
        //!
        bool setTOS(int tos);

        //!
        //! Set the multicast loop option.
        //!
        //! By default, the multicast packets are looped back on local interfaces.
        //! Use this to disable multicast loopback.
        //!
        //! @param [in] on It true, multicast loopback is on. When false, it is off.
        //! @return True on success, false on error.
        //!
        bool setMulticastLoop(bool on);

        //!
        //! Enable or disable the generation of receive timestamps.
        //!
        //! When enabled, each received UDP packets comes with a time stamp (see receive()).
        //! When possible, a hardware timestamp from the NIC is received. Otherwise, a software
        //! timestamp is generated by the kernel.
        //!
        //! When enabled, this option is a request, not a requirement.
        //! This option is supported on Linux and macOS only. It is ignored on other systems.
        //!
        //! @param [in] on If true, receive timestamps are activated on the socket. Otherwise, they are disabled.
        //! @return True on success, false on error.
        //!
        bool setReceiveTimestamps(bool on);

        //!
        //! Enable or disable the broadcast option.
        //!
        //! @param [in] on If true, broadcast is activated on the socket. Otherwise, it is disabled.
        //! @return True on success, false on error.
        //!
        bool setBroadcast(bool on);

        //!
        //! Enable or disable the broadcast option, based on an IP address.
        //!
        //! @param [in] destination An hypothetical destination address. If this address
        //! is the broadcast address of a local interface, the broadcast option is set.
        //! Otherwise, the broadcast option is unchanged.
        //! @return True on success, false on error.
        //!
        bool setBroadcastIfRequired(const IPAddress destination);

        //!
        //! Join a multicast group.
        //!
        //! This method indicates that the application wishes to receive multicast
        //! packets which are sent to a specific multicast address. Specifying a
        //! non-default @a source address, source-specific multicast (SSM) is used.
        //! Note that source-specific multicast exists on IPv4 only.
        //!
        //! @param [in] multicast Multicast IP address to listen to.
        //! @param [in] local IP address of a local interface on which to listen.
        //! If set to IPAddress::AnyAddress4, the application lets the system selects
        //! the appropriate local interface.
        //! @param [in] source Source address for SSM. Ignored on IPv6 socket.
        //! @return True on success, false on error.
        //!
        bool addMembership(const IPAddress& multicast, const IPAddress& local, const IPAddress& source = IPAddress())
        {
            return addMembershipImpl(multicast, local, -1, source);
        }

        //!
        //! Join a multicast group.
        //!
        //! This method indicates that the application wishes to receive multicast
        //! packets which are sent to a specific multicast address. Specifying a
        //! non-default @a source address, source-specific multicast (SSM) is used.
        //! Note that source-specific multicast exists on IPv4 only.
        //!
        //! @param [in] multicast Multicast IP address to listen to.
        //! @param [in] interface_index Index of a local interface on which to listen.
        //! If set to zero, the application lets the system selects the appropriate local interface.
        //! @param [in] source Source address for SSM. Ignored on IPv6 socket.
        //! @return True on success, false on error.
        //!
        bool addMembership(const IPAddress& multicast, int interface_index, const IPAddress& source = IPAddress())
        {
            return addMembershipImpl(multicast, IPAddress(), interface_index, source);
        }

        //!
        //! Join a multicast group.
        //!
        //! This method indicates that the application wishes to receive multicast
        //! packets which are sent to a specific multicast address. Specifying a
        //! non-default @a source address, source-specific multicast (SSM) is used.
        //!
        //! Using this method, the application listens on all local interfaces.
        //!
        //! @param [in] multicast Multicast IP address to listen to.
        //! @param [in] source Source address for SSM.
        //! @param [in] link_local If true, also add membership on link-local addresses, otherwise ignore them.
        //! @return True on success, false on error.
        //!
        bool addMembershipAll(const IPAddress& multicast, const IPAddress& source = IPAddress(), bool link_local = true);

        //!
        //! Join a multicast group.
        //!
        //! This method indicates that the application wishes to receive multicast
        //! packets which are sent to a specific multicast address. Specifying a
        //! non-default @a source address, source-specific multicast (SSM) is used.
        //!
        //! Using this method, the application lets the system selects the appropriate
        //! local interface.
        //!
        //! @param [in] multicast Multicast IP address to listen to.
        //! @param [in] source Source address for SSM.
        //! @return True on success, false on error.
        //!
        bool addMembershipDefault(const IPAddress& multicast, const IPAddress& source = IPAddress())
        {
            return addMembershipImpl(multicast, IPAddress(), -1, source);
        }

        //!
        //! Drop all multicast membership requests, including source-specific multicast.
        //! @return True on success, false on error.
        //!
        bool dropMembership();

        //!
        //! Send a message to a destination address and port.
        //!
        //! @param [in] data Address of the message to send.
        //! @param [in] size Size in bytes of the message to send.
        //! @param [in] destination Socket address of the destination.
        //! Both address and port are mandatory in the socket address, they cannot
        //! be set to IPAddress::AnyAddress4 or IPSocketAddress::AnyPort.
        //! @param [in,out] iosb Address of an IOSB structure. If non-null, the socket must be in non-blocking mode.
        //! When null, the socket must be in blocking mode (the default). See the description of IOSB.
        //! Important: The parameter @a iosb should not be used by applications. It should be used only by
        //! "reactive classes", which work in combination with a Reactor.
        //! @return True on success, false on error. In case of non-blocking mode, if the I/O is successfully started
        //! but still pending, iosb->pending is set to true and the method returns true.
        //!
        virtual bool send(const void* data, size_t size, const IPSocketAddress& destination, IOSB* iosb = nullptr);

        //!
        //! Send a message to the default destination address and port.
        //!
        //! @param [in] data Address of the message to send.
        //! @param [in] size Size in bytes of the message to send.
        //! @param [in,out] iosb Address of an IOSB structure. If non-null, the socket must be in non-blocking mode.
        //! When null, the socket must be in blocking mode (the default). See the description of IOSB.
        //! Important: The parameter @a iosb should not be used by applications. It should be used only by
        //! "reactive classes", which work in combination with a Reactor.
        //! @return True on success, false on error. In case of non-blocking mode, if the I/O is successfully started
        //! but still pending, iosb->pending is set to true and the method returns true.
        //!
        virtual bool send(const void* data, size_t size, IOSB* iosb = nullptr);

        //!
        //! Type of timestamp which is returned by receive().
        //!
        enum class TimeStampType {
            NONE,      //!< No timestamp.
            SOFTWARE,  //!< Software time stamp, generated by the kernel.
            HARDWARE,  //!< Hardware time stamp, generated by the NIC, when supported.
        };

        //!
        //! Receive a message.
        //!
        //! @param [out] data Address of the buffer for the received message.
        //! @param [in] max_size Size in bytes of the reception buffer.
        //! @param [out] ret_size Size in bytes of the received message.
        //! Will never be larger than @a max_size.
        //! @param [out] sender Socket address of the sender.
        //! @param [out] destination Socket address of the packet destination. Can be useful to check in multicast packets.
        //! @param [in] abort If non-zero, invoked when I/O is interrupted
        //! (in case of user-interrupt, return, otherwise retry).
        //! @param [out] timestamp When not null, return the receive timestamp in micro-seconds.
        //! Use setReceiveTimestamps() to enable the generation of receive timestamps.
        //! If the returned value is negative, no timestamp is available.
        //! @param [out] timestamp_type When not null, return the type of receive timestamp.
        //! @param [in,out] iosb Address of an IOSB structure. If non-null, the socket must be in non-blocking mode.
        //! When null, the socket must be in blocking mode (the default). See the description of IOSB.
        //! Important: The parameter @a iosb should not be used by applications. It should be used only by
        //! "reactive classes", which work in combination with a Reactor.
        //! @return True on success, false on error. In case of non-blocking mode, if the I/O is successfully started
        //! but still pending, iosb->pending is set to true and the method returns true.
        //!
        virtual bool receive(void* data,
                             size_t max_size,
                             size_t& ret_size,
                             IPSocketAddress& sender,
                             IPSocketAddress& destination,
                             const AbortInterface* abort = nullptr,
                             cn::microseconds* timestamp = nullptr,
                             TimeStampType* timestamp_type = nullptr,
                             IOSB* iosb = nullptr);

        //!
        //! Get the result of an asynchronous receive().
        //! This method shall be used with asynchronous I/O only. It returns an error when the system
        //! uses non-blocking I/O instead of asynchronous I/O.
        //! @param [in,out] iosb Address of the IOSB structure which was used when receive() was called.
        //! @param [out] sender Socket address of the sender.
        //! @param [out] destination Socket address of the packet destination. Can be useful to check in multicast packets.
        //! @param [out] timestamp When not null, return the receive timestamp in micro-seconds.
        //! Use setReceiveTimestamps() to enable the generation of receive timestamps.
        //! If the returned value is negative, no timestamp is available.
        //! @param [out] timestamp_type When not null, return the type of receive timestamp.
        //! @return True on success, false on error.
        //!
        bool getReceiveStatus(IOSB* iosb,
                              IPSocketAddress& sender,
                              IPSocketAddress& destination,
                              cn::microseconds* timestamp = nullptr,
                              TimeStampType* timestamp_type = nullptr) const;

    protected:
        // Implementation of Socket interface.
        virtual bool openImplementation(IP gen) override;
        virtual bool closeImplementation(bool silent) override;

    private:
        // Encapsulate an ip_mreq
        struct MReq: public InitZero<::ip_mreq>
        {
            MReq() = default;
            MReq(const IPAddress& mcast, const IPAddress& iface)
            {
                mcast.getAddress4(data.imr_multiaddr);
                iface.getAddress4(data.imr_interface);
            }
        };
        using MReqSet = std::set<MReq>;

        // Encapsulate an ipv6_mreq
        struct MReq6: public InitZero<::ipv6_mreq>
        {
            MReq6() = default;
            MReq6(const IPAddress& multicast, int interface_index) : InitZero<::ipv6_mreq>()
            {
                multicast.getAddress6(data.ipv6mr_multiaddr);
                data.ipv6mr_interface = static_cast<unsigned int>(interface_index);
            }
        };
        using MReq6Set = std::set<MReq6>;

        // Encapsulate an ip_mreq_source
#if !defined(TS_NO_SSM)
        struct SSMReq: public InitZero<::ip_mreq_source>
        {
            SSMReq() = default;
            SSMReq(const IPAddress& multicast_, const IPAddress& interface_, const IPAddress& source_) : InitZero<::ip_mreq_source>()
            {
                multicast_.getAddress4(data.imr_multiaddr);
                interface_.getAddress4(data.imr_interface);
                source_.getAddress4(data.imr_sourceaddr);
            }
        };
        using SSMReqSet = std::set<SSMReq>;
#endif

        // Private members
        IPSocketAddress _default_destination {};
        MReqSet         _mcast {};    // Current set of IPv4 multicast memberships
        MReq6Set        _mcast6 {};   // Current set of IPv6 multicast memberships
#if !defined(TS_NO_SSM)
        SSMReqSet       _ssmcast {};  // Current set of source-specific multicast memberships
#endif

        // Perform one receive operation. Hide the system mud. Return a system socket error code.
        int receiveOne(void* data, size_t max_size, size_t& ret_size, IPSocketAddress& sender, IPSocketAddress& destination,
                       cn::microseconds* timestamp, TimeStampType* timestamp_type, IOSB* iosb);

        // Add multicast membership common code, local interface by index or by address.
        bool addMembershipImpl(const IPAddress& multicast, const IPAddress& local, int interface_index, const IPAddress& source);

#if defined(TS_WINDOWS)
        // The functions WSASendTo and WSARecvMsg use complex structures of buffers.
        // For asynchronous I/O, we need to keep everything in one single structure
        // which lives during the I/O. This structure must be non-copyable because
        // it contains pointers to its own fields.
        // See https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms741687(v=vs.85)
        class TSCOREDLL AsyncBuffers: public Object
        {
            TS_NOCOPY(AsyncBuffers);
        public:
            ::WSAMSG msg {};                  // Reception parameters, contain pointers to subsequent fields.
            ::WSABUF buf {};                  // Pointer to user's buffer.
            ::sockaddr_storage peer_sock {};  // Send: destination socket address. Receive: receive the sender's socket address.
            int peer_sock_len = 0;            // Send: destination socket address length.
            ::CHAR ancil_data[1024] {};       // Receive: other reception data (destination socket, timestamp).

            // Constructor.
            AsyncBuffers() = default;
            virtual ~AsyncBuffers() override;

            // Before send: Initializes all internal structures and set the address and size of the user's send buffer.
            void setSendBuffer(const void* address, size_t size, const IPSocketAddress& dest);

            // Before receive: Initializes all internal structures and set the address and size of the user's reception buffer.
            void setReceiveBuffer(void* address, size_t size);

            // After receive: Extract the message characteristics.
            void getReceiveResult(const UDPSocket& socket, IPSocketAddress& sender, IPSocketAddress& destination, cn::microseconds* timestamp, TimeStampType* timestamp_type);
        };
#endif
    };
}
