/* 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.
 *
 * dwarfs 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 3 of the License, or
 * (at your option) any later version.
 *
 * dwarfs 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 dwarfs.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <algorithm>
#include <cassert>
#include <concepts>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <exception>
#include <limits>
#include <numeric>
#include <ostream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>

#include <fmt/format.h>

#include <dwarfs/compiler.h>
#include <dwarfs/error.h>
#include <dwarfs/file_view.h>
#include <dwarfs/logger.h>
#include <dwarfs/match.h>
#include <dwarfs/open_file_options.h>
#include <dwarfs/os_access.h>
#include <dwarfs/thrift_lite/demangle.h>
#include <dwarfs/util.h>
#include <dwarfs/writer/categorizer.h>
#include <dwarfs/writer/inode_options.h>

#include <dwarfs/internal/associative_vector_types.h>
#include <dwarfs/internal/worker_group.h>
#include <dwarfs/writer/internal/entry.h>
#include <dwarfs/writer/internal/inode_manager.h>
#include <dwarfs/writer/internal/inode_ordering.h>
#include <dwarfs/writer/internal/nilsimsa.h>
#include <dwarfs/writer/internal/progress.h>
#include <dwarfs/writer/internal/promise_receiver.h>
#include <dwarfs/writer/internal/scanner_progress.h>
#include <dwarfs/writer/internal/similarity.h>
#include <dwarfs/writer/internal/similarity_ordering.h>

#include <dwarfs/gen-cpp-lite/metadata_types.h>

namespace dwarfs::writer::internal {

using namespace dwarfs::internal;
namespace fs = std::filesystem;

namespace {

constexpr std::string_view const kScanContext{"[scanning] "};
constexpr std::string_view const kCategorizeContext{"[categorizing] "};

enum class scan_mode {
  skip_holes,
  include_holes,
};

} // namespace

class inode_ : public inode {
 public:
  using chunk_type = thrift::metadata::chunk;

  inode_() = default;

  void set_num(uint32_t num) override {
    DWARFS_CHECK((flags_ & kNumIsValid) == 0,
                 "attempt to set inode number multiple times");
    num_ = num;
    flags_ |= kNumIsValid;
  }

  uint32_t num() const override {
    DWARFS_CHECK((flags_ & kNumIsValid) != 0, "inode number is not set");
    return num_;
  }

  bool has_category(fragment_category cat) const override {
    DWARFS_CHECK(!fragments_.empty(),
                 "has_category() called with no fragments");
    return std::ranges::any_of(
        fragments_, [cat](auto const& f) { return f.category() == cat; });
  }

  std::optional<uint32_t>
  similarity_hash(fragment_category cat) const override {
    if (auto sim = find_similarity<uint32_t>(cat)) {
      return *sim;
    }
    return std::nullopt;
  }

  nilsimsa::hash_type const*
  nilsimsa_similarity_hash(fragment_category cat) const override {
    return find_similarity<nilsimsa::hash_type>(cat);
  }

  void set_files(files_vector&& fv) override {
    DWARFS_CHECK(files_.empty(), "files already set for inode");
    files_ = std::move(fv);
  }

  void populate(file_size_t size) override {
    assert(fragments_.empty());
    fragments_.emplace_back(categorizer_manager::default_category(), size);
  }

  void scan(file_view const& mm, inode_options const& opts,
            progress& prog) override {
    assert(fragments_.empty());

    categorizer_job catjob;

    // No job if categorizers are disabled
    if (opts.categorizer_mgr) {
      catjob = opts.categorizer_mgr->job(mm ? mm.path().string() : "<no-file>");
    }

    /// TODO: remove comments or move elsewhere
    ///
    /// 1. Run random access categorizers
    /// 2. If we *have* a best category already (need a call for that),
    ///    we can immediately compute similarity hashes for all fragments
    ///    (or not, if the category is configured not to use similarity)
    /// 3. If we *don't* have a best category yet, we can run similarity
    ///    hashing while running the sequential categorizer(s).
    /// 4. If we end up with multiple fragments after all, we might have
    ///    to re-run similarity hashing. This means we can also drop the
    ///    multi-fragment sequential categorizer check, as we can just
    ///    as well support that case.
    ///

    // If we don't have a mapping, we can't scan anything
    if (mm) {
      if (catjob) {
        // First, run random access categorizers. If we get a result here,
        // it's very likely going to be the best result.
        catjob.set_total_size(mm.size());
        catjob.categorize_random_access(mm);

        if (!catjob.best_result_found()) {
          // We must perform a sequential categorizer scan before scanning the
          // fragments, because the ordering is category-dependent.
          // TODO: we might be able to get away with a single scan if we
          //       optimistically assume the default category and perform
          //       both the sequential scan and the default-category order
          //       scan in parallel
          auto const chunk_size = prog.categorize.chunk_size.load();
          auto sp = make_progress_context(kCategorizeContext, mm, prog,
                                          4 * chunk_size);
          progress::scan_updater supd(prog.categorize, mm.size());
          catjob.categorize_sequential(mm, chunk_size, sp.get());
        }

        fragments_ = catjob.result();

        if (fragments_.size() > 1) {
          auto const chunk_size = prog.similarity.chunk_size.load();
          auto sp =
              make_progress_context(kScanContext, mm, prog, 4 * chunk_size);
          progress::scan_updater supd(prog.similarity, mm.size());
          scan_fragments(mm, sp.get(), opts, chunk_size);
        }
      }
    }

    // Add a fragment if nothing has been added so far. We need a single
    // fragment to store the inode's chunks. This won't use up any resources
    // as a single fragment is stored inline.
    if (fragments_.size() <= 1) {
      file_size_t size = mm ? mm.size() : 0;
      if (fragments_.empty()) {
        populate(size);
      }
      auto const chunk_size = prog.similarity.chunk_size.load();
      auto sp = make_progress_context(kScanContext, mm, prog, 4 * chunk_size);
      progress::scan_updater supd(prog.similarity, size);
      scan_full(mm, sp.get(), opts, chunk_size);
    }
  }

  file_size_t size() const override { return any()->size(); }

  file const* any() const override {
    DWARFS_CHECK(!files_.empty(), "inode has no file (any)");
    for (auto const& f : files_) {
      if (!f->is_invalid()) {
        return f;
      }
    }
    return files_.front();
  }

  files_vector const& all() const override { return files_; }

  bool append_chunks_to(
      std::vector<chunk_type>& vec,
      std::optional<inode_hole_mapper>& hole_mapper) const override {
    for (auto const& frag : fragments_) {
      if (!frag.chunks_are_consistent()) {
        return false;
      }
    }
    for (auto const& frag : fragments_) {
      for (auto const& src : frag.chunks()) {
        auto& chk = vec.emplace_back();
        if (src.is_hole()) {
          DWARFS_CHECK(hole_mapper.has_value(),
                       "inode has hole chunk but there's no hole mapper");
          auto& hm = hole_mapper.value();
          hm.map_hole(chk, src.size());
        } else {
          chk.block() = src.block();
          chk.offset() = src.offset();
          chk.size() = src.size();
        }
      }
    }
    return true;
  }

  inode_fragments& fragments() override { return fragments_; }

  void dump(std::ostream& os, inode_options const& options) const override {
    auto dump_category = [&os, &options](fragment_category const& cat) {
      if (options.categorizer_mgr) {
        os << "[" << options.categorizer_mgr->category_name(cat.value());
        if (cat.has_subcategory()) {
          os << "/" << cat.subcategory();
        }
        os << "] ";
      }
    };

    std::string ino_num{"?"};

    if (flags_ & kNumIsValid) {
      ino_num = std::to_string(num());
    }

    os << "inode " << ino_num << " (" << any()->size() << " bytes):\n";
    os << "  files:\n";

    for (auto const& f : files_) {
      os << "    " << f->path_as_string();
      if (f->is_invalid()) {
        os << " (invalid)";
      }
      os << "\n";
    }

    os << "  fragments:\n";

    for (auto const& f : fragments_.span()) {
      os << "    ";
      dump_category(f.category());
      os << "(" << f.size() << " bytes)\n";
      for (auto const& c : f.chunks()) {
        os << "      (" << c.block() << ", " << c.offset() << ", " << c.size()
           << ")\n";
      }
    }

    os << "  similarity: ";

    auto basic_hash_matcher = [&os](uint32_t sh) {
      os << fmt::format("basic ({0:08x})\n", sh);
    };

    auto nilsimsa_hash_matcher = [&os](nilsimsa::hash_type const& nh) {
      os << fmt::format("nilsimsa ({0:016x}{1:016x}{2:016x}{3:016x})\n", nh[0],
                        nh[1], nh[2], nh[3]);
    };

    auto similarity_map_matcher = [&](similarity_map_type const& map) {
      os << "map\n";
      for (auto const& [cat, val] : map) {
        os << "    ";
        dump_category(cat);
        val | match{
                  basic_hash_matcher,
                  nilsimsa_hash_matcher,
              };
      }
    };

    similarity_ | match{
                      [&os](std::monostate const&) { os << "none\n"; },
                      basic_hash_matcher,
                      nilsimsa_hash_matcher,
                      similarity_map_matcher,
                  };
  }

  void set_scan_error(file const* fp, std::exception_ptr ep) override {
    assert(!scan_error_);
    scan_error_ = std::make_unique<std::pair<file const*, std::exception_ptr>>(
        fp, std::move(ep));
  }

  std::optional<std::pair<file const*, std::exception_ptr>>
  get_scan_error() const override {
    if (scan_error_) {
      return *scan_error_;
    }
    return std::nullopt;
  }

  std::tuple<file_view, file const*,
             std::vector<std::pair<file const*, std::exception_ptr>>>
  mmap_any(os_access const& os,
           open_file_options const& of_opts) const override {
    file_view mm;
    std::vector<std::pair<file const*, std::exception_ptr>> errors;
    file const* rfp{nullptr};

    for (auto fp : files_) {
      if (!fp->is_invalid()) {
        try {
          mm = os.open_file_with_options(fp->fs_path(), of_opts);
          if (mm.size() != fp->size()) {
            auto const now_size = mm.size();
            mm.reset();
            throw std::runtime_error(fmt::format(
                "file size changed: was {}, now {}", fp->size(), now_size));
          }
          rfp = fp;
          break;
        } catch (...) {
          fp->set_invalid();
          errors.emplace_back(fp, std::current_exception());
        }
      }
    }

    return {std::move(mm), rfp, std::move(errors)};
  }

 private:
  std::shared_ptr<scanner_progress>
  make_progress_context(std::string_view context, file_view const& mm,
                        progress& prog, size_t min_size) const {
    if (mm) {
      if (auto size = mm.size(); std::cmp_greater_equal(size, min_size)) {
        return prog.create_context<scanner_progress>(
            context, path_to_utf8_string_sanitized(mm.path()), size);
      }
    }
    return nullptr;
  }

  template <typename T>
  T const* find_similarity(fragment_category cat) const {
    DWARFS_CHECK(!fragments_.empty(), fmt::format("inode has no fragments ({})",
                                                  thrift_lite::demangle<T>()));
    if (std::holds_alternative<std::monostate>(similarity_)) {
      return nullptr;
    }
    if (fragments_.size() == 1) {
      DWARFS_CHECK(
          fragments_.get_single_category() == cat,
          fmt::format("category mismatch ({})", thrift_lite::demangle<T>()));
      return &std::get<T>(similarity_);
    }
    auto& m = std::get<similarity_map_type>(similarity_);
    if (auto it = m.find(cat); it != m.end()) {
      return &std::get<T>(it->second);
    }
    return nullptr;
  }

  void scan_range(file_view const& mm, scanner_progress* sprog,
                  file_off_t offset, file_size_t size, size_t chunk_size,
                  std::invocable<std::span<uint8_t const>> auto&& scanner,
                  scan_mode mode = scan_mode::skip_holes) {
    auto&& scan = std::forward<decltype(scanner)>(scanner);

    auto advance = [&](file_size_t n) {
      if (sprog) {
        sprog->advance(n);
      }
    };

    for (auto const& ext : mm.extents({offset, size})) {
      if (ext.kind() == extent_kind::hole && mode == scan_mode::skip_holes) {
        advance(ext.size());
        continue;
      }

      for (auto const& seg : ext.segments(chunk_size)) {
        scan(seg.span<uint8_t>());
        advance(seg.size());
      }
    }
  }

  void
  scan_range(file_view const& mm, scanner_progress* sprog, size_t chunk_size,
             std::invocable<std::span<uint8_t const>> auto&& scanner,
             scan_mode mode = scan_mode::skip_holes) {
    scan_range(mm, sprog, 0, mm.size(), chunk_size,
               std::forward<decltype(scanner)>(scanner), mode);
  }

  void scan_fragments(file_view const& mm, scanner_progress* sprog,
                      inode_options const& opts, size_t chunk_size) {
    assert(mm);
    assert(fragments_.size() > 1);

    std::unordered_map<fragment_category, similarity> sc;
    std::unordered_map<fragment_category, nilsimsa> nc;

    for (auto [cat, size] : fragments_.get_category_sizes()) {
      if (auto max = opts.max_similarity_scan_size;
          max && std::cmp_greater(size, *max)) {
        continue;
      }

      switch (opts.fragment_order.get(cat).mode) {
      case fragment_order_mode::NONE:
      case fragment_order_mode::PATH:
      case fragment_order_mode::REVPATH:
      case fragment_order_mode::EXPLICIT:
        break;
      case fragment_order_mode::SIMILARITY:
        sc.try_emplace(cat);
        break;
      case fragment_order_mode::NILSIMSA:
        nc.try_emplace(cat);
        break;
      }
    }

    if (sc.empty() && nc.empty()) {
      return;
    }

    file_off_t pos = 0;

    for (auto const& f : fragments_.span()) {
      auto const size = f.size();

      if (auto i = sc.find(f.category()); i != sc.end()) {
        scan_range(mm, sprog, pos, size, chunk_size, i->second);
      } else if (auto i = nc.find(f.category()); i != nc.end()) {
        scan_range(mm, sprog, pos, size, chunk_size, i->second);
      }

      pos += size;
    }

    similarity_map_type tmp_map;

    for (auto const& [cat, hasher] : sc) {
      tmp_map.emplace(cat, hasher.finalize());
    }

    for (auto const& [cat, hasher] : nc) {
      // TODO: can we finalize in-place?
      nilsimsa::hash_type hash;
      hasher.finalize(hash);
      tmp_map.emplace(cat, hash);
    }

    similarity_.emplace<similarity_map_type>(std::move(tmp_map));
  }

  void scan_full(file_view const& mm, scanner_progress* sprog,
                 inode_options const& opts, size_t chunk_size) {
    assert(fragments_.size() <= 1);

    if (mm) {
      if (auto max = opts.max_similarity_scan_size;
          max && std::cmp_greater(mm.size(), *max)) {
        return;
      }
    }

    auto order_mode =
        fragments_.empty()
            ? opts.fragment_order.get().mode
            : opts.fragment_order.get(fragments_.get_single_category()).mode;

    switch (order_mode) {
    case fragment_order_mode::NONE:
    case fragment_order_mode::PATH:
    case fragment_order_mode::REVPATH:
    case fragment_order_mode::EXPLICIT:
      break;

    case fragment_order_mode::SIMILARITY: {
      similarity sc;
      if (mm) {
        scan_range(mm, sprog, chunk_size, sc);
      }
      similarity_.emplace<uint32_t>(sc.finalize());
    } break;

    case fragment_order_mode::NILSIMSA: {
      nilsimsa nc;
      if (mm) {
        scan_range(mm, sprog, chunk_size, nc);
      }
      // TODO: can we finalize in-place?
      nilsimsa::hash_type hash;
      nc.finalize(hash);
      similarity_.emplace<nilsimsa::hash_type>(hash);
    } break;
    }
  }

  using similarity_map_type = dwarfs::internal::small_vector_map<
      fragment_category, std::variant<nilsimsa::hash_type, uint32_t>>;

  static constexpr uint32_t const kNumIsValid{UINT32_C(1) << 0};

  uint32_t flags_{0};
  uint32_t num_{0};
  inode_fragments fragments_;
  files_vector files_;
  std::unique_ptr<std::pair<file const*, std::exception_ptr>> scan_error_;

  std::variant<
      // in case of no hashes at all
      std::monostate,

      // in case of only a single fragment
      nilsimsa::hash_type, // 32 bytes
      uint32_t,            //  4 bytes

      // in case of multiple fragments
      similarity_map_type // 24 bytes
      >
      similarity_;

  static_assert(sizeof(similarity_map_type) <= sizeof(nilsimsa::hash_type));
};

template <typename LoggerPolicy>
class inode_manager_ final : public inode_manager::impl {
 public:
  inode_manager_(logger& lgr, progress& prog, fs::path const& root_path,
                 inode_options const& opts, bool list_mode)
      : LOG_PROXY_INIT(lgr)
      , prog_(prog)
      , root_path_{root_path}
      , opts_{opts}
      , inodes_need_scanning_{inodes_need_scanning(opts_)}
      , list_mode_{list_mode} {}

  std::shared_ptr<inode> create_inode() override {
    auto ino = std::make_shared<inode_>();
    inodes_.push_back(ino);
    return ino;
  }

  size_t count() const override { return inodes_.size(); }

  void for_each_inode_in_order(
      std::function<void(std::shared_ptr<inode> const&)> const& fn)
      const override {
    auto span = sortable_span();
    span.all();
    inode_ordering(LOG_GET_LOGGER, prog_, opts_).by_inode_number(span);
    for (auto const& i : span) {
      fn(i);
    }
  }

  inode_manager::fragment_infos fragment_category_info() const override {
    inode_manager::fragment_infos rv;

    std::unordered_map<fragment_category::value_type, std::pair<size_t, size_t>>
        tmp;

    for (auto const& i : inodes_) {
      if (auto const& fragments = i->fragments(); !fragments.empty()) {
        for (auto const& frag : fragments) {
          auto s = frag.size();
          auto& mv = tmp[frag.category().value()];
          ++mv.first;
          mv.second += s;
          rv.category_size[frag.category()] += s;
          rv.total_size += s;
        }
      }
    }

    rv.info.reserve(tmp.size());

    for (auto const& [k, v] : tmp) {
      rv.info.emplace_back(k, v.first, v.second);
    }

    rv.categories.reserve(rv.category_size.size());

    for (auto cs : rv.category_size) {
      rv.categories.emplace_back(cs.first);
    }

    std::sort(rv.info.begin(), rv.info.end(), [](auto const& a, auto const& b) {
      return a.total_size > b.total_size ||
             (a.total_size == b.total_size && a.category < b.category);
    });

    if (opts_.categorizer_mgr) {
      std::sort(rv.categories.begin(), rv.categories.end(),
                [&catmgr = *opts_.categorizer_mgr](auto a, auto b) {
                  return catmgr.deterministic_less(a, b);
                });
    } else {
      std::ranges::sort(rv.categories);
    }

    return rv;
  }

  void scan_background(worker_group& wg, os_access const& os,
                       std::shared_ptr<inode> ino, file* p) const override;

  bool has_invalid_inodes() const override;

  void try_scan_invalid(worker_group& wg, os_access const& os) override;

  void dump(std::ostream& os) const override;

  sortable_inode_span sortable_span() const override {
    return sortable_inode_span(inodes_);
  }

  sortable_inode_span
  ordered_span(fragment_category cat, worker_group& wg) const override;

  size_t get_max_data_chunk_size() const override;

 private:
  void update_prog(std::shared_ptr<inode> const& ino, file const* p) const {
    if (p->size() > 0 && !p->is_invalid()) {
      prog_.fragments_found += ino->fragments().size();
    }
    ++prog_.inodes_scanned;
    ++prog_.files_scanned;
  }

  static bool inodes_need_scanning(inode_options const& opts) {
    if (opts.categorizer_mgr) {
      return true;
    }

    return opts.fragment_order.any_is([](auto const& order) {
      return order.mode == fragment_order_mode::SIMILARITY ||
             order.mode == fragment_order_mode::NILSIMSA;
    });
  }

  LOG_PROXY_DECL(LoggerPolicy);
  std::vector<std::shared_ptr<inode>> inodes_;
  progress& prog_;
  fs::path const root_path_;
  inode_options opts_;
  bool const inodes_need_scanning_;
  bool const list_mode_;
  std::atomic<size_t> mutable num_invalid_inodes_{0};
};

template <typename LoggerPolicy>
void inode_manager_<LoggerPolicy>::scan_background(worker_group& wg,
                                                   os_access const& os,
                                                   std::shared_ptr<inode> ino,
                                                   file* p) const {
  // TODO: I think the size check makes everything more complex.
  //       If we don't check the size, we get the code to run
  //       that ensures `fragments_` is updated. Also, there
  //       should only ever be one empty inode, so the check
  //       doesn't actually make much of a difference.
  if (inodes_need_scanning_ /* && p->size() > 0 */) {
    wg.add_job([this, &os, p, ino = std::move(ino)] {
      auto const size = p->size();
      file_view mm;

      if (size > 0 && !p->is_invalid()) {
        try {
          mm = os.open_file(p->fs_path());
        } catch (...) {
          p->set_invalid();
          // If this file *was* successfully mapped before, there's a slight
          // chance that there's another file with the same hash. We can only
          // figure this out later when all files have been hashed, so we
          // save the error and try again later (in `try_scan_invalid()`).
          ino->set_scan_error(p, std::current_exception());
          ++num_invalid_inodes_;
          return;
        }

        if (mm.size() != size) {
          ino->set_scan_error(
              p, std::make_exception_ptr(std::runtime_error(fmt::format(
                     "file size changed: was {}, now {}", size, mm.size()))));
          p->set_invalid();
          ++num_invalid_inodes_;
          return;
        }
      }

      ino->scan(mm, opts_, prog_);
      update_prog(ino, p);
    });
  } else {
    ino->populate(p->size());
    update_prog(ino, p);
  }
}

