#ifndef ARRAY_HPP
#define ARRAY_HPP

#include <utils/InvalidData.hpp>
#include <utils/NaNHelper.hpp>
#include <utils/PugsAssert.hpp>
#include <utils/PugsMacros.hpp>
#include <utils/PugsUtils.hpp>

#include <Kokkos_CopyViews.hpp>
#include <algorithm>

template <typename DataType>
class [[nodiscard]] Array
{
 public:
  using data_type  = DataType;
  using index_type = size_t;

 private:
  Kokkos::View<DataType*> m_values;

  // Allows const version to access our data
  friend Array<std::add_const_t<DataType>>;

 public:
  friend std::ostream& operator<<(std::ostream& os, const Array& x)
  {
    if (x.size() > 0) {
      os << 0 << ':' << NaNHelper(x[0]);
    }
    for (size_t i = 1; i < x.size(); ++i) {
      os << ' ' << i << ':' << NaNHelper(x[i]);
    }
    return os;
  }

  PUGS_INLINE size_t size() const noexcept
  {
    return m_values.extent(0);
  }

  friend PUGS_INLINE Array<std::remove_const_t<DataType>> copy(const Array<DataType>& source)
  {
    Array<std::remove_const_t<DataType>> image(source.size());
    Kokkos::deep_copy(image.m_values, source.m_values);

    return image;
  }

  friend PUGS_INLINE void copy_to(const Array<DataType>& source,
                                  const Array<std::remove_const_t<DataType>>& destination)
  {
    Assert(source.size() == destination.size());
    Kokkos::deep_copy(destination.m_values, source.m_values);
  }

  template <typename DataType2, typename... RT>
  friend PUGS_INLINE Array<DataType2> encapsulate(const Kokkos::View<DataType2*, RT...>& values);

  template <typename DataType2>
  friend PUGS_INLINE Array<DataType2> subArray(const Array<DataType2>& array,
                                               typename Array<DataType2>::index_type begin,
                                               typename Array<DataType2>::index_type size);

  PUGS_INLINE DataType& operator[](index_type i) const noexcept(NO_ASSERT)
  {
    Assert(i < m_values.extent(0));
    return m_values[i];
  }

  PUGS_INLINE
  void fill(const DataType& data) const
  {
    static_assert(not std::is_const_v<DataType>, "Cannot modify Array of const");

    Kokkos::deep_copy(m_values, data);
  }

  template <typename DataType2>
  PUGS_INLINE Array& operator=(const Array<DataType2>& array) noexcept
  {
    // ensures that DataType is the same as source DataType2
    static_assert(std::is_same<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>(),
                  "Cannot assign Array of different type");
    // ensures that const is not lost through copy
    static_assert(((std::is_const_v<DataType2> and std::is_const_v<DataType>) or not std::is_const_v<DataType2>),
                  "Cannot assign Array of const to Array of non-const");
    m_values = array.m_values;
    return *this;
  }

  PUGS_INLINE
  Array& operator=(const Array&) = default;

  PUGS_INLINE
  Array& operator=(Array&&) = default;

  PUGS_INLINE
  explicit Array(size_t size)
  {
    static_assert(not std::is_const<DataType>(), "Cannot allocate Array of const data: only view is "
                                                 "supported");
    if constexpr (std::is_arithmetic_v<DataType>) {
      m_values = Kokkos::View<DataType*>{Kokkos::view_alloc(Kokkos::WithoutInitializing, "anonymous"), size};
    } else {
      m_values = Kokkos::View<DataType*>{"anonymous", size};
    }

#ifndef NDEBUG
    if constexpr (not std::is_const_v<DataType>) {
      using T = std::decay_t<DataType>;
      if constexpr (std::is_arithmetic_v<T>) {
        this->fill(invalid_data_v<T>);
      } else if constexpr (is_tiny_vector_v<T>) {
        this->fill(T{});
      } else if constexpr (is_tiny_matrix_v<T>) {
        this->fill(T{});
      }
    }
#endif   // NDEBUG
  }

  PUGS_INLINE
  Array() = default;

  PUGS_INLINE
  Array(const Array&) = default;

  template <typename DataType2>
  PUGS_INLINE Array(const Array<DataType2>& array) noexcept
  {
    this->operator=(array);
  }

  PUGS_INLINE
  Array(Array &&) = default;

  PUGS_INLINE
  ~Array() = default;
};

template <typename DataType, typename... RT>
PUGS_INLINE Array<DataType>
encapsulate(const Kokkos::View<DataType*, RT...>& values)
{
  Array<DataType> array;
  array.m_values = values;
  return array;
}

template <typename DataType>
PUGS_INLINE Array<DataType>
subArray(const Array<DataType>& array,
         typename Array<DataType>::index_type begin,
         typename Array<DataType>::index_type size)
{
  Assert(begin < array.size());
  Assert(begin + size <= array.size());
  return encapsulate(Kokkos::View<DataType*>(array.m_values, std::make_pair(begin, begin + size)));
}

// map, multimap, unordered_map and stack cannot be copied this way
template <typename Container>
PUGS_INLINE Array<std::remove_const_t<typename Container::value_type>>
convert_to_array(const Container& given_container)
{
  using DataType = typename Container::value_type;
  Array<std::remove_const_t<DataType>> array(given_container.size());
  if (given_container.size() > 0) {
    std::copy(begin(given_container), end(given_container), &(array[0]));
  }
  return array;
}

#endif   // ARRAY_HPP