/* vim:set ts=2 sw=2 sts=2 et: */
/**
 * \author     Marcus Holland-Moritz (github@mhxnet.de)
 * \copyright  Copyright (c) Marcus Holland-Moritz
 *
 * This file is part of dwarfs.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the “Software”), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * SPDX-License-Identifier: MIT
 */

#include <algorithm>
#include <atomic>
#include <cassert>
#include <chrono>
#include <exception>
#include <future>
#include <iterator>
#include <mutex>
#include <new>
#include <shared_mutex>
#include <utility>
#include <vector>

#include <fmt/format.h>

#include <parallel_hashmap/phmap.h>

#include <dwarfs/file_view.h>
#include <dwarfs/logger.h>
#include <dwarfs/performance_monitor.h>
#include <dwarfs/reader/block_cache_options.h>
#include <dwarfs/reader/cache_tidy_config.h>
#include <dwarfs/scope_exit.h>
#include <dwarfs/util.h>

#include <dwarfs/internal/fs_section.h>
#include <dwarfs/internal/value_stream_quantile_estimator.h>
#include <dwarfs/internal/worker_group.h>
#include <dwarfs/reader/internal/block_cache.h>
#include <dwarfs/reader/internal/block_cache_byte_buffer_factory.h>
#include <dwarfs/reader/internal/cached_block.h>
#include <dwarfs/reader/internal/lru_cache.h>
#include <dwarfs/reader/internal/periodic_executor.h>

namespace dwarfs::reader::internal {

using namespace dwarfs::internal;

namespace {

class sequential_access_detector {
 public:
  virtual ~sequential_access_detector() = default;

  virtual void set_block_count(size_t) = 0;
  virtual void touch(size_t block_no) = 0;
  virtual std::optional<size_t> prefetch() const = 0;
};

class no_sequential_access_detector : public sequential_access_detector {
 public:
  void set_block_count(size_t) override {}
  void touch(size_t) override {}
  std::optional<size_t> prefetch() const override { return std::nullopt; }
};

class lru_sequential_access_detector : public sequential_access_detector {
 public:
  explicit lru_sequential_access_detector(size_t seq_blocks)
      : lru_{seq_blocks}
      , seq_blocks_{seq_blocks} {}

  void set_block_count(size_t num_blocks) override {
    std::lock_guard lock(mx_);
    num_blocks_ = num_blocks;
    lru_.clear();
    is_sequential_.reset();
  }

  void touch(size_t block_no) override {
    std::lock_guard lock(mx_);
    lru_.set(
        block_no, block_no, /* true, */
        // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
        [this](size_t, size_t&&) { is_sequential_.reset(); });
  }

  std::optional<size_t> prefetch() const override {
    std::lock_guard lock(mx_);

    if (lru_.size() < seq_blocks_) {
      return std::nullopt;
    }

    if (is_sequential_.has_value()) {
      return std::nullopt;
    }

    auto minmax = std::ranges::minmax_element(
        lru_, [](auto const& a, auto const& b) { return a.first < b.first; });

    auto min = minmax.min->first;
    auto max = minmax.max->first;

    is_sequential_ = max - min + 1 == seq_blocks_;

    if (*is_sequential_ && max + 1 < num_blocks_) {
      return max + 1;
    }

    return std::nullopt;
  }

 private:
  using lru_type = lru_cache<size_t, size_t>;

  std::mutex mutable mx_;
  lru_type lru_;
  std::optional<bool> mutable is_sequential_;
  size_t num_blocks_{0};
  size_t const seq_blocks_;
};

class block_request {
 public:
  block_request() = default;

  block_request(size_t begin, size_t end, std::promise<block_range>&& promise)
      : begin_(begin)
      , end_(end)
      , promise_(std::move(promise)) {
    DWARFS_CHECK(begin_ < end_, "invalid block_request");
  }

  block_request(block_request&&) = default;
  block_request& operator=(block_request&&) = default;

  bool operator<(block_request const& rhs) const { return end_ < rhs.end_; }

  size_t end() const { return end_; }

  void fulfill(std::shared_ptr<cached_block const> block) {
    promise_.set_value(block_range(std::move(block), begin_, end_ - begin_));
  }

  void error(std::exception_ptr error) {
    promise_.set_exception(std::move(error));
  }

 private:
  size_t begin_{0};
  size_t end_{0};
  std::promise<block_range> promise_;
};

class block_request_set {
 public:
  block_request_set(std::shared_ptr<cached_block> block, size_t block_no)
      : block_(std::move(block))
      , block_no_(block_no) {}

