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

#include "Definitions.h"
#include "compat/Filesystem.h"
#include "util/EnumDict.h"
#include "yaml/BaseTypes.h"
#include "yaml/Property.h"

#include <sstream>
#include <string_view>

#include <fmt/format.h>
#include <yaml-cpp/yaml.h>

namespace cbm::algo::yaml
{

  template<typename T>
  T Read(const YAML::Node& node);

  template<typename T, T... Values, typename Func>
  constexpr void ForEach(std::integer_sequence<T, Values...>, Func&& func)
  {
    (func(std::integral_constant<T, Values>{}), ...);
  }

  template<typename T, typename = void>
  struct GetFmtTag {
    static constexpr std::optional<YAML::EMITTER_MANIP> value = {};
  };

  template<typename T>
  struct GetFmtTag<T, std::void_t<decltype(T::FormatAs)>> {
    static constexpr std::optional<YAML::EMITTER_MANIP> value = T::FormatAs;
  };

  template<typename T, typename = void>
  struct ShouldMergeProperty {
    static constexpr bool value = false;
  };

  template<typename T>
  struct ShouldMergeProperty<T, std::void_t<decltype(T::MergeProperty)>> {
    static constexpr bool value = T::MergeProperty;
  };

  template<typename T>
  T ReadFromFile(fs::path path)
  {
    YAML::Node node = YAML::LoadFile(path.string());
    return Read<T>(node);
  }


