/*
 *  Copyright (c), 2017, Adrien Devresse <adrien.devresse@epfl.ch>
 *
 *  Distributed under the Boost Software License, Version 1.0.
 *    (See accompanying file LICENSE_1_0.txt or copy at
 *          http://www.boost.org/LICENSE_1_0.txt)
 *
 */
#pragma once

#include <string>
#include <complex>
#include <cstring>
#if HIGHFIVE_CXX_STD >= 17
#include <cstddef>
#endif

#include <H5Ppublic.h>
#include <H5Tpublic.h>

#ifdef H5_USE_HALF_FLOAT
#include <half.hpp>
#endif

#include "H5Inspector_misc.hpp"

namespace HighFive {

namespace detail {

inline hid_t h5t_copy(hid_t original) {
    auto copy = H5Tcopy(original);
    if (copy == H5I_INVALID_HID) {
        HDF5ErrMapper::ToException<DataTypeException>("Error copying datatype.");
    }

    return copy;
}

inline hsize_t h5t_get_size(hid_t hid) {
    hsize_t size = H5Tget_size(hid);
    if (size == 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Error getting size of datatype.");
    }

    return size;
}

inline H5T_cset_t h5t_get_cset(hid_t hid) {
    auto cset = H5Tget_cset(hid);
    if (cset == H5T_CSET_ERROR) {
        HDF5ErrMapper::ToException<DataTypeException>("Error getting cset of datatype.");
    }

    return cset;
}

inline H5T_str_t h5t_get_strpad(hid_t hid) {
    auto strpad = H5Tget_strpad(hid);
    if (strpad == H5T_STR_ERROR) {
        HDF5ErrMapper::ToException<DataTypeException>("Error getting strpad of datatype.");
    }

    return strpad;
}

inline void h5t_set_size(hid_t hid, hsize_t size) {
    if (H5Tset_size(hid, size) < 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Error setting size of datatype.");
    }
}

inline void h5t_set_cset(hid_t hid, H5T_cset_t cset) {
    if (H5Tset_cset(hid, cset) < 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Error setting cset of datatype.");
    }
}

inline void h5t_set_strpad(hid_t hid, H5T_str_t strpad) {
    if (H5Tset_strpad(hid, strpad) < 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Error setting strpad of datatype.");
    }
}
}  // namespace detail


namespace {  // unnamed
inline DataTypeClass convert_type_class(const H5T_class_t& tclass);
inline std::string type_class_string(DataTypeClass);
inline hid_t create_string(std::size_t length);
}  // namespace

inline bool DataType::empty() const noexcept {
    return _hid == H5I_INVALID_HID;
}

inline DataTypeClass DataType::getClass() const {
    return convert_type_class(H5Tget_class(_hid));
}

inline size_t DataType::getSize() const {
    return detail::h5t_get_size(_hid);
}

inline bool DataType::operator==(const DataType& other) const {
    return (H5Tequal(_hid, other._hid) > 0);
}

inline bool DataType::operator!=(const DataType& other) const {
    return !(*this == other);
}

inline bool DataType::isVariableStr() const {
    auto var_value = H5Tis_variable_str(_hid);
    if (var_value < 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Unable to define datatype size to variable");
    }
    return static_cast<bool>(var_value);
}

inline bool DataType::isFixedLenStr() const {
    return getClass() == DataTypeClass::String && !isVariableStr();
}

inline bool DataType::isReference() const {
    return H5Tequal(_hid, H5T_STD_REF_OBJ) > 0;
}

inline StringType DataType::asStringType() const {
    if (getClass() != DataTypeClass::String) {
        throw DataTypeException("Invalid conversion to StringType.");
    }

    if (isValid() && H5Iinc_ref(_hid) < 0) {
        throw ObjectException("Reference counter increase failure");
    }

    return StringType(_hid);
}

inline std::string DataType::string() const {
    return type_class_string(getClass()) + std::to_string(getSize() * 8);
}

inline StringPadding StringType::getPadding() const {
    return StringPadding(detail::h5t_get_strpad(_hid));
}

inline CharacterSet StringType::getCharacterSet() const {
    return CharacterSet(detail::h5t_get_cset(_hid));
}

inline FixedLengthStringType::FixedLengthStringType(size_t size,
                                                    StringPadding padding,
                                                    CharacterSet character_set) {
    if (size == 0 && padding == StringPadding::NullTerminated) {
        throw DataTypeException(
            "Fixed-length, null-terminated need at least one byte to store the null-character.");
    }

    _hid = detail::h5t_copy(H5T_C_S1);

    detail::h5t_set_size(_hid, hsize_t(size));
    detail::h5t_set_cset(_hid, H5T_cset_t(character_set));
    detail::h5t_set_strpad(_hid, H5T_str_t(padding));
}

inline VariableLengthStringType::VariableLengthStringType(CharacterSet character_set) {
    _hid = detail::h5t_copy(H5T_C_S1);

    detail::h5t_set_size(_hid, H5T_VARIABLE);
    detail::h5t_set_cset(_hid, H5T_cset_t(character_set));
}

// char mapping
template <>
inline AtomicType<char>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_CHAR);
}

