// SPDX-FileCopyrightText: 2021-2024 The Ikarus Developers mueller@ibb.uni-stuttgart.de
// SPDX-License-Identifier: LGPL-3.0-or-later

/**
 * \file algorithms.hh
 * \brief Implementation of several stl-like algorithms

 */

#pragma once

// This file contains stl-like algorithms
#include <iosfwd>
#include <ranges>

#include <ikarus/utils/traits.hh>
namespace Ikarus::utils {

/**
 * @brief Sorts and removes duplicate elements from a random access range.
 * \ingroup algos
 *    *\details
 * This function sorts the elements of the given random access range and removes duplicate elements,
 * leaving only unique elements in the range.
 *
 * @param r The random access range to be modified.
 */
void makeUniqueAndSort(std::ranges::random_access_range auto& r) {
  sort(r.begin(), r.end());
  r.erase(std::unique(r.begin(), r.end()), r.end());
}

/**
 * @brief Appends a value to the range if it is not already present.
 *\details
 * This function appends a value to the given random access range only if the value is not already present in the
 *range. It returns the index of the value in the range, whether it was added or already existed. \ingroup algos
 * @tparam Value The type of the value to be appended.
 * @param r The random access range to be modified.
 * @param v The value to be appended.
 * @return The index of the value in the range.
 */
template <typename Value>
auto appendUnique(std::ranges::random_access_range auto& r, Value&& v) {
  static_assert(std::is_same_v<typename decltype(begin(r))::value_type, std::remove_reference_t<decltype(v)>>);
  const auto it = find(begin(r), end(r), v);
  size_t index  = std::distance(begin(r), it);
  if (it == end(r))
    r.push_back(std::forward<Value>(v));

  return index;
}

/**
 * @brief Prints the contents of a container to the specified output stream.
 *\details
 * This function prints the contents of the given container to the specified output stream.
 * \ingroup algos
 * @tparam Container The type of the container to be printed.
 * @param c The container whose contents will be printed.
 * @param os The output stream where the contents will be printed. Default is std::cout.
 */
template <class Container>
void printContent(Container&& c, std::ostream& os = std::cout) {
  std::ranges::for_each(c, [&os](auto&& var) { os << var << '\n'; });
}

/**
 * @brief Transforms a value range to a pointer range.
 *
 * \ingroup algos
 * @tparam Container The type of the container containing values.
 * @param cont The container whose values will be transformed to pointers.
 * @return A subrange containing pointers to the values.
 */
template <class Container>
auto transformValueRangeToPointerRange(Container& cont) {
  auto transformValueToPointer = [](auto&& obj) { return &obj; };
  return (std::ranges::subrange(cont.begin(), cont.end()) | std::views::transform(transformValueToPointer));
}

/**
 * @brief Transforms a pointer range to a reference range.
 *\details
 * This function transforms a range of pointers to a range of references.
 * \ingroup algos
 * @tparam Container The type of the container containing pointers.
 * @param cont The container whose pointers will be transformed to references.
 * @return A subrange containing references to the pointed objects.
 */
template <class Container>
auto transformPointerRangeToReferenceRange(Container& cont) {
  auto transformValueToPointer = [](auto&& obj) -> auto& { return *obj; };
  return (std::ranges::subrange(cont.begin(), cont.end()) | std::views::transform(transformValueToPointer));
}

#ifndef DOXYGEN
// Forward declare functions
template <typename... Types>
auto makeNestedTupleFlat(std::tuple<Types...> tup);
#endif

namespace Impl {
  template <class Tuple, std::size_t... I>
  constexpr auto makeTupleSubsetImpl(Tuple&& t, std::index_sequence<I...>) {
    return std::make_tuple(std::get<I>(std::forward<Tuple>(t))...);
  }

  template <class Tuple, std::size_t... I>
  constexpr auto makeTupleFromTupleIndicesImpl(Tuple&& t, std::index_sequence<I...>) {
    return std::make_tuple(std::get<I>(std::forward<Tuple>(t))...);
  }

  template <typename T, typename... Ts>
  struct uniqueImpl : std::type_identity<T>
  {
  };

  template <typename... Ts, typename U, typename... Us>
  struct uniqueImpl<std::tuple<Ts...>, U, Us...>
      : std::conditional_t<(std::is_same_v<U, Ts> || ...), uniqueImpl<std::tuple<Ts...>, Us...>,
                           uniqueImpl<std::tuple<Ts..., U>, Us...>>
  {
  };

  template <typename... Ts>
  using unique_tupleImpl = typename uniqueImpl<std::tuple<>, Ts...>::type;