  ~block_request_set() { assert(queue_.empty()); }

  size_t range_end() const { return range_end_; }

  void add(size_t begin, size_t end, std::promise<block_range>&& promise) {
    range_end_ = std::max(end, range_end_);

    queue_.emplace_back(begin, end, std::move(promise));
    // NOLINTNEXTLINE(modernize-use-ranges)
    std::push_heap(queue_.begin(), queue_.end());
  }

  void merge(block_request_set& other) {
    queue_.reserve(queue_.size() + other.queue_.size());
    std::ranges::move(other.queue_, std::back_inserter(queue_));
    other.queue_.clear();
    // NOLINTNEXTLINE(modernize-use-ranges)
    std::make_heap(queue_.begin(), queue_.end());
    range_end_ = std::max(range_end_, other.range_end_);
  }

  block_request get() {
    // NOLINTNEXTLINE(modernize-use-ranges)
    std::pop_heap(queue_.begin(), queue_.end());
    block_request tmp = std::move(queue_.back());
    queue_.pop_back();
    return tmp;
  }

  bool empty() const { return queue_.empty(); }

  std::shared_ptr<cached_block> block() const { return block_; }

  size_t block_no() const { return block_no_; }

 private:
  std::vector<block_request> queue_;
  size_t range_end_{0};
  std::shared_ptr<cached_block> block_;
  size_t const block_no_;
};

// multi-threaded block cache
template <typename LoggerPolicy>
class block_cache_ final : public block_cache::impl {
 public:
  block_cache_(logger& lgr, os_access const& os, file_view const& mm,
               block_cache_options const& options,
               std::shared_ptr<performance_monitor const> const& perfmon
               [[maybe_unused]])
      : cache_(0)
      , tidy_runner_{mx_, {}, "tidy-blkcache", [this] { tidy_cache(); }}
      , mm_{mm}
      , buffer_factory_{block_cache_byte_buffer_factory::create(
            os, options.allocation_mode)}
      , LOG_PROXY_INIT(lgr)
      // clang-format off
      PERFMON_CLS_PROXY_INIT(perfmon, "block_cache")
      PERFMON_CLS_TIMER_INIT(get, "block_no", "offset", "size")
      PERFMON_CLS_TIMER_INIT(process, "block_no")
      PERFMON_CLS_TIMER_INIT(decompress, "range_end") // clang-format on
      , seq_access_detector_{create_seq_access_detector(
            options.sequential_access_detector_threshold)}
      , os_{os}
      , options_(options) {
    cache_.set_prune_hook(
        [this](size_t block_no, std::shared_ptr<cached_block>&& block) {
          on_block_removed("evicted", block_no, std::move(block));
          blocks_evicted_.fetch_add(1, std::memory_order_relaxed);
        });
  }

  ~block_cache_() noexcept override {
    LOG_DEBUG << "stopping cache workers";

    tidy_runner_.stop();

    {
      std::unique_lock lock(mx_wg_);

      if (wg_) {
        wg_.stop();
      }
    }

    if (!blocks_created_.load()) {
      return;
    }

    LOG_DEBUG << "cached blocks:";

    for (auto const& cb : cache_) {
      LOG_DEBUG << "  block " << cb.first << ", decompression ratio = "
                << static_cast<double>(cb.second->range_end()) /
                       static_cast<double>(cb.second->uncompressed_size());
      update_block_stats(*cb.second);
    }

    double fast_hit_rate =
        100.0 * (active_hits_fast_ + cache_hits_fast_) / range_requests_;
    double slow_hit_rate =
        100.0 * (active_hits_slow_ + cache_hits_slow_) / range_requests_;
    double miss_rate = 100.0 - (fast_hit_rate + slow_hit_rate);
    double avg_decompression =
        100.0 * total_decompressed_bytes_ / total_block_bytes_;

    // The same block can be evicted multiple times. Active requests may hold
    // on to a block that has been evicted from the cache and re-insert the
    // block after the request is complete. So it's not a bug to see the
    // number of evicted blocks outgrow the number of created blocks.
    LOG_VERBOSE << "blocks created: " << blocks_created_.load();
    LOG_VERBOSE << "blocks evicted: " << blocks_evicted_.load();
    LOG_VERBOSE << "blocks tidied: " << blocks_tidied_.load();
    LOG_VERBOSE << "request sets merged: " << sets_merged_.load();
    LOG_VERBOSE << "total requests: " << range_requests_.load();
    LOG_VERBOSE << "sequential prefetches: " << sequential_prefetches_.load();
    LOG_VERBOSE << "active hits (fast): " << active_hits_fast_.load();
    LOG_VERBOSE << "active hits (slow): " << active_hits_slow_.load();
    LOG_VERBOSE << "cache hits (fast): " << cache_hits_fast_.load();
    LOG_VERBOSE << "cache hits (slow): " << cache_hits_slow_.load();

    LOG_VERBOSE << "total bytes decompressed: " << total_decompressed_bytes_;
    LOG_VERBOSE << "average block decompression: "
                << fmt::format("{:.1f}", avg_decompression) << "%";

    LOG_VERBOSE << "fast hit rate: " << fmt::format("{:.3f}", fast_hit_rate)
                << "%";
    LOG_VERBOSE << "slow hit rate: " << fmt::format("{:.3f}", slow_hit_rate)
                << "%";
    LOG_VERBOSE << "miss rate: " << fmt::format("{:.3f}", miss_rate) << "%";

    LOG_VERBOSE << "expired active requests: " << active_expired_.load();

    auto pct = [&](double p) { return active_set_size_.quantile(p / 100.0); };

    LOG_VERBOSE << "active set size p50: " << pct(50) << ", p75: " << pct(75)
                << ", p90: " << pct(90) << ", p95: " << pct(95)
                << ", p99: " << pct(99);
  }

