//  Copyright (c) 2017 Denis Blank
//
//  SPDX-License-Identifier: BSL-1.0
//  Distributed under the Boost Software License, Version 1.0. (See accompanying
//  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#pragma once

#include <pika/config.hpp>
#include <pika/assert.hpp>
#include <pika/functional/detail/invoke.hpp>
#include <pika/pack_traversal/detail/container_category.hpp>
#include <pika/pack_traversal/traits/pack_traversal_rebind_container.hpp>
#include <pika/type_support/decay.hpp>
#include <pika/type_support/pack.hpp>
#include <pika/util/detail/reserve.hpp>

#include <cstddef>
#include <iterator>
#include <memory>
#include <tuple>
#include <type_traits>
#include <utility>

namespace pika::util::detail {
    /// Exposes useful facilities for dealing with 1:n mappings
    namespace spreading {
        /// A struct to mark a tuple to be unpacked into the parent context
        template <typename... T>
        class spread_box
        {
            std::tuple<T...> boxed_;

        public:
            explicit constexpr spread_box(std::tuple<T...> boxed)
              : boxed_(PIKA_MOVE(boxed))
            {
            }

            std::tuple<T...> unbox() { return PIKA_MOVE(boxed_); }
        };

        template <>
        class spread_box<>
        {
        public:
            explicit constexpr spread_box() noexcept {}
            explicit constexpr spread_box(std::tuple<> const&) noexcept {}

            constexpr std::tuple<> unbox() const noexcept { return std::tuple<>{}; }
        };

        /// Returns an empty spread box which represents an empty
        /// mapped object.
        constexpr inline spread_box<> empty_spread() noexcept { return spread_box<>{}; }

        /// Deduces to a true_type if the given type is a spread marker
        template <typename T>
        struct is_spread : std::false_type
        {
        };
        template <typename... T>
        struct is_spread<spread_box<T...>> : std::true_type
        {
        };

        /// Deduces to a true_type if the given type is an empty
        /// spread marker
        template <typename T>
        struct is_empty_spread : std::false_type
        {
        };
        template <>
        struct is_empty_spread<spread_box<>> : std::true_type
        {
        };

        /// Converts types to the type and spread_box objects to its
        /// underlying tuple.
        template <typename T>
        constexpr T unpack(T&& type)
        {
            return PIKA_FORWARD(T, type);
        }
        template <typename... T>
        constexpr auto unpack(spread_box<T...> type) -> decltype(type.unbox())
        {
            return type.unbox();
        }

        /// Deduces to the type unpack is returning when called with the
        /// the given type T.
        template <typename T>
        using unpacked_of_t = decltype(unpack(std::declval<T>()));

        /// Converts types to the type and spread_box objects to its
        /// underlying tuple. If the type is mapped to zero elements,
        /// the return type will be void.
        template <typename T>
        constexpr auto unpack_or_void(T&& type) -> decltype(unpack(PIKA_FORWARD(T, type)))
        {
            return unpack(PIKA_FORWARD(T, type));
        }
        inline void unpack_or_void(spread_box<>) noexcept {}

        /// Converts types to the a tuple carrying the single type and
        /// spread_box objects to its underlying tuple.
        template <typename T>
        constexpr std::tuple<T> undecorate(T&& type)
        {
            return std::tuple<T>{PIKA_FORWARD(T, type)};
        }
        template <typename... T>
        constexpr auto undecorate(spread_box<T...> type) -> decltype(type.unbox())
        {
            return type.unbox();
        }

        /// A callable object which maps its content back to a
        /// tuple like type.
        template <typename EmptyType, template <typename...> class Type>
        struct tupelizer_base
        {
            // We overload with one argument here so Clang and GCC don't
            // have any issues with overloading against zero arguments.
            template <typename First, typename... T>
            constexpr Type<First, T...> operator()(First&& first, T&&... args) const
            {
                return Type<First, T...>{PIKA_FORWARD(First, first), PIKA_FORWARD(T, args)...};
            }

