//
// Created by jonathan.hanks on 2/20/20.
//

#ifndef PUB_SUB2_PUB_INTL_HH
#define PUB_SUB2_PUB_INTL_HH

#include <algorithm>
#include <cstdlib>
#include <sstream>
#include <boost/asio.hpp>
#include <boost/smart_ptr/intrusive_ptr.hpp>

#include <cds-pubsub/pub_plugin.hh>
#include "pub_sub_intl.hh"
#include "pipeline_parser.hh"

namespace pub_sub
{
    namespace detail
    {
        /*!
         * @brief The BoundedList is a intrusive singly linked list of Node
         * objects.  It maintains a head/tail and will cap its list size to a
         * given value.
         * @tparam Node The object type to hold in the list, it be referenced by
         * an intrusive pointer, meaning that items in the BoundedList may only
         * be in one bounded list at a time.
         * @details Memory is managed by the intrusive pointers, so nodes may
         * be kept alive after being 'removed' from the list if another
         * object has a reference to the nodes.
         * @note This is not thread safe.
         */
        template < typename Node >
        class BoundedList
        {
        public:
            using Pointer = boost::intrusive_ptr< Node >;

            BoundedList( ) = default;
            BoundedList( const BoundedList& ) = delete;
            BoundedList( BoundedList&& ) = delete;
            BoundedList& operator=( const BoundedList& ) = delete;
            BoundedList& operator=( BoundedList&& ) = delete;

            Pointer
            head( )
            {
                return head_;
            }

            Pointer
            tail( )
            {
                return tail_;
            }

            /*!
             * @brief Push a new item into the list, removing an old item if
             * needed.
             * @param new_node Pointer to the node to add
             */
            void
            push( Pointer new_node )
            {
                if ( empty( ) )
                {
                    tail_ = new_node;
                    head_ = new_node;
                    cur_size_ = 1;
                    return;
                }

                head_->next( new_node );
                head_ = new_node;
                if ( size( ) < capacity( ) )
                {
                    cur_size_++;
                }
                else
                {
                    /* Advance the tail if we are full, dropping our reference
                     * to the node.
                     */
                    tail_ = tail_->next( );
                }
            }

            bool
            empty( ) const noexcept
            {
                return size( ) == 0;
            }

            std::size_t
            size( ) const noexcept
            {
                return cur_size_;
            }

            std::size_t
            capacity( ) const noexcept
            {
                return max_size_;
            }

        private:
            Pointer     head_{ nullptr };
            Pointer     tail_{ nullptr };
            std::size_t max_size_{ 8 };
            std::size_t cur_size_{ 0 };
        };

        /*!
         * @brief The MessageBlock class contains enough information to send
         * a Message and clean up after it.
         */
        class MessageBlock
        {
        public:
            using BuffersType = std::array< boost::asio::const_buffer, 2 >;

            /*!
             * @brief Construct a MessageBlock
             * @param key The application key value
             * @param msg The message to send
             * @param next The next pointer needed for inclusion in a
             * BoundedList
             */
            MessageBlock( KeyType                              key,
                          Message                              msg,
                          boost::intrusive_ptr< MessageBlock > next )
                : header_{ key, msg.length }, msg_{ msg }, next_{ std::move(
                                                               next ) }
            {
                asio_buffers_[ 0 ] =
                    boost::asio::const_buffer( &header_, sizeof( header_ ) );
                asio_buffers_[ 1 ] =
                    boost::asio::const_buffer( msg_.data( ), msg.length );
            }

            ~MessageBlock( )
            {
                //                std::cout << "Message block " << header_.key
                //                << " destroyed"
                //                          << std::endl;
            }

            KeyType
            key( ) const noexcept
            {
                return header_.key;
            }

            const Message&
            msg( ) const noexcept
            {
                return msg_;
            }
            Message&
            msg( ) noexcept
            {
                return msg_;
            }

            boost::intrusive_ptr< MessageBlock >
            next( ) noexcept
            {
                return next_;
            }

            /*!
             * @brief update the next pointer
             * @param new_next New next object
             */
            void
            next( boost::intrusive_ptr< MessageBlock > new_next )
            {
                next_ = std::move( new_next );
            }

