#ifndef ITEM_VALUE_HPP
#define ITEM_VALUE_HPP

#include <PastisAssert.hpp>
#include <PastisOStream.hpp>

#include <Array.hpp>

#include <ItemType.hpp>
#include <ItemId.hpp>

#include <IConnectivity.hpp>
#include <memory>

template <typename DataType,
          ItemType item_type,
          typename ConnectivityPtr = std::shared_ptr<const IConnectivity>>
class ItemValue
{
 public:
  static constexpr ItemType item_t{item_type};
  using data_type = DataType;

  using ItemId = ItemIdT<item_type>;
  using index_type = ItemId;

 private:
  using ConnectivitySharedPtr = std::shared_ptr<const IConnectivity>;
  using ConnectivityWeakPtr = std::weak_ptr<const IConnectivity>;

  static_assert(std::is_same_v<ConnectivityPtr, ConnectivitySharedPtr> or
                std::is_same_v<ConnectivityPtr, ConnectivityWeakPtr>);

  ConnectivityPtr m_connectivity_ptr;

  Array<DataType> m_values;

  // Allow const std:shared_ptr version to access our data
  friend ItemValue<std::add_const_t<DataType>, item_type,
                   ConnectivitySharedPtr>;

  // Allow const std:weak_ptr version to access our data
  friend ItemValue<std::add_const_t<DataType>, item_type,
                   ConnectivityWeakPtr>;

  friend PASTIS_INLINE
  ItemValue<std::remove_const_t<DataType>,item_type, ConnectivityPtr>
  copy(const ItemValue<DataType, item_type, ConnectivityPtr>& source)
  {
    ItemValue<std::remove_const_t<DataType>, item_type, ConnectivityPtr> image(source);

    image.m_values = copy(source.m_values);
    return image;
  }

 public:
  PASTIS_INLINE
  bool isBuilt() const noexcept
  {
    return m_connectivity_ptr.use_count() != 0;
  }

  PASTIS_INLINE
  std::shared_ptr<const IConnectivity> connectivity_ptr() const noexcept
  {
    if constexpr (std::is_same_v<ConnectivityPtr, ConnectivitySharedPtr>) {
      return m_connectivity_ptr;
    } else {
      return m_connectivity_ptr.lock();
    }
  }

  PASTIS_INLINE
  size_t size() const noexcept(NO_ASSERT)
  {
    Assert(this->isBuilt());
    return m_values.size();
  }

  PASTIS_INLINE
  void fill(const DataType& data) const noexcept
  {
    static_assert(not std::is_const_v<DataType>,
                  "Cannot modify ItemValue of const");
    m_values.fill(data);
  }

  // Following Kokkos logic, these classes are view and const view does allow
  // changes in data
  PASTIS_FORCEINLINE
  DataType& operator[](const ItemId& i) const noexcept(NO_ASSERT)
  {
    Assert(this->isBuilt());
    return m_values[i];
  }

  template <typename IndexType>
  DataType& operator[](const IndexType&) const noexcept(NO_ASSERT)
  {
    static_assert(std::is_same_v<IndexType,ItemId>,
                  "ItemValue must be indexed by ItemId");
  }

  PASTIS_INLINE
  size_t numberOfItems() const noexcept(NO_ASSERT)
  {
    Assert(this->isBuilt());
    return m_values.size();
  }

  template <typename DataType2>
  PASTIS_INLINE
  ItemValue&
  operator=(const Array<DataType2>& values) noexcept(NO_ASSERT)
  {
    // ensures that DataType is the same as source DataType2
    static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                  "Cannot assign ItemValue 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 ItemValue of const to ItemValue of non-const");

    Assert((m_values.size() == values.size()),
           "Cannot assign an array of values of a different size\n");

    Assert ((values.size() == 0) or this->isBuilt(),
            "Cannot assign array of values to a non-built ItemValue\n");

    m_values = values;

    return *this;
  }

  template <typename DataType2,
            typename ConnectivityPtr2>
  PASTIS_INLINE
  ItemValue&
  operator=(const ItemValue<DataType2, item_type, ConnectivityPtr2>& value_per_item) noexcept
  {
    // ensures that DataType is the same as source DataType2
    static_assert(std::is_same_v<std::remove_const_t<DataType>, std::remove_const_t<DataType2>>,
                  "Cannot assign ItemValue 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 ItemValue of const to ItemValue of non-const");

    m_values = value_per_item.m_values;
    if constexpr (std::is_same_v<ConnectivityPtr, ConnectivitySharedPtr> and
                  std::is_same_v<ConnectivityPtr2, ConnectivityWeakPtr>) {
      m_connectivity_ptr = value_per_item.m_connectivity_ptr.lock();
    } else {
      m_connectivity_ptr = value_per_item.m_connectivity_ptr;
    }

    return *this;
  }

  template <typename DataType2,
            typename ConnectivityPtr2>
  PASTIS_INLINE
  ItemValue(const ItemValue<DataType2, item_type, ConnectivityPtr2>& value_per_item) noexcept
  {
    this->operator=(value_per_item);
  }

  PASTIS_INLINE
  ItemValue() = default;

  PASTIS_INLINE
  ItemValue(const IConnectivity& connectivity) noexcept
      : m_connectivity_ptr{connectivity.shared_ptr()},
        m_values{connectivity.numberOf<item_type>()}
  {
    static_assert(not std::is_const_v<DataType>,
                  "Cannot allocate ItemValue of const data: only view is supported"); ;
  }

  PASTIS_INLINE
  ~ItemValue() = default;
};

template <typename DataType>
using NodeValue = ItemValue<DataType, ItemType::node>;

template <typename DataType>
using EdgeValue = ItemValue<DataType, ItemType::edge>;

template <typename DataType>
using FaceValue = ItemValue<DataType, ItemType::face>;

template <typename DataType>
using CellValue = ItemValue<DataType, ItemType::cell>;

// Weak versions: should not be used outside of Connectivity

template <typename DataType,
          ItemType item_type>
using WeakItemValue = ItemValue<DataType, item_type, std::weak_ptr<const IConnectivity>>;

template <typename DataType>
using WeakNodeValue = WeakItemValue<DataType, ItemType::node>;

template <typename DataType>
using WeakEdgeValue = WeakItemValue<DataType, ItemType::edge>;

template <typename DataType>
using WeakFaceValue = WeakItemValue<DataType, ItemType::face>;

template <typename DataType>
using WeakCellValue = WeakItemValue<DataType, ItemType::cell>;

#endif // ITEM_VALUE_HPP