
#ifndef PLS_PARALLEL_FOR_IMPL_H
#define PLS_PARALLEL_FOR_IMPL_H

#include "pls/internal/scheduling/scheduler.h"
#include "pls/internal/scheduling/thread_state.h"
#include "pls/internal/helpers/range.h"

namespace pls {
namespace algorithm {
namespace internal {

template<typename RandomIt, typename Function>
pls::internal::scheduling::parallel_result<int> for_each(const RandomIt first,
                                                         const RandomIt last,
                                                         const Function function,
                                                         const long min_elements) {
  using namespace ::pls::internal::scheduling;

  const long num_elements = std::distance(first, last);
  if (num_elements <= min_elements) {
    // calculate last elements in loop to avoid overhead
    for (auto current = first; current != last; current++) {
      function(*current);
    }

    return parallel_result<int>{0};
  } else {
    // Cut in half recursively
    const long middle_index = num_elements / 2;

    return scheduler::par([first, middle_index, last, function, min_elements] {
      return internal::for_each(first,
                                first + middle_index,
                                function,
                                min_elements);
    }, [first, middle_index, last, function, min_elements] {
      return internal::for_each(first + middle_index,
                                last,
                                function,
                                min_elements);
    }).then([](int, int) {
      return parallel_result<int>{0};
    });
  }
}

}

class dynamic_strategy {
 public:
  explicit dynamic_strategy(const unsigned int tasks_per_thread = 4) : tasks_per_thread_{tasks_per_thread} {};

  long calculate_min_elements(long num_elements) const {
    const long num_threads = pls::internal::scheduling::thread_state::get().scheduler_->num_threads();
    return num_elements / (num_threads * tasks_per_thread_);
  }
 private:
  unsigned const int tasks_per_thread_;
};

class fixed_strategy {
 public:
  explicit fixed_strategy(const long min_elements_per_task) : min_elements_per_task_{min_elements_per_task} {};

  long calculate_min_elements(long /*num_elements*/) const {
    return min_elements_per_task_;
  }
 private:
  const long min_elements_per_task_;
};

template<typename RandomIt, typename Function, typename ExecutionStrategy>
pls::internal::scheduling::parallel_result<int> for_each(RandomIt first,
                                                         RandomIt last,
                                                         const Function &function,
                                                         ExecutionStrategy execution_strategy) {
  long num_elements = std::distance(first, last);
  return internal::for_each(first, last, function, execution_strategy.calculate_min_elements(num_elements));
}

template<typename RandomIt, typename Function>
pls::internal::scheduling::parallel_result<int> for_each(RandomIt first, RandomIt last, const Function &function) {
  return for_each(first, last, function, dynamic_strategy{4});
}

template<typename Function, typename ExecutionStrategy>
pls::internal::scheduling::parallel_result<int> for_each_range(unsigned long first,
                                                               unsigned long last,
                                                               const Function &function,
                                                               ExecutionStrategy execution_strategy) {
  auto range = pls::internal::helpers::range(first, last);
  return for_each(range.begin(), range.end(), function, execution_strategy);
}

template<typename Function>
pls::internal::scheduling::parallel_result<int> for_each_range(unsigned long first,
                                                               unsigned long last,
                                                               const Function &function) {
  auto range = pls::internal::helpers::range(first, last);
  return for_each(range.begin(), range.end(), function);
}

}
}

#endif //PLS_INVOKE_PARALLEL_IMPL_H
