/**
 * Copyright 2015-2024, XGBoost Contributors
 * \file multiclass_metric.cc
 * \brief evaluation metrics for multiclass classification.
 * \author Kailong Chen, Tianqi Chen
 */
#include <xgboost/metric.h>

#include <array>
#include <atomic>
#include <cmath>
#include <numeric>  // for accumulate

#include "../common/math.h"
#include "../common/threading_utils.h"
#include "metric_common.h"  // MetricNoCache

#if defined(XGBOOST_USE_CUDA)
#include <thrust/functional.h>        // thrust::plus<>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/transform_reduce.h>

#include "../common/cuda_context.cuh"  // for CUDAContext
#include "../common/device_helpers.cuh"
#endif  // XGBOOST_USE_CUDA

namespace xgboost::metric {
// tag the this file, used by force static link later.
DMLC_REGISTRY_FILE_TAG(multiclass_metric);

template <typename EvalRowPolicy>
class MultiClassMetricsReduction {
  void CheckLabelError(int32_t label_error, size_t n_class) const {
    CHECK(label_error >= 0 && label_error < static_cast<int32_t>(n_class))
        << "MultiClassEvaluation: label must be in [0, num_class),"
        << " num_class=" << n_class << " but found " << label_error << " in label";
  }

 public:
  MultiClassMetricsReduction() = default;

  [[nodiscard]] PackedReduceResult CpuReduceMetrics(const HostDeviceVector<bst_float>& weights,
                                                    const HostDeviceVector<bst_float>& labels,
                                                    const HostDeviceVector<bst_float>& preds,
                                                    const size_t n_class, int32_t n_threads) const {
    size_t ndata = labels.Size();

    const auto& h_labels = labels.HostVector();
    const auto& h_weights = weights.HostVector();
    const auto& h_preds = preds.HostVector();

    std::atomic<int> label_error {0};
    bool const is_null_weight = weights.Size() == 0;

    std::vector<double> scores_tloc(n_threads, 0);
    std::vector<double> weights_tloc(n_threads, 0);
    common::ParallelFor(ndata, n_threads, [&](size_t idx) {
        bst_float weight = is_null_weight ? 1.0f : h_weights[idx];
        auto label = static_cast<int>(h_labels[idx]);
        if (label >= 0 && label < static_cast<int>(n_class)) {
          auto t_idx = omp_get_thread_num();
          scores_tloc[t_idx] +=
              EvalRowPolicy::EvalRow(label, h_preds.data() + idx * n_class,
                                     n_class) *
              weight;
          weights_tloc[t_idx] += weight;
        } else {
          label_error = label;
        }
    });

    double residue_sum =
        std::accumulate(scores_tloc.cbegin(), scores_tloc.cend(), 0.0);
    double weights_sum =
        std::accumulate(weights_tloc.cbegin(), weights_tloc.cend(), 0.0);

    CheckLabelError(label_error, n_class);
    PackedReduceResult res { residue_sum, weights_sum };

    return res;
  }

#if defined(XGBOOST_USE_CUDA)

  PackedReduceResult DeviceReduceMetrics(Context const* ctx,
                                         const HostDeviceVector<bst_float>& weights,
                                         const HostDeviceVector<bst_float>& labels,
                                         const HostDeviceVector<bst_float>& preds,
                                         const size_t n_class) {
    size_t n_data = labels.Size();

    thrust::counting_iterator<size_t> begin(0);
    thrust::counting_iterator<size_t> end = begin + n_data;

    auto s_labels = labels.DeviceSpan();
    auto s_preds = preds.DeviceSpan();
    auto s_weights = weights.DeviceSpan();

    bool const is_null_weight = weights.Size() == 0;
    auto s_label_error = label_error_.GetSpan<int32_t>(1);
    s_label_error[0] = 0;

    PackedReduceResult result = thrust::transform_reduce(
        ctx->CUDACtx()->CTP(),
        begin, end,
        [=] XGBOOST_DEVICE(size_t idx) {
          bst_float weight = is_null_weight ? 1.0f : s_weights[idx];
          bst_float residue = 0;
          auto label = static_cast<int>(s_labels[idx]);
          if (label >= 0 && label < static_cast<int32_t>(n_class)) {
            residue = EvalRowPolicy::EvalRow(
                label, &s_preds[idx * n_class], n_class) * weight;
          } else {
            s_label_error[0] = label;
          }
          return PackedReduceResult{ residue, weight };
        },
        PackedReduceResult(),
        thrust::plus<PackedReduceResult>());
    CheckLabelError(s_label_error[0], n_class);

    return result;
  }

#endif  // XGBOOST_USE_CUDA