            /*!
             * @brief Return a series of asio buffers to allow the message
             * to be sent.
             * @details 2 buffers are returned, one for the basic header that
             * is sent, and one with the message body.
             * @return A reference to a series of buffers.
             */
            const BuffersType&
            asio_buffers( ) const
            {
                return asio_buffers_;
            }

        private:
            int                                  ref_cnt_{ 0 };
            BlockHeader                          header_;
            Message                              msg_;
            boost::intrusive_ptr< MessageBlock > next_{ nullptr };

            BuffersType asio_buffers_;

            friend void intrusive_ptr_add_ref( MessageBlock* );
            friend void intrusive_ptr_release( MessageBlock* );
        };
        using MessageBlockPtr = boost::intrusive_ptr< MessageBlock >;
        inline MessageBlockPtr
        make_message_block( KeyType key, Message msg, MessageBlockPtr next )
        {
            auto m =
                std::make_unique< MessageBlock >( key, msg, std::move( next ) );
            return m.release( );
        }
        inline void
        intrusive_ptr_add_ref( MessageBlock* mb )
        {
            if ( !mb )
            {
                return;
            }
            mb->ref_cnt_++;
        }

        inline void
        intrusive_ptr_release( MessageBlock* mb )
        {
            if ( !mb )
            {
                return;
            }
            mb->ref_cnt_--;
            if ( mb->ref_cnt_ <= 0 )
            {
                std::unique_ptr< MessageBlock > m{ mb };
            }
        }

        /*!
         * @brief base class for a publisher internal
         */
        struct PublisherIntl
        {
            explicit PublisherIntl( PubDebugNotices* debug )
                : debug_hooks_{ safe_debug( debug ) }
            {
            }

            void
            load_plugin(
                std::shared_ptr< pub_sub::plugins::PublisherPluginApi > plugin )
            {
                plugins_.emplace_back( std::move( plugin ) );
            }
            void
            add_destination( const std::string& address )
            {
                auto pipeline_stages =
                    pub_sub::detail::parse_pipeline( address );

                if ( pipeline_stages.empty( ) )
                {
                    throw std::runtime_error( "Invalid connection string" );
                }
                std::reverse( pipeline_stages.begin( ),
                              pipeline_stages.end( ) );

                pub_sub::plugins::UniquePublisherInstance cur_stage = nullptr;

                for ( const auto& stage_name : pipeline_stages )
                {
                    auto it = std::find_if(
                        plugins_.begin( ),
                        plugins_.end( ),
                        [&stage_name](
                            const pub_sub::plugins::SharedPublisherPlugin&
                                plugin ) -> bool {
                            return stage_name.find( plugin->prefix( ) ) == 0;
                        } );
                    if ( it == plugins_.end( ) )
                    {
                        std::ostringstream os;
                        os << "Could not find plugin to handle " << stage_name;
                        throw std::runtime_error( os.str( ) );
                    }
                    cur_stage = ( *it )->publish(
                        stage_name, *debug_hooks_, std::move( cur_stage ) );
                }
                if ( cur_stage == nullptr )
                {
                    throw std::runtime_error( "Unconfigured publisher" );
                }
                publishers_.emplace_back( std::move( cur_stage ) );
            }

            /*!
             * @brief Insert a message to be published
             * @param key
             * @param msg
             * @note This is called from the main application thread and
             * sends its data in a thread safe manner to the background thread
             */
            void
            publish( KeyType key, Message msg )
            {
                std::for_each(
                    publishers_.begin( ),
                    publishers_.end( ),
                    [key,
                     &msg]( pub_sub::plugins::UniquePublisherInstance& pub ) {
                        pub->publish( key, msg );
                    } );
            }

        private:
            static PubDebugNotices*
            safe_debug( PubDebugNotices* debug )
            {
                return ( debug ? debug : detail::NullPubDebugNotices( ) );
            }

            std::vector< pub_sub::plugins::UniquePublisherInstance >
                                                                   publishers_{};
            std::vector< pub_sub::plugins::SharedPublisherPlugin > plugins_{};
            PubDebugNotices* debug_hooks_{ nullptr };
        };

        std::unique_ptr< detail::PublisherIntl >
        create_publisher_intl( PubDebugNotices* debug );

    } // namespace detail
} // namespace pub_sub

#endif // PUB_SUB2_PUB_INTL_HH
