/* pub_sub publisher implementation
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/>.
*/
#include <cds-pubsub/pub.hh>
#include "pub_sub_intl.hh"

#include <array>
#include <iostream>
#include <sstream>
#include <thread>

#include <boost/circular_buffer.hpp>

#include "pub_tcp.hh"
#include "pub_udp.hh"
#include "pub_zstd.hh"
#include "make_unique.hh"

namespace pub_sub
{
    namespace detail
    {
        //        std::unique_ptr< PublisherIntl >
        //        create_publisher( const std::string& address, PubDebugNotices*
        //        debug )
        //        {
        //            const std::string tcp_prefix = "tcp://";
        //            const std::string udp_prefix = "udp://";
        //            const std::string multi_prefix = "multi://";
        //
        //            if ( !debug )
        //            {
        //                debug = NullPubDebugNotices( );
        //            }
        //
        //            if ( address.find( tcp_prefix ) == 0 )
        //            {
        //                auto address_pair =
        //                    split_address( address.substr( tcp_prefix.size( )
        //                    ) );
        //                return std::make_unique< PublisherIntlTcp >(
        //                    address_pair.first, address_pair.second, *debug );
        //            }
        //            else if ( address.find( udp_prefix ) == 0 )
        //            {
        //                auto root = address.substr( udp_prefix.size( ) );
        //                auto sep = root.find( '/' );
        //                if ( sep == std::string::npos )
        //                {
        //                    throw std::runtime_error(
        //                        "Udp connection string is "
        //                        "udp://local_addr/remote_addr:port" );
        //                }
        //                auto local_addr = root.substr( 0, sep );
        //                auto remote_addr = split_address( root.substr( sep + 1
        //                ) ); return std::make_unique< PublisherIntlUdp >(
        //                    local_addr, remote_addr.first, remote_addr.second,
        //                    *debug );
        //            }
        //            else if ( address.find( multi_prefix ) == 0 )
        //            {
        //                auto root = address.substr( multi_prefix.size( ) );
        //                auto sep = root.find( '/' );
        //                if ( sep == std::string::npos )
        //                {
        //                    throw std::runtime_error(
        //                        "Multicast connection string is "
        //                        "multi://local_addr/multicast addr:port" );
        //                }
        //                auto local_addr = root.substr( 0, sep );
        //                auto remote_addr = split_address( root.substr( sep + 1
        //                ) ); return std::make_unique< PublisherIntlUdp >(
        //                    local_addr, remote_addr.first, remote_addr.second,
        //                    *debug );
        //            }
        //            auto address_pair = split_address( address );
        //            return std::make_unique< PublisherIntlTcp >(
        //                address_pair.first, address_pair.second, *debug );
        //        }

        class TcpPublisherPluginApi
            : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            TcpPublisherPluginApi( ) = default;
            ~TcpPublisherPluginApi( ) override = default;

            const std::string&
            prefix( ) const override
            {
                static const std::string my_prefix{ "tcp://" };
                return my_prefix;
            }
            const std::string&
            version( ) const override
            {
                static const std::string my_version = "0";
                return my_version;
            }
            const std::string&
            name( ) const override
            {
                static const std::string my_name = "tcp-unicast";
                return my_name;
            }

            pub_sub::plugins::UniquePublisherInstance
            publish(
                const std::string&                        address,
                PubDebugNotices&                          debug_hooks,
                pub_sub::plugins::UniquePublisherInstance next_stage ) override
            {
                if ( next_stage != nullptr )
                {
                    throw std::runtime_error(
                        "The tcp plugin does not support a next stage" );
                }
                if ( address.find( prefix( ) ) != 0 )
                {
                    throw std::runtime_error(
                        "Requesting a invalid address from the tcp plugin" );
                }
                auto address_pair =
                    split_address( address.substr( prefix( ).size( ) ) );
                return std::make_unique< PublisherIntlTcp >(
                    address_pair.first, address_pair.second, debug_hooks );
            }
        };

        class UdpPublisherPluginApi
            : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            UdpPublisherPluginApi( ) = default;
            ~UdpPublisherPluginApi( ) override = default;

            const std::string&
            prefix( ) const override
            {
                static const std::string my_prefix{ "udp://" };
                return my_prefix;
            }
            const std::string&
            version( ) const override
            {
                static const std::string my_version = "0";
                return my_version;
            }
            const std::string&
            name( ) const override
            {
                static const std::string my_name = "udp-broadcast";
                return my_name;
            }