  template<typename T>
  T Read(const YAML::Node& node)
  {
// Disable uninitialized warning for the whole function.
// GCC 11.4 sometimes incorrectly warns about uninitialized variables in constexpr if branches.
// I'm fairly certain this is a compiler bug, because it only happens when
// explicitly instantiating the function template. And the error message
// references a variable 't' that doesn't exist in the function!
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
    using Type = std::remove_cv_t<std::remove_reference_t<T>>;

    static_assert(!IsEnum<T> || detail::EnumHasDict_v<T>, "Enum must have a dictionary to be deserializable");

    // TODO: error handling
    if constexpr (IsFundamental<Type>) {
      return node.as<Type>();
    }
    else if constexpr (IsEnum<Type>) {
      std::optional<T> maybet = FromString<T>(node.as<std::string>());
      if (!maybet) {
        throw std::runtime_error(fmt::format("Invalid enum value: {}", node.as<std::string>()));
      }
      return *maybet;
    }
    else if constexpr (IsVector<Type>) {
      Type vector;
      for (const auto& element : node) {
        vector.push_back(Read<typename Type::value_type>(element));
      }
      return vector;
    }
    else if constexpr (IsArray<Type>) {
      Type array  = {};
      auto vector = Read<std::vector<typename Type::value_type>>(node);
      if (vector.size() != array.size()) {
        throw std::runtime_error(fmt::format("Array size mismatch: expected {}, got {}", array.size(), vector.size()));
      }
      std::copy(vector.begin(), vector.end(), array.begin());
      return array;
    }
    else if constexpr (IsSet<Type>) {
      Type set;
      for (const auto& element : node) {
        set.insert(Read<typename Type::value_type>(element));
      }
      return set;
    }
    else if constexpr (IsMap<Type>) {
      using Key_t = typename Type::key_type;
      using Val_t = typename Type::mapped_type;

      static_assert(IsScalar<Key_t>, "Map key must be a fundamental or enum type");

      Type map{};
      for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
        const auto& key       = it->first;
        const auto& value     = it->second;
        map[Read<Key_t>(key)] = Read<Val_t>(value);
      }
      return map;
    }
    else {
      Type object{};

      constexpr auto nProperties = std::tuple_size<decltype(Type::Properties)>::value;

      if constexpr (nProperties == 1 && ShouldMergeProperty<T>::value) {
        auto& property   = std::get<0>(Type::Properties);
        using ValueType  = std::remove_cv_t<std::remove_reference_t<decltype(property.Get(std::declval<Type>()))>>;
        ValueType& value = property.Get(object);
        value            = Read<ValueType>(node);
      }
      else {
        ForEach(std::make_integer_sequence<std::size_t, nProperties>{}, [&](auto index) {
          auto& property   = std::get<index>(Type::Properties);
          using ValueType  = std::remove_cv_t<std::remove_reference_t<decltype(property.Get(object))>>;
          ValueType& value = property.Get(object);
          value            = Read<ValueType>(node[std::string{property.Key()}]);
        });
      }

      return object;
    }

#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic pop
#endif
  }

  template<typename T>
  std::string MakeDocString(int indent = 0)
  {
    using Type = std::remove_cv_t<std::remove_reference_t<T>>;

    std::stringstream docString;

    if constexpr (IsFundamental<Type>) {
      docString << Typename<Type>();
    }
    else if constexpr (IsVector<Type> || IsArray<Type>) {
      using ChildType = typename Type::value_type;
      if constexpr (IsFundamental<ChildType>) {
        docString << std::string(indent, ' ') << "list of " << Typename<ChildType>() << std::endl;
      }
      else {
        docString << std::string(indent, ' ') << "list of" << std::endl;
        docString << MakeDocString<ChildType>(indent + 2);
      }
    }
    else {
      constexpr auto nProperties = std::tuple_size<decltype(Type::Properties)>::value;

      ForEach(std::make_integer_sequence<std::size_t, nProperties>{}, [&](auto index) {
        using ChildType = std::remove_cv_t<
          std::remove_reference_t<decltype(std::get<index>(Type::Properties).Get(std::declval<Type>()))>>;
        auto& property = std::get<index>(Type::Properties);
        if constexpr (IsFundamental<ChildType>) {
          docString << std::string(indent, ' ') << property.Key() << ": " << property.Description() << " ["
                    << Typename<ChildType>() << "]" << std::endl;
        }
        else {
          docString << std::string(indent, ' ') << property.Key() << ": " << property.Description() << std::endl;
          docString << MakeDocString<ChildType>(indent + 2);
        }
      });
    }
    return docString.str();
  }

  class Dump {

   public:
    template<typename T>
    std::string operator()(const T& object, int floatPrecision = 6)
    {
      YAML::Emitter ss;
      ss << YAML::BeginDoc;
      ss << YAML::Precision(floatPrecision);
      DoDump(object, ss);
      ss << YAML::EndDoc;
      return ss.c_str();
    }

   private:
    template<typename T>
    void DoDump(const T& object, YAML::Emitter& ss, std::optional<YAML::EMITTER_MANIP> formatEntries = {})
    {
      static_assert(!IsEnum<T> || detail::EnumHasDict_v<T>, "Enum must have a dictionary");

      if constexpr (IsFundamental<T>) {
        // Take care that i8 and u8 are printed as integers not as characters
        if constexpr (std::is_same_v<T, i8> || std::is_same_v<T, u8>)
          ss << i32(object);
        else
          ss << object;
      }
      else if constexpr (IsEnum<T>) {
        ss << std::string{ToString<T>(object)};
      }
      else if constexpr (IsVector<T> || IsArray<T> || IsSet<T>) {
        ss << YAML::BeginSeq;
        // Special case for vector<bool> because it is not a real vector
        // Clang does not the compile the generic version of the loop
        // in this case.
        if constexpr (std::is_same_v<T, std::vector<bool>>) {
          for (bool element : object) {
            if (formatEntries.has_value()) {
              ss << formatEntries.value();
            }
            ss << element;
          }
        }
        else {
          for (const auto& element : object) {
            if (formatEntries.has_value()) {
              ss << formatEntries.value();
            }
            DoDump(element, ss);
          }
        }
        ss << YAML::EndSeq;
      }
      else if constexpr (IsMap<T>) {
        ss << YAML::BeginMap;
        for (const auto& [key, value] : object) {
          if (formatEntries.has_value()) {
            ss << formatEntries.value();
          }
          ss << YAML::Key << key;
          ss << YAML::Value;
          DoDump(value, ss);
        }
        ss << YAML::EndMap;
      }
      else {
        constexpr auto nProperties = std::tuple_size<decltype(T::Properties)>::value;
        if (auto fmtTag = GetFmtTag<T>::value; fmtTag.has_value()) {
          ss << fmtTag.value();
        }

        if constexpr (nProperties == 1 && ShouldMergeProperty<T>::value) {
          auto& property = std::get<0>(T::Properties);
          auto& value    = property.Get(object);
          auto format    = property.Format();
          if (format.has_value()) {
            ss << format.value();
          }
          DoDump(value, ss, property.FormatEntries());
        }
        else {
          ss << YAML::BeginMap;
          ForEach(std::make_integer_sequence<std::size_t, nProperties>{}, [&](auto index) {
            auto& property = std::get<index>(T::Properties);
            auto& value    = property.Get(object);
            auto format    = property.Format();
            ss << YAML::Key << std::string{property.Key()};
            if (format.has_value()) {
              ss << format.value();
            }
            ss << YAML::Value;
            DoDump(value, ss, property.FormatEntries());
          });
          ss << YAML::EndMap;
        }
      }
    }
  };

}  // namespace cbm::algo::yaml

/**
 * @brief Declare the external instantiation of the Read and Dump functions for a type.
 * @param type The type to declare the external instantiation for.
 * @note This macro should be used in a header file to declare the external instantiation of the Read and Dump.
 * Must be paired with CBM_YAML_INSTANTIATE in a source file.
 **/
#define CBM_YAML_EXTERN_DECL(type)                                                                                     \
  extern template type cbm::algo::yaml::Read<type>(const YAML::Node& node);                                            \
  extern template std::string cbm::algo::yaml::Dump::operator()<type>(const type& value, int floatPrecision)

/**
 * @brief Explicitly instantiate the Read and Dump functions for a type.
 * @see CBM_YAML_EXTERN_DECL
 */
#define CBM_YAML_INSTANTIATE(type)                                                                                     \
  template type cbm::algo::yaml::Read<type>(const YAML::Node& node);                                                   \
  template std::string cbm::algo::yaml::Dump::operator()<type>(const type& value, int floatPrecision);

#endif  // CBM_YAML_YAML_H