            // Specifically return the empty object which can be different
            // from a tuple.
            constexpr EmptyType operator()() const noexcept(noexcept(EmptyType{}))
            {
                return EmptyType{};
            }
        };

        /// A callable object which maps its content back to a tuple.
        template <template <typename...> class Type = std::tuple>
        using tupelizer_of_t = tupelizer_base<std::tuple<>, Type>;

        /// A callable object which maps its content back to a tuple like
        /// type if it wasn't empty. For empty types arguments an empty
        /// spread box is returned instead. This is useful to propagate
        /// empty mappings back to the caller.
        template <template <typename...> class Type = std::tuple>
        using flat_tupelizer_of_t = tupelizer_base<spread_box<>, Type>;

        /// A callable object which maps its content back to an
        /// array like type.
        /// This transform can only be used for (flat) mappings which
        /// return an empty mapping back to the caller.
        template <template <typename, std::size_t> class Type>
        struct flat_arraylizer
        {
            /// Deduces to the array type when the array is instantiated
            /// with the given arguments.
            template <typename First, typename... Rest>
            using array_type_of_t = Type<std::decay_t<First>, 1 + sizeof...(Rest)>;

            // We overload with one argument here so Clang and GCC don't
            // have any issues with overloading against zero arguments.
            template <typename First, typename... T>
            constexpr auto operator()(First&& first, T&&... args) const
                -> array_type_of_t<First, T...>
            {
                return array_type_of_t<First, T...>{
                    {PIKA_FORWARD(First, first), PIKA_FORWARD(T, args)...}};
            }

            constexpr auto operator()() const noexcept -> decltype(empty_spread())
            {
                return empty_spread();
            }
        };

        /// Use the recursive instantiation for a variadic pack which
        /// may contain spread types
        template <typename C, typename... T>
        constexpr auto apply_spread_impl(std::true_type, C&& callable, T&&... args)
            -> decltype(std::apply(
                PIKA_FORWARD(C, callable), std::tuple_cat(undecorate(PIKA_FORWARD(T, args))...)))
        {
            return std::apply(
                PIKA_FORWARD(C, callable), std::tuple_cat(undecorate(PIKA_FORWARD(T, args))...));
        }

        /// Use the linear instantiation for variadic packs which don't
        /// contain spread types.
        template <typename C, typename... T>
        constexpr auto apply_spread_impl(std::false_type, C&& callable, T&&... args)
            -> std::invoke_result_t<C, T...>
        {
            return PIKA_INVOKE(PIKA_FORWARD(C, callable), PIKA_FORWARD(T, args)...);
        }

        /// Deduces to a true_type if any of the given types marks
        /// the underlying type to be spread into the current context.
        template <typename... T>
        using is_any_spread_t = util::detail::any_of<is_spread<T>...>;

        template <typename C, typename... T>
        constexpr auto map_spread(C&& callable, T&&... args) -> decltype(apply_spread_impl(
            is_any_spread_t<T...>{}, PIKA_FORWARD(C, callable), PIKA_FORWARD(T, args)...))
        {
            // Check whether any of the args is a detail::flatted_tuple_t,
            // if not, use the linear called version for better
            // compilation speed.
            return apply_spread_impl(
                is_any_spread_t<T...>{}, PIKA_FORWARD(C, callable), PIKA_FORWARD(T, args)...);
        }

        /// Converts the given variadic arguments into a tuple in a way
        /// that spread return values are inserted into the current pack.
        template <typename... T>
        constexpr auto tupelize(T&&... args)
            -> decltype(map_spread(tupelizer_of_t<>{}, PIKA_FORWARD(T, args)...))
        {
            return map_spread(tupelizer_of_t<>{}, PIKA_FORWARD(T, args)...);
        }

        /// Converts the given variadic arguments into a tuple in a way
        /// that spread return values are inserted into the current pack.
        /// If the arguments were mapped to zero arguments, the empty
        /// mapping is propagated backwards to the caller.
        template <template <typename...> class Type, typename... T>
        constexpr auto flat_tupelize_to(T&&... args)
            -> decltype(map_spread(flat_tupelizer_of_t<Type>{}, PIKA_FORWARD(T, args)...))
        {
            return map_spread(flat_tupelizer_of_t<Type>{}, PIKA_FORWARD(T, args)...);
        }

