#pragma once
#include "Tracked.h"
#include "Utils.h"
#include <boost/tuple/tuple.hpp>
#include <boost/type_traits.hpp>
#include <iostream>
#include <cassert>
#include <map>

template <class ReferenceCounted>
class AutoPtr;

/**
 * @brief Reference-counted objects host a reference counter which triggers the deletion of said object
 *        when it reaches 0. 
 *        The acquire() and release() methods are used to, respectively, increment and decrement 
 *        the reference counter. Alternatively, one can use AutoPtr<T> to automatically manage the 
 *        necessary acquisition and releases.
 *        Additional debugging features are added if _DEBUG_COMPONENT is set, including tracking
 *        the individual instances to detect memory leaks.
 */
class ReferenceCounted : public virtual Tracked
{
public:
  ReferenceCounted()
    : m_nbRef(0)
  {
  }

  ReferenceCounted(const ReferenceCounted&)
    : m_nbRef(0)
  {
  }

  virtual ~ReferenceCounted()
  {
  }

  void _markAsStatic()
  {
    // This reference is never going to be released as to not accidentally delete a static object
    ++m_nbRef;
  }

  /**
   * @brief Adds an another acquisition by the specified acquirer, which increments the reference count.
   * @tparam Acquirer Type of acquirer.
   * @param acquirer The object which acquires this reference-counted instance.
   */
  template <class Acquirer>
  void acquire(const Acquirer *acquirer) const
  {
    ++m_nbRef;

    _addAcquirer(acquirer);
  }

  /**
   * @brief Releases the acquisition made by a previous acquirer, which decrements this reference count.
   *        If the counter reaches 0, the instance is deleted.
   * @tparam Acquirer Type of acquirer.
   * @param acquirer The object which previously acquired this reference-counted instance.
   */
  template <class Acquirer>
  void release(const Acquirer *acquirer) const
  {
    _removeAcquirer(acquirer);

    if (--m_nbRef == 0)
    {
      delete this;
    }
  }

  /**
   * @brief Deletes this reference-counted instance if it is not referenced any more.
   */
  void considerDeletion() const
  {
    if (m_nbRef == 0)
    {
      delete this;
    }
  }

  ReferenceCounted& operator=(const ReferenceCounted&)
  {
    // Do not modify ref-count (same semantics as copy constructor)
    return *this;
  }

#ifdef _DEBUG_COMPONENT
  /**
   * @brief Prints memory leaks.
   */
  static size_t _signalMemoryLeaks();
#endif

protected:
  template <class ReferenceCounted>
  friend class AutoPtr;

  template <class Acquirer>
  void _addAcquirer(const Acquirer* acquirer) const
  {
#ifdef _DEBUG_COMPONENT
    std::pair<std::map<const void*, boost::tuple<const type_info*, size_t, bool>>::iterator, bool> result = 
      m_acquirers.insert(std::make_pair(_convertToVoidPtr(acquirer), 
                                        boost::make_tuple(&typeid(acquirer), 0, _isReferenceCounted(acquirer))));

    ++(result.first->second.get<1>());
#else
    unused(acquirer);
#endif
  }

  template <class Acquirer>
  void _removeAcquirer(const Acquirer* acquirer) const
  {
#ifdef _DEBUG_COMPONENT
    std::map<const void*, boost::tuple<const type_info*, size_t, bool>>::iterator it = m_acquirers.find(_convertToVoidPtr(acquirer));
    if (it == m_acquirers.end()) {
      const char* typeName = typeid(acquirer).name();
      std::cout << "Error: '" << typeName
                << " " << std::hex << _convertToVoidPtr(acquirer) << "' attempted to release '"
                << *this << "' without acquiring it first" << std::endl;
    }
    else
    {
      if (--(it->second.get<1>()) == 0)
        m_acquirers.erase(it);
    }
#else
    unused(acquirer);
#endif
  }

  struct ReferenceCountedType
  {
    template <class T>
    static const void* _convertToVoidPtr(const T* t)
    {
      const ReferenceCounted* r = dynamic_cast<const ReferenceCounted*>(t);
      assert(r != NULL);
      return r;
    }
    static void considerDeletion(const ReferenceCounted& referenceCounted)
    {
      referenceCounted.considerDeletion();
    }
  };

  struct NotReferenceCountedType
  {
    template <class T>
    static const void* _convertToVoidPtr(const T* t)
    {
      return static_cast<const void*>(t);
    }
    template <class T>
    static void considerDeletion(const T& t)
    {
      delete &t;
    }
  };