template <>
inline AtomicType<signed char>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_SCHAR);
}

template <>
inline AtomicType<unsigned char>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_UCHAR);
}

// short mapping
template <>
inline AtomicType<short>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_SHORT);
}

template <>
inline AtomicType<unsigned short>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_USHORT);
}

// integer mapping
template <>
inline AtomicType<int>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_INT);
}

template <>
inline AtomicType<unsigned>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_UINT);
}

// long mapping
template <>
inline AtomicType<long>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_LONG);
}

template <>
inline AtomicType<unsigned long>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_ULONG);
}

// long long mapping
template <>
inline AtomicType<long long>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_LLONG);
}

template <>
inline AtomicType<unsigned long long>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_ULLONG);
}

// half-float, float, double and long double mapping
#ifdef H5_USE_HALF_FLOAT
using float16_t = half_float::half;

template <>
inline AtomicType<float16_t>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_FLOAT);
    // Sign position, exponent position, exponent size, mantissa position, mantissa size
    H5Tset_fields(_hid, 15, 10, 5, 0, 10);
    // Total datatype size (in bytes)
    detail::h5t_set_size(_hid, 2);
    // Floating point exponent bias
    H5Tset_ebias(_hid, 15);
}
#endif

template <>
inline AtomicType<float>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_FLOAT);
}

template <>
inline AtomicType<double>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_DOUBLE);
}

template <>
inline AtomicType<long double>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_LDOUBLE);
}

// std string
template <>
inline AtomicType<std::string>::AtomicType() {
    _hid = create_string(H5T_VARIABLE);
}

#if HIGHFIVE_CXX_STD >= 17
// std byte
template <>
inline AtomicType<std::byte>::AtomicType() {
    _hid = detail::h5t_copy(H5T_NATIVE_B8);
}
#endif

// Fixed-Length strings
// require class specialization templated for the char length
template <size_t StrLen>
class AtomicType<char[StrLen]>: public DataType {
  public:
    inline AtomicType()
        : DataType(create_string(StrLen)) {}
};

template <size_t StrLen>
class AtomicType<FixedLenStringArray<StrLen>>: public DataType {
  public:
    inline AtomicType()
        : DataType(create_string(StrLen)) {}
};

template <typename T>
class AtomicType<std::complex<T>>: public DataType {
  public:
    inline AtomicType()
        : DataType(
              CompoundType({{"r", create_datatype<T>(), 0}, {"i", create_datatype<T>(), sizeof(T)}},
                           sizeof(std::complex<T>))) {
        static_assert(std::is_arithmetic<T>::value,
                      "std::complex accepts only floating point and integral numbers.");
    }
};

// For boolean we act as h5py
inline EnumType<details::Boolean> create_enum_boolean() {
    return {{"FALSE", details::Boolean::HighFiveFalse}, {"TRUE", details::Boolean::HighFiveTrue}};
}

// Other cases not supported. Fail early with a user message
template <typename T>
AtomicType<T>::AtomicType() {
    static_assert(details::inspector<T>::recursive_ndim == 0,
                  "Atomic types cant be arrays, except for char[] (fixed-length strings)");
    static_assert(details::inspector<T>::recursive_ndim > 0, "Type not supported");
}


// class FixedLenStringArray<N>

template <std::size_t N>
inline FixedLenStringArray<N>::FixedLenStringArray(const char array[][N], std::size_t length) {
    datavec.resize(length);
    std::memcpy(datavec[0].data(), array[0].data(), N * length);
}

template <std::size_t N>
inline FixedLenStringArray<N>::FixedLenStringArray(const std::string* iter_begin,
                                                   const std::string* iter_end) {
    datavec.reserve(static_cast<std::size_t>(iter_end - iter_begin));
    for (std::string const* it = iter_begin; it != iter_end; ++it) {
        push_back(*it);
    }
}

