/*
 * 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, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#ifndef __BLI_RESOURCE_COLLECTOR_HH__
#define __BLI_RESOURCE_COLLECTOR_HH__

/** \file
 * \ingroup bli
 *
 * A ResourceCollector holds an arbitrary set of resources, that will be destructed and/or freed
 * when the ResourceCollector is destructed. This is useful when some object has to take ownership
 * of other objects, but it does not know the type of those other objects.
 *
 * Resources owned by the ResourceCollector will be freed in reverse order. That allows resources
 * that are added later to depend on resources that have been added before.
 */

#include "BLI_linear_allocator.hh"
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"

namespace blender {

class ResourceCollector : NonCopyable, NonMovable {
 private:
  struct ResourceData {
    void *data;
    void (*free)(void *data);
    const char *debug_name;
  };

  LinearAllocator<> m_allocator;
  Vector<ResourceData> m_resources;

 public:
  ResourceCollector() = default;

  ~ResourceCollector()
  {
    /* Free in reversed order. */
    for (int64_t i = m_resources.size(); i--;) {
      ResourceData &data = m_resources[i];
      data.free(data.data);
    }
  }

  /**
   * Pass ownership of the resource to the ResourceCollector. It will be destructed and freed when
   * the collector is destructed.
   */
  template<typename T> void add(std::unique_ptr<T> resource, const char *name)
  {
    BLI_assert(resource.get() != nullptr);
    this->add(
        resource.release(),
        [](void *data) {
          T *typed_data = reinterpret_cast<T *>(data);
          delete typed_data;
        },
        name);
  }

  /**
   * Pass ownership of the resource to the ResourceCollector. It will be destructed when the
   * collector is destructed.
   */
  template<typename T> void add(destruct_ptr<T> resource, const char *name)
  {
    /* There is no need to keep track of such types. */
    if (std::is_trivially_destructible_v<T>) {
      resource.release();
      return;
    }

    BLI_assert(resource.get() != nullptr);
    this->add(
        resource.release(),
        [](void *data) {
          T *typed_data = reinterpret_cast<T *>(data);
          typed_data->~T();
        },
        name);
  }

  /**
   * Pass ownership of some resource to the ResourceCollector. The given free function will be
   * called when the collector is destructed.
   */
  void add(void *userdata, void (*free)(void *), const char *name)
  {
    ResourceData data;
    data.debug_name = name;
    data.data = userdata;
    data.free = free;
    m_resources.append(data);
  }

  /**
   * Returns a reference to a linear allocator that is owned by the ResourcesCollector. Memory
   * allocated through this allocator will be freed when the collector is destructed.
   */
  LinearAllocator<> &linear_allocator()
  {
    return m_allocator;
  }

  /**
   * Utility method to construct an instance of type T that will be owned by the ResourceCollector.
   */
  template<typename T, typename... Args> T &construct(const char *name, Args &&... args)
  {
    T *value = m_allocator.construct<T>(std::forward<Args>(args)...);
    this->add(destruct_ptr<T>(value), name);
    return *value;
  }

  /**
   * Print the names of all the resources that are owned by this ResourceCollector. This can be
   * useful for debugging.
   */
  void print(StringRef name) const
  {
    if (m_resources.size() == 0) {
      std::cout << "\"" << name << "\" has no resources.\n";
      return;
    }
    else {
      std::cout << "Resources for \"" << name << "\":\n";
      for (const ResourceData &data : m_resources) {
        std::cout << "  " << data.data << ": " << data.debug_name << '\n';
      }
    }
  }
};

}  // namespace blender

#endif /* __BLI_RESOURCE_COLLECTOR_HH__ */
