/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, 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/config.h>  // MRPT_IS_BIG_ENDIAN
#include <mrpt/core/Clock.h>
#include <mrpt/core/is_shared_ptr.h>
#include <mrpt/core/reverse_bytes.h>
#include <mrpt/serialization/CSerializable.h>
#include <mrpt/typemeta/TEnumType.h>
#include <mrpt/typemeta/TTypeName.h>

#include <cstdint>
#include <cstring>	// memcpy
#include <stdexcept>
#include <string>
#include <type_traits>	// remove_reference_t, is_polymorphic
#include <variant>
#include <vector>

// See: https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=258854
// See: https://stackoverflow.com/a/46507150/1631514
#if defined(__clang__) && (__clang_major__ <= 7)
#define HAS_BROKEN_CLANG_STD_VISIT
#endif

namespace mrpt::serialization
{
class CMessage;

/** Used in mrpt::serialization::CArchive */
class CExceptionEOF : public std::runtime_error
{
   public:
	CExceptionEOF(const std::string& s) : std::runtime_error(s) {}
};

/** Virtual base class for "archives": classes abstracting I/O streams.
 * This class separates the implementation details of serialization (in
 * CSerializable) and data storage (CArchive children: files, sockets,...).
 *
 * Two main sets of implementations are provided:
 * - archiveFrom: for MRPT mrpt::io::CArchive objects, and
 * - CArchiveStdIStream and CArchiveStdOStream: for std::istream and
 * std::ostream, respectively.
 *
 * \sa mrpt::io::CArchive, mrpt::serialization::CSerializable
 * \ingroup mrpt_serialization_grp
 */
class CArchive
{
   public:
	CArchive() = default;
	virtual ~CArchive() = default;

	using Ptr = std::shared_ptr<CArchive>;
	using UniquePtr = std::unique_ptr<CArchive>;

	/** @name Serialization API for generic "archives" I/O streams
	 * @{ */
	/** Reads a block of bytes from the stream into Buffer
	 *	\exception std::exception On any error, or if ZERO bytes are read.
	 *  \return The amound of bytes actually read.
	 * \note This method is endianness-dependent.
	 * \sa ReadBufferImmediate ; Important, see: ReadBufferFixEndianness,
	 */
	size_t ReadBuffer(void* Buffer, size_t Count);

	/** Reads a sequence of elemental datatypes, taking care of reordering their
	 *bytes from the MRPT stream standard (little endianness) to the format of
	 *the running architecture.
	 *  \param ElementCount The number of elements (not bytes) to read.
	 *  \param ptr A pointer to the first output element in an array (or
	 *std::vector<>, etc...).
	 *  \return The amound of *bytes* (not elements) actually read (under error
	 *situations, the last element may be invalid if the data stream abruptly
	 *ends).
	 *  Example of usage:
	 *  \code
	 *   uint32_t  N;
	 *   s >> N;
	 *   vector<float>  vec(N);
	 *   if (N)
	 *     s.ReadBufferFixEndianness<float>(&vec[0],N);
	 *  \endcode
	 *	\exception std::exception On any error, or if ZERO bytes are read.
	 * \sa ReadBufferFixEndianness, ReadBuffer
	 */
	template <typename T>
	size_t ReadBufferFixEndianness(T* ptr, size_t ElementCount)
	{
#if !MRPT_IS_BIG_ENDIAN
		// little endian: no conversion needed.
		return ReadBuffer(ptr, ElementCount * sizeof(T));
#else
		// big endian: convert.
		const size_t nread = ReadBuffer(ptr, ElementCount * sizeof(T));
		for (size_t i = 0; i < ElementCount; i++)
			mrpt::reverseBytesInPlace(ptr[i]);
		return nread;
#endif
	}

	/** Writes a block of bytes to the stream from Buffer.
	 *	\exception std::exception On any error
	 *  \sa Important, see: WriteBufferFixEndianness
	 * \note This method is endianness-dependent.
	 */
	void WriteBuffer(const void* Buffer, size_t Count);

