#ifndef SUBITEM_VALUE_PER_ITEM_HPP
#define SUBITEM_VALUE_PER_ITEM_HPP

#include <mesh/ConnectivityMatrix.hpp>
#include <mesh/IConnectivity.hpp>
#include <mesh/ItemId.hpp>
#include <mesh/ItemOfItemType.hpp>
#include <mesh/ItemType.hpp>
#include <utils/Array.hpp>
#include <utils/PugsAssert.hpp>

#include <memory>

template <typename DataType, typename ItemOfItem, typename ConnectivityPtr = std::shared_ptr<const IConnectivity>>
class SubItemValuePerItem
{
 public:
  static constexpr ItemType item_type{ItemOfItem::item_type};
  static constexpr ItemType sub_item_type{ItemOfItem::sub_item_type};

  using ItemOfItemType = ItemOfItem;
  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;

  typename ConnectivityMatrix::HostRowType m_host_row_map;
  Array<DataType> m_values;

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

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

  // Allow const std:shared_ptr version to access our data
  friend SubItemValuePerItem<std::remove_const_t<DataType>, ItemOfItem, ConnectivitySharedPtr>;

  // Allow const std:weak_ptr version to access our data
  friend SubItemValuePerItem<std::remove_const_t<DataType>, ItemOfItem, ConnectivityWeakPtr>;

 public:
  using ToShared = SubItemValuePerItem<DataType, ItemOfItem, ConnectivitySharedPtr>;

  class SubView
  {
   public:
    using data_type = DataType;

   private:
    PUGS_RESTRICT DataType* const m_sub_values;
    const size_t m_size;

   public:
    template <typename IndexType>
    PUGS_INLINE const DataType&
    operator[](IndexType i) const noexcept(NO_ASSERT)
    {
      static_assert(std::is_integral_v<IndexType>, "SubView is indexed by integral values");
      Assert(i < m_size);
      return m_sub_values[i];
    }

    template <typename IndexType>
    PUGS_FORCEINLINE DataType&
    operator[](IndexType i) noexcept(NO_ASSERT)
    {
      static_assert(std::is_integral_v<IndexType>, "SubView is indexed by integral values");
      Assert(i < m_size);
      return m_sub_values[i];
    }

    PUGS_INLINE
    size_t
    size() const noexcept
    {
      return m_size;
    }

    SubView(const SubView&) = delete;

    PUGS_INLINE
    SubView(SubView&&) noexcept = default;

    PUGS_INLINE
    SubView(const Array<DataType>& values, size_t begin, size_t end) noexcept(NO_ASSERT)
      : m_sub_values(&(values[begin])), m_size(end - begin)
    {
      Assert(begin <= end);
      Assert(end <= values.size());
    }
  };

  friend PUGS_INLINE SubItemValuePerItem<std::remove_const_t<DataType>, ItemOfItem, ConnectivityPtr>
  copy(SubItemValuePerItem<DataType, ItemOfItem, ConnectivityPtr>& source)
  {
    SubItemValuePerItem<std::remove_const_t<DataType>, ItemOfItem, ConnectivityPtr> image;

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

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

  PUGS_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();
    }
  }

  template <typename IndexType, typename SubIndexType>
  PUGS_FORCEINLINE DataType&
  operator()(IndexType item_id, SubIndexType i) const noexcept(NO_ASSERT)
  {
    static_assert(std::is_same_v<IndexType, ItemId>, "first index must be of the correct ItemId type");
    static_assert(std::is_integral_v<SubIndexType>, "second index must be an integral type");
    Assert(this->isBuilt());
    Assert(i + m_host_row_map(size_t{item_id}) < m_host_row_map(size_t{item_id} + 1));
    return m_values[m_host_row_map(size_t{item_id}) + i];
  }

  // Following Kokkos logic, these classes are view and const view does allow
  // changes in data
  template <typename ArrayIndexType>
  DataType&
  operator[](const ArrayIndexType& i) const noexcept(NO_ASSERT)
  {
    static_assert(std::is_integral_v<ArrayIndexType>, "index must be an integral type");
    Assert(this->isBuilt());
    Assert(static_cast<size_t>(i) < m_values.size());
    return m_values[i];
  }

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

  PUGS_INLINE
  size_t
  numberOfItems() const noexcept(NO_ASSERT)
  {
    Assert(this->isBuilt());
    Assert(m_host_row_map.extent(0) > 0);
    return m_host_row_map.extent(0) - 1;
  }

  template <typename IndexType>
  PUGS_INLINE size_t
  numberOfSubValues(IndexType item_id) const noexcept(NO_ASSERT)
  {
    static_assert(std::is_same_v<IndexType, ItemId>, "index must be an ItemId");
    Assert(this->isBuilt());
    Assert(item_id < this->numberOfItems());
    return m_host_row_map(size_t{item_id} + 1) - m_host_row_map(size_t{item_id});
  }

  template <typename IndexType>
  PUGS_INLINE SubView
  itemValues(IndexType item_id) noexcept(NO_ASSERT)
  {
    static_assert(std::is_same_v<IndexType, ItemId>, "index must be an ItemId");
    Assert(this->isBuilt());
    Assert(item_id < this->numberOfItems());
    const auto& item_begin = m_host_row_map(size_t{item_id});
    const auto& item_end   = m_host_row_map(size_t{item_id} + 1);
    return SubView(m_values, item_begin, item_end);
  }

  // Following Kokkos logic, these classes are view and const view does allow
  // changes in data
  template <typename IndexType>
  PUGS_INLINE SubView
  itemValues(IndexType item_id) const noexcept(NO_ASSERT)
  {
    static_assert(std::is_same_v<IndexType, ItemId>, "index must be an ItemId");
    Assert(this->isBuilt());
    Assert(item_id < this->numberOfItems());
    const auto& item_begin = m_host_row_map(size_t{item_id});
    const auto& item_end   = m_host_row_map(size_t{item_id} + 1);
    return SubView(m_values, item_begin, item_end);
  }

  template <typename DataType2, typename ConnectivityPtr2>
  PUGS_INLINE SubItemValuePerItem&
  operator=(const SubItemValuePerItem<DataType2, ItemOfItem, ConnectivityPtr2>& sub_item_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 SubItemValuePerItem 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 SubItemValuePerItem of const data to "
                  "SubItemValuePerItem of non-const data");
    m_host_row_map = sub_item_value_per_item.m_host_row_map;
    m_values       = sub_item_value_per_item.m_values;

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

    return *this;
  }

  template <typename DataType2, typename ConnectivityPtr2>
  PUGS_INLINE
  SubItemValuePerItem(
    const SubItemValuePerItem<DataType2, ItemOfItem, ConnectivityPtr2>& sub_item_value_per_item) noexcept
  {
    this->operator=(sub_item_value_per_item);
  }

  SubItemValuePerItem() = default;

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

    ConnectivityMatrix connectivity_matrix = connectivity._getMatrix(item_type, sub_item_type);

    m_host_row_map = connectivity_matrix.rowsMap();
    m_values       = Array<std::remove_const_t<DataType>>(connectivity_matrix.numEntries());
  }

  ~SubItemValuePerItem() = default;
};

