/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under both the BSD-style license (found in the
 * LICENSE file in the root directory of this source tree) and the GPLv2 (found
 * in the COPYING file in the root directory of this source tree).
 */
#pragma once

#include <cassert>
#include <functional>
#include <memory>
#include <mutex>
#include <vector>

namespace pzstd {

/**
 * An unbounded pool of resources.
 * A `ResourcePool<T>` requires a factory function that takes allocates `T*` and
 * a free function that frees a `T*`.
 * Calling `ResourcePool::get()` will give you a new `ResourcePool::UniquePtr`
 * to a `T`, and when it goes out of scope the resource will be returned to the
 * pool.
 * The `ResourcePool<T>` *must* survive longer than any resources it hands out.
 * Remember that `ResourcePool<T>` hands out mutable `T`s, so make sure to clean
 * up the resource before or after every use.
 */
template <typename T>
class ResourcePool {
 public:
  class Deleter;
  using Factory = std::function<T*()>;
  using Free = std::function<void(T*)>;
  using UniquePtr = std::unique_ptr<T, Deleter>;

 private:
  std::mutex mutex_;
  Factory factory_;
  Free free_;
  std::vector<T*> resources_;
  unsigned inUse_;

 public:
  /**
   * Creates a `ResourcePool`.
   *
   * @param factory  The function to use to create new resources.
   * @param free     The function to use to free resources created by `factory`.
   */
  ResourcePool(Factory factory, Free free)
      : factory_(std::move(factory)), free_(std::move(free)), inUse_(0) {}

  /**
   * @returns  A unique pointer to a resource.  The resource is null iff
   *           there are no available resources and `factory()` returns null.
   */
  UniquePtr get() {
    std::lock_guard<std::mutex> lock(mutex_);
    if (!resources_.empty()) {
      UniquePtr resource{resources_.back(), Deleter{*this}};
      resources_.pop_back();
      ++inUse_;
      return resource;
    }
    UniquePtr resource{factory_(), Deleter{*this}};
    ++inUse_;
    return resource;
  }

  ~ResourcePool() noexcept {
    assert(inUse_ == 0);
    for (const auto resource : resources_) {
      free_(resource);
    }
  }

  class Deleter {
    ResourcePool *pool_;
  public:
    explicit Deleter(ResourcePool &pool) : pool_(&pool) {}

    void operator() (T *resource) {
      std::lock_guard<std::mutex> lock(pool_->mutex_);
      // Make sure we don't put null resources into the pool
      if (resource) {
        pool_->resources_.push_back(resource);
      }
      assert(pool_->inUse_ > 0);
      --pool_->inUse_;
    }
  };
};

}