        /// Converts the given variadic arguments into an array in a way
        /// that spread return values are inserted into the current pack.
        /// Through this the size of the array like type might change.
        /// If the arguments were mapped to zero arguments, the empty
        /// mapping is propagated backwards to the caller.
        template <template <typename, std::size_t> class Type, typename... T>
        constexpr auto flat_arraylize_to(T&&... args)
            -> decltype(map_spread(flat_arraylizer<Type>{}, PIKA_FORWARD(T, args)...))
        {
            return map_spread(flat_arraylizer<Type>{}, PIKA_FORWARD(T, args)...);
        }

        /// Converts an empty tuple to void
        template <typename First, typename... Rest>
        constexpr std::tuple<First, Rest...> voidify_empty_tuple(std::tuple<First, Rest...> val)
        {
            return val;
        }
        inline void voidify_empty_tuple(std::tuple<> const&) noexcept {}

        /// Converts the given variadic arguments into a tuple in a way
        /// that spread return values are inserted into the current pack.
        ///
        /// If the returned tuple is empty, void is returned instead.
        template <typename... T>
        constexpr auto tupelize_or_void(T&&... args)
            -> decltype(voidify_empty_tuple(tupelize(PIKA_FORWARD(T, args)...)))
        {
            return voidify_empty_tuple(tupelize(PIKA_FORWARD(T, args)...));
        }
    }    // end namespace spreading

    /// Just traverses the pack with the given callable object,
    /// no result is returned or preserved.
    struct strategy_traverse_tag
    {
    };
    /// Remaps the variadic pack with the return values from the mapper.
    struct strategy_remap_tag
    {
    };

    /// Deduces to a true type if the type leads to at least one effective
    /// call to the mapper.
    template <typename Mapper, typename T>
    using is_effective_t = std::is_invocable<typename Mapper::traversor_type, T>;

    /// Deduces to a true type if any type leads to at least one effective
    /// call to the mapper.
    template <typename Mapper, typename... T>
    struct is_effective_any_of_t;

    template <typename Mapper, typename First, typename... Rest>
    struct is_effective_any_of_t<Mapper, First, Rest...>
      : std::conditional<is_effective_t<Mapper, First>::value, std::true_type,
            is_effective_any_of_t<Mapper, Rest...>>::type
    {
    };
    template <typename Mapper>
    struct is_effective_any_of_t<Mapper> : std::false_type
    {
    };

    /// Provides utilities for remapping the whole content of a
    /// container like type to the same container holding different types.
    namespace container_remapping {
        /// Deduces to a true type if the given parameter T
        /// has a push_back method that accepts a type of E.
        template <typename T, typename E, typename = void>
        struct has_push_back : std::false_type
        {
        };
        template <typename T, typename E>
        struct has_push_back<T, E,
            std::void_t<decltype(std::declval<T>().push_back(std::declval<E>()))>> : std::true_type
        {
        };

        /// Specialization for a container with a single type T
        template <typename NewType, typename Container>
        auto rebind_container(Container const& container)
            -> decltype(traits::pack_traversal_rebind_container<NewType, Container>::call(
                std::declval<Container>()))
        {
            return traits::pack_traversal_rebind_container<NewType, Container>::call(container);
        }

        /// Returns the default iterators of the container in case
        /// the container was passed as an l-value reference.
        /// Otherwise move iterators of the container are returned.
        template <typename C, typename = void>
        class container_accessor
        {
            static_assert(
                std::is_lvalue_reference<C>::value, "This should be a lvalue reference here!");

            C container_;

        public:
            container_accessor(C container)
              : container_(container)
            {
            }

            auto begin() -> decltype(container_.begin()) { return container_.begin(); }