  size_t block_count() const override { return block_.size(); }

  void insert(fs_section const& section) override {
    block_.emplace_back(section);
    seq_access_detector_->set_block_count(block_.size());
  }

  void set_block_size(size_t size) override {
    DWARFS_CHECK(size > 0, "block size is zero");

    auto max_blocks = std::max<size_t>(options_.max_bytes / size, 1);

    if (!block_.empty() && max_blocks > block_.size()) {
      max_blocks = block_.size();
    }

    std::lock_guard lock(mx_);
    cache_.set_max_size(max_blocks);
  }

  void set_num_workers(size_t num) override {
    std::unique_lock lock(mx_wg_);

    if (wg_) {
      wg_.stop();
    }

    wg_ = worker_group(LOG_GET_LOGGER, os_, "blkcache", num);
  }

  void set_tidy_config(cache_tidy_config const& cfg) override {
    if (cfg.strategy == cache_tidy_strategy::NONE) {
      tidy_runner_.stop();
    } else {
      if (cfg.interval == std::chrono::milliseconds::zero()) {
        DWARFS_THROW(runtime_error, "tidy interval is zero");
      }

      {
        std::lock_guard lock(mx_);
        tidy_config_ = cfg;
      }

      tidy_runner_.set_period(cfg.interval);

      if (!tidy_runner_.running()) {
        tidy_runner_.start();
      }
    }
  }

