/* Built-in publisher plugins
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_PLUGINS_INTL_HH
#define PUB_SUB2_PUB_PLUGINS_INTL_HH

#include <sstream>
#include <string>

#include <cds-pubsub/pub_plugin.hh>
#include <cds-pubsub/stage_address.hh>

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

namespace pub_sub
{
    namespace detail
    {
        /*!
         * @brief The built-in transport plugins accept per-destination
         * publisher options as query parameters on their stage, e.g.
         * tcp://host:port?max_depth=16.  Options given in
         * the connection string override the environment-derived options
         * the plugin was constructed with; invalid options throw.
         */
        class TcpPublisherPluginApi
            : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            explicit TcpPublisherPluginApi(
                PublisherOptions options =
                    PublisherOptions::from_environment( ) )
                : options_{ options }
            {
            }
            ~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" );
                }
                auto stage = parse_stage_address( address );
                if ( stage.scheme + "://" != prefix( ) )
                {
                    throw std::runtime_error(
                        "Requesting a invalid address from the tcp plugin" );
                }
                auto options = options_.with_arguments( stage.arguments );
                auto address_pair = split_address( stage.path );
                return std::make_unique< PublisherIntlTcp >(
                    address_pair.first,
                    address_pair.second,
                    debug_hooks,
                    options );
            }

        private:
            PublisherOptions options_;
        };

        class UdpPublisherPluginApi
            : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            explicit UdpPublisherPluginApi(
                PublisherOptions options =
                    PublisherOptions::from_environment( ) )
                : options_{ options }
            {
            }
            ~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" );
                }
                auto stage = parse_stage_address( address );
                if ( stage.scheme + "://" != prefix( ) )
                {
                    throw std::runtime_error(
                        "Requesting a invalid address from the udp plugin" );
                }
                auto options = options_.with_arguments( stage.arguments );

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

        private:
            PublisherOptions options_;
        };

        class UdpMultiPublisherPluginApi
            : public pub_sub::plugins::PublisherPluginApi
        {
        public:
            explicit UdpMultiPublisherPluginApi(
                PublisherOptions options =
                    PublisherOptions::from_environment( ) )
                : options_{ options }
            {
            }
            ~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" );
                }
                auto stage = parse_stage_address( address );
                if ( stage.scheme + "://" != prefix( ) )
                {
                    throw std::runtime_error(
                        "Requesting a invalid address from the udp plugin" );
                }
                auto options = options_.with_arguments( stage.arguments );

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

        private:
            PublisherOptions options_;
        };

        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 ) );
            }
        };
    } // namespace detail
} // namespace pub_sub

#endif // PUB_SUB2_PUB_PLUGINS_INTL_HH