            auto end() -> decltype(container_.end()) { return container_.end(); }
        };
        template <typename C>
        class container_accessor<C, std::enable_if_t<std::is_rvalue_reference<C&&>::value>>
        {
            C&& container_;

        public:
            container_accessor(C&& container)
              : container_(PIKA_MOVE(container))
            {
            }

            auto begin() -> decltype(std::make_move_iterator(container_.begin()))
            {
                return std::make_move_iterator(container_.begin());
            }

            auto end() -> decltype(std::make_move_iterator(container_.end()))
            {
                return std::make_move_iterator(container_.end());
            }
        };

        template <typename T>
        container_accessor<T> container_accessor_of(T&& container)
        {
            // Don't use any decay here
            return container_accessor<T>(PIKA_FORWARD(T, container));
        }

        /// Deduces to the type the homogeneous container is containing
        ///
        /// This alias deduces to the same type on which
        /// container_accessor<T> is iterating.
        ///
        /// The basic idea is that we deduce to the type the homogeneous
        /// container T is carrying as reference while preserving the
        /// original reference type of the container:
        /// - If the container was passed as l-value its containing
        ///   values are referenced through l-values.
        /// - If the container was passed as r-value its containing
        ///   values are referenced through r-values.
        template <typename Container>
        using element_of_t = typename std::conditional<std::is_rvalue_reference<Container&&>::value,
            decltype(PIKA_MOVE(*(std::declval<Container>().begin()))),
            decltype(*(std::declval<Container>().begin()))>::type;

        /// Removes all qualifier and references from the given type
        /// if the type is a l-value or r-value reference.
        template <typename T>
        using dereferenced_of_t =
            typename std::conditional<std::is_reference<T>::value, std::decay_t<T>, T>::type;

        /// Returns the type which is resulting if the mapping is applied to
        /// an element in the container.
        ///
        /// Since standard containers don't allow to be instantiated with
        /// references we try to construct the container from a copied
        /// version.
        template <typename Container, typename Mapping>
        using mapped_type_from_t = dereferenced_of_t<
            spreading::unpacked_of_t<std::invoke_result_t<Mapping, element_of_t<Container>>>>;

        /// Deduces to a true_type if the mapping maps to zero elements.
        template <typename T, typename M>
        using is_empty_mapped =
            spreading::is_empty_spread<std::decay_t<std::invoke_result_t<M, element_of_t<T>>>>;

        /// We are allowed to reuse the container if we map to the same
        /// type we are accepting and when we have
        /// the full ownership of the container.
        template <typename T, typename M>
        using can_reuse = std::integral_constant<bool,
            std::is_same<element_of_t<T>, mapped_type_from_t<T, M>>::value &&
                std::is_rvalue_reference<T&&>::value>;

        /// Categorizes a mapping of a homogeneous container
        ///
        /// \tparam IsEmptyMapped Identifies whether the mapping maps to
        ///         to zero arguments.
        /// \tparam CanReuse Identifies whether the container can be
        ///         re-used through the mapping.
        template <bool IsEmptyMapped, bool CanReuse>
        struct container_mapping_tag
        {
        };

        /// Categorizes the given container through a container_mapping_tag
        template <typename T, typename M>
        using container_mapping_tag_of_t =
            container_mapping_tag<is_empty_mapped<T, M>::value, can_reuse<T, M>::value>;

        /// We create a new container, which may hold the resulting type
        template <typename M, typename T>
        auto remap_container(container_mapping_tag<false, false>, M&& mapper, T&& container)
            -> decltype(rebind_container<mapped_type_from_t<T, M>>(container))
        {
            static_assert(has_push_back<std::decay_t<T>, element_of_t<T>>::value,
                "Can only remap containers that provide a push_back method!");

            // Create the new container, which is capable of holding
            // the re-mapped types.
            auto remapped = rebind_container<mapped_type_from_t<T, M>>(container);

            // We try to reserve the original size from the source
            // container to the destination container.
            traits::detail::reserve_if_reservable(remapped, container.size());

            // Perform the actual value remapping from the source to
            // the destination.
            // We could have used std::transform for this, however,
            // I didn't want to pull a whole header for it in.
            for (auto&& val : container_accessor_of(PIKA_FORWARD(T, container)))
            {
                remapped.push_back(
                    spreading::unpack(PIKA_FORWARD(M, mapper)(PIKA_FORWARD(decltype(val), val))));
            }

            return remapped;    // RVO
        }

