#ifndef GI_BOXED_HPP
#define GI_BOXED_HPP

#include <memory>

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

namespace gi
{
namespace detail
{
// class tags
class Boxed
{};
class CBoxed : public Boxed
{};
class GBoxed : public Boxed
{};

template<typename CType>
class SharedWrapper
{
public:
  typedef SharedWrapper self;
  typedef CType BaseObjectType;

protected:
  std::shared_ptr<CType> data_;

public:
  CType *gobj_() { return data_.get(); }
  const CType *gobj_() const { return data_.get(); }

  explicit operator bool() { return (bool)data_; }

  bool operator==(const SharedWrapper &other) const
  {
    return data_ == other.data_;
  }

  bool operator==(std::nullptr_t o) const { return data_ == o; }

  bool operator!=(const SharedWrapper &other) const
  {
    return data_ != other.data_;
  }

  bool operator!=(std::nullptr_t o) const { return data_ != o; }

  bool use_count() const { return data_ ? data_.use_count() : 0; }
};

template<typename CppType, typename CType>
struct GBoxedFuncs
{
  static CType *copy(const void *data)
  {
    return (CType *)g_boxed_copy(CppType::get_type_(), data);
  }
  static void free(void *data) { g_boxed_free(CppType::get_type_(), data); }
};

struct CBoxedFuncsBase
{
  static void free(void *data) { g_free(data); }
};

template<typename CppType, typename CType>
struct CBoxedFuncs : CBoxedFuncsBase
{
  static CType *copy(const void *data)
  {
#if GLIB_CHECK_VERSION(2, 68, 0)
    return (CType *)g_memdup2(data, sizeof(CType));
#else
    return (CType *)g_memdup(data, sizeof(CType));
#endif
  }
};

template<typename CType, typename Funcs, typename TagType>
class BoxedWrapper : public SharedWrapper<CType>, public TagType
{
  typedef BoxedWrapper self;

protected:
  static void _deleter(CType *obj)
  {
    if (obj)
      Funcs::free(obj);
  }

  static void _dummy_deleter(CType *) {}

  static CType *_copy(const CType *obj)
  {
    return obj ? Funcs::copy(obj) : nullptr;
  }

public:
  BoxedWrapper(CType *obj = nullptr, bool own = true, bool copy = true)
  {
    this->data_ = std::shared_ptr<CType>(own ? obj : (copy ? _copy(obj) : obj),
        own || (!own && copy) ? _deleter : _dummy_deleter);
  }

  CType *gobj_copy_() const { return _copy(this->gobj_()); }

  template<typename Cpp>
  static Cpp wrap(const typename Cpp::BaseObjectType *obj, bool own, bool copy)
  {
    static_assert(sizeof(Cpp) == sizeof(self), "type wrap not supported");
    static_assert(std::is_base_of<self, Cpp>::value, "type wrap not supported");
    BoxedWrapper w(const_cast<typename Cpp::BaseObjectType *>(obj), own, copy);
    return *static_cast<Cpp *>(&w);
  }
};

// in templates below, Base should be a subclass of BoxedWrapper
// so we re-use the members it provides, as well as the wrap template
// to avoid ambiguous reference to the latter
// (if BoxedWrapper were inherited from again)

// the nullptr_t constructor (indirectly) supports `= nullptr` (assignment)

// basis for registered boxed types
template<typename CppType, typename CType,
    typename Base = BoxedWrapper<CType, GBoxedFuncs<CppType, CType>, GBoxed>>
class GBoxedWrapper : public Base
{
  typedef Base super;

public:
  using super::super;

  GBoxedWrapper(std::nullptr_t = nullptr) {}

  void allocate_()
  {
    if (this->data_)
      return;
    // make sure we match boxed allocation with boxed free
    // still guessing here that all-0 makes for a decent init :-(
    CType tmp;
    memset(&tmp, 0, sizeof(tmp));
    this->data_.reset(
        (CType *)g_boxed_copy(this->get_type_(), &tmp), this->_deleter);
  }

  // essentially g_boxed_copy that can be overridden by subclass if so
  // provided
  CppType copy()
  {
    return super::template wrap<CppType>(this->gobj_copy_(), true, false);
  }
};

// basis for non-registered plain C boxed type
template<typename CppType, typename CType,
    typename Base = BoxedWrapper<CType, CBoxedFuncs<CppType, CType>, CBoxed>>
class CBoxedWrapper : public Base
{
  typedef Base super;

public:
  CBoxedWrapper(std::nullptr_t = nullptr) {}

  void allocate_()
  {
    if (this->data_)
      return;
    this->data_.reset(g_new0(CType, 1), this->_deleter);
  }

  static CppType new_()
  {
    return super::template wrap<CppType>(g_new0(CType, 1), true, false);
  }
};

// allocate helper;
// dispatch to method if available
template<typename T, typename Enable = void>
struct allocator : public std::false_type
{
  static void allocate(T &) {}
};

template<typename T>
struct allocator<T, decltype(T().allocate_())> : public std::true_type
{
  static void allocate(T &v) { v.allocate_(); }
};

template<typename T>
void
allocate(T &v)
{
  allocator<T>::allocate(v);
}

} // namespace detail

} // namespace gi

#endif // GI_BOXED_HPP
