/*
 * Copyright (C) 2013-2017 Canonical Ltd.
 * Copyright (C) 2022 SUSE Software Solutions Germany GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by: Jussi Pakkanen <jussi.pakkanen@canonical.com>
 *              Pete Woods <pete.woods@canonical.com>
 *              Benjamin Zeller <bzeller@suse.com>
 */

#ifndef ZYPP_GLIB_UTIL_GOBJECTMEMORY_H
#define ZYPP_GLIB_UTIL_GOBJECTMEMORY_H

#include <memory>
#include <stdexcept>
#include <glib-object.h>

#include <zypp-glib/utils/RetainPtr>
#include <zypp-glib/utils/ResourcePtr>
#include <zypp-glib/utils/GLibMemory>

namespace zypp::glib {

template<typename T>
using GObjectPtr = RetainPtr<T, typename GLibTypeTraits<T>::RetainTrait>;

namespace {
    inline static void check_floating_gobject(gpointer t) {
        if (G_IS_OBJECT(t) && g_object_is_floating(G_OBJECT(t))) {
            throw std::invalid_argument("cannot manage floating GObject reference - call g_object_ref_sink(o) first");
        }
    }
}

namespace internal {

    template <typename Type>
    Type *zypp_gobject_ref_helper( Type *obj ) {
        return g_object_ref(obj);
    }

    template<typename SP>
    class GObjectAssigner
    {
    public:
        using ElementType = typename SP::element_type ;

        GObjectAssigner(SP& smart_ptr) noexcept:
                smart_ptr_(smart_ptr)
        {
        }

        GObjectAssigner(const GObjectAssigner& other) = delete;

        GObjectAssigner(GObjectAssigner&& other) noexcept:
                ptr_(other.ptr_), smart_ptr_(other.smart_ptr_)
        {
            other.ptr_ = nullptr;
        }

        ~GObjectAssigner() noexcept
        {
            smart_ptr_ = SP(ptr_, GLibTypeTraits<ElementType>());
        }

        GObjectAssigner& operator=(const GObjectAssigner& other) = delete;

        operator ElementType**() noexcept
        {
            return &ptr_;
        }

    private:
        ElementType* ptr_ = nullptr;

        SP& smart_ptr_;
    };

    template <typename T>
    struct GObjectSignalUnsubscriber
    {
        void operator()(gulong handle) noexcept
        {
            if (handle != 0 && G_IS_OBJECT(obj_.get())) {
                g_signal_handler_disconnect(obj_.get(), handle);
            }
        }

        GObjectPtr<T> obj_;
    };
}

/*!
 * Simple wrapper around a GObject weak pointer.
 * Currently this is not thread safe but we could consider to switch to GWeakRef if required
 */
template <typename T>
class GObjectWeakPtr
{
    public:
        GObjectWeakPtr( ) : _weakRef( nullptr ) { }

        GObjectWeakPtr( T *ptr ) : _weakRef( nullptr ) {
            g_set_weak_pointer( &_weakRef, ptr );
        }

        GObjectWeakPtr ( const GObjectWeakPtr & other ) {
            if ( other ) {
                g_set_weak_pointer( &_weakRef, other._weakRef );
            }
        }

        ~GObjectWeakPtr() {
            reset();
        }

        void operator= ( const GObjectWeakPtr &other ) {
            reset( other._weakRef );
        }

        void reset ( T* ptr = nullptr ) {
            g_clear_weak_pointer( &_weakRef );
            if ( ptr ) {
                g_set_weak_pointer( &_weakRef, ptr );
            }
        }

        operator bool () const {
            return (_weakRef != nullptr);
        }

        GObjectPtr<T> lock () const {
            if ( _weakRef )
                return { _weakRef, retain_object };
            return {};
        }

        T* weakGet() const{
          return _weakRef;
        }

    private:
        T* _weakRef;
};

} // namespace zypp::glib


#define ZYPP_DEFINE_GOBJECTSMARTPTR(  ModuleObjName, module_obj_name, MODULE, OBJ_NAME ) \
  namespace zypp::glib { \
    using ModuleObjName##WeakRef = GObjectWeakPtr<ModuleObjName>; \
    \
    template <typename Target, typename _Up> \
    std::enable_if_t< std::is_same_v<Target,ModuleObjName>, \
    ModuleObjName##Ref > gobject_pointer_cast( const GObjectPtr<_Up> &o ) { \
        if ( !MODULE##_IS_##OBJ_NAME(o.get()) ) \
            return {}; \
        \
        return ModuleObjName##Ref( MODULE##_##OBJ_NAME( o.get() ), retain_object ); \
    } \
  }

// return unique_gobject(G_TYPE_CHECK_INSTANCE_CAST(ptr, object_type, T));