  template <typename T, typename... Types>
  auto makeNestedTupleFlatImpl() {
    constexpr bool isTuple = traits::isSpecialization<std::tuple, T>::value;
    if constexpr (sizeof...(Types) > 0) {
      if constexpr (isTuple)
        return std::tuple_cat(makeNestedTupleFlat(T()), makeNestedTupleFlatImpl<Types...>());
      else
        return std::tuple_cat(std::make_tuple(T()), makeNestedTupleFlatImpl<Types...>());
    } else {
      if constexpr (isTuple)
        return makeNestedTupleFlat(T());
      else
        return std::make_tuple(T());
    }
  }

  template <typename T, typename... Types>
  auto makeNestedTupleFlatAndStoreReferencesImpl(const std::tuple<T, Types...>& tup) {
    constexpr bool isTuple = traits::isSpecialization<std::tuple, std::remove_cvref_t<T>>::value;
    if constexpr (sizeof...(Types) > 0) {
      if constexpr (isTuple)
        return std::tuple_cat(
            makeNestedTupleFlatAndStoreReferencesImpl(std::get<0>(tup)),
            std::apply(
                [](const T&, const Types&... args) {
                  return makeNestedTupleFlatAndStoreReferencesImpl(std::make_tuple(std::cref(args)...));
                },
                tup));
      else
        return std::tuple_cat(
            std::make_tuple(std::cref(std::get<0>(tup))),
            std::apply(
                [](const T&, const Types&... args) {
                  return makeNestedTupleFlatAndStoreReferencesImpl(std::make_tuple(std::cref(args)...));
                },
                tup));
    } else {
      if constexpr (isTuple)
        return makeNestedTupleFlatAndStoreReferencesImpl(std::get<0>(tup));
      else
        return std::make_tuple(std::cref(std::get<0>(tup)));
    }
  }

