/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <list>
#include <mutex>
#include <utility>  // std::pair

namespace mrpt::system
{
/** A generic system for versatile memory pooling.
 *   This class implements the singleton pattern so a unique instance exists
 *for each combination of template parameters.
 *   All methods are thread-safe.
 *
 *   Basic usage:
 *     - When needed, call \a request_memory() to check the availability of
 *memory in the pool.
 *		- At your class destructor, donate the memory to the pool with \a
 *dump_to_pool().
 *
 *   Notice that memory requests are checked against memory blocks in the pool
 *via a user-defined function:
 *
 *    bool POOLABLE_DATA::isSuitable(const POOLABLE_DATA & req) const { ... }
 *
 *   For an example of how to handle a memory pool, see the class
 *mrpt::obs::CObservation3DRangeScan
 *
 *  \tparam POOLABLE_DATA A struct with user-defined objects which actually
 *contain the memory blocks (e.g. one or more std::vector).
 *  \tparam DATA_PARAMS A struct with user information about each memory block
 *(e.g. size of a std::vector)
 * \ingroup mrpt_memory
 */
template <class DATA_PARAMS, class POOLABLE_DATA>
class CGenericMemoryPool
{
 private:
  using TList = std::list<std::pair<DATA_PARAMS, POOLABLE_DATA*>>;
  TList m_pool;
  std::mutex m_pool_cs;
  size_t m_maxPoolEntries;
  /** With this trick we get rid of the "global destruction order fiasco" ;-)
   */
  bool& m_was_destroyed;

  CGenericMemoryPool(size_t max_pool_entries, bool& was_destroyed) :
      m_maxPoolEntries(max_pool_entries), m_was_destroyed(was_destroyed)
  {
    m_was_destroyed = false;
  }

 public:
  inline size_t getMemoryPoolMaxSize() const { return m_maxPoolEntries; }
  inline void setMemoryPoolMaxSize(size_t maxNumEntries) { m_maxPoolEntries = maxNumEntries; }

  /** Construct-on-first-use (~singleton) pattern: Return the unique instance
   * of this class for a given template arguments,
   *  or nullptr if it was once created but it's been destroyed (which means
   * we're in the program global destruction phase).
   */
  static CGenericMemoryPool<DATA_PARAMS, POOLABLE_DATA>* getInstance(size_t max_pool_entries = 5)
  {
    static bool was_destroyed = false;
    static CGenericMemoryPool<DATA_PARAMS, POOLABLE_DATA> inst(max_pool_entries, was_destroyed);
    return was_destroyed ? nullptr : &inst;
  }

  /** Request a block of data which fulfils the size requirements stated in \a
   * params.
   *  Notice that the decision on the suitability of each pool'ed block is
   * done by DATA_PARAMS::isSuitable().
   *  \return The block of data, or nullptr if none suitable was found in the
   * pool.
   *  \note It is a responsibility of the user to free with "delete" the
   * "POOLABLE_DATA" object itself once the memory has been extracted from its
   * elements.
   */
  POOLABLE_DATA* request_memory(const DATA_PARAMS& params)
  {
    // A quick check first:
    if (m_pool.empty()) return nullptr;

    std::lock_guard<std::mutex> lock(m_pool_cs);
    for (auto it = m_pool.begin(); it != m_pool.end(); ++it)
    {
      if (it->first.isSuitable(params))
      {
        POOLABLE_DATA* ret = it->second;
        m_pool.erase(it);
        return ret;
      }
    }
    return nullptr;
  }

  /** Saves the passed data block (characterized by \a params) to the pool.
   *  If the overall size of the pool is above the limit, the oldest entry is
   * removed.
   *  \note It is a responsibility of the user to allocate in dynamic memory
   * the "POOLABLE_DATA" object with "new".
   */
  void dump_to_pool(const DATA_PARAMS& params, POOLABLE_DATA* block)
  {
    std::lock_guard<std::mutex> lock(m_pool_cs);

    while (m_pool.size() >= m_maxPoolEntries)  // Free old data if needed
    {
      if (m_pool.begin()->second) delete m_pool.begin()->second;
      m_pool.erase(m_pool.begin());
    }

    m_pool.push_back(typename TList::value_type(params, block));
  }

  ~CGenericMemoryPool()
  {
    m_was_destroyed = true;
    // Free remaining memory blocks:
    std::lock_guard<std::mutex> lock(m_pool_cs);
    for (auto it = m_pool.begin(); it != m_pool.end(); ++it) delete it->second;
    m_pool.clear();
  }
};

}  // namespace mrpt::system
