#ifndef CONNECTIVITY_HPP
#define CONNECTIVITY_HPP

#include <PastisMacros.hpp>
#include <PastisAssert.hpp>

#include <PastisOStream.hpp>
#include <PastisUtils.hpp>

#include <PastisTraits.hpp>

#include <TinyVector.hpp>

#include <ItemValue.hpp>

#include <IConnectivity.hpp>

#include <ConnectivityMatrix.hpp>
#include <ItemToItemMatrix.hpp>

#include <SubItemValuePerItem.hpp>
#include <ConnectivityComputer.hpp>

#include <vector>
#include <algorithm>

#include <CellType.hpp>

#include <CSRGraph.hpp>

#include <RefId.hpp>
#include <ItemType.hpp>
#include <RefNodeList.hpp>
#include <RefFaceList.hpp>

#include <SynchronizerManager.hpp>

#include <tuple>
#include <algorithm>
#include <set>

class ConnectivityDescriptor
{
 private:
  std::vector<RefFaceList> m_ref_face_list;

 public:
  std::vector<std::vector<unsigned int>> cell_to_node_vector;

#warning should be set in 2d
  std::vector<std::vector<unsigned int>> cell_to_face_vector;
  // std::vector<std::vector<unsigned int>> face_to_cell_vector;
  std::vector<std::vector<unsigned int>> face_to_node_vector;

  template <typename ItemOfItemT>
  auto& itemOfItemVector()
  {
    if constexpr (std::is_same_v<ItemOfItemT,NodeOfCell>) {
      return cell_to_node_vector;
    } else if constexpr (std::is_same_v<ItemOfItemT,FaceOfCell>) {
      return cell_to_face_vector;
    } else if constexpr (std::is_same_v<ItemOfItemT,NodeOfCell>) {
      return face_to_node_vector;
    } else {
      static_assert(is_false_v<ItemOfItemT>, "Unexpected item of item type");
    }
  }

  std::vector<Array<bool>> cell_face_is_reversed_vector;

  std::vector<CellType> cell_type_vector;

  std::vector<int> cell_number_vector;
  std::vector<int> face_number_vector;
  std::vector<int> node_number_vector;

  std::vector<int> cell_owner_vector;
  std::vector<int> face_owner_vector;
  std::vector<int> node_owner_vector;

  const std::vector<RefFaceList>& refFaceList() const
  {
    return m_ref_face_list;
  }

  void addRefFaceList(const RefFaceList& ref_face_list)
  {
    m_ref_face_list.push_back(ref_face_list);
  }

  ConnectivityDescriptor& operator=(const ConnectivityDescriptor&) = delete;
  ConnectivityDescriptor& operator=(ConnectivityDescriptor&&) = delete;

  ConnectivityDescriptor() = default;
  ConnectivityDescriptor(const ConnectivityDescriptor&) = default;
  ConnectivityDescriptor(ConnectivityDescriptor&&) = delete;
  ~ConnectivityDescriptor() = default;
};