  template <typename T, typename... Types>
  auto makeNestedTupleFlatAndStoreReferencesNonConstImpl(const std::tuple<T, Types...>& tupconst) {
    auto& tup              = const_cast<std::tuple<T, Types...>&>(tupconst);
    constexpr bool isTuple = traits::isSpecialization<std::tuple, std::remove_cvref_t<T>>::value;
    if constexpr (sizeof...(Types) > 0) {
      if constexpr (isTuple)
        return std::tuple_cat(
            makeNestedTupleFlatAndStoreReferencesNonConstImpl(std::get<0>(tup)),
            std::apply(
                [](T&, Types&... args) {
                  return makeNestedTupleFlatAndStoreReferencesNonConstImpl(std::make_tuple(std::ref(args)...));
                },
                tup));
      else
        return std::tuple_cat(
            std::make_tuple(std::ref(std::get<0>(tup))),
            std::apply(
                [](T&, Types&... args) {
                  return makeNestedTupleFlatAndStoreReferencesNonConstImpl(std::make_tuple(std::ref(args)...));
                },
                tup));
    } else {
      if constexpr (isTuple)
        return makeNestedTupleFlatAndStoreReferencesNonConstImpl(std::get<0>(tup));
      else
        return std::make_tuple(std::ref(std::get<0>(tup)));
    }
  }

} // namespace Impl

/**
 * @brief Finds the index of the first element in the tuple satisfying a predicate.
 *
 * @tparam Tuple Type of the input tuple.
 * @tparam Predicate Type of the predicate function.
 * @param tuple Input tuple.
 * @param pred Predicate function to check each element.
 * @return Index of the first element satisfying the predicate. If no element satisfies the
 *         predicate, it returns the size of the tuple.
 *
 * \details This function takes a tuple and a predicate function and finds the index of the first element in the
 * tuple that satisfies the given predicate. It uses Dune::Hybrid::forEach to iterate through the tuple.
 *
 * \ingroup algos
 */
template <typename Tuple, typename Predicate>
constexpr size_t find_if(Tuple&& tuple, Predicate pred) {
  size_t index        = std::tuple_size<std::remove_reference_t<Tuple>>::value;
  size_t currentIndex = 0;
  bool found          = false;

  Dune::Hybrid::forEach(tuple, [&](auto&& value) {
    if (!found && pred(value)) {
      index = currentIndex;
      found = true;
    }
    ++currentIndex;
  });
  return index;
}

/**
 * @brief Checks if none of the elements in the tuple satisfy a given predicate.
 *
 * @tparam Tuple Type of the input tuple.
 * @tparam Predicate Type of the predicate function.
 * @param tuple Input tuple.
 * @param pred Predicate function to check each element.
 * @return bool True if none of the elements satisfy the predicate, false otherwise.
 *
 *
 * \ingroup algos
 */
template <typename Tuple, typename Predicate>
bool none_of(Tuple&& tuple, Predicate pred) {
  return find_if(tuple, pred) == std::tuple_size<std::decay_t<Tuple>>::value;
}

/**
 * @brief Checks if any of the elements in the tuple satisfy a given predicate.
 *
 * @tparam Tuple Type of the input tuple.
 * @tparam Predicate Type of the predicate function.
 * @param tuple Input tuple.
 * @param pred Predicate function to check each element.
 * @return bool True if any of the elements satisfy the predicate, false otherwise.
 *
 *
 * \ingroup algos
 */
template <typename Tuple, typename Predicate>
bool any_of(Tuple&& tuple, Predicate pred) {
  return !none_of(tuple, pred);
}

/**
 * @brief Filters the elements of a tuple based on a given predicate.
 *
 * @tparam Tuple Type of the input tuple.
 * @tparam Predicate Type of the predicate function.
 * @param tuple Input tuple.
 * @param pred Predicate function to filter the elements.
 * @return auto Tuple containing elements that satisfy the predicate.
 *
 * \details This function applies the given predicate to each element of the tuple. It constructs a new tuple
 * containing only those elements for which the predicate returns true. The resulting tuple is returned.
 *
 * \ingroup algos
 */
template <typename Tuple, typename Predicate>
auto filter(Tuple&& tuple, Predicate pred) {
  return std::apply(
      [&pred](auto... ts) {
        return std::tuple_cat(std::conditional_t<pred(ts), std::tuple<decltype(ts)>, std::tuple<>>{}...);
      },
      tuple);
}

/**
 * @brief Creates a tuple with unique types from the given tuple.
 *
 * @tparam Types Variadic template parameters representing types.
 * @param tuple Input tuple.
 * @return auto Tuple with unique types.
 *
 * \details This function takes a tuple and returns a new tuple containing only unique types from the input tuple.
 *
 * \ingroup algos
 */
template <typename... Types>
constexpr auto unique([[maybe_unused]] std::tuple<Types...>&& tuple) {
  return Impl::unique_tupleImpl<Types...>();
}

/**
 * @brief Counts the number of elements in the tuple satisfying the given predicate.
 *
 * @tparam Tuple Type of the tuple.
 * @tparam Predicate Predicate function determining whether an element satisfies the condition.
 * @param tuple Input tuple.
 * @param pred Predicate function.
 * @return constexpr size_t Number of elements satisfying the predicate.
 *
 * \details This function counts the number of elements in the tuple that satisfy the given predicate.
 *
 * \ingroup algos
 */
template <typename Tuple, typename Predicate>
constexpr size_t count_if(Tuple&& tuple, Predicate pred) {
  size_t counter = 0;
  Dune::Hybrid::forEach(tuple, [&](auto&& value) {
    if (pred(value))
      ++counter;
  });
  return counter;
}

/**
 * @brief Finds the index of the first element in the tuple that is a specialization of the given template type.
 *
 * @tparam Type Template type to search for.
 * @tparam Tuple Type of the tuple.
 * @return Index of the first specialization in the tuple
 *
 * \details This function finds the index of the first element in the tuple that is a specialization of the given
 * template type. It returns the size of the tuple if the template type is not found
 *
 * \ingroup algos
 */
template <template <auto...> class Type, typename Tuple>
constexpr int findTypeSpecialization() {
  return find_if(std::remove_cvref_t<Tuple>(),
                 []<typename T>(T&&) { return traits::isSpecializationNonTypes<Type, std::remove_cvref_t<T>>::value; });
}

/**
 * @brief Gets the specialization of the given template type from the tuple.
 *
 * @tparam Type Template type to search for.
 * @tparam Tuple Type of the tuple.
 * @param tuple The tuple containing elements.
 * @return The specialization element
 *
 * \details This function retrieves the specialization of a template type from the tuple.
 *
 * \ingroup algos
 */
template <template <auto...> class Type, typename Tuple>
auto getSpecialization(Tuple&& tuple) {
  constexpr int index = findTypeSpecialization<Type, Tuple>();
  static_assert(index < std::tuple_size_v<std::remove_cvref_t<Tuple>>,
                "The found index has to be smaller than the tuple size");
  return std::get<index>(tuple);
}

/**
 * @brief Checks if a tuple has a specialization of a template type.
 *
 * @tparam Type Template type to check for.
 * @tparam Tuple Type of the tuple.
 * @return true if the tuple has the specialization; otherwise, false.
 *
 * \details This function checks if a tuple has a specialization of a template type.
 * It uses `find_if` to search for the type and returns true if the index is less than the tuple size; otherwise,
 * false.
 *
 * \ingroup algos
 */
template <template <auto...> class Type, typename Tuple>
constexpr bool hasTypeSpecialization() {
  return (find_if(std::remove_cvref_t<Tuple>(), []<typename T>(T&&) {
            return traits::isSpecializationNonTypes<Type, std::remove_cvref_t<T>>::value;
          }) < std::tuple_size_v<std::remove_cvref_t<Tuple>>);
}

/**
 * @brief Counts the occurrences of a specialization of a template type in a tuple.
 *
 * @tparam Type Template type to count occurrences for.
 * @tparam Tuple Type of the tuple.
 * @return The count of occurrences of the specialization.
 *
 * \details This function counts the occurrences of a specialization of a template type in a tuple.
 *
 * \ingroup algos
 */
template <template <auto...> class Type, typename Tuple>
constexpr bool countTypeSpecialization() {
  return count_if(
      Tuple(), []<typename T>(T&&) { return traits::isSpecializationNonTypes<Type, std::remove_cvref_t<T>>::value; });
}

/**
 * @brief Variable template for counting the occurrences of a specialization of a template type in a tuple.
 *
 * @tparam Type Template type to count occurrences for.
 * @tparam Tuple Type of the tuple.
 *
 * \details This variable template provides a compile-time constant for the count of occurrences
 * of a specialization of a template type in a tuple.
 *
 * \ingroup algos
 */
template <template <auto...> class Type, typename Tuple>
static constexpr bool countTypeSpecialization_v = countTypeSpecialization<Type, Tuple>();

/**
 * @brief Creates a subset tuple with the first N elements from the given tuple.
 *
 * @tparam N Number of elements in the subset.
 * @tparam Tuple Type of the original tuple.
 * @param t The original tuple.
 * @return A new tuple containing the first N elements of the original tuple.
 *
 * \details This function creates a subset tuple with the first N elements from the given tuple.
 *
 * \ingroup algos
 */
template <int N, class Tuple>
constexpr auto makeTupleSubset(Tuple&& t) {
  static_assert(N < std::tuple_size_v<std::remove_reference_t<Tuple>>,
                "The requested size needs to be smaller than the size of the tuple.");

  return Impl::makeTupleSubsetImpl(std::forward<Tuple>(t), std::make_index_sequence<N>{});
}

/**
 * @brief Creates a new tuple using indices from the original tuple.
 *
 * @tparam Tuple Type of the original tuple.
 * @tparam I Indices to include in the new tuple.
 * @param t The original tuple.
 * @return A new tuple containing elements from the original tuple based on the specified indices.
 *
 * \details This function creates a new tuple using indices from the original tuple.
 * It uses `makeTupleFromTupleIndicesImpl` from the `Impl` namespace to implement the tuple creation.
 *
 * \ingroup algos
 */
template <class Tuple, std::size_t... I>
constexpr auto makeTupleFromTupleIndices(Tuple&& t) {
  return Impl::makeTupleFromTupleIndicesImpl(std::forward<Tuple>(t), std::index_sequence<I...>{});
}

/**
 * @brief Creates a flattened nested tuple.
 *
 * @tparam Types Types contained in the original tuple.
 * @return A new flattened nested tuple.
 *
 */
template <typename... Types>
auto makeNestedTupleFlat(std::tuple<Types...>) {
  return decltype(Impl::makeNestedTupleFlatImpl<Types...>())();
}

/**
 * @brief Creates a flattened nested tuple and stores references.
 *
 * @tparam Tuple Type of the original tuple.
 * @param tup The original tuple.
 * @return A new tuple with stored references.
 *
 * \details This function creates a flattened nested tuple and stores references.
 */
template <typename Tuple>
auto makeNestedTupleFlatAndStoreReferences(Tuple&& tup) {
  if constexpr (std::tuple_size_v<std::remove_cvref_t<Tuple>> == 0)
    return tup;
  else if constexpr (!std::is_const_v<std::remove_reference_t<Tuple>>)
    return Impl::makeNestedTupleFlatAndStoreReferencesNonConstImpl(std::forward<Tuple>(tup));
  else
    return Impl::makeNestedTupleFlatAndStoreReferencesImpl(std::forward<Tuple>(tup));
}

/**
 * @brief Returns a reference or std::nullopt if the object is a nullptr.
 * \ingroup tr
 * @tparam T Type of the pointer.
 * @param v Pointer value.
 * @return Reference or std::nullopt.
 */
template <typename T>
requires traits::Pointer<T>
auto& returnReferenceOrNulloptIfObjectIsNullPtr(T v) {
  if constexpr (!std::is_same_v<T, std::nullptr_t>)
    return *v;
  else
    return std::nullopt;
}

} // namespace Ikarus::utils