            pub_sub::plugins::UniquePublisherInstance
            publish(
                const std::string&                        address,
                PubDebugNotices&                          debug_hooks,
                pub_sub::plugins::UniquePublisherInstance next_stage ) override
            {
                if ( next_stage != nullptr )
                {
                    throw std::runtime_error(
                        "The tcp plugin does not support a next stage" );
                }
                if ( address.find( prefix( ) ) != 0 )
                {
                    throw std::runtime_error(
                        "Requesting a invalid address from the udp plugin" );
                }

                auto root = address.substr( prefix( ).size( ) );
                auto sep = root.find( '/' );
                if ( sep == std::string::npos )
                {
                    throw std::runtime_error(
                        "Udp connection string is "
                        "udp://local_addr/remote_addr:port" );
                }
                auto local_addr = root.substr( 0, sep );
                auto remote_addr = split_address( root.substr( sep + 1 ) );
                return std::make_unique< PublisherIntlUdp >( local_addr,
                                                             remote_addr.first,
                                                             remote_addr.second,
                                                             debug_hooks );
            }
        };

        class UdpMultiPublisherPluginApi
            : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            UdpMultiPublisherPluginApi( ) = default;
            ~UdpMultiPublisherPluginApi( ) override = default;

            const std::string&
            prefix( ) const override
            {
                static const std::string my_prefix{ "multi://" };
                return my_prefix;
            }
            const std::string&
            version( ) const override
            {
                static const std::string my_version = "0";
                return my_version;
            }
            const std::string&
            name( ) const override
            {
                static const std::string my_name = "udp-multicast";
                return my_name;
            }

            pub_sub::plugins::UniquePublisherInstance
            publish(
                const std::string&                        address,
                PubDebugNotices&                          debug_hooks,
                pub_sub::plugins::UniquePublisherInstance next_stage ) override
            {
                if ( next_stage != nullptr )
                {
                    throw std::runtime_error(
                        "The udp plugin does not support a next stage" );
                }
                if ( address.find( prefix( ) ) != 0 )
                {
                    throw std::runtime_error(
                        "Requesting a invalid address from the udp plugin" );
                }

                auto root = address.substr( prefix( ).size( ) );
                auto sep = root.find( '/' );
                if ( sep == std::string::npos )
                {
                    throw std::runtime_error(
                        "Multicast connection string is "
                        "multi://local_addr/multicast addr:port" );
                }
                auto local_addr = root.substr( 0, sep );
                auto remote_addr = split_address( root.substr( sep + 1 ) );
                return std::make_unique< PublisherIntlUdp >( local_addr,
                                                             remote_addr.first,
                                                             remote_addr.second,
                                                             debug_hooks );
            }
        };

        class PubPluginZLibFilter : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            ~PubPluginZLibFilter( ) override = default;

            const std::string&
            prefix( ) const override
            {
                const static std::string my_prefix = "zstd://";
                return my_prefix;
            }
            const std::string&
            version( ) const override
            {
                const static std::string my_version = "0";
                return my_version;
            }
            const std::string&
            name( ) const override
            {
                const static std::string my_name = "zstd compression filter";
                return my_name;
            }
            pub_sub::plugins::UniquePublisherInstance
            publish(
                const std::string&                        address,
                pub_sub::PubDebugNotices&                 debug_hooks,
                pub_sub::plugins::UniquePublisherInstance next_stage ) override
            {
                if ( next_stage == nullptr )
                {
                    throw std::runtime_error( "The zstd plugin must not be the "
                                              "last plugin in a pipeline" );
                }
                if ( address.find( prefix( ) ) != 0 )
                {
                    throw std::runtime_error(
                        "Invalid publisher type passed to the zstd publisher" );
                }
                std::istringstream level( address.substr( prefix( ).size( ) ) );
                int                compression_level = ZSTD_CLEVEL_DEFAULT;
                level >> compression_level;
                if ( level.bad( ) )
                {
                    compression_level = ZSTD_CLEVEL_DEFAULT;
                }
                return make_unique_ptr< detail::ZStdPublisherInstance >(
                    compression_level, debug_hooks, std::move( next_stage ) );
            }
        };

        std::unique_ptr< detail::PublisherIntl >
        create_publisher_intl( PubDebugNotices* debug )
        {
            auto publisher = std::make_unique< detail::PublisherIntl >( debug );
            publisher->load_plugin(
                std::make_shared< TcpPublisherPluginApi >( ) );
            publisher->load_plugin(
                std::make_shared< UdpPublisherPluginApi >( ) );
            publisher->load_plugin(
                std::make_shared< UdpMultiPublisherPluginApi >( ) );
            publisher->load_plugin(
                std::make_shared< PubPluginZLibFilter >( ) );
            return publisher;
        }
    } // namespace detail

    Publisher::Publisher( PubDebugNotices* debug )
        : p_{ detail::create_publisher_intl( debug ) }
    {
    }

    Publisher::Publisher( const std::string& address, PubDebugNotices* debug )
        : p_{ detail::create_publisher_intl( debug ) }
    {
        p_->add_destination( address );
    }
    Publisher::~Publisher( ) = default;

    void
    Publisher::load_plugin(
        std::shared_ptr< pub_sub::plugins::PublisherPluginApi > plugin )
    {
        if ( !plugin )
        {
            return;
        }
        p_->load_plugin( std::move( plugin ) );
    }

    void
    Publisher::add_destination( const std::string& address )
    {
        if ( address.empty( ) )
        {
            return;
        }
        p_->add_destination( address );
    }

    void
    Publisher::publish( KeyType key, Message msg )
    {
        if ( msg.length > MAX_MSG_SIZE( ) )
        {
            throw std::runtime_error( "The given message is too big" );
        }
        p_->publish( key, msg );
    }
} // namespace pub_sub