/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2016 Kicad Developers, see change_log.txt for contributors.
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/

#ifndef COMMON_OBSERVABLE_H__
#define COMMON_OBSERVABLE_H__

#include <cassert>
#include <memory>
#include <vector>
#include <utility>

/*
model subscriber implementation using links to represent connections.
subscribers can be removed during notification.
if no observers are registered, size is the size of a shared_ptr.
*/

namespace UTIL {

    class LINK;

    namespace DETAIL {

        struct OBSERVABLE_BASE {
        public:
            OBSERVABLE_BASE();
            OBSERVABLE_BASE( OBSERVABLE_BASE& other );

            ~OBSERVABLE_BASE();

            size_t size() const;

        private:
            friend class UTIL::LINK;

            struct IMPL
            {
                IMPL( OBSERVABLE_BASE* owned_by = nullptr );
                bool is_shared() const;
                void set_shared();
                ~IMPL();

                void add_observer( void* observer );
                void remove_observer( void* observer );
                void collect();

                bool is_iterating() const;

                void enter_iteration();
                void leave_iteration();

                std::vector<void*> observers_;
                unsigned int iteration_count_;
                OBSERVABLE_BASE* owned_by_;
            };

            void allocate_impl();
            void allocate_shared_impl();

            void deallocate_impl();

            std::shared_ptr<IMPL> get_shared_impl();

        protected:
            void on_observers_empty();

            void enter_iteration();
            void leave_iteration();

            void add_observer( void* observer );
            void remove_observer( void* observer );

            std::shared_ptr<IMPL> impl_;
        };

    }

    //
    // Simple RAII-handle to a subscription.
    //
    class LINK {
    public:
        LINK();
        LINK( std::shared_ptr<DETAIL::OBSERVABLE_BASE::IMPL> token, void* observer );
        LINK( LINK&& other );
        LINK( const LINK& ) = delete;

        void operator=( const LINK& ) = delete;
        LINK& operator=( LINK&& other );

        void reset();

        explicit operator bool() const;

        ~LINK();

    private:
        std::shared_ptr<DETAIL::OBSERVABLE_BASE::IMPL> token_;
        void* observer_;
    };

    //
    //
    //
    template<typename ObserverInterface>
    class OBSERVABLE :
        public DETAIL::OBSERVABLE_BASE
    {
    public:
        /**
        * Function Observable()
        * Constructor. Constructs an observable with empty non-shared subscription list.
        */
        OBSERVABLE() {}

        /**
        * Function Observable(OBSERVABLE&)
        * Constructor. Constructs an observable with a shared subscription list.
        * @param aInherit Observable to share the subscription list with.
        */
        OBSERVABLE( OBSERVABLE& aInherit )
            : OBSERVABLE_BASE( aInherit )
        {}

        /**
         * Function SubscribeUnmanaged
         * adds a subscription without RAII link.
         * @param aObserver Observer to subscribe
         */
        void SubscribeUnmanaged( ObserverInterface* aObserver )
        {
            OBSERVABLE_BASE::add_observer( static_cast<void*>(aObserver) );
        }

        /**
         * Function Subscribe
         * adds a subscription returning an RAII link
         * @param aObserver observer to subscribe
         * @return RAII link controlling the lifetime of the subscription
         */
        LINK Subscribe( ObserverInterface* aObserver ) {
            OBSERVABLE_BASE::add_observer( static_cast<void*>(aObserver) );
            return LINK( impl_, static_cast<void*>(aObserver) );
        }

        /**
         * Function Unsubscribe
         * cancels the subscription of a subscriber. Can be called during notification calls.
         * @param aObserver observer to remove from the subscription list
         */
        void Unsubscribe( ObserverInterface* aObserver )
        {
            OBSERVABLE_BASE::remove_observer( static_cast<void*>(aObserver) );
        }

        /**
         * Function Notify
         * Notifies event to all subscribed observers.
         * @param Ptr pointer to method of the Observer-interface
         * @param aArgs list of arguments to each notification call, will be perfectly forwarded.
         */
        template< typename... Args1, typename... Args2 >
        void Notify( void(ObserverInterface::*Ptr)(Args1...), Args2&&... aArgs )
        {
            static_assert(sizeof...(Args1) == sizeof...(Args2), "argument counts don't match");

            if( impl_ )
            {
                enter_iteration();
                try {
                    for( auto* void_ptr : impl_->observers_ )
                    {
                        if( void_ptr )
                        {
                            auto* typed_ptr = static_cast<ObserverInterface*>(void_ptr);
                            (typed_ptr->*Ptr)(std::forward<Args2>( aArgs )...);
                        }
                    }
                }
                catch(...)
                {
                    leave_iteration();
                    throw;
                }

                leave_iteration();
            }
        }

        /**
        * Function Notify
        * Notifies event to all subscribed observers but one to be ignore.
        * @param Ptr pointer to method of the Observer-interface
        * @param aIgnore observer to ignore during this notification
        * @param aArgs list of arguments to each notification call, will be perfectly forwarded.
        */
        template< typename... Args1, typename... Args2 >
        void NotifyIgnore( void(ObserverInterface::*Ptr)(Args1...), ObserverInterface* aIgnore,
                           Args2&&... aArgs )
        {
            static_assert(sizeof...(Args1) == sizeof...(Args2), "argument counts don't match");

            if( impl_ )
            {
                enter_iteration();

                try
                {
                    for(auto* void_ptr : impl_->observers_)
                    {
                        if( void_ptr && void_ptr != aIgnore )
                        {
                            auto* typed_ptr = static_cast<ObserverInterface*>(void_ptr);
                            (typed_ptr->*Ptr)(std::forward<Args2>( aArgs )...);
                        }
                    }
                }
                catch(...)
                {
                    leave_iteration();
                    throw;
                }

                leave_iteration();
            }
        }

    };

}

#endif