	/** Writes a sequence of elemental datatypes, taking care of reordering
	 * their bytes from the running architecture to MRPT stream standard (little
	 * endianness).
	 *  \param ElementCount The number of elements (not bytes) to write.
	 *  \param ptr A pointer to the first input element in an array (or
	 * std::vector<>, etc...).
	 *  Example of usage:
	 *  \code
	 *   vector<float>  vec = ...
	 *   uint32_t N = vec.size();
	 *   s << N
	 *   if (N)
	 *     s.WriteBufferFixEndianness<float>(&vec[0],N);
	 *  \endcode
	 *  \exception std::exception On any error
	 *  \sa WriteBuffer
	 */
	template <typename T>
	void WriteBufferFixEndianness(const T* ptr, size_t ElementCount)
	{
#if !MRPT_IS_BIG_ENDIAN
		// little endian: no conversion needed.
		return WriteBuffer(ptr, ElementCount * sizeof(T));
#else
		// big endian: the individual "<<" functions already convert endiannes
		for (size_t i = 0; i < ElementCount; i++)
			(*this) << ptr[i];
#endif
	}
	/** Read a value from a stream stored in a type different of the target
	 * variable, making the conversion via static_cast. Useful for coding
	 * backwards compatible de-serialization blocks */
	template <typename STORED_TYPE, typename CAST_TO_TYPE>
	void ReadAsAndCastTo(CAST_TO_TYPE& read_here)
	{
		STORED_TYPE var;
		(*this) >> var;
		read_here = static_cast<CAST_TO_TYPE>(var);
	}
	/** De-serialize a variable and returns it by value. */
	template <typename STORED_TYPE>
	STORED_TYPE ReadAs()
	{
		STORED_TYPE var;
		(*this) >> var;
		return var;
	}
	template <typename TYPE_TO_STORE, typename TYPE_FROM_ACTUAL>
	CArchive& WriteAs(const TYPE_FROM_ACTUAL& value)
	{
		(*this) << static_cast<TYPE_TO_STORE>(value);
		return *this;
	}
	/** Writes an object to the stream.
	 */
	void WriteObject(const CSerializable* o);
	void WriteObject(const CSerializable& o) { WriteObject(&o); }

	/** Requires to serialize variants without a proper value. */
	CArchive& operator<<(const std::monostate&);

	/** Reads an object from stream, its class determined at runtime, and
	 * returns a smart pointer to the object.
	 * \exception std::exception On I/O error or undefined class.
	 * \exception CExceptionEOF On an End-Of-File condition found
	 * at a correct place: an EOF that abruptly finishes in the middle of one
	 * object raises a plain std::exception instead.
	 */
	CSerializable::Ptr ReadObject() { return ReadObject<CSerializable>(); }
	/** Reads an object from stream, its class determined at runtime, and
	 * returns a smart pointer to the object. This version is similar to
	 * std::make_shared<T>.
	 * \exception std::exception On I/O error or undefined class.
	 * \exception CExceptionEOF On an End-Of-File condition found
	 * at a correct place: an EOF that abruptly finishes in the middle of one
	 * object raises a plain std::exception instead.
	 */
	template <typename T>
	typename T::Ptr ReadObject()
	{
		CSerializable::Ptr obj;
		std::string strClassName;
		bool isOldFormat{false};
		int8_t version{-1};
		internal_ReadObjectHeader(strClassName, isOldFormat, version);
		if (strClassName != "nullptr")
		{
			const mrpt::rtti::TRuntimeClassId* classId =
				mrpt::rtti::findRegisteredClass(strClassName);
			if (!classId)
				THROW_EXCEPTION_FMT(
					"Stored object has class '%s' which is not registered!",
					strClassName.c_str());
			obj = mrpt::ptr_cast<CSerializable>::from(classId->createObject());
		}
		internal_ReadObject(
			obj.get() /* may be nullptr */, strClassName, isOldFormat,
			version);  // must be called to read the END FLAG byte
		if (!obj) { return typename T::Ptr(); }
		else
		{
			return std::dynamic_pointer_cast<T>(obj);
		}
	}

