/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2016-20 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>

/**
 * A 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_;
    };

} // namespace DETAIL


/**
 * 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:
    /**
     * Construct an observable with empty non-shared subscription list.
     */
    OBSERVABLE() {}

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

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

    /**
     * Add 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 ) );
    }

    /**
     * Cancel the subscription of a subscriber.
     *
     * This 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 ) );
    }

    /**
     * Notify event to all subscribed observers.
     *
     * @param Ptr is a pointer to method of the observer interface.
     * @param aArgs is a 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();
        }
    }

    /**
     * Notify event to all subscribed observers but one to be ignore.
     *
     * @param Ptr is a pointer to method of the observer interface.
     * @param aIgnore is an observer to ignore during this notification.
     * @param aArgs is a 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();
        }
    }
};

} // namespace UTIL

#endif