#ifndef ITEM_VALUE_UTILS_HPP
#define ITEM_VALUE_UTILS_HPP

#include <utils/Messenger.hpp>

#include <mesh/Connectivity.hpp>
#include <mesh/ItemValue.hpp>
#include <mesh/Synchronizer.hpp>
#include <mesh/SynchronizerManager.hpp>

#include <iostream>

template <typename DataType, ItemType item_type, typename ConnectivityPtr>
std::remove_const_t<DataType>
min(const ItemValue<DataType, item_type, ConnectivityPtr>& item_value)
{
  using ItemValueType   = ItemValue<DataType, item_type, ConnectivityPtr>;
  using ItemIsOwnedType = ItemValue<const bool, item_type>;
  using data_type       = std::remove_const_t<typename ItemValueType::data_type>;
  using index_type      = typename ItemValueType::index_type;

  static_assert(not std::is_same_v<data_type, bool>, "min cannot be called on boolean arrays");

  class ItemValueMin
  {
   private:
    const ItemValueType& m_item_value;
    const ItemIsOwnedType m_is_owned;

   public:
    PUGS_INLINE
    operator data_type()
    {
      data_type reduced_value;
      parallel_reduce(m_item_value.size(), *this, reduced_value);
      return reduced_value;
    }

    PUGS_INLINE
    void
    operator()(const index_type& i, data_type& data) const
    {
      if ((m_is_owned[i]) and (m_item_value[i] < data)) {
        data = m_item_value[i];
      }
    }

    PUGS_INLINE
    void
    join(volatile data_type& dst, const volatile data_type& src) const
    {
      if (src < dst) {
        // cannot be reached if initial value is the min
        dst = src;   // LCOV_EXCL_LINE
      }
    }

    PUGS_INLINE
    void
    init(data_type& value) const
    {
      value = std::numeric_limits<data_type>::max();
    }

    PUGS_INLINE
    ItemValueMin(const ItemValueType& item_value)
      : m_item_value(item_value), m_is_owned([&](const IConnectivity& connectivity) {
          Assert((connectivity.dimension() > 0) and (connectivity.dimension() <= 3),
                 "unexpected connectivity dimension");

          switch (connectivity.dimension()) {
          case 1: {
            const auto& connectivity_1d = static_cast<const Connectivity1D&>(connectivity);
            return connectivity_1d.isOwned<item_type>();
            break;
          }
          case 2: {
            const auto& connectivity_2d = static_cast<const Connectivity2D&>(connectivity);
            return connectivity_2d.isOwned<item_type>();
            break;
          }
          case 3: {
            const auto& connectivity_3d = static_cast<const Connectivity3D&>(connectivity);
            return connectivity_3d.isOwned<item_type>();
            break;
          }
            // LCOV_EXCL_START
          default: {
            throw UnexpectedError("unexpected dimension");
          }
            // LCOV_EXCL_STOP
          }
        }(*item_value.connectivity_ptr()))
    {
      ;
    }

    PUGS_INLINE
    ~ItemValueMin() = default;
  };

  const DataType local_min = ItemValueMin{item_value};
  return parallel::allReduceMin(local_min);
}

template <typename DataType, ItemType item_type, typename ConnectivityPtr>
std::remove_const_t<DataType>
max(const ItemValue<DataType, item_type, ConnectivityPtr>& item_value)
{
  using ItemValueType   = ItemValue<DataType, item_type, ConnectivityPtr>;
  using ItemIsOwnedType = ItemValue<const bool, item_type>;
  using data_type       = std::remove_const_t<typename ItemValueType::data_type>;
  using index_type      = typename ItemValueType::index_type;

  static_assert(not std::is_same_v<data_type, bool>, "min cannot be called on boolean arrays");
  class ItemValueMax
  {
   private:
    const ItemValueType& m_item_value;
    const ItemIsOwnedType m_is_owned;

   public:
    PUGS_INLINE
    operator data_type()
    {
      data_type reduced_value;
      parallel_reduce(m_item_value.size(), *this, reduced_value);
      return reduced_value;
    }

    PUGS_INLINE
    void
    operator()(const index_type& i, data_type& data) const
    {
      if ((m_is_owned[i]) and (m_item_value[i] > data)) {
        data = m_item_value[i];
      }
    }

    PUGS_INLINE
    void
    join(volatile data_type& dst, const volatile data_type& src) const
    {
      if (src > dst) {
        // cannot be reached if initial value is the max
        dst = src;   // LCOV_EXCL_LINE
      }
    }

    PUGS_INLINE
    void
    init(data_type& value) const
    {
      value = std::numeric_limits<data_type>::min();
    }

    PUGS_INLINE
    ItemValueMax(const ItemValueType& item_value)
      : m_item_value(item_value), m_is_owned([&](const IConnectivity& connectivity) {
          Assert((connectivity.dimension() > 0) and (connectivity.dimension() <= 3),
                 "unexpected connectivity dimension");

          switch (connectivity.dimension()) {
          case 1: {
            const auto& connectivity_1d = static_cast<const Connectivity1D&>(connectivity);
            return connectivity_1d.isOwned<item_type>();
            break;
          }
          case 2: {
            const auto& connectivity_2d = static_cast<const Connectivity2D&>(connectivity);
            return connectivity_2d.isOwned<item_type>();
            break;
          }
          case 3: {
            const auto& connectivity_3d = static_cast<const Connectivity3D&>(connectivity);
            return connectivity_3d.isOwned<item_type>();
            break;
          }
            // LCOV_EXCL_START
          default: {
            throw UnexpectedError("unexpected dimension");
          }
            // LCOV_EXCL_STOP
          }
        }(*item_value.connectivity_ptr()))
    {
      ;
    }

    PUGS_INLINE
    ~ItemValueMax() = default;
  };

  const DataType local_max = ItemValueMax{item_value};
  return parallel::allReduceMax(local_max);
}