	/** If redefined in derived classes, allows finding a human-friendly
	 * description of the underlying stream (e.g. filename) */
	virtual std::string getArchiveDescription() const
	{
		return "generic CArchive";
	}

   private:
	template <typename RET>
	RET ReadVariant_helper(CSerializable::Ptr& ptr)
	{
		throw std::runtime_error("Can't match variant type");
		return RET();
	}

	template <typename RET, typename T, typename... R>
	RET ReadVariant_helper(
		CSerializable::Ptr& ptr,
		std::enable_if_t<mrpt::is_shared_ptr<T>::value>* = nullptr)
	{
		if (IS_CLASS(*ptr, typename T::element_type))
			return std::dynamic_pointer_cast<typename T::element_type>(ptr);
		return ReadVariant_helper<RET, R...>(ptr);
	}

	template <typename RET, typename T, typename... R>
	RET ReadVariant_helper(
		CSerializable::Ptr& ptr,
		std::enable_if_t<!mrpt::is_shared_ptr<T>::value>* = nullptr)
	{
		if (IS_CLASS(*ptr, T)) return dynamic_cast<T&>(*ptr);
		return ReadVariant_helper<RET, R...>(ptr);
	}

   public:
	/** Reads a variant from stream, its class determined at runtime, and
	 * returns a variant to the object.
	 * To be compatible with the current polymorphic system this support smart
	 * pointer types.
	 * For pointer types, This will bind to the first possible pointer type.
	 * variant<CSerializable::Ptr, CRenderizable::Ptr>
	 * \exception std::exception On I/O error or undefined class.
	 * \exception CExceptionEOF On an End-Of-File condition found
	 * at a correct place: an EOF that abruptly finishes in the middle of one
	 * object raises a plain std::exception instead.
	 */
	template <typename... T>
	typename std::variant<T...> ReadVariant()
	{
		CSerializable::Ptr obj;
		std::string strClassName;
		bool isOldFormat;
		int8_t version;
		internal_ReadObjectHeader(strClassName, isOldFormat, version);
		if (strClassName == "std::monostate") return {};
		const mrpt::rtti::TRuntimeClassId* classId =
			mrpt::rtti::findRegisteredClass(strClassName);
		if (!classId)
			THROW_EXCEPTION_FMT(
				"Stored object has class '%s' which is not registered!",
				strClassName.c_str());
		if (strClassName != "nullptr")
		{
			obj = mrpt::ptr_cast<CSerializable>::from(classId->createObject());
		}
		internal_ReadObject(obj.get(), strClassName, isOldFormat, version);
		if (!obj) { return std::variant<T...>(); }
		else
		{
			return ReadVariant_helper<std::variant<T...>, T...>(obj);
		}
	}

#if !defined(HAS_BROKEN_CLANG_STD_VISIT)
	/** Writes a Variant to the stream */
	template <typename T>
	void WriteVariant(T t)
	{
		std::visit([&](auto& o) { *this << o; }, t);
	}
#endif

	/** Reads a simple POD type and returns by value. Useful when `stream >>
	 * var;`
	 * cannot be used becuase of errors of misaligned reference binding.
	 * Use with macro `MRPT_READ_POD` to avoid typing the type T yourself.
	 * \note [New in MRPT 2.0.0]
	 * \note Write operator `s << var;` is safe for misaligned variables.
	 */
	template <typename T>
	T ReadPOD()
	{
		T ret;
		ReadBufferFixEndianness(&ret, 1);
		return ret;
	}

	/** Reads an object from stream, where its class must be the same
	 *    as the supplied object, where the loaded object will be stored in.
	 * \exception std::exception On I/O error or different class found.
	 * \exception CExceptionEOF On an End-Of-File condition found
	 * at a correct place: an EOF that abruptly finishes in the middle of one
	 * object raises a plain std::exception instead.
	 */
	void ReadObject(CSerializable* existingObj);