template <std::size_t N>
inline FixedLenStringArray<N>::FixedLenStringArray(const std::vector<std::string>& vec)
    : FixedLenStringArray(vec.data(), vec.data() + vec.size()) {}

template <std::size_t N>
inline FixedLenStringArray<N>::FixedLenStringArray(
    const std::initializer_list<std::string>& init_list)
    : FixedLenStringArray(init_list.begin(), init_list.end()) {}

template <std::size_t N>
inline void FixedLenStringArray<N>::push_back(const std::string& src) {
    datavec.emplace_back();
    const size_t length = std::min(N - 1, src.length());
    std::memcpy(datavec.back().data(), src.c_str(), length);
    datavec.back()[length] = 0;
}

template <std::size_t N>
inline void FixedLenStringArray<N>::push_back(const std::array<char, N>& src) {
    datavec.emplace_back();
    std::copy(src.begin(), src.end(), datavec.back().data());
}

template <std::size_t N>
inline std::string FixedLenStringArray<N>::getString(std::size_t i) const {
    return std::string(datavec[i].data());
}

// Internal
// Reference mapping
template <>
inline AtomicType<Reference>::AtomicType() {
    _hid = detail::h5t_copy(H5T_STD_REF_OBJ);
}

inline size_t find_first_atomic_member_size(hid_t hid) {
    // Recursive exit condition
    if (H5Tget_class(hid) == H5T_COMPOUND) {
        auto number_of_members = H5Tget_nmembers(hid);
        if (number_of_members == -1) {
            throw DataTypeException("Cannot get members of CompoundType with hid: " +
                                    std::to_string(hid));
        }
        if (number_of_members == 0) {
            throw DataTypeException("No members defined for CompoundType with hid: " +
                                    std::to_string(hid));
        }

        auto member_type = H5Tget_member_type(hid, 0);
        auto size = find_first_atomic_member_size(member_type);
        H5Tclose(member_type);
        return size;
    } else if (H5Tget_class(hid) == H5T_STRING) {
        return 1;
    }
    return detail::h5t_get_size(hid);
}

// Calculate the padding required to align an element of a struct
// For padding see explanation here: https://en.cppreference.com/w/cpp/language/object#Alignment
// It is to compute padding following last element inserted inside a struct
// 1) We want to push back an element padded to the structure
// 'current_size' is the size of the structure before adding the new element.
// 'member_size' the size of the element we want to add.
// 2) We want to compute the final padding for the global structure
// 'current_size' is the size of the whole structure without final padding
// 'member_size' is the maximum size of all element of the struct
//
// The basic formula is only to know how much we need to add to 'current_size' to fit
// 'member_size'.
// And at the end, we do another computation because the end padding, should fit the biggest
// element of the struct.
//
// As we are with `size_t` element, we need to compute everything inside R+
#define _H5_STRUCT_PADDING(current_size, member_size)                                \
    (((member_size) >= (current_size))                                               \
         ? (((member_size) - (current_size)) % (member_size))                        \
         : ((((member_size) - (((current_size) - (member_size)) % (member_size)))) % \
            (member_size)))

inline void CompoundType::create(size_t size) {
    if (size == 0) {
        size_t current_size = 0, max_atomic_size = 0;

        // Do a first pass to find the total size of the compound datatype
        for (auto& member: members) {
            size_t member_size = detail::h5t_get_size(member.base_type.getId());

            if (member_size == 0) {
                throw DataTypeException("Cannot get size of DataType with hid: " +
                                        std::to_string(member.base_type.getId()));
            }

            size_t first_atomic_size = find_first_atomic_member_size(member.base_type.getId());

            // Set the offset of this member within the struct according to the
            // standard alignment rules. The c++ standard specifies that:
            // > objects have an alignment requirement of which their size is a multiple
            member.offset = current_size + _H5_STRUCT_PADDING(current_size, first_atomic_size);

            // Set the current size to the end of the new member
            current_size = member.offset + member_size;

            // Keep track of the highest atomic member size because it's needed
            // for the padding of the complete compound type.
            max_atomic_size = std::max(max_atomic_size, first_atomic_size);
        }

        size = current_size + _H5_STRUCT_PADDING(current_size, max_atomic_size);
    }

    // Create the HDF5 type
    if ((_hid = H5Tcreate(H5T_COMPOUND, size)) < 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Could not create new compound datatype");
    }

    // Loop over all the members and insert them into the datatype
    for (const auto& member: members) {
        if (H5Tinsert(_hid, member.name.c_str(), member.offset, member.base_type.getId()) < 0) {
            HDF5ErrMapper::ToException<DataTypeException>("Could not add new member to datatype");
        }
    }
}

