#ifndef CPP_UTILITIES_MULTI_ARRAY_H
#define CPP_UTILITIES_MULTI_ARRAY_H

#include <array>
#include <tuple>
#include <vector>

namespace CppUtilities {

/// \cond
namespace Detail {
template <class Tuple, std::size_t N> struct DimensionsHelper {
    static std::size_t requiredSize(const Tuple &dimensionSizes)
    {
        return DimensionsHelper<Tuple, N - 1>::requiredSize(dimensionSizes) * static_cast<std::size_t>(std::get<N - 1>(dimensionSizes));
    }
    static std::size_t offset(const Tuple &dimensions, const Tuple &indices, std::size_t factor)
    {
        return DimensionsHelper<Tuple, N - 1>::offset(dimensions, indices, factor * static_cast<std::size_t>(std::get<N - 1>(dimensions)))
            + (factor * static_cast<std::size_t>(std::get<N - 1>(indices)));
    }
};
template <class Tuple> struct DimensionsHelper<Tuple, 1> {
    static std::size_t requiredSize(const Tuple &dimensionSizes)
    {
        return static_cast<std::size_t>(std::get<0>(dimensionSizes));
    }
    static std::size_t offset(const Tuple &, const Tuple &indices, std::size_t factor)
    {
        return factor * static_cast<std::size_t>(std::get<0>(indices));
    }
};
} // namespace Detail
/// \endcond

/// \brief The VectorBasedMultiArray struct allows using an std::vector with custom allocator as underlying container for the MultiArray class.
template <typename Allocator> struct VectorBasedMultiArray {
    template <typename T> using Type = std::vector<T, Allocator>;
    template <typename T> static constexpr Type<T> init(std::size_t requiredSize)
    {
        return Type<T>(requiredSize);
    }
};

/// \brief The VectorBasedMultiArray struct allows using an std::vector as underlying container for the MultiArray class.
template <> struct VectorBasedMultiArray<void> {
    template <typename T> using Type = std::vector<T, std::allocator<T>>;
    template <typename T> static constexpr Type<T> init(std::size_t requiredSize)
    {
        return Type<T>(requiredSize);
    }
};

/// \brief The ArrayBasedMultiArray struct allows using a fixed size array as underlying container for the MultiArray class.
template <std::size_t size> struct ArrayBasedMultiArray {
    template <typename T> using Type = std::array<T, size>;
    template <typename T> static constexpr Type<T> init(std::size_t)
    {
        return Type<T>();
    }
};

/// \brief The NoneOwningMultiArray struct allows using a caller-managed buffer array as underlying container for the MultiArray class.
struct NoneOwningMultiArray {
    template <typename T> using Type = T *;
    template <typename T> static constexpr Type<T> init(std::size_t)
    {
        return nullptr;
    }
};

/// \brief The MultiArray class provides an *N*-dimensional array.
/// \tparam T Specifies the type of the data the MultiArray is supposed to contain.
/// \tparam UnderlyingContainer Specifies the type of the underlying container to use.
/// \tparam Dimensions Specifies the types used to store the limit/size of the dimensions. Must be safely castable to std::size_t.
template <typename T, typename UnderlyingContainer, typename... Dimensions> class MultiArray {
public:
    MultiArray(Dimensions... dimensionSizes);
    std::size_t totalSize() const;
    static constexpr std::size_t dimensionCount();
    template <std::size_t index> std::size_t dimensionSize() const;
    T &at(Dimensions... indices);
    const T &at(Dimensions... indices) const;
    T *data();
    const T *data() const;
    typename UnderlyingContainer::template Type<T> &buffer();

private:
    using HelperType = Detail::DimensionsHelper<std::tuple<Dimensions...>, dimensionCount()>;
    const std::tuple<Dimensions...> m_dims;
    const std::size_t m_size;
    typename UnderlyingContainer::template Type<T> m_buff;
};

/// \brief Constructs a new *N*-dimensional array. The sizes for the dimensions are passed as arguments and must be greater than zero.
/// \remarks The number of dimensions *N* is deduced from the number of \a dimensionSizes.
/// \sa makeMultiArray(), makeFixedSizeMultiArray() and makeNoneOwningMultiArray() for more convenient construction
template <typename T, typename UnderlyingContainer, typename... Dimensions>
MultiArray<T, UnderlyingContainer, Dimensions...>::MultiArray(Dimensions... dimensionSizes)
    : m_dims(std::make_tuple(dimensionSizes...))
    , m_size(HelperType::requiredSize(m_dims))
    , m_buff(UnderlyingContainer::template init<T>(m_size))
{
}

/// \brief Returns the total number of elements.
template <typename T, typename UnderlyingContainer, typename... Dimensions>
std::size_t MultiArray<T, UnderlyingContainer, Dimensions...>::totalSize() const
{
    return m_size;
}

/// \brief Returns the number of dimensions for that type of array.
template <typename T, typename UnderlyingContainer, typename... Dimensions>
constexpr std::size_t MultiArray<T, UnderlyingContainer, Dimensions...>::dimensionCount()
{
    return std::tuple_size<std::tuple<Dimensions...>>::value;
}

/// \brief Returns the number of elements in the specified dimension.
template <typename T, typename UnderlyingContainer, typename... Dimensions>
template <std::size_t index>
std::size_t MultiArray<T, UnderlyingContainer, Dimensions...>::dimensionSize() const
{
    return static_cast<std::size_t>(std::get<index>(m_dims));
}

/// \brief Returns the element at the position specified via \a indices.
/// \remarks The number of \a indices must equal dimensionCount().
template <typename T, typename UnderlyingContainer, typename... Dimensions>
T &MultiArray<T, UnderlyingContainer, Dimensions...>::at(Dimensions... indices)
{
    return m_buff[HelperType::offset(m_dims, std::make_tuple(indices...), 1)];
}

/// \brief Returns the element at the position specified via \a indices.
/// \remarks The number of \a indices must equal dimensionCount().
template <typename T, typename UnderlyingContainer, typename... Dimensions>
const T &MultiArray<T, UnderlyingContainer, Dimensions...>::at(Dimensions... indices) const
{
    return m_buff[HelperType::offset(m_dims, std::make_tuple(indices...), 1)];
}

/// \brief Returns a pointer to the raw data.
/// \remarks Intended for debugging purposes only. The underlying data structure might change in future versions.
template <typename T, typename UnderlyingContainer, typename... Dimensions> T *MultiArray<T, UnderlyingContainer, Dimensions...>::data()
{
    return m_buff.data();
}

/// \brief Returns a pointer to the raw data.
/// \remarks Intended for debugging purposes only. The underlying data structure might change in future versions.
template <typename T, typename UnderlyingContainer, typename... Dimensions> const T *MultiArray<T, UnderlyingContainer, Dimensions...>::data() const
{
    return m_buff.data();
}

/// \brief Allows accessing the underlying buffer directly.
/// \remarks Assign the custom buffer using this method when using NoneOwningMultiArray as UnderlyingContainer.
template <typename T, typename UnderlyingContainer, typename... Dimensions>
typename UnderlyingContainer::template Type<T> &MultiArray<T, UnderlyingContainer, Dimensions...>::buffer()
{
    return m_buff;
}

/// \brief Constructs a new *N*-dimensional array using an std::vector with std::allocator as underlying container.
///        The sizes for the dimensions are passed as arguments.
/// \remarks The number of dimensions *N* is deduced from the number of \a dimensionSizes.
template <typename ValueType, typename... DimensionSizes> inline auto makeMultiArray(DimensionSizes... dimensionSizes)
{
    return MultiArray<ValueType, VectorBasedMultiArray<void>, DimensionSizes...>(dimensionSizes...);
}

/// \brief Constructs a new *N*-dimensional array using a fixed size array as underlying container.
///        The sizes for the dimensions are passed as arguments.
/// \remarks The number of dimensions *N* is deduced from the number of \a dimensionSizes.
template <typename ValueType, std::size_t size, typename... DimensionSizes> inline auto makeFixedSizeMultiArray(DimensionSizes... dimensionSizes)
{
    return MultiArray<ValueType, ArrayBasedMultiArray<size>, DimensionSizes...>(dimensionSizes...);
}

/// \brief Constructs a new *N*-dimensional array using a caller-managed buffer as underlying container.
///        The sizes for the dimensions are passed as arguments.
/// \remarks The number of dimensions *N* is deduced from the number of \a dimensionSizes.
template <typename ValueType, typename... DimensionSizes> inline auto makeNoneOwningMultiArray(DimensionSizes... dimensionSizes)
{
    return MultiArray<ValueType, NoneOwningMultiArray, DimensionSizes...>(dimensionSizes...);
}

} // namespace CppUtilities

#endif // CPP_UTILITIES_MULTI_ARRAY_H
