/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/serialization/CArchive.h>
#include <mrpt/serialization/metaprogramming_serialization.h>
#include <mrpt/typemeta/TTypeName_stl.h>  // TTypeName<> for STL templates, needed for serialization of STL templates

#include <algorithm>  // for_each()
#include <array>
#include <deque>
#include <list>
#include <map>
#include <set>
#include <vector>

namespace mrpt::serialization
{
/** \addtogroup mrpt_serialization_stlext_grp
 * @{ */

#define MRPTSTL_SERIALIZABLE_SEQ_CONTAINER(CONTAINER)                                  \
  /** Template method to serialize a sequential STL container  */                      \
  template <class T, class _Ax>                                                        \
  CArchive& operator<<(CArchive& out, const CONTAINER<T, _Ax>& obj)                    \
  {                                                                                    \
    out << std::string(#CONTAINER) << mrpt::typemeta::TTypeName<T>::get();             \
    out.WriteAs<uint32_t>(obj.size());                                                 \
    std::for_each(obj.begin(), obj.end(), metaprogramming::ObjectWriteToStream(&out)); \
    return out;                                                                        \
  }                                                                                    \
  /** Template method to deserialize a sequential STL container */                     \
  template <class T, class _Ax>                                                        \
  CArchive& operator>>(CArchive& in, CONTAINER<T, _Ax>& obj)                           \
  {                                                                                    \
    obj.clear();                                                                       \
    std::string pref, stored_T;                                                        \
    in >> pref;                                                                        \
    if (pref != #CONTAINER)                                                            \
      THROW_EXCEPTION_FMT(                                                             \
          "Error: serialized container %s<%s>'s preambles is wrong: "                  \
          "'%s'",                                                                      \
          #CONTAINER, mrpt::typemeta::TTypeName<T>::get().c_str(), pref.c_str());      \
    in >> stored_T;                                                                    \
    if (stored_T != std::string(mrpt::typemeta::TTypeName<T>::get().c_str()))          \
      THROW_EXCEPTION_FMT(                                                             \
          "Error: serialized container %s< %s != %s >", #CONTAINER, stored_T.c_str(),  \
          mrpt::typemeta::TTypeName<T>::get().c_str());                                \
    const uint32_t n = in.ReadAs<uint32_t>();                                          \
    obj.resize(n);                                                                     \
    std::for_each(obj.begin(), obj.end(), metaprogramming::ObjectReadFromStream(&in)); \
    return in;                                                                         \
  }

template <typename T>
using is_map_like = std::
    is_same<typename T::value_type, std::pair<const typename T::key_type, typename T::mapped_type>>;

template <typename T>
using is_map = std::is_same<
    T,
    typename std::map<
        typename T::key_type,
        typename T::mapped_type,
        typename T::key_compare,
        typename T::allocator_type>>;

template <typename T>
using is_multimap = std::is_same<
    T,
    typename std::multimap<
        typename T::key_type,
        typename T::mapped_type,
        typename T::key_compare,
        typename T::allocator_type>>;

template <typename T, std::enable_if_t<is_map<T>::value, int> = 0>
std::string containerName()
{
  return "std::map";
}
template <typename T, std::enable_if_t<is_multimap<T>::value, int> = 0>
std::string containerName()
{
  return "std::multimap";
}

/** Template method to serialize an associative STL container  */
template <class T, std::enable_if_t<is_map_like<T>::value, int> = 0>
CArchive& operator<<(CArchive& out, const T& obj)
{
  out << containerName<T>() << mrpt::typemeta::TTypeName<typename T::key_type>::get()
      << mrpt::typemeta::TTypeName<std::decay_t<typename T::mapped_type>>::get();
  out.WriteAs<uint32_t>(obj.size());
  for (typename T::const_iterator it = obj.begin(); it != obj.end(); ++it)
    out << it->first << it->second;
  return out;
}
/** Template method to deserialize an associative STL container */
template <class T, std::enable_if_t<is_map_like<T>::value, int> = 0>
CArchive& operator>>(CArchive& in, T& obj)
{
  obj.clear();
  std::string pref, stored_K, stored_V;
  in >> pref;
  const auto kType = mrpt::typemeta::TTypeName<typename T::key_type>::get();
  const auto mTp = mrpt::typemeta::TTypeName<typename T::mapped_type>::get();
  if (pref != containerName<T>())
    THROW_EXCEPTION(format(
        "Error: serialized container %s<%s,%s>'s preamble is "
        "wrong: '%s'",
        containerName<T>().c_str(), kType.c_str(), mTp.c_str(), pref.c_str()));
  in >> stored_K;
  if (stored_K != std::string(kType.c_str()))
    THROW_EXCEPTION(format(
        "Error: serialized container %s key type %s != %s", containerName<T>().c_str(),
        stored_K.c_str(), kType.c_str()));
  in >> stored_V;
  if (stored_V != std::string(mTp.c_str()))
    THROW_EXCEPTION(format(
        "Error: serialized container %s value type %s != %s", containerName<T>().c_str(),
        stored_V.c_str(), mTp.c_str()));
  const uint32_t n = in.ReadAs<uint32_t>();
  for (uint32_t i = 0; i < n; i++)
  {
    typename T::key_type key_obj;
    in >> key_obj;
    /* Create an pair (Key, empty), then read directly into the
     * ".second": */
    typename T::iterator it_new =
        obj.insert(obj.end(), std::make_pair(key_obj, typename T::mapped_type()));
    in >> it_new->second;
  }
  return in;
}

#define MRPTSTL_SERIALIZABLE_SIMPLE_ASSOC_CONTAINER(CONTAINER)                              \
  /** Template method to serialize an associative STL container  */                         \
  template <class K, class _Pr, class _Alloc>                                               \
  CArchive& operator<<(CArchive& out, const CONTAINER<K, _Pr, _Alloc>& obj)                 \
  {                                                                                         \
    out << std::string(#CONTAINER) << mrpt::typemeta::TTypeName<K>::get();                  \
    out.WriteAs<uint32_t>(obj.size());                                                      \
    for (const auto& e : obj) out << e;                                                     \
    return out;                                                                             \
  }                                                                                         \
  /** Template method to deserialize an associative STL container */                        \
  template <class K, class _Pr, class _Alloc>                                               \
  CArchive& operator>>(CArchive& in, CONTAINER<K, _Pr, _Alloc>& obj)                        \
  {                                                                                         \
    obj.clear();                                                                            \
    std::string pref, stored_K;                                                             \
    const auto kType = mrpt::typemeta::TTypeName<K>::get();                                 \
    in >> pref;                                                                             \
    if (pref != #CONTAINER)                                                                 \
      THROW_EXCEPTION_FMT(                                                                  \
          "Error: serialized container %s<%s>'s preamble is wrong: "                        \
          "'%s'",                                                                           \
          #CONTAINER, kType.c_str(), pref.c_str());                                         \
    in >> stored_K;                                                                         \
    if (stored_K != std::string(kType.c_str()))                                             \
      THROW_EXCEPTION_FMT(                                                                  \
          "Error: serialized container %s key type %s != %s", #CONTAINER, stored_K.c_str(), \
          kType.c_str());                                                                   \
    const uint32_t n = in.ReadAs<uint32_t>();                                               \
    for (uint32_t i = 0; i < n; i++)                                                        \
    {                                                                                       \
      K key_obj;                                                                            \
      in >> key_obj;                                                                        \
      obj.insert(key_obj);                                                                  \
    }                                                                                       \
    return in;                                                                              \
  }

MRPTSTL_SERIALIZABLE_SEQ_CONTAINER(std::vector)
MRPTSTL_SERIALIZABLE_SEQ_CONTAINER(std::deque)
MRPTSTL_SERIALIZABLE_SEQ_CONTAINER(std::list)

MRPTSTL_SERIALIZABLE_SIMPLE_ASSOC_CONTAINER(std::set)
MRPTSTL_SERIALIZABLE_SIMPLE_ASSOC_CONTAINER(std::multiset)

/** Template method to serialize a std::array<T,N> */
template <class T, size_t N>
CArchive& operator<<(CArchive& out, const std::array<T, N>& obj)
{
  out << std::string("std::array") << static_cast<uint32_t>(N)
      << mrpt::typemeta::TTypeName<T>::get();
  std::for_each(obj.begin(), obj.end(), metaprogramming::ObjectWriteToStream(&out));
  return out;
}

/** Template method to deserialize a std::array<T,N>  */
template <class T, size_t N>
CArchive& operator>>(CArchive& in, std::array<T, N>& obj)
{
  std::string pref, stored_T;
  uint32_t stored_N;
  in >> pref >> stored_N;
  if (pref != "std::array" || stored_N != N)
    THROW_EXCEPTION_FMT(
        "Error: serialized container %s's preambles is wrong: "
        "'%s'",
        mrpt::typemeta::TTypeName<std::array<T, N>>::get().c_str(), pref.c_str());
  in >> stored_T;
  if (stored_T != std::string(mrpt::typemeta::TTypeName<T>::get().c_str()))
    THROW_EXCEPTION_FMT(
        "Error: serialized container std::array< %s != %s >", stored_T.c_str(),
        mrpt::typemeta::TTypeName<T>::get().c_str());
  std::for_each(obj.begin(), obj.end(), metaprogramming::ObjectReadFromStream(&in));
  return in;
}

/** Template method to serialize a STL pair */
template <class T1, class T2>
CArchive& operator<<(CArchive& out, const std::pair<T1, T2>& obj)
{
  out << std::string("std::pair") << mrpt::typemeta::TTypeName<T1>::get()
      << mrpt::typemeta::TTypeName<T2>::get();
  out << obj.first << obj.second;
  return out;
}
/** Template method to deserialize a STL pair */
template <class T1, class T2>
CArchive& operator>>(CArchive& in, std::pair<T1, T2>& obj)
{
  std::string pref, stored_K, stored_V;
  in >> pref;
  if (pref != "std::pair")
    THROW_EXCEPTION(format(
        "Error: serialized std::pair<%s,%s>'s preamble is wrong: '%s'",
        mrpt::typemeta::TTypeName<T1>::get().c_str(), mrpt::typemeta::TTypeName<T2>::get().c_str(),
        pref.c_str()));
  in >> stored_K;
  if (stored_K != std::string(mrpt::typemeta::TTypeName<T1>::get().c_str()))
    THROW_EXCEPTION(format(
        "Error: serialized std::pair first type %s != %s", stored_K.c_str(),
        mrpt::typemeta::TTypeName<T1>::get().c_str()));
  in >> stored_V;
  if (stored_V != std::string(mrpt::typemeta::TTypeName<T2>::get().c_str()))
    THROW_EXCEPTION(format(
        "Error: serialized std::pair second type %s != %s", stored_V.c_str(),
        mrpt::typemeta::TTypeName<T2>::get().c_str()));
  in >> obj.first >> obj.second;
  return in;
}

/** @} */  // end of grouping
}  // namespace mrpt::serialization