  std::future<block_range>
  get(size_t block_no, size_t offset, size_t size) const override {
    PERFMON_CLS_SCOPED_SECTION(get)
    PERFMON_SET_CONTEXT(block_no, offset, size)

    seq_access_detector_->touch(block_no);

    scope_exit do_prefetch{[&] {
      if (auto next = seq_access_detector_->prefetch()) {
        std::lock_guard lock(mx_);

        if (cache_.find(*next, false) == cache_.end() &&
            active_.find(*next) == active_.end()) {
          sequential_prefetches_.fetch_add(1, std::memory_order_relaxed);
          LOG_TRACE << "prefetching block " << *next;
          create_cached_block(*next, std::promise<block_range>{}, 0,
                              std::numeric_limits<size_t>::max());
        }
      }
    }};

    range_requests_.fetch_add(1, std::memory_order_relaxed);

    std::promise<block_range> promise;
    auto future = promise.get_future();

    // First, let's see if it's an uncompressed block, in which case we
    // can completely bypass the cache
    try {
      if (block_no >= block_.size()) {
        DWARFS_THROW(runtime_error,
                     fmt::format("block number out of range {0} >= {1}",
                                 block_no, block_.size()));
      }

      // TODO: we probably want to change the interface for `section.data()`
      //       so that we don't need multiple different `data()` methods
      if (mm_.supports_raw_bytes()) {
        auto const& section = DWARFS_NOTHROW(block_.at(block_no));

        if (section.compression() == compression_type::NONE) {
          LOG_TRACE << "block " << block_no
                    << " is uncompressed, bypassing cache";
          promise.set_value(
              block_range(section.raw_bytes(mm_).data(), offset, size));
          return future;
        }
      }
    } catch (...) {
      promise.set_exception(std::current_exception());
      return future;
    }

    // That is a mighty long lock, let's see how it works...
    std::lock_guard lock(mx_);

    auto const range_end = offset + size;

    // See if the block is currently active (about-to-be decompressed)
    auto ia = active_.find(block_no);

    std::shared_ptr<block_request_set> brs;

    if (ia != active_.end()) {
      LOG_TRACE << "active sets found for block " << block_no;

      bool add_to_set = false;

      // Try to find a suitable request set to hook on to
      auto end =
          std::remove_if(ia->second.begin(), ia->second.end(),
                         [&brs, range_end, &add_to_set](
                             std::weak_ptr<block_request_set> const& wp) {
                           if (auto rs = wp.lock()) {
                             bool can_add_to_set = range_end <= rs->range_end();

                             if (!brs || (can_add_to_set && !add_to_set)) {
                               brs = std::move(rs);
                               add_to_set = can_add_to_set;
                             }
                             return false;
                           }
                           return true;
                         });

      if (end != ia->second.end()) {
        active_expired_.fetch_add(std::distance(end, ia->second.end()),
                                  std::memory_order_relaxed);

        // Remove all expired weak pointers
        ia->second.erase(end, ia->second.end());
      }

      if (ia->second.empty()) {
        // No request sets left at all? M'kay.
        assert(!brs);
        active_.erase(ia);
      } else if (brs) {
        // That's the one
        // Check if by any chance the block has already
        // been decompressed far enough to fulfill the
        // promise immediately, otherwise add a new
        // request to the request set.

        LOG_TRACE << "block " << block_no << " found in active set";

        auto block = brs->block();

        if (range_end <= block->range_end()) {
          // We can immediately satisfy the promise
          promise.set_value(block_range(std::move(block), offset, size));
          active_hits_fast_.fetch_add(1, std::memory_order_relaxed);
        } else {
          if (!add_to_set) {
            // Make a new set for the same block
            brs =
                std::make_shared<block_request_set>(std::move(block), block_no);
          }

          // Promise will be fulfilled asynchronously
          brs->add(offset, range_end, std::move(promise));
          active_hits_slow_.fetch_add(1, std::memory_order_relaxed);

          if (!add_to_set) {
            ia->second.emplace_back(brs);
            active_set_size_.add(ia->second.size());
            enqueue_job(std::move(brs));
          }
        }

        return future;
      }

      LOG_TRACE << "block " << block_no << " not found in active set";
    }

    // See if it's cached (fully or partially decompressed)
    auto ic = cache_.find(block_no);

    if (ic != cache_.end()) {
      // Nice, at least the block is already there.

      LOG_TRACE << "block " << block_no << " found in cache";

      auto block = ic->second;

      if (range_end <= block->range_end()) {
        // We can immediately satisfy the promise
        promise.set_value(block_range(std::move(block), offset, size));
        cache_hits_fast_.fetch_add(1, std::memory_order_relaxed);
      } else {
        // Make a new set for the block
        brs = std::make_shared<block_request_set>(std::move(block), block_no);

        // Promise will be fulfilled asynchronously
        brs->add(offset, range_end, std::move(promise));
        cache_hits_slow_.fetch_add(1, std::memory_order_relaxed);

        auto& active = active_[block_no];
        active.emplace_back(brs);
        active_set_size_.add(active.size());
        enqueue_job(std::move(brs));
      }

      return future;
    }

    // Bummer. We don't know anything about the block.

    LOG_TRACE << "block " << block_no << " not found";

    create_cached_block(block_no, std::move(promise), offset, range_end);

    return future;
  }

 private:
  // NOLINTBEGIN(cppcoreguidelines-rvalue-reference-param-not-moved)
  void on_block_removed(std::string_view action, size_t block_no,
                        std::shared_ptr<cached_block>&& block) {
    // NOLINTEND(cppcoreguidelines-rvalue-reference-param-not-moved)
    LOG_DEBUG << "block " << block_no << " " << action
              << " from cache, decompression ratio = "
              << static_cast<double>(block->range_end()) /
                     static_cast<double>(block->uncompressed_size());
    update_block_stats(*block);
  }

