/*!
 * Copyright 2015-2021 by Contributors
 * \file sparse_page_dmatrix.h
 * \brief External-memory version of DMatrix.
 * \author Tianqi Chen
 */
#ifndef XGBOOST_DATA_SPARSE_PAGE_DMATRIX_H_
#define XGBOOST_DATA_SPARSE_PAGE_DMATRIX_H_

#include <xgboost/data.h>
#include <xgboost/logging.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <map>

#include "ellpack_page_source.h"
#include "gradient_index_page_source.h"
#include "sparse_page_source.h"

namespace xgboost {
namespace data {
/**
 * \brief DMatrix used for external memory.
 *
 * The external memory is created for controlling memory usage by splitting up data into
 * multiple batches.  However that doesn't mean we will actually process exact 1 batch at
 * a time, which would be terribly slow considering that we have to loop through the
 * whole dataset for every tree split.  So we use async pre-fetch and let caller to decide
 * how many batches it wants to process by returning data as shared pointer.  The caller
 * can use async function to process the data or just stage those batches, making the
 * decision is out of the scope for sparse page dmatrix.  These 2 optimizations might
 * defeat the purpose of splitting up dataset since if you load all the batches then the
 * memory usage is even worse than using a single batch.  Essentially we need to control
 * how many batches can be in memory at the same time.
 *
 * Right now the write to the cache is sequential operation and is blocking, reading from
 * cache is async but with a hard coded limit of 4 pages as an heuristic.  So by sparse
 * dmatrix itself there can be only 9 pages in main memory (might be of different types)
 * at the same time: 1 page pending for write, 4 pre-fetched sparse pages, 4 pre-fetched
 * dependent pages.  If the caller stops iteration at the middle and start again, then the
 * number of pages in memory can hit 16 due to pre-fetching, but this should be a bug in
 * caller's code (XGBoost doesn't discard a large portion of data at the end, there's not
 * sampling algo that samples only the first portion of data).
 *
 * Of course if the caller decides to retain some batches to perform parallel processing,
 * then we might load all pages in memory, which is also considered as a bug in caller's
 * code.  So if the algo supports external memory, it must be careful that queue for async
 * call must have an upper limit.
 *
 * Another assumption we make is that the data must be immutable so caller should never
 * change the data.  Sparse page source returns const page to make sure of that.  If you
 * want to change the generated page like Ellpack, pass parameter into `GetBatches` to
 * re-generate them instead of trying to modify the pages in-place.
 *
 * A possible optimization is dropping the sparse page once dependent pages like ellpack
 * are constructed and cached.
 */
class SparsePageDMatrix : public DMatrix {
  MetaInfo info_;
  BatchParam batch_param_;
  std::map<std::string, std::shared_ptr<Cache>> cache_info_;

  DMatrixHandle proxy_;
  DataIterHandle iter_;
  DataIterResetCallback *reset_;
  XGDMatrixCallbackNext *next_;

  float missing_;
  GenericParameter ctx_;
  std::string cache_prefix_;
  uint32_t n_batches_ {0};
  // sparse page is the source to other page types, we make a special member function.
  void InitializeSparsePage();
  // Non-virtual version that can be used in constructor
  BatchSet<SparsePage> GetRowBatchesImpl();

 public:
  explicit SparsePageDMatrix(DataIterHandle iter, DMatrixHandle proxy,
                             DataIterResetCallback *reset,
                             XGDMatrixCallbackNext *next, float missing,
                             int32_t nthreads, std::string cache_prefix);

  ~SparsePageDMatrix() override {
    // Clear out all resources before deleting the cache file.
    sparse_page_source_.reset();
    ellpack_page_source_.reset();
    column_source_.reset();
    sorted_column_source_.reset();
    ghist_index_source_.reset();

    for (auto const &kv : cache_info_) {
      CHECK(kv.second);
      auto n = kv.second->ShardName();
      TryDeleteCacheFile(n);
    }
  }

  MetaInfo& Info() override;

  const MetaInfo& Info() const override;

  bool SingleColBlock() const override { return false; }
  DMatrix *Slice(common::Span<int32_t const>) override {
    LOG(FATAL) << "Slicing DMatrix is not supported for external memory.";
    return nullptr;
  }

 private:
  BatchSet<SparsePage> GetRowBatches() override;
  BatchSet<CSCPage> GetColumnBatches() override;
  BatchSet<SortedCSCPage> GetSortedColumnBatches() override;
  BatchSet<EllpackPage> GetEllpackBatches(const BatchParam& param) override;
  BatchSet<GHistIndexMatrix> GetGradientIndex(const BatchParam&) override;

  // source data pointers.
  std::shared_ptr<SparsePageSource> sparse_page_source_;
  std::shared_ptr<EllpackPageSource> ellpack_page_source_;
  std::shared_ptr<CSCPageSource> column_source_;
  std::shared_ptr<SortedCSCPageSource> sorted_column_source_;
  std::shared_ptr<GHistIndexMatrix> ghist_index_page_;  // hist
  std::shared_ptr<GradientIndexPageSource> ghist_index_source_;

  bool EllpackExists() const override {
    return static_cast<bool>(ellpack_page_source_);
  }
  bool SparsePageExists() const override {
    return static_cast<bool>(sparse_page_source_);
  }
};

inline std::string MakeId(std::string prefix, SparsePageDMatrix *ptr) {
  std::stringstream ss;
  ss << ptr;
  return prefix + "-" + ss.str();
}

inline std::string
MakeCache(SparsePageDMatrix *ptr, std::string format, std::string prefix,
          std::map<std::string, std::shared_ptr<Cache>> *out) {
  auto &cache_info = *out;
  auto name = MakeId(prefix, ptr);
  auto id = name + format;
  auto it = cache_info.find(id);
  if (it == cache_info.cend()) {
    cache_info[id].reset(new Cache{false, name, format});
    LOG(INFO) << "Make cache:" << name << std::endl;
  }
  return id;
}
}  // namespace data
}  // namespace xgboost
#endif  // XGBOOST_DATA_SPARSE_PAGE_DMATRIX_H_
