/* Internal definitions for pub_sub
Copyright (C) 2020 Jonathan Hanks

This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef PUB_SUB2_PUB_SUB_INTL_HH
#define PUB_SUB2_PUB_SUB_INTL_HH

#include <chrono>

#include <boost/asio.hpp>
#include <cds-pubsub/common.hh>

/*!
 * @file pub_sub_intl.hh
 * @brief Basic protocol definitions
 * @details The protocol is very simple.
 * 1. Client connect
 * 2. Client sends a SubscriptionRequest block to tell the publisher
 * what message to start sending from.
 * 3. Messages are sent back to the client in the form of:
 *   BlockHeader
 *   Message data
 * 4. Goto 3
 */

namespace pub_sub
{
    namespace detail
    {
        using TimePoint = decltype( std::chrono::steady_clock::now( ) );
        using MilliSec = std::chrono::milliseconds;

        using boost::asio::ip::tcp;
        using boost::asio::ip::udp;
        using io_context = boost::asio::io_context;
        using work_guard =
            boost::asio::executor_work_guard< io_context::executor_type >;

        extern SubDebugNotices* NullSubDebugNotices( );
        extern PubDebugNotices* NullPubDebugNotices( );

        /*!
         * @brief The layout of a subscription request.
         * @details A subscription request is simply the key of the
         * last message received or NEXT_PUB_MSG().
         *
         * If NEXT_PUB_MSG() is sent in the SubscriptionRequest then the
         * next published message will be sent to the subscriber.  Otherwise
         * the next message with a KeyId > the given value will be sent.
         */
        struct SubcriptionRequest
        {
            SubcriptionRequest( ) : request( NEXT_PUB_MSG( ) )
            {
            }
            explicit SubcriptionRequest( KeyType key ) : request{ key }
            {
            }
            SubcriptionRequest( const SubcriptionRequest& ) = default;
            SubcriptionRequest&
            operator=( const SubcriptionRequest& ) = default;

            KeyType request{ NEXT_PUB_MSG( ) };
        };
        static_assert( sizeof( SubcriptionRequest ) == 8,
                       "SubscriptionRequest must be 8 bytes long" );
        static_assert( offsetof( SubcriptionRequest, request ) == 0,
                       "request is at byte 0" );

        /*!
         * @brief The block header that preceeds a message.
         * @detials This is simply a KeyId, size pair.  The next
         * size bytes after this is the message body.
         */
        struct BlockHeader
        {
            using size_type = std::uint64_t;

            BlockHeader( KeyType new_key, std::uint64_t new_size )
                : key{ new_key }, size{ new_size }
            {
            }
            BlockHeader( const BlockHeader& other ) = default;
            BlockHeader& operator=( const BlockHeader& other ) = default;

            KeyType       key{ 0 };
            std::uint64_t size{ 0 };
        };
        static_assert( sizeof( BlockHeader::size_type ) >=
                           sizeof( std::size_t ),
                       "size_type must be able to hold a std::size_t" );
        static_assert( sizeof( BlockHeader ) == 16,
                       "BlockHeaders are 16 bytes long" );
        static_assert( offsetof( BlockHeader, key ) == 0,
                       "key is the first field" );
        static_assert( offsetof( BlockHeader, size ) == 8,
                       "size starts at 8 bytes" );

        enum class UdpBlockType : std::uint8_t
        {
            DATA = 0,
            RETRANSMISSION = 1,
        };

        struct UdpHeader
        {
            using size_type = std::uint16_t;
            using block_index = std::uint16_t;

            KeyType       key;
            block_index   block_number;
            block_index   block_count;
            size_type     final_block_size;
            std::uint32_t sender_address;
            std::uint16_t sender_port;
            std::uint8_t  flags{ 0 };
            std::uint8_t  dummy{};

            bool
            valid( ) const
            {
                return !( ( block_count < 1 ) ||
                          final_block_size > MAX_CHUNK( ) ||
                          ( block_number >= block_count ) );
            }

            std::size_t
            offset_in_message( ) const
            {
                return block_number * MAX_CHUNK( );
            }

            bool
            is_last_block( ) const
            {
                return ( block_number + 1 == block_count );
            }
            std::size_t
            payload_size( ) const
            {
                return ( is_last_block( ) ? final_block_size : MAX_CHUNK( ) );
            }

            std::size_t
            message_size( ) const
            {
                return ( block_count - 1 ) * MAX_CHUNK( ) + final_block_size;
            }

            constexpr static std::size_t
            MAX_CHUNK( )
            {
                return 9000 - 8 - 20 - sizeof( UdpHeader );
            }
        };

        static_assert( sizeof( UdpHeader ) == 24,
                       "UdpHeader must be 16 bytes long" );

        struct UdpResendRequest
        {
            KeyType                msg_key;
            std::uint32_t          sender_id;
            std::uint32_t          req_number;
            UdpHeader::block_index start_block;
            UdpHeader::size_type   block_count;
            std::uint32_t          dummy;

            bool
            operator==( const UdpResendRequest& other ) const noexcept
            {
                return msg_key == other.msg_key &&
                    sender_id == other.sender_id &&
                    req_number == other.req_number &&
                    start_block == other.start_block &&
                    block_count == other.block_count;
            }
        };

        static_assert( sizeof( UdpResendRequest ) == 24,
                       "UdpResendRequest must be 20 bytes long" );

        /*!
         * @brief split a name:port pair into name and port
         * @param input A string of name:port
         * @return a pair with name, port as string, int.
         */
        inline std::pair< std::string, int >
        split_address( const std::string& input )
        {
            auto split_point = input.find( ':' );
            if ( split_point != std::string::npos )
            {
                std::size_t dummy;
                std::string port_str = input.substr( split_point + 1 );
                int         port = std::stoi( port_str, &dummy );
                return std::make_pair( input.substr( 0, split_point ), port );
            }
            return std::make_pair( input, 0 );
        }

    } // namespace detail
} // namespace pub_sub

#endif // PUB_SUB2_PUB_SUB_INTL_HH