        /// The remapper optimized for the case that we map to the same
        /// type we accepted such as int -> int.
        template <typename M, typename T>
        auto remap_container(container_mapping_tag<false, true>, M&& mapper, T&& container)
            -> std::decay_t<T>
        {
            for (auto&& val : container_accessor_of(PIKA_FORWARD(T, container)))
            {
                val = spreading::unpack(PIKA_FORWARD(M, mapper)(PIKA_FORWARD(decltype(val), val)));
            }
            return PIKA_FORWARD(T, container);
        }

        /// Remap the container to zero arguments
        template <typename M, typename T>
        auto remap_container(container_mapping_tag<true, false>, M&& mapper, T&& container)
            -> decltype(spreading::empty_spread())
        {
            for (auto&& val : container_accessor_of(PIKA_FORWARD(T, container)))
            {
                // Don't save the empty mapping for each invocation
                // of the mapper.
                PIKA_FORWARD(M, mapper)(PIKA_FORWARD(decltype(val), val));
            }
            // Return one instance of an empty mapping for the container
            return spreading::empty_spread();
        }

        /// Remaps the content of the given container with type T,
        /// to a container of the same type which may contain
        /// different types.
        template <typename T, typename M>
        auto remap(strategy_remap_tag, T&& container, M&& mapper,
            typename std::enable_if<is_effective_t<M, element_of_t<T>>::value>::type* = nullptr)
            -> decltype(remap_container(container_mapping_tag_of_t<T, M>{}, PIKA_FORWARD(M, mapper),
                PIKA_FORWARD(T, container)))
        {
            return remap_container(container_mapping_tag_of_t<T, M>{}, PIKA_FORWARD(M, mapper),
                PIKA_FORWARD(T, container));
        }

        /// Just call the visitor with the content of the container
        template <typename T, typename M>
        void remap(strategy_traverse_tag, T&& container, M&& mapper,
            typename std::enable_if<is_effective_t<M, element_of_t<T>>::value>::type* = nullptr)
        {
#if defined(PIKA_CUDA_VERSION) || defined(PIKA_NVHPC_VERSION)
            for (auto&& element : std::forward<T>(container))
#else
            for (auto&& element : PIKA_FORWARD(T, container))
#endif

            {
                PIKA_FORWARD(M, mapper)
                (PIKA_FORWARD(decltype(element), element));
            }
        }
    }    // end namespace container_remapping

    /// Provides utilities for remapping the whole content of a
    /// tuple like type to the same type holding different types.
    namespace tuple_like_remapping {
        template <typename Strategy, typename Mapper, typename T, typename Enable = void>
        struct tuple_like_remapper
        {
        };

        /// Specialization for std::tuple like types which contain
        /// an arbitrary amount of heterogeneous arguments.
        template <typename M, template <typename...> class Base, typename... OldArgs>
        struct tuple_like_remapper<strategy_remap_tag, M, Base<OldArgs...>,
            // Support for skipping completely untouched types
            typename std::enable_if<is_effective_any_of_t<M, OldArgs...>::value>::type>
        {
            M mapper_;

            template <typename... Args>
            auto operator()(Args&&... args) -> decltype(spreading::flat_tupelize_to<Base>(
                std::declval<M>()(PIKA_FORWARD(Args, args))...))
            {
                return spreading::flat_tupelize_to<Base>(mapper_(PIKA_FORWARD(Args, args))...);
            }
        };
        template <typename M, template <typename...> class Base, typename... OldArgs>
        struct tuple_like_remapper<strategy_traverse_tag, M, Base<OldArgs...>,
            // Support for skipping completely untouched types
            typename std::enable_if<is_effective_any_of_t<M, OldArgs...>::value>::type>
        {
            M mapper_;

            template <typename... Args>
            auto operator()(Args&&... args) -> std::void_t<std::invoke_result_t<M, OldArgs>...>
            {
                int dummy[] = {0, ((void) mapper_(PIKA_FORWARD(Args, args)), 0)...};
                (void) dummy;
            }
        };