// Item values at nodes

template <typename DataType>
using NodeValuePerEdge = SubItemValuePerItem<DataType, NodeOfEdge>;

template <typename DataType>
using NodeValuePerFace = SubItemValuePerItem<DataType, NodeOfFace>;

template <typename DataType>
using NodeValuePerCell = SubItemValuePerItem<DataType, NodeOfCell>;

// Item values at edges

template <typename DataType>
using EdgeValuePerNode = SubItemValuePerItem<DataType, EdgeOfNode>;

template <typename DataType>
using EdgeValuePerFace = SubItemValuePerItem<DataType, EdgeOfFace>;

template <typename DataType>
using EdgeValuePerCell = SubItemValuePerItem<DataType, EdgeOfCell>;

// Item values at faces

template <typename DataType>
using FaceValuePerNode = SubItemValuePerItem<DataType, FaceOfNode>;

template <typename DataType>
using FaceValuePerEdge = SubItemValuePerItem<DataType, FaceOfEdge>;

template <typename DataType>
using FaceValuePerCell = SubItemValuePerItem<DataType, FaceOfCell>;

// Item values at cells

template <typename DataType>
using CellValuePerNode = SubItemValuePerItem<DataType, CellOfNode>;

template <typename DataType>
using CellValuePerEdge = SubItemValuePerItem<DataType, CellOfEdge>;

template <typename DataType>
using CellValuePerFace = SubItemValuePerItem<DataType, CellOfFace>;

// Weak versions: should not be used outside of Connectivity
// Item values at nodes

template <typename DataType, typename ItemOfItem>
using WeakSubItemValuePerItem = SubItemValuePerItem<DataType, ItemOfItem, std::weak_ptr<const IConnectivity>>;

template <typename DataType>
using WeakNodeValuePerEdge = WeakSubItemValuePerItem<DataType, NodeOfEdge>;

template <typename DataType>
using WeakNodeValuePerFace = WeakSubItemValuePerItem<DataType, NodeOfFace>;

template <typename DataType>
using WeakNodeValuePerCell = WeakSubItemValuePerItem<DataType, NodeOfCell>;

// Item values at edges

template <typename DataType>
using WeakEdgeValuePerNode = WeakSubItemValuePerItem<DataType, EdgeOfNode>;

template <typename DataType>
using WeakEdgeValuePerFace = WeakSubItemValuePerItem<DataType, EdgeOfFace>;

template <typename DataType>
using WeakEdgeValuePerCell = WeakSubItemValuePerItem<DataType, EdgeOfCell>;

// Item values at faces

template <typename DataType>
using WeakFaceValuePerNode = WeakSubItemValuePerItem<DataType, FaceOfNode>;

template <typename DataType>
using WeakFaceValuePerEdge = WeakSubItemValuePerItem<DataType, FaceOfEdge>;

template <typename DataType>
using WeakFaceValuePerCell = WeakSubItemValuePerItem<DataType, FaceOfCell>;

// Item values at cells

template <typename DataType>
using WeakCellValuePerNode = WeakSubItemValuePerItem<DataType, CellOfNode>;

template <typename DataType>
using WeakCellValuePerEdge = WeakSubItemValuePerItem<DataType, CellOfEdge>;

template <typename DataType>
using WeakCellValuePerFace = WeakSubItemValuePerItem<DataType, CellOfFace>;

#endif   // SUBITEM_VALUE_PER_ITEM_HPP