// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// SPDX-FileCopyrightText: Copyright Contributors to the Kokkos project

#ifndef KOKKOS_STD_ALGORITHMS_REDUCE_IMPL_HPP
#define KOKKOS_STD_ALGORITHMS_REDUCE_IMPL_HPP

#include <Kokkos_Macros.hpp>
#ifdef KOKKOS_ENABLE_EXPERIMENTAL_CXX20_MODULES
import kokkos.core;
#else
#include <Kokkos_Core.hpp>
#endif
#include "Kokkos_Constraints.hpp"
#include "Kokkos_HelperPredicates.hpp"
#include "Kokkos_ReducerWithArbitraryJoinerNoNeutralElement.hpp"
#include <std_algorithms/Kokkos_Distance.hpp>
#include <string>

namespace Kokkos {
namespace Experimental {
namespace Impl {

template <class IteratorType, class ValueType>
struct StdReduceDefaultFunctor {
  using index_type = typename IteratorType::difference_type;

  const IteratorType m_first;

  KOKKOS_FUNCTION
  void operator()(const index_type i, ValueType& update) const {
    update += m_first[i];
  }
};

template <class ValueType>
struct StdReduceDefaultJoinFunctor {
  KOKKOS_FUNCTION
  constexpr ValueType operator()(const ValueType& a, const ValueType& b) const {
    return a + b;
  }
};

template <class IteratorType, class ReducerType>
struct StdReduceFunctor {
  using red_value_type = typename ReducerType::value_type;
  using index_type     = typename IteratorType::difference_type;

  const IteratorType m_first;
  const ReducerType m_reducer;

  KOKKOS_FUNCTION
  void operator()(const index_type i, red_value_type& red_value) const {
    auto tmp_wrapped_value = red_value_type{m_first[i], false};

    if (red_value.is_initial) {
      red_value = tmp_wrapped_value;
    } else {
      m_reducer.join(red_value, tmp_wrapped_value);
    }
  }

  KOKKOS_FUNCTION
  StdReduceFunctor(IteratorType first, ReducerType reducer)
      : m_first(std::move(first)), m_reducer(std::move(reducer)) {}
};

template <typename ValueType>
using has_reduction_identity_sum_t =
    decltype(Kokkos::reduction_identity<ValueType>::sum());

//
// exespace impl
//

//-------------------------------------
// reduce_custom_functors_exespace_impl
//-------------------------------------
template <class ExecutionSpace, class IteratorType, class ValueType,
          class JoinerType>
ValueType reduce_custom_functors_exespace_impl(
    const std::string& label, const ExecutionSpace& ex, IteratorType first,
    IteratorType last, ValueType init_reduction_value, JoinerType joiner) {
  // checks
  Impl::static_assert_random_access_and_accessible(ex, first);
  Impl::static_assert_is_not_openmptarget(ex);
  Impl::expect_valid_range(first, last);

  if (first == last) {
    // init is returned, unmodified
    return init_reduction_value;
  }

  // aliases
  using reducer_type =
      ReducerWithArbitraryJoinerNoNeutralElement<ValueType, JoinerType>;
  using reduction_value_type = typename reducer_type::value_type;

  // run
  reduction_value_type result;
  reducer_type reducer(result, joiner);
  const auto num_elements = Kokkos::Experimental::distance(first, last);
  ::Kokkos::parallel_reduce(label,
                            RangePolicy<ExecutionSpace>(ex, 0, num_elements),
                            StdReduceFunctor(first, reducer), reducer);

  // fence not needed since reducing into scalar
  return joiner(result.val, init_reduction_value);
}

template <class ExecutionSpace, class IteratorType, class ValueType>
ValueType reduce_default_functors_exespace_impl(
    const std::string& label, const ExecutionSpace& ex, IteratorType first,
    IteratorType last, ValueType init_reduction_value) {
  // checks
  Impl::static_assert_random_access_and_accessible(ex, first);
  Impl::static_assert_is_not_openmptarget(ex);
  Impl::expect_valid_range(first, last);

  using value_type = std::remove_cvref_t<ValueType>;

  if (::Kokkos::is_detected<has_reduction_identity_sum_t, value_type>::value) {
    if (first == last) {
      // init is returned, unmodified
      return init_reduction_value;
    }

    using functor_type =
        Impl::StdReduceDefaultFunctor<IteratorType, value_type>;

    // run
    value_type tmp;
    const auto num_elements = Kokkos::Experimental::distance(first, last);
    ::Kokkos::parallel_reduce(label,
                              RangePolicy<ExecutionSpace>(ex, 0, num_elements),
                              functor_type{first}, tmp);
    // fence not needed since reducing into scalar
    tmp += init_reduction_value;
    return tmp;
  } else {
    using joiner_type = Impl::StdReduceDefaultJoinFunctor<value_type>;
    return reduce_custom_functors_exespace_impl(
        label, ex, first, last, std::move(init_reduction_value), joiner_type());
  }
}

//
// team impl
//

//---------------------------------
// reduce_custom_functors_team_impl
//---------------------------------
template <class TeamHandleType, class IteratorType, class ValueType,
          class JoinerType>
KOKKOS_FUNCTION ValueType reduce_custom_functors_team_impl(
    const TeamHandleType& teamHandle, IteratorType first, IteratorType last,
    ValueType init_reduction_value, JoinerType joiner) {
  // checks
  Impl::static_assert_random_access_and_accessible(teamHandle, first);
  Impl::static_assert_is_not_openmptarget(teamHandle);
  Impl::expect_valid_range(first, last);

  if (first == last) {
    // init is returned, unmodified
    return init_reduction_value;
  }

  // aliases
  using reducer_type =
      ReducerWithArbitraryJoinerNoNeutralElement<ValueType, JoinerType>;
  using reduction_value_type = typename reducer_type::value_type;

  // run
  reduction_value_type result;
  reducer_type reducer(result, joiner);
  const auto num_elements = Kokkos::Experimental::distance(first, last);
  ::Kokkos::parallel_reduce(TeamThreadRange(teamHandle, 0, num_elements),
                            StdReduceFunctor(first, reducer), reducer);

  teamHandle.team_barrier();

  return joiner(result.val, init_reduction_value);
}

template <class TeamHandleType, class IteratorType, class ValueType>
KOKKOS_FUNCTION ValueType reduce_default_functors_team_impl(
    const TeamHandleType& teamHandle, IteratorType first, IteratorType last,
    ValueType init_reduction_value) {
  // checks
  Impl::static_assert_random_access_and_accessible(teamHandle, first);
  Impl::static_assert_is_not_openmptarget(teamHandle);
  Impl::expect_valid_range(first, last);

  using value_type = std::remove_cvref_t<ValueType>;

  if (::Kokkos::is_detected<has_reduction_identity_sum_t, value_type>::value) {
    if (first == last) {
      // init is returned, unmodified
      return init_reduction_value;
    }

    using functor_type =
        Impl::StdReduceDefaultFunctor<IteratorType, value_type>;

    // run
    value_type tmp;
    const auto num_elements = Kokkos::Experimental::distance(first, last);
    ::Kokkos::parallel_reduce(TeamThreadRange(teamHandle, 0, num_elements),
                              functor_type{first}, tmp);

    teamHandle.team_barrier();

    tmp += init_reduction_value;
    return tmp;
  } else {
    using joiner_type = Impl::StdReduceDefaultJoinFunctor<value_type>;
    return reduce_custom_functors_team_impl(teamHandle, first, last,
                                            std::move(init_reduction_value),
                                            joiner_type());
  }
}

}  // namespace Impl
}  // namespace Experimental
}  // namespace Kokkos

#endif
