/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
   SPDX-License-Identifier: GPL-3.0-only
   Authors: Felix Weiglhofer [committer] */
#ifndef CBM_ALGO_BASE_UTIL_SERIALIZABLEENUM_H
#define CBM_ALGO_BASE_UTIL_SERIALIZABLEENUM_H

#include <boost/algorithm/string/predicate.hpp>

#include <algorithm>
#include <iosfwd>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <vector>

#include <fmt/format.h>
#include <xpu/defines.h>

namespace cbm::algo
{
  namespace detail
  {
    template<typename T>
    using EnumDict_t = std::vector<std::pair<std::string_view, T>>;

    template<typename T>
    inline const EnumDict_t<T> EnumDict;

    template<typename T>
    struct EnumHasDict : std::false_type {
    };

    template<typename T>
    inline constexpr bool EnumHasDict_v = EnumHasDict<T>::value;

    template<typename T, typename = std::enable_if_t<detail::EnumHasDict_v<T>>>
    std::vector<std::string_view> ValidEntries()
    {
      std::vector<std::string_view> entries;
      for (const auto& pair : EnumDict<T>) {
        entries.push_back(pair.first);
      }
      return entries;
    }

    void RaiseUnknownEntry(std::string_view str, const std::vector<std::string_view>& validEntries);
  }  // namespace detail

  template<typename T, typename = std::enable_if_t<detail::EnumHasDict_v<T>>>
  std::optional<T> FromString(std::string_view str, bool caseSensitive = false)
  {
    const auto& dict = detail::EnumDict<T>;
    auto it          = std::find_if(dict.begin(), dict.end(), [&](const auto& pair) {
      if (caseSensitive)
        return pair.first == str;
      else
        return boost::iequals(pair.first, str);
    });
    if (it == dict.end()) return std::nullopt;
    return it->second;
  }

  template<typename T, typename = std::enable_if_t<detail::EnumHasDict_v<T>>>
  std::string_view ToString(T t)
  {
    const auto& dict = detail::EnumDict<T>;
    auto it          = std::find_if(dict.begin(), dict.end(), [t](const auto& pair) { return pair.second == t; });
    if (it == dict.end()) throw std::runtime_error(fmt::format("Entry {} for enum missing!", static_cast<int>(t)));
    return it->first;
  }
}  // namespace cbm::algo

#if XPU_IS_CPU
/**
   * @brief Convert enums to strings and back.
   *
   * @param type The enum type.
   *
   * Example:
   * @code{.cpp}
   * enum class Detector {
   *  STS,
   *  TOF,
   * };
   *
   * CBM_ENUM_DICT(Detector,
   *  {"sts", Detector::STS},
   *  {"tof", Detector::TOF}
   * );
   *
   * // Use it like this:
   * L_(info) << ToString(Detector::STS); // Prints "sts"
   *
   * std::optional<Detector> d = FromString<Detector>("tof"); // *d == Detector::TOF
   * std::optional<Detector> d2 = FromString<Detector>("invalid"); // d2 == std::nullopt
   * @endcode
   */
#define CBM_ENUM_DICT(type, ...)                                                                                       \
  template<>                                                                                                           \
  inline const cbm::algo::detail::EnumDict_t<type> cbm::algo::detail::EnumDict<type> = {__VA_ARGS__};                  \
  template<>                                                                                                           \
  struct cbm::algo::detail::EnumHasDict<type> : std::true_type {                                                       \
  }
#else  // XPU_IS_CPU
// Disable macro in GPU code, causes some issues with nvcc
#define CBM_ENUM_DICT(type, ...)
#endif  // XPU_IS_CPU

// Stream operators for enums
// Placed in global namespace to be found by ADL e.g. for std::ostream_iterator
namespace std
{
  template<typename T, typename = std::enable_if_t<cbm::algo::detail::EnumHasDict_v<T>>>
  std::ostream& operator<<(std::ostream& os, T t)
  {
    os << cbm::algo::ToString(t);
    return os;
  }

  template<typename T, typename = std::enable_if_t<cbm::algo::detail::EnumHasDict_v<T>>>
  std::istream& operator>>(std::istream& is, T& t)
  {
    std::string str;
    is >> str;
    auto maybet = cbm::algo::FromString<T>(str);

    if (!maybet) {
      cbm::algo::detail::RaiseUnknownEntry(str, cbm::algo::detail::ValidEntries<T>());
    }
    t = *maybet;

    return is;
  }
}  // namespace std

#endif  //CBM_ALGO_BASE_UTIL_SERIALIZABLEENUM_H