#undef _H5_STRUCT_PADDING

inline void CompoundType::commit(const Object& object, const std::string& name) const {
    H5Tcommit2(object.getId(), name.c_str(), getId(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
}

template <typename T>
inline void EnumType<T>::create() {
    // Create the HDF5 type
    if ((_hid = H5Tenum_create(AtomicType<typename std::underlying_type<T>::type>{}.getId())) < 0) {
        HDF5ErrMapper::ToException<DataTypeException>("Could not create new enum datatype");
    }

    // Loop over all the members and insert them into the datatype
    for (const auto& member: members) {
        if (H5Tenum_insert(_hid, member.name.c_str(), &(member.value)) < 0) {
            HDF5ErrMapper::ToException<DataTypeException>(
                "Could not add new member to this enum datatype");
        }
    }
}

template <typename T>
inline void EnumType<T>::commit(const Object& object, const std::string& name) const {
    H5Tcommit2(object.getId(), name.c_str(), getId(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
}

namespace {

inline hid_t create_string(size_t length) {
    hid_t _hid = detail::h5t_copy(H5T_C_S1);
    detail::h5t_set_size(_hid, length);
    detail::h5t_set_cset(_hid, H5T_CSET_UTF8);
    return _hid;
}


inline DataTypeClass convert_type_class(const H5T_class_t& tclass) {
    switch (tclass) {
    case H5T_TIME:
        return DataTypeClass::Time;
    case H5T_INTEGER:
        return DataTypeClass::Integer;
    case H5T_FLOAT:
        return DataTypeClass::Float;
    case H5T_STRING:
        return DataTypeClass::String;
    case H5T_BITFIELD:
        return DataTypeClass::BitField;
    case H5T_OPAQUE:
        return DataTypeClass::Opaque;
    case H5T_COMPOUND:
        return DataTypeClass::Compound;
    case H5T_REFERENCE:
        return DataTypeClass::Reference;
    case H5T_ENUM:
        return DataTypeClass::Enum;
    case H5T_VLEN:
        return DataTypeClass::VarLen;
    case H5T_ARRAY:
        return DataTypeClass::Array;
    case H5T_NO_CLASS:
    case H5T_NCLASSES:
    default:
        return DataTypeClass::Invalid;
    }
}


inline std::string type_class_string(DataTypeClass tclass) {
    switch (tclass) {
    case DataTypeClass::Time:
        return "Time";
    case DataTypeClass::Integer:
        return "Integer";
    case DataTypeClass::Float:
        return "Float";
    case DataTypeClass::String:
        return "String";
    case DataTypeClass::BitField:
        return "BitField";
    case DataTypeClass::Opaque:
        return "Opaque";
    case DataTypeClass::Compound:
        return "Compound";
    case DataTypeClass::Reference:
        return "Reference";
    case DataTypeClass::Enum:
        return "Enum";
    case DataTypeClass::VarLen:
        return "Varlen";
    case DataTypeClass::Array:
        return "Array";
    default:
        return "(Invalid)";
    }
}

}  // unnamed namespace


/// \brief Create a DataType instance representing type T
template <typename T>
inline DataType create_datatype() {
    return AtomicType<T>();
}


/// \brief Create a DataType instance representing type T and perform a sanity check on its size
template <typename T>
inline DataType create_and_check_datatype() {
    DataType t = create_datatype<T>();
    if (t.empty()) {
        throw DataTypeException("Type given to create_and_check_datatype is not valid");
    }

    // Skip check if the base type is a variable length string
    if (t.isVariableStr()) {
        return t;
    }

    // Check that the size of the template type matches the size that HDF5 is
    // expecting.
    if (t.isReference() || t.isFixedLenStr()) {
        return t;
    }
    if (sizeof(T) != t.getSize()) {
        std::ostringstream ss;
        ss << "Size of array type " << sizeof(T) << " != that of memory datatype " << t.getSize()
           << std::endl;
        throw DataTypeException(ss.str());
    }

    return t;
}

}  // namespace HighFive
HIGHFIVE_REGISTER_TYPE(HighFive::details::Boolean, HighFive::create_enum_boolean)