  template <class Acquirer>
  static const void* _convertToVoidPtr(const Acquirer* acquirer)
  {
    return static_if_else<boost::is_convertible<Acquirer*, ReferenceCounted*>::value, 
                          ReferenceCountedType, 
                          NotReferenceCountedType>::type::_convertToVoidPtr(acquirer);
  }

  template <class Acquirer>
  static bool _isReferenceCounted(const Acquirer* acquirer)
  {
    unused(acquirer);
    return boost::is_convertible<Acquirer*, ReferenceCounted*>::value;
  }

  mutable size_t m_nbRef;

#ifdef _DEBUG_COMPONENT
  mutable std::map<const void*, boost::tuple<const type_info*, size_t, bool>> m_acquirers;
#endif

};

template <class ReferenceCounted>
class AutoPtr
{
public:
  template <class Acquirer>
  AutoPtr(const Acquirer& acquirer, ReferenceCounted* ref)
    : m_ref(ref)
    , m_acquirer(ReferenceCounted::_convertToVoidPtr(&acquirer))
  {
    if (m_ref != NULL)
      m_ref->acquire(&acquirer);
  }
  AutoPtr(const AutoPtr& other)
    : m_ref(other.m_ref)
    , m_acquirer(other.m_acquirer)
  {
    if (m_ref != NULL)
      m_ref->acquire(m_acquirer);
  }
  ~AutoPtr()
  {
    if (m_ref != NULL)
      m_ref->release(m_acquirer);
  }
  ReferenceCounted* operator->() const
  {
    return m_ref;
  }
  ReferenceCounted& operator*() const
  {
    return *m_ref;
  }
  operator ReferenceCounted*() const
  {
    return m_ref;
  }
  bool operator<(const AutoPtr& other) const
  {
    return m_ref < other.m_ref;
  }
  AutoPtr& operator=(ReferenceCounted* ref)
  {
    if (ref != NULL)
      ref->acquire(m_acquirer);
    if (m_ref != NULL)
      m_ref->release(m_acquirer);
    m_ref = ref;
    return *this;
  }

  AutoPtr& operator=(const AutoPtr& other)
  {
    if (other.m_ref != NULL)
      other.m_ref->acquire(m_acquirer);
    if (m_ref != NULL)
      m_ref->release(m_acquirer);
    m_ref = other.m_ref;
    return *this;
  }

protected:
  const void* const m_acquirer;
  ReferenceCounted* m_ref;
};

namespace memory
{
  // These methods are wrapped in 'memory' to avoid name clashes with ReferenceCounted methods (like acquire())

  template <class Acquirer>
  void acquire(const Acquirer* acquirer, const ReferenceCounted& referenceCounted)
  {
    referenceCounted.acquire(acquirer);
  }

  template <class Acquirer, class T>
  void acquire(const Acquirer* acquirer, const T* const t)
  {
    acquire(acquirer, *t);
  }

  template <class Acquirer, class T>
  void acquire(const Acquirer* acquirer, T* const t)
  {
    acquire(acquirer, *t);
  }

  template <class Acquirer, class Iterator>
  void acquire(const Acquirer* acquirer, Iterator begin, Iterator end)
  {
    for (; begin != end; ++begin)
    {
      acquire(acquirer, *begin);
    }
  }

  template <class Acquirer>
  void release(const Acquirer* acquirer, const ReferenceCounted& referenceCounted)
  {
    referenceCounted.release(acquirer);
  }

  template <class Acquirer, class T>
  void release(const Acquirer* acquirer, const T* const t)
  {
    release(acquirer, *t);
  }

  template <class Acquirer, class T>
  void release(const Acquirer* acquirer, T* const t)
  {
    release(acquirer, *t);
  }

  template <class Acquirer, class Iterator>
  void release(const Acquirer* acquirer, Iterator begin, Iterator end)
  {
    for (; begin != end; ++begin)
    {
      release(acquirer, *begin);
    }
  }

  inline void considerDeletion(const ReferenceCounted& referenceCounted)
  {
    referenceCounted.considerDeletion();
  }

  template <class T>
  void considerDeletion(const T* const t)
  {
    considerDeletion(*t);
  }

  template <class T>
  void considerDeletion(T* const t)
  {
    considerDeletion(*t);
  }

  template <class Iterator>
  void considerDeletion(Iterator begin, Iterator end)
  {
    for (; begin != end; ++begin)
    {
      considerDeletion(*begin);
    }
  }
};

template <class T>
std::string toString(const AutoPtr<T>& ptr)
{
  return toString(*ptr);
}