        /// Specialization for std::array like types, which contains a
        /// compile-time known amount of homogeneous types.
        template <typename M, template <typename, std::size_t> class Base, typename OldArg,
            std::size_t Size>
        struct tuple_like_remapper<strategy_remap_tag, M, Base<OldArg, Size>,
            // Support for skipping completely untouched types
            std::enable_if_t<is_effective_t<M, OldArg>::value>>
        {
            M mapper_;

            template <typename... Args>
            auto operator()(Args&&... args) -> decltype(spreading::flat_arraylize_to<Base>(
                mapper_(PIKA_FORWARD(Args, args))...))
            {
                return spreading::flat_arraylize_to<Base>(mapper_(PIKA_FORWARD(Args, args))...);
            }
        };
        template <typename M, template <typename, std::size_t> class Base, typename OldArg,
            std::size_t Size>
        struct tuple_like_remapper<strategy_traverse_tag, M, Base<OldArg, Size>,
            // Support for skipping completely untouched types
            std::enable_if_t<is_effective_t<M, OldArg>::value>>
        {
            M mapper_;

            template <typename... Args>
            auto operator()(Args&&... args) -> std::invoke_result_t<std::invoke_result_t<M, OldArg>>
            {
                int dummy[] = {0, ((void) mapper_(PIKA_FORWARD(Args, args)), 0)...};
                (void) dummy;
            }
        };

        /// Remaps the content of the given tuple like type T,
        /// to a container of the same type which may contain
        /// different types.
        template <typename Strategy, typename T, typename M>
        auto remap(Strategy, T&& container, M&& mapper) -> decltype(std::apply(
            tuple_like_remapper<Strategy, std::decay_t<M>, std::decay_t<T>>{
                PIKA_FORWARD(M, mapper)},
            PIKA_FORWARD(T, container))

        )
        {
            return std::apply(
                tuple_like_remapper<Strategy, std::decay_t<M>, std::decay_t<T>>{
                    PIKA_FORWARD(M, mapper)},
                PIKA_FORWARD(T, container));
        }
    }    // end namespace tuple_like_remapping

    /// Base class for making strategy dependent behavior available
    /// to the mapping_helper class.
    template <typename Strategy>
    struct mapping_strategy_base
    {
        template <typename T>
        decltype(auto) may_void(T&& element) const
        {
            return PIKA_FORWARD(T, element);
        }
    };
    template <>
    struct mapping_strategy_base<strategy_traverse_tag>
    {
        template <typename T>
        void may_void(T&& /*element*/) const noexcept
        {
        }
    };

    /// A helper class which applies the mapping or
    /// routes the element through
    template <typename Strategy, typename M>
    class mapping_helper : protected mapping_strategy_base<Strategy>
    {
        M mapper_;

        class traversal_callable_base
        {
            mapping_helper* helper_;

        public:
            explicit traversal_callable_base(mapping_helper* helper)
              : helper_(helper)
            {
            }

        protected:
            mapping_helper* get_helper() noexcept { return helper_; }
        };

        /// A callable object which forwards its invocations
        /// to mapping_helper::traverse.
        class traversor : public traversal_callable_base
        {
        public:
            using traversal_callable_base::traversal_callable_base;

            /// SFINAE helper
            template <typename T>
            auto operator()(T&& element)
                -> decltype(std::declval<traversor>().get_helper()->traverse(
                    Strategy{}, PIKA_FORWARD(T, element)))
            {
                PIKA_UNREACHABLE;
            }

            /// An alias to this type
            using traversor_type = traversor;
        };