template <typename LoggerPolicy>
bool inode_manager_<LoggerPolicy>::has_invalid_inodes() const {
  assert(inodes_need_scanning_ || num_invalid_inodes_.load() == 0);
  return num_invalid_inodes_.load() > 0;
}

template <typename LoggerPolicy>
void inode_manager_<LoggerPolicy>::try_scan_invalid(worker_group& wg,
                                                    os_access const& os) {
  LOG_VERBOSE << "trying to scan " << num_invalid_inodes_.load()
              << " invalid inodes...";

  for (auto const& ino : inodes_) {
    if (auto scan_err = ino->get_scan_error()) {
      assert(ino->fragments().empty());

      std::vector<std::pair<file const*, std::exception_ptr>> errors;
      auto const& fv = ino->all();

      if (fv.size() > 1) {
        auto [mm, p, err] = ino->mmap_any(os, {});

        if (mm) {
          LOG_DEBUG << "successfully opened: " << p->path_as_string();

          // TODO: p = p is a workaround for older Clang versions
          wg.add_job([this, p = p, ino, mm = std::move(mm)] {
            ino->scan(mm, opts_, prog_);
            update_prog(ino, p);
          });

          continue;
        }

        errors = std::move(err);
      }

      assert(ino->any()->is_invalid());

      ino->scan({}, opts_, prog_);
      update_prog(ino, ino->any());

      errors.emplace_back(scan_err.value());

      for (auto const& [fp, ep] : errors) {
        LOG_ERROR << "failed to map file \"" << fp->path_as_string()
                  << "\": " << exception_str(ep) << ", creating empty inode";
        ++prog_.errors;
      }
    }
  }
}