  static std::unique_ptr<sequential_access_detector>
  create_seq_access_detector(size_t threshold) {
    if (threshold == 0) {
      return std::make_unique<no_sequential_access_detector>();
    }

    return std::make_unique<lru_sequential_access_detector>(threshold);
  }

  void create_cached_block(size_t block_no, std::promise<block_range>&& promise,
                           size_t offset, size_t range_end) const {
    try {
      auto const& section = DWARFS_NOTHROW(block_.at(block_no));

      std::shared_ptr<cached_block> block = cached_block::create(
          LOG_GET_LOGGER, section, section.segment(mm_), buffer_factory_,
          options_.disable_block_integrity_check);
      blocks_created_.fetch_add(1, std::memory_order_relaxed);

      // Make a new set for the block
      auto brs =
          std::make_shared<block_request_set>(std::move(block), block_no);

      // Promise will be fulfilled asynchronously
      brs->add(offset, range_end, std::move(promise));

      auto& active = active_[block_no];
      active.emplace_back(brs);
      active_set_size_.add(active.size());
      enqueue_job(std::move(brs));
    } catch (...) {
      promise.set_exception(std::current_exception());
    }
  }

  void update_block_stats(cached_block const& cb) {
    if (cb.range_end() < cb.uncompressed_size()) {
      partially_decompressed_.fetch_add(1, std::memory_order_relaxed);
    }
    total_decompressed_bytes_.fetch_add(cb.range_end(),
                                        std::memory_order_relaxed);
    total_block_bytes_.fetch_add(cb.uncompressed_size(),
                                 std::memory_order_relaxed);
  }

  void init_worker_group() const {
    std::unique_lock lock(mx_wg_);

    if (!wg_) {
      wg_ = worker_group(LOG_GET_LOGGER, os_, "blkcache",
                         std::max(options_.num_workers > 0
                                      ? options_.num_workers
                                      : hardware_concurrency(),
                                  static_cast<size_t>(1)));
    }
  }

  void enqueue_job(std::shared_ptr<block_request_set> brs) const {
    // lazy initialization of worker group
    std::call_once(wg_init_flag_, [this] { init_worker_group(); });

    std::shared_lock lock(mx_wg_);

    // Lambda needs to be mutable so we can actually move out of it
    wg_.add_job([this, brs = std::move(brs)]() mutable {
      process_job(std::move(brs));
    });
  }

  void process_job(std::shared_ptr<block_request_set> brs) const {
    PERFMON_CLS_SCOPED_SECTION(process)

    auto block_no = brs->block_no();
    PERFMON_SET_CONTEXT(block_no)

    LOG_TRACE << "processing block " << block_no;

    // Check if another worker is already processing this block
    {
      std::lock_guard lock_dec(mx_dec_);

      auto di = decompressing_.find(block_no);

      if (di != decompressing_.end()) {
        std::lock_guard lock(mx_);

        if (auto other = di->second.lock()) {
          LOG_TRACE << "merging sets for block " << block_no;
          other->merge(*brs);
          sets_merged_.fetch_add(1, std::memory_order_relaxed);
          brs.reset();
          return;
        }
      }

      decompressing_[block_no] = brs;
    }

    auto block = brs->block();

    for (;;) {
      block_request req;
      bool is_last_req = false;

      // Fetch the next request, if any
      {
        std::lock_guard lock(mx_);

        if (brs->empty()) {
          // This is absolutely crucial! At this point, we can no longer
          // allow other code to add to this request set, so we need to
          // expire all weak pointer from within this critial section.
          brs.reset();
          break;
        }

        req = brs->get();
        is_last_req = brs->empty();
      }

      // Process this request!

      size_t range_end = req.end();
      auto max_end = block->uncompressed_size();

      if (range_end == std::numeric_limits<size_t>::max()) {
        range_end = max_end;
      }

      if (is_last_req) {
        double ratio =
            static_cast<double>(range_end) / static_cast<double>(max_end);
        if (ratio > options_.decompress_ratio) {
          LOG_TRACE << "block " << block_no << " over ratio: " << ratio << " > "
                    << options_.decompress_ratio;
          range_end = max_end;
        }
      }

      try {
        if (range_end > block->range_end()) {
          PERFMON_CLS_SCOPED_SECTION(decompress)
          PERFMON_SET_CONTEXT(range_end)

          LOG_TRACE << "decompressing block " << block_no << " until position "
                    << range_end;

          block->decompress_until(range_end);
        }

        req.fulfill(block);
      } catch (...) {
        req.error(std::current_exception());
      }
    }

    // Finally, put the block into the cache; it might already be
    // in there, in which case we just promote it to the front of
    // the LRU queue.
    {
      std::lock_guard lock(mx_);

      if (tidy_config_.strategy == cache_tidy_strategy::EXPIRY_TIME) {
        block->touch();
      }

      LOG_DEBUG << "inserting block " << block_no << " into cache";

      cache_.set(block_no, std::move(block));
    }
  }