  PackedReduceResult Reduce(Context const* ctx, size_t n_class,
                            const HostDeviceVector<bst_float>& weights,
                            const HostDeviceVector<bst_float>& labels,
                            const HostDeviceVector<bst_float>& preds) {
    PackedReduceResult result;

    if (ctx->IsCPU()) {
      result = CpuReduceMetrics(weights, labels, preds, n_class, ctx->Threads());
    }
#if defined(XGBOOST_USE_CUDA)
    else {  // NOLINT
      preds.SetDevice(ctx->Device());
      labels.SetDevice(ctx->Device());
      weights.SetDevice(ctx->Device());

      dh::safe_cuda(cudaSetDevice(ctx->Ordinal()));
      result = DeviceReduceMetrics(ctx, weights, labels, preds, n_class);
    }
#endif  // defined(XGBOOST_USE_CUDA)
    return result;
  }

 private:
#if defined(XGBOOST_USE_CUDA)
  dh::PinnedMemory label_error_;
#endif  // defined(XGBOOST_USE_CUDA)
};

/*!
 * \brief base class of multi-class evaluation
 * \tparam Derived the name of subclass
 */
template<typename Derived>
struct EvalMClassBase : public MetricNoCache {
  double Eval(const HostDeviceVector<float> &preds, const MetaInfo &info) override {
    if (info.labels.Size() == 0) {
      CHECK_EQ(preds.Size(), 0);
    } else {
      CHECK(preds.Size() % info.labels.Size() == 0) << "label and prediction size not match";
    }
    std::array<double, 2> dat{0.0, 0.0};
    if (info.labels.Size() != 0) {
      const size_t nclass = preds.Size() / info.labels.Size();
      CHECK_GE(nclass, 1U)
          << "mlogloss and merror are only used for multi-class classification,"
          << " use logloss for binary classification";
      auto result = reducer_.Reduce(this->ctx_, nclass, info.weights_, *info.labels.Data(), preds);
      dat[0] = result.Residue();
      dat[1] = result.Weights();
    }
    auto rc = collective::GlobalSum(ctx_, info, linalg::MakeVec(dat.data(), dat.size()));
    collective::SafeColl(rc);
    return Derived::GetFinal(dat[0], dat[1]);
  }
  /*!
   * \brief to be implemented by subclass,
   *   get evaluation result from one row
   * \param label label of current instance
   * \param pred prediction value of current instance
   * \param nclass number of class in the prediction
   */
  XGBOOST_DEVICE static bst_float EvalRow(int label,
                                          const bst_float *pred,
                                          size_t nclass);
  /*!
   * \brief to be overridden by subclass, final transformation
   * \param esum the sum statistics returned by EvalRow
   * \param wsum sum of weight
   */
  inline static double GetFinal(double esum, double wsum) {
    return esum / wsum;
  }

 private:
  MultiClassMetricsReduction<Derived> reducer_;
  // used to store error message
  const char *error_msg_;
};

/*! \brief match error */
struct EvalMatchError : public EvalMClassBase<EvalMatchError> {
  const char* Name() const override {
    return "merror";
  }
  XGBOOST_DEVICE static bst_float EvalRow(int label,
                                          const bst_float *pred,
                                          size_t nclass) {
    return common::FindMaxIndex(pred, pred + nclass) != pred + static_cast<int>(label);
  }
};

/*! \brief match error */
struct EvalMultiLogLoss : public EvalMClassBase<EvalMultiLogLoss> {
  const char* Name() const override {
    return "mlogloss";
  }
  XGBOOST_DEVICE static bst_float EvalRow(int label, const bst_float* pred, size_t /*nclass*/) {
    const bst_float eps = 1e-16f;
    auto k = static_cast<size_t>(label);
    if (pred[k] > eps) {
      return -std::log(pred[k]);
    } else {
      return -std::log(eps);
    }
  }
};

XGBOOST_REGISTER_METRIC(MatchError, "merror")
    .describe("Multiclass classification error.")
    .set_body([](const char*) { return new EvalMatchError(); });

XGBOOST_REGISTER_METRIC(MultiLogLoss, "mlogloss")
    .describe("Multiclass negative loglikelihood.")
    .set_body([](const char*) { return new EvalMultiLogLoss(); });
}  // namespace xgboost::metric
