// Copyright (c) 2013-2020, SIB - Swiss Institute of Bioinformatics and
//                          Biozentrum - University of Basel
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//   http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/// \brief Define everything needed for portable I/O.
/// Here we include any custom Serialize function.
/// Main classes are PortableBinaryDataSink and PortableBinaryDataSource.
/// (see portableIO doc for details)

#ifndef PROMOD3_PORTABLE_BINARY_SERIALIZER_HH
#define PROMOD3_PORTABLE_BINARY_SERIALIZER_HH

#include <promod3/core/portable_binary_data_sink.hh>
#include <promod3/core/portable_binary_data_source.hh>
#include <promod3/core/message.hh>
#include <boost/type_traits/is_fundamental.hpp>
#include <boost/type_traits/is_enum.hpp>
#include <vector>
#include <utility>

// IMPLEMENTATION NOTES:
// - cannot include promod3 headers which reinclude this!
//   -> if this is REALLY needed, put the overloads in the header there
// - cannot compare raw write with serialize output on binary level
//   -> padding bytes can be filled arbitrarily!

namespace promod3 { namespace core {

namespace {

// std::pair for portable serializers
template <typename DS, typename T1, typename T2>
inline void PortableSerializePairs(DS& ds, std::pair<T1,T2>& t)
{
  if (boost::is_fundamental<T1>::value) {
    throw promod3::Error("PortableSerializePairs cannot serialize "
                         "fundamental T1.");
  }
  if (boost::is_fundamental<T2>::value) {
    throw promod3::Error("PortableSerializePairs cannot serialize "
                         "fundamental T2.");
  }
  ds & t.first;
  ds & t.second;
}

// std::vector for portable serializers
template <typename DS, typename T>
inline void PortableSerializeVectors(DS& ds, std::vector<T>& v)
{
  if (boost::is_fundamental<T>::value) {
    throw promod3::Error("PortableSerializeVectors cannot serialize "
                         "fundamental T.");
  }
  uint64_t N_v;
  if (!ds.IsSource()) N_v = v.size();
  ds & N_v;
  if (ds.IsSource()) v.resize(N_v);
  for (uint64_t i = 0; i < N_v; ++i) {
    ds & v[i];
  }
}

} // anon ns

/////////////////////////////////////////////////////////////////////

/// \brief Serialize object given source/sink.
/// Calls object.Serialize(ds) unless this function is overloaded.
/// Existing overloads for integers, floating-points and anything down here.
template <typename DS, typename SERIALIZABLE>
inline void Serialize(DS& ds, SERIALIZABLE& object) {
  object.Serialize(ds);
}

/// \brief Wrapper for ost sources/sinks (write w/o conversion unless enum)
template <typename BINARY_TYPE, typename DS, typename SERIALIZABLE>
inline void ConvertBaseType(DS& ds, SERIALIZABLE& value) {
  // force conversion for any serializer for enums
  if (boost::is_enum<SERIALIZABLE>::value) {
    BINARY_TYPE b_value;
    if (!ds.IsSource()) b_value = static_cast<BINARY_TYPE>(value);
    ds & b_value;
    if (ds.IsSource()) value = static_cast<SERIALIZABLE>(b_value);
  } else {
    ds & value;
  }
}

/// \brief Base type writing with conversion.
/// Call as "ConvertBaseType<BINARY_TYPE>(sink, value)"
/// Req: "BINARY_TYPE b_value = static_cast<BINARY_TYPE>(value)" must work
template <typename BINARY_TYPE, typename SERIALIZABLE>
inline void ConvertBaseType(PortableBinaryDataSink& sink,
                            SERIALIZABLE value) {
  BINARY_TYPE b_value = static_cast<BINARY_TYPE>(value);
  Serialize(sink, b_value);
}

/// \brief Base type reading with conversion.
/// Call as "ConvertBaseType<BINARY_TYPE>(source, value)"
/// Req: "value = static_cast<SERIALIZABLE>(b_value)" must work for
/// "BINARY_TYPE b_value"
template <typename BINARY_TYPE, typename SERIALIZABLE>
inline void ConvertBaseType(PortableBinaryDataSource& source,
                            SERIALIZABLE& value) {
  BINARY_TYPE b_value;
  Serialize(source, b_value);
  value = static_cast<SERIALIZABLE>(b_value);
}

/// \brief Serialize/convert a fixed-sized vector (e.g. Vec3).
template <typename BINARY_TYPE, typename DS, typename Vec>
inline void ConvertFixedSizedVector(DS& ds, Vec& vec, uint size) {
  for (uint i = 0; i < size; ++i) {
    ConvertBaseType<BINARY_TYPE>(ds, vec[i]);
  }
}

/// \brief Serialize a fixed-sized C array.
/// Only to be used for non-fundamental types!
/// Use ConvertFixedSizedVector for fundamental types.
/// Works also for N dim. arrays (guaranteed contiguous). E.g. for "A v[2][3]",
/// use "SerializeArray(ds, &v[0][0], 2*3)".
template <typename DS, typename T>
inline void SerializeArray(DS& ds, T* v, uint size) {
  if (boost::is_fundamental<T>::value) {
    throw promod3::Error("PortableSerializeVectors cannot serialize "
                         "fundamental T.");
  }
  for (uint i = 0; i < size; ++i) {
    ds & v[i];
  }
}

/////////////////////////////////////////////////////////////////////

/// \brief Convert integer or floating point type to little endian.
/// NOTE: x86-def: little endian, so we convert all to that
template <typename T>
inline void EndianConvertWrite(PortableBinaryDataSink& sink, T value) {
  // OST convert function: Convert<OST_LITTLE_ENDIAN,T>::ToIP(T* v)
  // -> change *v from local endian to little endian
  ost::io::Convert<ost::io::OST_LITTLE_ENDIAN, T>::ToIP(&value);
  sink.Stream().write(reinterpret_cast<char*>(&value), sizeof(T));
}
template <typename T>
inline void EndianConvertRead(PortableBinaryDataSource& source, T& value) {
  // OST convert function: Convert<OST_LITTLE_ENDIAN,T>::FromIP(T* v)
  // -> change *v from little endian to local endian
  source.Stream().read(reinterpret_cast<char*>(&value), sizeof(T));
  ost::io::Convert<ost::io::OST_LITTLE_ENDIAN, T>::FromIP(&value);
}

// overload for char (always a distinct type (by standard))
inline void Serialize(PortableBinaryDataSink& sink, char value) {
  sink.Stream().write(&value, 1);
}
inline void Serialize(PortableBinaryDataSource& source, char& value) {
  source.Stream().read(&value, 1);
}

// overload for integer types
inline void Serialize(PortableBinaryDataSink& sink, int8_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, uint8_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, int16_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, uint16_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, int32_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, uint32_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, int64_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, uint64_t value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSource& source, int8_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, uint8_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, int16_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, uint16_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, int32_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, uint32_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, int64_t& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, uint64_t& value) {
  EndianConvertRead(source, value);
}

// overload for floating point types
inline void Serialize(PortableBinaryDataSink& sink, float value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSink& sink, double value) {
  EndianConvertWrite(sink, value);
}
inline void Serialize(PortableBinaryDataSource& source, float& value) {
  EndianConvertRead(source, value);
}
inline void Serialize(PortableBinaryDataSource& source, double& value) {
  EndianConvertRead(source, value);
}

// overload for bool
inline void Serialize(PortableBinaryDataSink& sink, bool value) {
  ConvertBaseType<uint8_t>(sink, value);
}
inline void Serialize(PortableBinaryDataSource& source, bool& value) {
  ConvertBaseType<uint8_t>(source, value);
}

/////////////////////////////////////////////////////////////////////

// overload for strings
inline void Serialize(PortableBinaryDataSink& sink, const String& str) {
  // dump string size and chars
  uint32_t str_len = str.length();
  Serialize(sink, str_len);
  sink.Stream().write(str.c_str(), str_len);
}
inline void Serialize(PortableBinaryDataSink& sink, String& str) {
  const String& strc = str;
  Serialize(sink, strc);
}
inline void Serialize(PortableBinaryDataSource& source, String& str) {
  // static char buffer
  // (tests with g++ -O2 show that this is just as fast as a static char array)
  static std::vector<char> tmp_buf;
  // get string size
  uint32_t str_len;
  Serialize(source, str_len);
  // resize only if needed
  if (tmp_buf.size() < str_len) tmp_buf.resize(str_len);
  source.Stream().read(&tmp_buf[0], str_len);
  str.assign(&tmp_buf[0], str_len);
}

// std::pair (only to be used for non-fundamental types)
template <typename T1, typename T2>
inline void Serialize(PortableBinaryDataSink& ds, std::pair<T1,T2>& t) {
  PortableSerializePairs(ds, t);
}
template <typename T1, typename T2>
inline void Serialize(PortableBinaryDataSource& ds, std::pair<T1,T2>& t) {
  PortableSerializePairs(ds, t);
}

// std::vector (only to be used for non-fundamental types)
template <typename T>
inline void Serialize(PortableBinaryDataSink& ds, std::vector<T>& t) {
  PortableSerializeVectors(ds, t);
}
template <typename T>
inline void Serialize(PortableBinaryDataSource& ds, std::vector<T>& t) {
  PortableSerializeVectors(ds, t);
}

}} // ns

#endif