  template <typename Pred>
  void remove_block_if(Pred const& predicate) {
    auto it = cache_.begin();

    while (it != cache_.end()) {
      if (predicate(*it->second)) {
        LOG_TRACE << "tidying block " << it->first;
        it = cache_.erase(
            it, [this](size_t block_no, std::shared_ptr<cached_block>&& block) {
              on_block_removed("tidied", block_no, std::move(block));
              blocks_tidied_.fetch_add(1, std::memory_order_relaxed);
            });
      } else {
        LOG_TRACE << "keeping block " << it->first;
        ++it;
      }
    }
  }

  void tidy_cache() {
    switch (tidy_config_.strategy) {
    case cache_tidy_strategy::EXPIRY_TIME:
      LOG_DEBUG << "tidying cache by expiry time";
      remove_block_if([tp = std::chrono::steady_clock::now() -
                            tidy_config_.expiry_time](cached_block const& blk) {
        return blk.last_used_before(tp);
      });
      break;

    case cache_tidy_strategy::BLOCK_SWAPPED_OUT: {
      LOG_DEBUG << "tidying cache by swapped out blocks";
      std::vector<uint8_t> tmp;
      remove_block_if([&tmp](cached_block const& blk) {
        return blk.any_pages_swapped_out(tmp);
      });
    } break;

    default:
      break;
    }
  }

  using lru_type = lru_cache<size_t, std::shared_ptr<cached_block>>;
  template <typename Key, typename Value>
  using fast_map_type = phmap::flat_hash_map<Key, Value>;

  mutable std::mutex mx_;
  mutable lru_type cache_;
  mutable fast_map_type<size_t, std::vector<std::weak_ptr<block_request_set>>>
      active_;
  periodic_executor tidy_runner_;

  mutable std::mutex mx_dec_;
  mutable fast_map_type<size_t, std::weak_ptr<block_request_set>>
      decompressing_;

  mutable std::atomic<size_t> blocks_created_{0};
  mutable std::atomic<size_t> blocks_evicted_{0};
  mutable std::atomic<size_t> sets_merged_{0};
  mutable std::atomic<size_t> range_requests_{0};
  mutable std::atomic<size_t> active_hits_fast_{0};
  mutable std::atomic<size_t> active_hits_slow_{0};
  mutable std::atomic<size_t> cache_hits_fast_{0};
  mutable std::atomic<size_t> cache_hits_slow_{0};
  mutable std::atomic<size_t> partially_decompressed_{0};
  mutable std::atomic<size_t> total_block_bytes_{0};
  mutable std::atomic<size_t> total_decompressed_bytes_{0};
  mutable std::atomic<size_t> blocks_tidied_{0};
  mutable std::atomic<size_t> active_expired_{0};
  mutable std::atomic<size_t> sequential_prefetches_{0};
  mutable value_stream_quantile_estimator active_set_size_{0.5, 0.75, 0.9, 0.95,
                                                           0.99};

  mutable std::shared_mutex mx_wg_;
  mutable worker_group wg_;
  mutable std::once_flag wg_init_flag_;
  std::vector<fs_section> block_;
  file_view mm_;
  byte_buffer_factory buffer_factory_;
  LOG_PROXY_DECL(LoggerPolicy);
  PERFMON_CLS_PROXY_DECL
  PERFMON_CLS_TIMER_DECL(get)
  PERFMON_CLS_TIMER_DECL(process)
  PERFMON_CLS_TIMER_DECL(decompress)
  std::unique_ptr<sequential_access_detector> seq_access_detector_;
  os_access const& os_;
  block_cache_options const options_;
  cache_tidy_config tidy_config_;
};

} // namespace

block_cache::block_cache(
    logger& lgr, os_access const& os, file_view const& mm,
    block_cache_options const& options,
    std::shared_ptr<performance_monitor const> const& perfmon)
    : impl_(make_unique_logging_object<impl, block_cache_, logger_policies>(
          lgr, os, mm, options, perfmon)) {}

} // namespace dwarfs::reader::internal