template <size_t Dim>
class Connectivity final
    : public IConnectivity
{
 public:
  PASTIS_INLINE
  static std::shared_ptr<Connectivity<Dim>>
  build(const ConnectivityDescriptor&);

 private:
  constexpr static auto& itemTId = ItemTypeId<Dim>::itemTId;

 public:
  static constexpr size_t Dimension = Dim;

  PASTIS_INLINE
  size_t dimension() const final
  {
    return Dimension;
  }
 private:
  ConnectivityMatrix m_item_to_item_matrix[Dimension+1][Dimension+1];
  WeakCellValue<const CellType> m_cell_type;

#warning is m_cell_global_index really necessary? should it be computed on demand instead?
  WeakCellValue<const int> m_cell_global_index;

  WeakCellValue<const int> m_cell_number;
  WeakFaceValue<const int> m_face_number;
#warning check that m_edge_number is filled correctly
  WeakEdgeValue<const int> m_edge_number;
  WeakNodeValue<const int> m_node_number;

  WeakCellValue<const int> m_cell_owner;
  WeakCellValue<const bool> m_cell_is_owned;

  WeakFaceValue<const int> m_face_owner;
  WeakFaceValue<const bool> m_face_is_owned;

  WeakEdgeValue<const int> m_edge_owner;
  WeakEdgeValue<const bool> m_edge_is_owned;

  WeakNodeValue<const int> m_node_owner;
  WeakNodeValue<const bool> m_node_is_owned;

  WeakFaceValuePerCell<const bool> m_cell_face_is_reversed;

  WeakNodeValuePerCell<const unsigned short> m_cell_local_numbers_in_their_nodes;
  WeakEdgeValuePerCell<const unsigned short> m_cell_local_numbers_in_their_edges;
  WeakFaceValuePerCell<const unsigned short> m_cell_local_numbers_in_their_faces;

  WeakCellValuePerFace<const unsigned short> m_face_local_numbers_in_their_cells;
  WeakEdgeValuePerFace<const unsigned short> m_face_local_numbers_in_their_edges;
  WeakNodeValuePerFace<const unsigned short> m_face_local_numbers_in_their_nodes;

  WeakCellValuePerEdge<const unsigned short> m_edge_local_numbers_in_their_cells;
  WeakFaceValuePerEdge<const unsigned short> m_edge_local_numbers_in_their_faces;
  WeakNodeValuePerEdge<const unsigned short> m_edge_local_numbers_in_their_nodes;

  WeakCellValuePerNode<const unsigned short> m_node_local_numbers_in_their_cells;
  WeakEdgeValuePerNode<const unsigned short> m_node_local_numbers_in_their_edges;
  WeakFaceValuePerNode<const unsigned short> m_node_local_numbers_in_their_faces;

  ConnectivityComputer m_connectivity_computer;

  std::vector<RefFaceList> m_ref_face_list;
  std::vector<RefNodeList> m_ref_node_list;

  WeakCellValue<const double> m_inv_cell_nb_nodes;

  void _computeCellFaceAndFaceNodeConnectivities();

  template <typename SubItemValuePerItemType>
  PASTIS_INLINE
  const SubItemValuePerItemType&
  _lazzyBuildItemNumberInTheirChild(const SubItemValuePerItemType& sub_item_value_per_item) const
  {
    using ReversedItemOfItem = typename SubItemValuePerItemType::ItemOfItemType::Reversed;
    if (not sub_item_value_per_item.isBuilt()) {
      const_cast<SubItemValuePerItemType&>(sub_item_value_per_item)
          = m_connectivity_computer
          . computeLocalItemNumberInChildItem<ReversedItemOfItem>(*this);
    }
    return sub_item_value_per_item;
  }

  friend class ConnectivityComputer;

  PASTIS_INLINE
  const ConnectivityMatrix& _getMatrix(const ItemType& item_type_0,
                                       const ItemType& item_type_1) const
  {
    const ConnectivityMatrix& connectivity_matrix
        = m_item_to_item_matrix[itemTId(item_type_0)][itemTId(item_type_1)];
    if (not connectivity_matrix.isBuilt()) {
      const_cast<ConnectivityMatrix&>(connectivity_matrix)
          = m_connectivity_computer
          . computeConnectivityMatrix(*this, item_type_0, item_type_1);

    }
    return connectivity_matrix;
  }

 public:
  PASTIS_INLINE
  const auto& cellType() const
  {
    return m_cell_type;
  }

  PASTIS_INLINE
  const auto& cellNumber() const
  {
    return m_cell_number;
  }

  PASTIS_INLINE
  const auto& faceNumber() const
  {
    return m_face_number;
  }

  PASTIS_INLINE
  const auto& edgeNumber() const
  {
    return m_edge_number;
  }

  PASTIS_INLINE
  const auto& nodeNumber() const
  {
    return m_node_number;
  }

  template <ItemType item_type>
  PASTIS_INLINE
  const auto& number() const
  {
    if constexpr(item_type == ItemType::cell) {
      return m_cell_number;
    } else if constexpr(item_type == ItemType::face) {
      return m_face_number;
    } else if constexpr(item_type == ItemType::edge) {
      return m_edge_number;
    } else if constexpr(item_type == ItemType::node) {
      return m_node_number;
    } else {
      static_assert(is_false_item_type_v<item_type>, "unknown ItemType");
    }
  }

  PASTIS_INLINE
  const auto& cellOwner() const
  {
    return m_cell_owner;
  }

  PASTIS_INLINE
  const auto& faceOwner() const
  {
    return m_face_owner;
  }

  PASTIS_INLINE
  const auto& edgeOwner() const
  {
    perr() << __FILE__ << ':' << __LINE__ << ": edge owner not built\n";
    std::terminate();
    return m_edge_owner;
  }

  PASTIS_INLINE
  const auto& nodeOwner() const
  {
    return m_node_owner;
  }

  template <ItemType item_type>
  PASTIS_INLINE
  const auto& owner() const
  {
    if constexpr(item_type == ItemType::cell) {
      return m_cell_owner;
    } else if constexpr(item_type == ItemType::face) {
      return m_face_owner;
    } else if constexpr(item_type == ItemType::edge) {
      return m_edge_owner;
    } else if constexpr(item_type == ItemType::node) {
      return m_node_owner;
    } else {
      static_assert(is_false_item_type_v<item_type>, "unknown ItemType");
    }
  }

  PASTIS_INLINE
  const auto& cellIsOwned() const
  {
    return m_cell_is_owned;
  }

  PASTIS_INLINE
  const auto& faceIsOwned() const
  {
    return m_face_is_owned;
  }

  PASTIS_INLINE
  const auto& edgeIsOwned() const
  {
    perr() << __FILE__ << ':' << __LINE__ << ": edge is owned not built\n";
    std::terminate();
    return m_edge_is_owned;
  }

  PASTIS_INLINE
  const auto& nodeIsOwned() const
  {
    return m_node_is_owned;
  }

  template <ItemType item_type>
  PASTIS_INLINE
  const auto& isOwned() const
  {
    if constexpr(item_type == ItemType::cell) {
      return m_cell_is_owned;
    } else if constexpr(item_type == ItemType::face) {
      return m_face_is_owned;
    } else if constexpr(item_type == ItemType::edge) {
      return m_edge_is_owned;
    } else if constexpr(item_type == ItemType::node) {
      return m_node_is_owned;
    } else {
      static_assert(is_false_item_type_v<item_type>, "unknown ItemType");
    }
  }

  PASTIS_INLINE
  const bool& isConnectivityMatrixBuilt(const ItemType& item_type_0,
                                        const ItemType& item_type_1) const
  {
    const ConnectivityMatrix& connectivity_matrix
        = m_item_to_item_matrix[itemTId(item_type_0)][itemTId(item_type_1)];
    return connectivity_matrix.isBuilt();
  }
  template <ItemType source_item_type,
            ItemType target_item_type>
  PASTIS_INLINE
  auto getItemToItemMatrix() const
  {
    return ItemToItemMatrix<source_item_type,
                            target_item_type>(_getMatrix(source_item_type,
                                                         target_item_type));
  }

  PASTIS_INLINE
  auto cellToFaceMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::cell, ItemType::face>();
  }

  PASTIS_INLINE
  auto cellToEdgeMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::cell, ItemType::edge>();
  }

  PASTIS_INLINE
  auto cellToNodeMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::cell, ItemType::node>();
  }

  PASTIS_INLINE
  auto faceToCellMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::face, ItemType::cell>();
  }

  PASTIS_INLINE
  auto faceToEdgeMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::face, ItemType::edge>();
  }

  PASTIS_INLINE
  auto faceToNodeMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::face, ItemType::node>();
  }

  PASTIS_INLINE
  auto edgeToCellMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::edge, ItemType::cell>();
  }

  PASTIS_INLINE
  auto edgeToFaceMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::edge, ItemType::face>();
  }

  PASTIS_INLINE
  auto edgeToNodeMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::edge, ItemType::node>();
  }

  PASTIS_INLINE
  auto nodeToCellMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::node, ItemType::cell>();
  }

  PASTIS_INLINE
  auto nodeToFaceMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::node, ItemType::face>();
  }

  PASTIS_INLINE
  auto nodeToEdgeMatrix() const
  {
    return this->template getItemToItemMatrix<ItemType::node, ItemType::edge>();
  }

  PASTIS_INLINE
  const auto& cellFaceIsReversed() const
  {
    static_assert(Dimension>1, "reversed faces makes no sense in dimension 1");
    return m_cell_face_is_reversed;
  }

  PASTIS_INLINE
  const auto& cellLocalNumbersInTheirNodes() const
  {
    return _lazzyBuildItemNumberInTheirChild(m_cell_local_numbers_in_their_nodes);
  }

  PASTIS_INLINE
  const auto& cellLocalNumbersInTheirEdges() const
  {
    if constexpr (Dimension>2) {
      return _lazzyBuildItemNumberInTheirChild(m_cell_local_numbers_in_their_edges);
    } else {
      return cellLocalNumbersInTheirFaces();
    }
  }

  PASTIS_INLINE
  const auto& cellLocalNumbersInTheirFaces() const
  {
    if constexpr (Dimension>1) {
      return _lazzyBuildItemNumberInTheirChild(m_cell_local_numbers_in_their_faces);
    } else {
      return cellLocalNumbersInTheirNodes();
    }
  }

  PASTIS_INLINE
  const auto& faceLocalNumbersInTheirCells() const
  {
    if constexpr(Dimension>1) {
      return _lazzyBuildItemNumberInTheirChild(m_face_local_numbers_in_their_cells);
    } else {
      return nodeLocalNumbersInTheirCells();
    }
  }

  PASTIS_INLINE
  const auto& faceLocalNumbersInTheirEdges() const
  {
    static_assert(Dimension>2,"this function has no meaning in 1d or 2d");
    return _lazzyBuildItemNumberInTheirChild(m_face_local_numbers_in_their_edges);
  }

  PASTIS_INLINE
  const auto& faceLocalNumbersInTheirNodes() const
  {
    static_assert(Dimension>1,"this function has no meaning in 1d");
    return _lazzyBuildItemNumberInTheirChild(m_face_local_numbers_in_their_nodes);
  }

  PASTIS_INLINE
  const auto& edgeLocalNumbersInTheirCells() const
  {
    if constexpr (Dimension>2) {
      return _lazzyBuildItemNumberInTheirChild(m_edge_local_numbers_in_their_cells);
    } else {
      return faceLocalNumbersInTheirCells();
    }
  }

  PASTIS_INLINE
  const auto& edgeLocalNumbersInTheirFaces() const
  {
    static_assert(Dimension>2, "this function has no meaning in 1d or 2d");
    return _lazzyBuildItemNumberInTheirChild(m_edge_local_numbers_in_their_faces);
  }

  PASTIS_INLINE
  const auto& edgeLocalNumbersInTheirNodes() const
  {
    static_assert(Dimension>2, "this function has no meaning in 1d or 2d");
    return _lazzyBuildItemNumberInTheirChild(m_edge_local_numbers_in_their_nodes);
  }

  PASTIS_INLINE
  const auto& nodeLocalNumbersInTheirCells() const
  {
    return _lazzyBuildItemNumberInTheirChild(m_node_local_numbers_in_their_cells);
  }

  PASTIS_INLINE
  const auto& nodeLocalNumbersInTheirEdges() const
  {
    static_assert(Dimension>2, "this function has no meaning in 1d or 2d");
    return _lazzyBuildItemNumberInTheirChild(m_node_local_numbers_in_their_edges);
  }

  PASTIS_INLINE
  const auto& nodeLocalNumbersInTheirFaces() const
  {
    static_assert(Dimension>1,"this function has no meaning in 1d");
    return _lazzyBuildItemNumberInTheirChild(m_node_local_numbers_in_their_faces);
  }

  size_t numberOfRefFaceList() const
  {
    return m_ref_face_list.size();
  }

  const RefFaceList& refFaceList(const size_t& i) const
  {
    return m_ref_face_list[i];
  }

  void addRefNodeList(const RefNodeList& ref_node_list)
  {
    m_ref_node_list.push_back(ref_node_list);
  }

  size_t numberOfRefNodeList() const
  {
    return m_ref_node_list.size();
  }

  const RefNodeList& refNodeList(const size_t& i) const
  {
    return m_ref_node_list[i];
  }

  PASTIS_INLINE
  CSRGraph cellToCellGraph() const
  {
    std::vector<std::set<int>> cell_cells(this->numberOfCells());
    if constexpr (true) {
      const auto& face_to_cell_matrix
          = this->faceToCellMatrix();

      for (FaceId l=0; l<this->numberOfFaces(); ++l) {
        const auto& face_to_cell = face_to_cell_matrix[l];
        if (face_to_cell.size() > 1) {
          const CellId cell_0 = face_to_cell[0];
          const CellId cell_1 = face_to_cell[1];

          cell_cells[cell_0].insert(cell_1);
          cell_cells[cell_1].insert(cell_0);
        }
      }
    } else {
      const auto& node_to_cell_matrix
          = this->nodeToCellMatrix();

      for (NodeId l=0; l<this->numberOfNodes(); ++l) {
        const auto& node_to_cell = node_to_cell_matrix[l];
        for (size_t i_cell=0; i_cell<node_to_cell.size(); ++i_cell) {
          const CellId cell_0 = node_to_cell[i_cell];
          for (size_t j_cell=0; j_cell<i_cell; ++j_cell) {
            const CellId cell_1 = node_to_cell[j_cell];
            cell_cells[cell_0].insert(cell_1);
            cell_cells[cell_1].insert(cell_0);
          }
        }
      }
    }

    Array<int> entries(this->numberOfCells()+1);
    entries[0]=0;
    for (size_t j=0; j<this->numberOfCells(); ++j) {
      entries[j+1] = entries[j]+cell_cells[j].size();
    }
    Array<int> neighbors(entries[this->numberOfCells()]);
    {
      size_t k=0;
      for (size_t j=0; j<this->numberOfCells(); ++j) {
        for (CellId cell_id : cell_cells[j]) {
          neighbors[k] = m_cell_global_index[cell_id];
          ++k;
        }
      }
    }
    return CSRGraph(entries, neighbors);
  }

  PASTIS_INLINE
  size_t numberOfNodes() const final
  {
    const auto& node_to_cell_matrix
        = this->_getMatrix(ItemType::node,ItemType::cell);
    return node_to_cell_matrix.numRows();
  }

  PASTIS_INLINE
  size_t numberOfEdges() const final
  {
    const auto& edge_to_node_matrix
        = this->_getMatrix(ItemType::edge,ItemType::node);
    return edge_to_node_matrix.numRows();
  }

  PASTIS_INLINE
  size_t numberOfFaces() const final
  {
    const auto& face_to_node_matrix
        = this->_getMatrix(ItemType::face,ItemType::cell);
    return face_to_node_matrix.numRows();
  }

  PASTIS_INLINE
  size_t numberOfCells() const final
  {
    const auto& cell_to_node_matrix
        = this->_getMatrix(ItemType::cell,ItemType::node);
    return cell_to_node_matrix.numRows();
  }

  CellValue<const double> invCellNbNodes() const
  {
#warning add calculation on demand when variables will be defined
    return m_inv_cell_nb_nodes;
  }

  Connectivity(const Connectivity&) = delete;

 private:
  Connectivity();
  void _buildFrom(const ConnectivityDescriptor& descriptor);

 public:
  ~Connectivity()
  {
    auto& manager = SynchronizerManager::instance();
    manager.deleteConnectivitySynchronizer(this);
  }
};

template <size_t Dim>
PASTIS_INLINE
std::shared_ptr<Connectivity<Dim>>
Connectivity<Dim>::build(const ConnectivityDescriptor& descriptor)
{
  std::shared_ptr<Connectivity<Dim>> connectivity_ptr(new Connectivity<Dim>);
  connectivity_ptr->_buildFrom(descriptor);

  return connectivity_ptr;
}

using Connectivity3D = Connectivity<3>;
using Connectivity2D = Connectivity<2>;
using Connectivity1D = Connectivity<1>;

#endif // CONNECTIVITY_HPP