        /// A callable object which forwards its invocations
        /// to mapping_helper::try_traverse.
        ///
        /// This callable object will accept any input,
        /// since elements passed to it are passed through,
        /// if the provided mapper doesn't accept it.
        class try_traversor : public traversal_callable_base
        {
        public:
            using traversal_callable_base::traversal_callable_base;

            template <typename T>
            auto operator()(T&& element)
                -> decltype(std::declval<try_traversor>().get_helper()->try_traverse(
                    Strategy{}, PIKA_FORWARD(T, element)))
            {
                return this->get_helper()->try_traverse(Strategy{}, PIKA_FORWARD(T, element));
            }

            /// An alias to the traversor type
            using traversor_type = traversor;
        };

        /// Invokes the real mapper with the given element
        template <typename T>
        auto invoke_mapper(T&& element)
            -> decltype(std::declval<mapping_helper>().mapper_(PIKA_FORWARD(T, element)))
        {
            return mapper_(PIKA_FORWARD(T, element));
        }

        /// SFINAE helper for plain elements not satisfying the tuple like
        /// or container requirements.
        ///
        /// We use the proxy function invoke_mapper here,
        /// because some compilers (MSVC) tend to instantiate the invocation
        /// before matching the tag, which leads to build failures.
        template <typename T>
        auto match(container_category_tag<false, false>, T&& element)
            -> decltype(std::declval<mapping_helper>().invoke_mapper(PIKA_FORWARD(T, element)));

        /// SFINAE helper for elements satisfying the container
        /// requirements, which are not tuple like.
        template <typename T>
        auto match(container_category_tag<true, false>, T&& container)
            -> decltype(container_remapping::remap(
                Strategy{}, PIKA_FORWARD(T, container), std::declval<traversor>()));

        /// SFINAE helper for elements which are tuple like and
        /// that also may satisfy the container requirements
        template <bool IsContainer, typename T>
        auto match(container_category_tag<IsContainer, true>, T&& tuple_like)
            -> decltype(tuple_like_remapping::remap(
                Strategy{}, PIKA_FORWARD(T, tuple_like), std::declval<traversor>()));

        /// This method implements the functionality for routing
        /// elements through, that aren't accepted by the mapper.
        /// Since the real matcher methods below are failing through SFINAE,
        /// the compiler will try to specialize this function last,
        /// since it's the least concrete one.
        /// This works recursively, so we only call the mapper
        /// with the minimal needed set of accepted arguments.
        template <typename MatcherTag, typename T>
        auto try_match(MatcherTag, T&& element)
            -> decltype(std::declval<mapping_helper>().may_void(PIKA_FORWARD(T, element)))
        {
            return this->may_void(PIKA_FORWARD(T, element));
        }

        /// Match plain elements not satisfying the tuple like or
        /// container requirements.
        ///
        /// We use the proxy function invoke_mapper here,
        /// because some compilers (MSVC) tend to instantiate the invocation
        /// before matching the tag, which leads to build failures.
        template <typename T>
        auto try_match(container_category_tag<false, false>, T&& element)
            -> decltype(std::declval<mapping_helper>().invoke_mapper(PIKA_FORWARD(T, element)))
        {
            // T could be any non container or non tuple like type here,
            // take int or pika::future<int> as an example.
            return invoke_mapper(PIKA_FORWARD(T, element));
        }

        /// Match elements satisfying the container requirements,
        /// which are not tuple like.
        template <typename T>
        auto try_match(container_category_tag<true, false>, T&& container)
            -> decltype(container_remapping::remap(
                Strategy{}, PIKA_FORWARD(T, container), std::declval<try_traversor>()))
        {
            return container_remapping::remap(
                Strategy{}, PIKA_FORWARD(T, container), try_traversor{this});
        }

        /// Match elements which are tuple like and that also may
        /// satisfy the container requirements
        /// -> We match tuple like types over container like ones
        template <bool IsContainer, typename T>
        auto try_match(container_category_tag<IsContainer, true>, T&& tuple_like)
            -> decltype(tuple_like_remapping::remap(
                Strategy{}, PIKA_FORWARD(T, tuple_like), std::declval<try_traversor>()))
        {
            return tuple_like_remapping::remap(
                Strategy{}, PIKA_FORWARD(T, tuple_like), try_traversor{this});
        }