template <typename DataType, ItemType item_type>
std::remove_const_t<DataType>
sum(const ItemValue<DataType, item_type>& item_value)
{
  using ItemValueType   = ItemValue<DataType, item_type>;
  using ItemIsOwnedType = ItemValue<const bool, item_type>;
  using data_type       = std::remove_const_t<typename ItemValueType::data_type>;
  using index_type      = typename ItemValueType::index_type;

  static_assert(not std::is_same_v<data_type, bool>, "sum cannot be called on boolean arrays");

  class ItemValueSum
  {
   private:
    const ItemValueType& m_item_value;
    const ItemIsOwnedType m_is_owned;

   public:
    PUGS_INLINE
    operator data_type()
    {
      data_type reduced_value;
      parallel_reduce(m_item_value.size(), *this, reduced_value);
      return reduced_value;
    }

    PUGS_INLINE
    void
    operator()(const index_type& i, data_type& data) const
    {
      if (m_is_owned[i]) {
        data += m_item_value[i];
      }
    }

    PUGS_INLINE
    void
    join(volatile data_type& dst, const volatile data_type& src) const
    {
      dst += src;
    }

    PUGS_INLINE
    void
    init(data_type& value) const
    {
      if constexpr (std::is_arithmetic_v<data_type>) {
        value = 0;
      } else {
        value = zero;
      }
    }

    PUGS_INLINE
    ItemValueSum(const ItemValueType& item_value)
      : m_item_value(item_value), m_is_owned([&](const IConnectivity& connectivity) {
          Assert((connectivity.dimension() > 0) and (connectivity.dimension() <= 3),
                 "unexpected connectivity dimension");

          switch (connectivity.dimension()) {
          case 1: {
            const auto& connectivity_1d = static_cast<const Connectivity1D&>(connectivity);
            return connectivity_1d.isOwned<item_type>();
            break;
          }
          case 2: {
            const auto& connectivity_2d = static_cast<const Connectivity2D&>(connectivity);
            return connectivity_2d.isOwned<item_type>();
            break;
          }
          case 3: {
            const auto& connectivity_3d = static_cast<const Connectivity3D&>(connectivity);
            return connectivity_3d.isOwned<item_type>();
            break;
          }
            // LCOV_EXCL_START
          default: {
            throw UnexpectedError("unexpected dimension");
          }
            // LCOV_EXCL_STOP
          }
        }(*item_value.connectivity_ptr()))
    {
      ;
    }

    PUGS_INLINE
    ~ItemValueSum() = default;
  };

  const DataType local_sum = ItemValueSum{item_value};
  return parallel::allReduceSum(local_sum);
}

template <typename DataType, ItemType item_type, typename ConnectivityPtr>
void
synchronize(ItemValue<DataType, item_type, ConnectivityPtr>& item_value)
{
  static_assert(not std::is_const_v<DataType>, "cannot synchronize ItemValue of const data");
  if (parallel::size() > 1) {
    auto& manager                     = SynchronizerManager::instance();
    const IConnectivity* connectivity = item_value.connectivity_ptr().get();
    Synchronizer& synchronizer        = manager.getConnectivitySynchronizer(connectivity);
    synchronizer.synchronize(item_value);
  }
}

#endif   // ITEM_VALUE_UTILS_HPP