#define ZYPP_DEFINE_GOBJECTSMARTPTR_CREATE( ModuleObjName, module_obj_name, MODULE, OBJ_NAME ) \
  namespace zypp::glib { \
    template <typename ...Args> \
    ModuleObjName##Ref module_obj_name##_create ( gchar *first_property_name, Args&&... args ) { \
        gpointer ptr = g_object_new( module_obj_name##_get_type(), first_property_name, std::forward<Args>(args)..., nullptr ); \
        if (G_IS_OBJECT(ptr) && g_object_is_floating(ptr)) { \
            g_object_ref_sink(ptr); \
        } \
        \
        return ModuleObjName##Ref( MODULE##_##OBJ_NAME( ptr ), adopt_object ); \
    } \
  }


#define ZYPP_GLIB_ADD_GLIB_GOBJECTTRAIT( ModuleObjName, module_obj_name ) \
    struct GObjectTrait { \
        static auto gobjectType() { \
            return module_obj_name##_get_type();\
        } \
        static auto gobjectCast( GObject *object ) { \
          return (G_TYPE_CHECK_INSTANCE_CAST ((object), gobjectType(), ModuleObjName)); \
        } \
    }; \
    static constexpr auto is_g_object_type = true;


/**
 * @brief Convencience macro to define the standard traits and smart pointer types for GObject types
 */
#define ZYPP_DEFINE_GOBJECT_SIMPLE( ModuleObjName, module_obj_name, MODULE, OBJ_NAME, ...) \
ZYPP_GLIB_DEFINE_GLIB_REF_TYPE_FULL( ModuleObjName, zypp::glib::internal::zypp_gobject_ref_helper, g_object_unref, \
    ZYPP_GLIB_ADD_GLIB_GOBJECTTRAIT( ModuleObjName, module_obj_name ) \
    __VA_ARGS__ \
)\
ZYPP_DEFINE_GOBJECTSMARTPTR( ModuleObjName, module_obj_name, MODULE, OBJ_NAME ) \
ZYPP_DEFINE_GOBJECTSMARTPTR_CREATE( ModuleObjName, module_obj_name, MODULE, OBJ_NAME )

// define the GObject smart pointers and traits
ZYPP_GLIB_DEFINE_GLIB_REF_TYPE_FULL( GObject, zypp::glib::internal::zypp_gobject_ref_helper, g_object_unref,  ZYPP_GLIB_ADD_GLIB_GOBJECTTRAIT( GObject, g_object ) )
ZYPP_DEFINE_GOBJECTSMARTPTR( GObject, g_object, G, OBJECT )

namespace zypp::glib {

/**
 \brief Helper method to wrap a shared_ptr around an existing GObject.

 Useful if the GObject class you are constructing already has a
 dedicated factory from the C library it comes from, and you
 intend to share it.

 Example:
 \code{.cpp}
 auto obj = adopt_gobject(foo_bar_new("name"));
 \endcode
 */
template<typename T>
inline GObjectPtr<T> adopt_gobject(T* ptr)
{
    check_floating_gobject(ptr);
    return GObjectPtr<T>( ptr, adopt_object );
}

/**
 \brief Helper method to construct a gobj_ptr-wrapped GObject class.

 Uses the same signature as the g_object_new() method.
 Requires the zyppng GObject Type Traits.

 Example:
 \code{.cpp}
 auto obj = g_object_create<FooBar>("name", "banana", nullptr);
 \endcode
 */
template<typename Obj, typename ... Args>
inline GObjectPtr<Obj> g_object_create( const gchar *first_property_name = nullptr, Args&&... args ) noexcept
{
  gpointer ptr = g_object_new( GLibTypeTraits<Obj>::GObjectTrait::gobjectType(), first_property_name, std::forward<Args>(args)..., nullptr );
  if (G_IS_OBJECT(ptr) && g_object_is_floating(ptr))
  {
    g_object_ref_sink(ptr);
  }
  return GObjectPtr<Obj>( GLibTypeTraits<Obj>::GObjectTrait::gobjectCast( G_OBJECT(ptr)), adopt_object );
}

/**
 \brief Helper method to take ownership of GObjects assigned from a reference.

 Example:
 \code{.cpp}
 GObjectRefPtr o;
 method_that_assigns_a_foobar(assign_gobject(o));
 \endcode
 */
template<typename SP>
inline internal::GObjectAssigner<SP> assign_gobject(SP& smart_ptr) noexcept
{
    return internal::GObjectAssigner<SP>(smart_ptr);
}

template<typename T>
using GObjectSignalConnection = ResourcePtr<gulong, internal::GObjectSignalUnsubscriber<T>>;

/**
 \brief Simple wrapper to manage the lifecycle of GObject signal connections.

 When 'nameConnection_' goes out of scope or is dealloc'ed, the source will be removed:
 \code{.cpp}
 GObjectSignalConnection<FooBar> nameConnection_;
 nameConnection_ = gobject_signal_connection(g_signal_connect(o.get(), "notify::name", G_CALLBACK(on_notify_name), this), o);
 \endcode
 */
template <typename T>
inline GObjectSignalConnection<T> gobject_signal_connection( gulong id, const stdx::retain_ptr<T, typename GLibTypeTraits<T>::RetainTrait> &obj )
{
    return GObjectSignalConnection<T>(id, internal::GObjectSignalUnsubscriber<T>{obj});
}

} // namespace zypp::glib

#endif