        /// Traverses a single element.
        ///
        /// SFINAE helper: Doesn't allow routing through elements,
        /// that aren't accepted by the mapper
        template <typename T>
        auto traverse(Strategy, T&& element) -> decltype(std::declval<mapping_helper>().match(
            std::declval<container_category_of_t<typename pika::detail::decay_unwrap<T>::type>>(),
            std::declval<T>()));

        /// \copybrief traverse
        template <typename T>
        auto
        try_traverse(Strategy, T&& element) -> decltype(std::declval<mapping_helper>().try_match(
            std::declval<container_category_of_t<typename pika::detail::decay_unwrap<T>::type>>(),
            std::declval<T>()))
        {
            // We use tag dispatching here, to categorize the type T whether
            // it satisfies the container or tuple like requirements.
            // Then we can choose the underlying implementation accordingly.
            return try_match(
                container_category_of_t<typename pika::detail::decay_unwrap<T>::type>{},
                PIKA_FORWARD(T, element));
        }

    public:
        explicit mapping_helper(M mapper)
          : mapper_(PIKA_MOVE(mapper))
        {
        }

        /// \copybrief try_traverse
        template <typename T>
        auto init_traverse(strategy_remap_tag, T&& element) -> decltype(spreading::unpack_or_void(
            std::declval<mapping_helper>().try_traverse(strategy_remap_tag{}, std::declval<T>())))
        {
            return spreading::unpack_or_void(
                try_traverse(strategy_remap_tag{}, PIKA_FORWARD(T, element)));
        }
        template <typename T>
        void init_traverse(strategy_traverse_tag, T&& element)
        {
            try_traverse(strategy_traverse_tag{}, PIKA_FORWARD(T, element));
        }

        /// Calls the traversal method for every element in the pack,
        /// and returns a tuple containing the remapped content.
        template <typename First, typename Second, typename... T>
        auto init_traverse(strategy_remap_tag strategy, First&& first, Second&& second, T&&... rest)
            -> decltype(spreading::tupelize_or_void(
                std::declval<mapping_helper>().try_traverse(strategy, PIKA_FORWARD(First, first)),
                std::declval<mapping_helper>().try_traverse(strategy, PIKA_FORWARD(Second, second)),
                std::declval<mapping_helper>().try_traverse(strategy, PIKA_FORWARD(T, rest))...))
        {
            return spreading::tupelize_or_void(try_traverse(strategy, PIKA_FORWARD(First, first)),
                try_traverse(strategy, PIKA_FORWARD(Second, second)),
                try_traverse(strategy, PIKA_FORWARD(T, rest))...);
        }

        /// Calls the traversal method for every element in the pack,
        /// without preserving the return values of the mapper.
        template <typename First, typename Second, typename... T>
        void
        init_traverse(strategy_traverse_tag strategy, First&& first, Second&& second, T&&... rest)
        {
            try_traverse(strategy, PIKA_FORWARD(First, first));
            try_traverse(strategy, PIKA_FORWARD(Second, second));
            int dummy[] = {0, ((void) try_traverse(strategy, PIKA_FORWARD(T, rest)), 0)...};
            (void) dummy;
        }
    };

    /// Traverses the given pack with the given mapper and strategy
    template <typename Strategy, typename Mapper, typename... T>
    auto apply_pack_transform(Strategy strategy, Mapper&& mapper, T&&... pack)
        -> decltype(std::declval<mapping_helper<Strategy, std::decay_t<Mapper>>>().init_traverse(
            strategy, PIKA_FORWARD(T, pack)...))
    {
        mapping_helper<Strategy, std::decay_t<Mapper>> helper(PIKA_FORWARD(Mapper, mapper));
        return helper.init_traverse(strategy, PIKA_FORWARD(T, pack)...);
    }
}    // namespace pika::util::detail