template <typename LoggerPolicy>
void inode_manager_<LoggerPolicy>::dump(std::ostream& os) const {
  for_each_inode_in_order(
      [this, &os](auto const& ino) { ino->dump(os, opts_); });
}

template <typename LoggerPolicy>
auto inode_manager_<LoggerPolicy>::ordered_span(fragment_category cat,
                                                worker_group& wg) const
    -> sortable_inode_span {
  auto prefix = category_prefix(opts_.categorizer_mgr, cat);
  auto opts = opts_.fragment_order.get(cat);

  auto span = sortable_span();
  span.select([cat](auto const& v) { return v->has_category(cat); });

  inode_ordering order(LOG_GET_LOGGER, prog_, opts_);

  switch (opts.mode) {
  case fragment_order_mode::NONE:
    if (list_mode_) {
      LOG_VERBOSE << prefix << "ordering " << span.size()
                  << " inodes by input list order...";
      auto tv = LOG_CPU_TIMED_VERBOSE;
      order.by_input_order(span);
      tv << prefix << span.size() << " inodes ordered";
    } else {
      LOG_VERBOSE << prefix << "keeping inode order";
    }
    break;

  case fragment_order_mode::PATH: {
    LOG_VERBOSE << prefix << "ordering " << span.size()
                << " inodes by path name...";
    auto tv = LOG_CPU_TIMED_VERBOSE;
    order.by_path(span);
    tv << prefix << span.size() << " inodes ordered";
  } break;

  case fragment_order_mode::REVPATH: {
    LOG_VERBOSE << prefix << "ordering " << span.size()
                << " inodes by reverse path name...";
    auto tv = LOG_CPU_TIMED_VERBOSE;
    order.by_reverse_path(span);
    tv << prefix << span.size() << " inodes ordered";
  } break;

  case fragment_order_mode::SIMILARITY: {
    LOG_VERBOSE << prefix << "ordering " << span.size()
                << " inodes by similarity...";
    auto tv = LOG_CPU_TIMED_VERBOSE;
    order.by_similarity(span, cat);
    tv << prefix << span.size() << " inodes ordered";
  } break;

  case fragment_order_mode::NILSIMSA: {
    LOG_VERBOSE << prefix << "ordering " << span.size()
                << " inodes using nilsimsa similarity...";
    similarity_ordering_options soo;
    soo.context = prefix;
    soo.max_children = opts.nilsimsa_max_children;
    soo.max_cluster_size = opts.nilsimsa_max_cluster_size;
    auto tv = LOG_TIMED_VERBOSE;
    order.by_nilsimsa(wg, soo, span, cat);
    tv << prefix << span.size() << " inodes ordered";
  } break;

  case fragment_order_mode::EXPLICIT: {
    LOG_VERBOSE << prefix << "ordering " << span.size()
                << " inodes by explicit order...";
    auto tv = LOG_CPU_TIMED_VERBOSE;
    order.by_explicit_order(span, root_path_, opts);
    tv << prefix << span.size() << " inodes ordered";
  } break;
  }

  return span;
}

template <typename LoggerPolicy>
size_t inode_manager_<LoggerPolicy>::get_max_data_chunk_size() const {
  file_size_t max_chunk_size{0};

  for (auto const& ino : inodes_) {
    for (auto const& frag : ino->fragments().span()) {
      for (auto const& chk : frag.chunks()) {
        if (chk.is_data()) {
          max_chunk_size = std::max(max_chunk_size, chk.size());
        }
      }
    }
  }

  return max_chunk_size;
}

inode_manager::inode_manager(logger& lgr, progress& prog,
                             fs::path const& root_path,
                             inode_options const& opts, bool list_mode)
    : impl_(make_unique_logging_object<impl, internal::inode_manager_,
                                       logger_policies>(lgr, prog, root_path,
                                                        opts, list_mode)) {}

} // namespace dwarfs::writer::internal