	/** Send a message to the device.
	 *  Note that only the low byte from the "type" field will be used.
	 *
	 *  For frames of size < 255 the frame format is an array of bytes in this
	 * order:
	 *  \code
	 *  <START_FLAG> <HEADER> <LENGTH> <BODY> <END_FLAG>
	 *  	<START_FLAG> 	= 0x69
	 *  	<HEADER> 		= A header byte
	 *  	<LENGHT>		= Number of bytes of BODY
	 *  	<BODY>			= N x bytes
	 *  	<END_FLAG>		= 0X96
	 *  Total length 	= 	<LENGTH> + 4
	 *  \endcode
	 *
	 *  For frames of size > 255 the frame format is an array of bytes in this
	 * order:
	 *  \code
	 *  <START_FLAG> <HEADER> <HIBYTE(LENGTH)> <LOBYTE(LENGTH)> <BODY>
	 * <END_FLAG>
	 *  	<START_FLAG> 	= 0x79
	 *  	<HEADER> 		= A header byte
	 *  	<LENGHT>		= Number of bytes of BODY
	 *  	<BODY>			= N x bytes
	 *  	<END_FLAG>		= 0X96
	 *  Total length 	= 	<LENGTH> + 5
	 *  \endcode
	 *
	 * \exception std::exception On communication errors
	 */
	void sendMessage(const CMessage& msg);

	/** Tries to receive a message from the device.
	 * \exception std::exception On communication errors
	 * \returns True if successful, false if there is no new data from the
	 * device (but communications seem to work fine)
	 * \sa The frame format is described in sendMessage()
	 */
	bool receiveMessage(CMessage& msg);

	/** Write a CSerializable object to a stream in the binary MRPT format */
	CArchive& operator<<(const CSerializable& obj);
	/** \overload */
	CArchive& operator<<(const CSerializable::Ptr& pObj);
	/** Reads a CSerializable object from the stream */
	CArchive& operator>>(CSerializable& obj);
	/** \overload */
	CArchive& operator>>(CSerializable::Ptr& pObj);

	/** @} */

   protected:
	/** @name Virtual methods of the CArchive interface
	 * @{ */
	/** Writes a block of bytes.
	 * \exception std::exception On any error
	 * \return Number of bytes actually written.
	 */
	virtual size_t write(const void* buf, size_t len) = 0;
	/** Reads a block of bytes.
	 * \exception std::exception On any error, or if ZERO bytes are read.
	 * \return Number of bytes actually read if >0.
	 */
	virtual size_t read(void* buf, size_t len) = 0;
	/** @} */

	/** Read the object */
	void internal_ReadObject(
		CSerializable* newObj, const std::string& className, bool isOldFormat,
		int8_t version);

	/** Read the object Header*/
	void internal_ReadObjectHeader(
		std::string& className, bool& isOldFormat, int8_t& version);
};

// Note: write op accepts parameters by value on purpose, to avoid misaligned
// reference binding errors.
template <class T, class... Ts>
using is_any = std::disjunction<std::is_same<T, Ts>...>;

template <typename T>
using is_simple_type = is_any<
	T, bool, uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t,
	int64_t, float, double, long double>;

#if MRPT_IS_BIG_ENDIAN
// Big endian system: Convert into little-endian for streaming
template <typename T, std::enable_if_t<is_simple_type<T>::value, int> = 0>
CArchive& operator<<(CArchive& out, T a)
{
	mrpt::reverseBytesInPlace(a);
	out.WriteBuffer((void*)&a, sizeof(a));
	return out;
}

template <typename T, std::enable_if_t<is_simple_type<T>::value, int> = 0>
CArchive& operator>>(CArchive& in, T& a)
{
	T b;
	in.ReadBuffer((void*)&b, sizeof(a));
	mrpt::reverseBytesInPlace(b);
	std::memcpy(&a, &b, sizeof(b));
	return in;
}

#else
// Little endian system:
template <typename T, std::enable_if_t<is_simple_type<T>::value, int> = 0>
CArchive& operator<<(CArchive& out, const T& a)
{
	out.WriteBuffer((void*)&a, sizeof(a));
	return out;
}

template <typename T, std::enable_if_t<is_simple_type<T>::value, int> = 0>
CArchive& operator>>(CArchive& in, T& a)
{
	in.ReadBuffer((void*)&a, sizeof(a));
	return in;
}
#endif

CArchive& operator<<(CArchive& out, const mrpt::Clock::time_point& a);
CArchive& operator>>(CArchive& in, mrpt::Clock::time_point& a);

#define MRPT_READ_POD(_STREAM, _VARIABLE)                                      \
	do                                                                         \
	{                                                                          \
		const std::remove_cv_t<std::remove_reference_t<decltype(_VARIABLE)>>   \
			val = _STREAM.ReadPOD<std::remove_cv_t<                            \
				std::remove_reference_t<decltype(_VARIABLE)>>>();              \
		std::memcpy(&_VARIABLE, &val, sizeof(val));                            \
	} while (0)

// Why this shouldn't be templatized?: There's a more modern system
// in MRPT that serializes any kind of vector<T>, deque<T>, etc... but
// to keep COMPATIBILITY with old serialized objects we must preserve
// the ones listed here:

// Write --------------------
CArchive& operator<<(CArchive& s, const std::string& str);

CArchive& operator<<(CArchive&, const std::vector<int32_t>& a);
CArchive& operator<<(CArchive&, const std::vector<uint32_t>& a);
CArchive& operator<<(CArchive&, const std::vector<uint16_t>& a);
CArchive& operator<<(CArchive&, const std::vector<int16_t>& a);
CArchive& operator<<(CArchive&, const std::vector<uint32_t>& a);
CArchive& operator<<(CArchive&, const std::vector<int64_t>& a);
CArchive& operator<<(CArchive&, const std::vector<uint8_t>& a);
CArchive& operator<<(CArchive&, const std::vector<int8_t>& a);

CArchive& operator<<(CArchive&, const std::vector<bool>& a);
CArchive& operator<<(CArchive&, const std::vector<std::string>&);

#if MRPT_WORD_SIZE != 32  // If it's 32 bit, size_t <=> uint32_t
CArchive& operator<<(CArchive&, const std::vector<size_t>& a);
#endif

// Read --------------------
CArchive& operator>>(CArchive& in, std::string& str);

CArchive& operator>>(CArchive& in, std::vector<int32_t>& a);
CArchive& operator>>(CArchive& in, std::vector<uint32_t>& a);
CArchive& operator>>(CArchive& in, std::vector<uint16_t>& a);
CArchive& operator>>(CArchive& in, std::vector<int16_t>& a);
CArchive& operator>>(CArchive& in, std::vector<int64_t>& a);
CArchive& operator>>(CArchive& in, std::vector<uint8_t>& a);
CArchive& operator>>(CArchive& in, std::vector<int8_t>& a);
CArchive& operator>>(CArchive& in, std::vector<bool>& a);

CArchive& operator>>(CArchive& in, std::vector<std::string>& a);

// For backward compatibility, since in MRPT<0.8.1 vector_XXX and
// std::vector<XXX> were exactly equivalent, now there're not.
CArchive& operator>>(CArchive& s, std::vector<float>& a);
CArchive& operator>>(CArchive& s, std::vector<double>& a);
CArchive& operator<<(CArchive& s, const std::vector<float>& a);
CArchive& operator<<(CArchive& s, const std::vector<double>& a);

#if MRPT_WORD_SIZE != 32  // If it's 32 bit, size_t <=> uint32_t
CArchive& operator>>(CArchive& s, std::vector<size_t>& a);
#endif
//

template <
	typename T,
	std::enable_if_t<
		std::is_base_of_v<mrpt::serialization::CSerializable, T>>* = nullptr>
CArchive& operator>>(CArchive& in, typename std::shared_ptr<T>& pObj)
{
	pObj = in.ReadObject<T>();
	return in;
}

template <typename... T>
CArchive& operator>>(CArchive& in, typename std::variant<T...>& pObj)
{
	pObj = in.ReadVariant<T...>();
	return in;
}

template <typename... T>
CArchive& operator<<(CArchive& out, const typename std::variant<T...>& pObj)
{
	std::visit([&](auto& o) { out << o; }, pObj);
	return out;
}

/** Write a shared_ptr to a non-CSerializable object */
template <
	class T,
	std::enable_if_t<
		!std::is_base_of_v<mrpt::serialization::CSerializable, T>>* = nullptr>
CArchive& operator<<(CArchive& out, const std::shared_ptr<T>& pObj)
{
	if (pObj)
	{
		out << mrpt::typemeta::TTypeName<T>::get();
		out << *pObj;
	}
	else
	{
		out << std::string("nullptr");
	}
	return out;
}

/** Read a smart pointer to a non-CSerializable (POD,...) data type*/
template <
	class T,
	std::enable_if_t<
		!std::is_base_of_v<mrpt::serialization::CSerializable, T>>* = nullptr>
CArchive& operator>>(CArchive& in, std::shared_ptr<T>& pObj)
{
	std::string stored_name;
	in >> stored_name;
	const std::string expected_name =
		mrpt::typemeta::TTypeName<T>::get().c_str();
	if (stored_name == std::string("nullptr")) { pObj.reset(); }
	else
	{
		ASSERT_EQUAL_(expected_name, stored_name);
		pObj = std::make_shared<T>();
		in >> *pObj;
	}
	return in;
}

template <
	class ENUM_TYPE, std::enable_if_t<std::is_enum_v<ENUM_TYPE>>* = nullptr>
CArchive& operator<<(CArchive& out, const ENUM_TYPE& pEnum)
{
	const std::string value =
		mrpt::typemeta::TEnumType<std::remove_cv_t<ENUM_TYPE>>::value2name(
			pEnum);

	out << value;
	return out;
}

template <
	class ENUM_TYPE, std::enable_if_t<std::is_enum_v<ENUM_TYPE>>* = nullptr>
CArchive& operator>>(CArchive& in, ENUM_TYPE& pEnum)
{
	std::string readValue;
	in >> readValue;

	pEnum = mrpt::typemeta::TEnumType<std::remove_cv_t<ENUM_TYPE>>::name2value(
		readValue);
	return in;
}

/** CArchive for mrpt::io::CStream classes (use as template argument).
 * \sa Easier to use via function archiveFrom() */
template <class STREAM>
class CArchiveStreamBase : public CArchive
{
	STREAM& m_s;

   public:
	CArchiveStreamBase(STREAM& s) : m_s(s) {}

	std::string getArchiveDescription() const override
	{
		return m_s.getStreamDescription();
	}

   protected:
	size_t write(const void* d, size_t n) override { return m_s.Write(d, n); }
	size_t read(void* d, size_t n) override { return m_s.Read(d, n); }
};

/** Helper function to create a templatized wrapper CArchive object for a:
 * MRPT's `CStream`, `std::istream`, `std::ostream`, `std::stringstream`.
 * \note Use with `std::{.*}stream` requires including
 * `<mrpt/serialization/archiveFrom_std_streams.h>` and explicitly
 * specifying the template parameter like: `archiveFrom<std::istream>` or
 * `archiveFrom<std::ostream>`.
 * \sa \ref mrpt_serialization_grp, and example serialization_stl/test.cpp
 */
template <class STREAM>
CArchiveStreamBase<STREAM> archiveFrom(STREAM& s)
{
	return CArchiveStreamBase<STREAM>(s);
}

/** Like archiveFrom(), returning a shared_ptr<>. */
template <class STREAM>
CArchive::Ptr archivePtrFrom(STREAM& s)
{
	return std::make_shared<CArchiveStreamBase<STREAM>>(s);
}

/** Like archiveFrom(), returning a unique_ptr<>. */
template <class STREAM>
CArchive::UniquePtr archiveUniquePtrFrom(STREAM& s)
{
	return std::make_unique<CArchiveStreamBase<STREAM>>(s);
}

}  // namespace mrpt::serialization

namespace mrpt::rtti
{
// for std::variant
template <>
struct CLASS_ID_impl<std::monostate>
{
	static constexpr const mrpt::rtti::TRuntimeClassId* get()
	{
		return nullptr;
	}
};

}  // namespace mrpt::rtti
