// Copyright (C) 2016-2018 T. Zachary Laine
//
// 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)
#ifndef BOOST_YAP_DETAIL_TRANSFORM_HPP_INCLUDED
#define BOOST_YAP_DETAIL_TRANSFORM_HPP_INCLUDED

#include <boost/yap/algorithm_fwd.hpp>

#include <boost/hana/transform.hpp>

#include <cassert>


namespace boost { namespace yap { namespace detail {

    template<int I, typename T, typename... Ts>
    struct nth_element_impl
    {
        using type = typename nth_element_impl<I - 1, Ts...>::type;
    };

    template<typename T, typename... Ts>
    struct nth_element_impl<0, T, Ts...>
    {
        using type = T;
    };

    template<int I, typename... Ts>
    using nth_element = typename nth_element_impl<I, Ts...>::type;

    template<typename T, bool RemoveRefs = std::is_rvalue_reference<T>::value>
    struct rvalue_ref_to_value;

    template<typename T>
    struct rvalue_ref_to_value<T, true>
    {
        using type = typename std::remove_reference<T>::type;
    };

    template<typename T>
    struct rvalue_ref_to_value<T, false>
    {
        using type = T;
    };

    template<typename T>
    using rvalue_ref_to_value_t = typename rvalue_ref_to_value<T>::type;

    template<bool IsRvalueRef>
    struct rvalue_mover
    {
        template<typename T>
        constexpr decltype(auto) operator()(T && t) const
        {
            return static_cast<T &&>(t);
        }
    };

    template<>
    struct rvalue_mover<true>
    {
        template<typename T>
        constexpr std::remove_reference_t<T> operator()(T && t) const
        {
            return std::move(t);
        }
    };

    template<typename... PlaceholderArgs>
    struct placeholder_transform_t
    {
        using tuple_t = hana::tuple<rvalue_ref_to_value_t<PlaceholderArgs>...>;

        placeholder_transform_t(PlaceholderArgs &&... args) :
            placeholder_args_(static_cast<PlaceholderArgs &&>(args)...)
        {}

        template<long long I>
        constexpr decltype(auto)
        operator()(expr_tag<expr_kind::terminal>, boost::yap::placeholder<I>) const
        {
            static_assert(
                I <= decltype(hana::size(std::declval<tuple_t>()))::value,
                "Out of range placeholder index,");
            using nth_type = nth_element<I - 1, PlaceholderArgs...>;
            return as_expr<minimal_expr>(
                rvalue_mover<!std::is_lvalue_reference<nth_type>::value>{}(
                    placeholder_args_[hana::llong<I - 1>{}]));
        }

        tuple_t placeholder_args_;
    };

    template<typename... PlaceholderArgs>
    struct evaluation_transform_t
    {
        using tuple_t = hana::tuple<rvalue_ref_to_value_t<PlaceholderArgs>...>;

        constexpr evaluation_transform_t(PlaceholderArgs &&... args) :
            placeholder_args_(static_cast<PlaceholderArgs &&>(args)...)
        {}

        template<long long I>
        constexpr decltype(auto)
        operator()(expr_tag<expr_kind::terminal>, boost::yap::placeholder<I>) const
        {
            static_assert(
                I <= decltype(hana::size(std::declval<tuple_t>()))::value,
                "Out of range placeholder index,");
            using nth_type = nth_element<I - 1, PlaceholderArgs...>;
            return rvalue_mover<!std::is_lvalue_reference<nth_type>::value>{}(
                placeholder_args_[hana::llong<I - 1>{}]);
        }

        template<typename T>
        constexpr decltype(auto) operator()(expr_tag<expr_kind::terminal>, T && t) const
        {
            return static_cast<T &&>(t);
        }

#define BOOST_YAP_UNARY_OPERATOR_CASE(op, op_name)                             \
    template<typename T>                                                       \
    constexpr decltype(auto) operator()(expr_tag<expr_kind::op_name>, T && t) const \
    {                                                                          \
        return op transform(                                                   \
            as_expr<minimal_expr>(static_cast<T &&>(t)), *this);               \
    }

        BOOST_YAP_UNARY_OPERATOR_CASE(+, unary_plus)
        BOOST_YAP_UNARY_OPERATOR_CASE(-, negate)
        BOOST_YAP_UNARY_OPERATOR_CASE(*, dereference)
        BOOST_YAP_UNARY_OPERATOR_CASE(~, complement)
        BOOST_YAP_UNARY_OPERATOR_CASE(&, address_of)
        BOOST_YAP_UNARY_OPERATOR_CASE(!, logical_not)
        BOOST_YAP_UNARY_OPERATOR_CASE(++, pre_inc)
        BOOST_YAP_UNARY_OPERATOR_CASE(--, pre_dec)

        template<typename T>
        constexpr decltype(auto) operator()(expr_tag<expr_kind::post_inc>, T && t) const
        {
            return transform(
                as_expr<minimal_expr>(static_cast<T &&>(t)), *this)++;
        }
        template<typename T>
        constexpr decltype(auto) operator()(expr_tag<expr_kind::post_dec>, T && t) const
        {
            return transform(
                as_expr<minimal_expr>(static_cast<T &&>(t)), *this)--;
        }

#undef BOOST_YAP_UNARY_OPERATOR_CASE

#define BOOST_YAP_BINARY_OPERATOR_CASE(op, op_name)                            \
    template<typename T, typename U>                                           \
    constexpr decltype(auto) operator()(expr_tag<expr_kind::op_name>, T && t, U && u) const \
    {                                                                          \
        return transform(as_expr<minimal_expr>(static_cast<T &&>(t)), *this)   \
            op transform(as_expr<minimal_expr>(static_cast<U &&>(u)), *this);  \
    }

        BOOST_YAP_BINARY_OPERATOR_CASE(<<, shift_left)
        BOOST_YAP_BINARY_OPERATOR_CASE(>>, shift_right)
        BOOST_YAP_BINARY_OPERATOR_CASE(*, multiplies)
        BOOST_YAP_BINARY_OPERATOR_CASE(/, divides)
        BOOST_YAP_BINARY_OPERATOR_CASE(%, modulus)
        BOOST_YAP_BINARY_OPERATOR_CASE(+, plus)
        BOOST_YAP_BINARY_OPERATOR_CASE(-, minus)
        BOOST_YAP_BINARY_OPERATOR_CASE(<, less)
        BOOST_YAP_BINARY_OPERATOR_CASE(>, greater)
        BOOST_YAP_BINARY_OPERATOR_CASE(<=, less_equal)
        BOOST_YAP_BINARY_OPERATOR_CASE(>=, greater_equal)
        BOOST_YAP_BINARY_OPERATOR_CASE(==, equal_to)
        BOOST_YAP_BINARY_OPERATOR_CASE(!=, not_equal_to)
        BOOST_YAP_BINARY_OPERATOR_CASE(||, logical_or)
        BOOST_YAP_BINARY_OPERATOR_CASE(&&, logical_and)
        BOOST_YAP_BINARY_OPERATOR_CASE(&, bitwise_and)
        BOOST_YAP_BINARY_OPERATOR_CASE(|, bitwise_or)
        BOOST_YAP_BINARY_OPERATOR_CASE (^, bitwise_xor)

        // clang-format off
//[ evaluation_transform_comma
        template<typename T, typename U>
        constexpr decltype(auto) operator()(expr_tag<expr_kind::comma>, T && t, U && u) const
        {
            return transform(
                       as_expr<minimal_expr>(static_cast<T &&>(t)), *this),
                   transform(
                       as_expr<minimal_expr>(static_cast<U &&>(u)), *this);
        }
//]
        // clang-format on

        BOOST_YAP_BINARY_OPERATOR_CASE(->*, mem_ptr)
        BOOST_YAP_BINARY_OPERATOR_CASE(=, assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(<<=, shift_left_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(>>=, shift_right_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(*=, multiplies_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(/=, divides_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(%=, modulus_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(+=, plus_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(-=, minus_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(&=, bitwise_and_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(|=, bitwise_or_assign)
        BOOST_YAP_BINARY_OPERATOR_CASE(^=, bitwise_xor_assign)

        template<typename T, typename U>
        constexpr decltype(auto)
        operator()(expr_tag<expr_kind::subscript>, T && t, U && u) const
        {
            return transform(
                as_expr<minimal_expr>(static_cast<T &&>(t)), *this)[transform(
                as_expr<minimal_expr>(static_cast<U &&>(u)), *this)];
        }

#undef BOOST_YAP_BINARY_OPERATOR_CASE

        template<typename T, typename U, typename V>
        constexpr decltype(auto)
        operator()(expr_tag<expr_kind::if_else>, T && t, U && u, V && v) const
        {
            return transform(as_expr<minimal_expr>(static_cast<T &&>(t)), *this)
                       ? transform(
                             as_expr<minimal_expr>(static_cast<U &&>(u)), *this)
                       : transform(
                             as_expr<minimal_expr>(static_cast<V &&>(v)),
                             *this);
        }

        // clang-format off
//[ evaluation_transform_call
        template<typename Callable, typename... Args>
        constexpr decltype(auto) operator()(
            expr_tag<expr_kind::call>, Callable && callable, Args &&... args) const
        {
            return transform(as_expr<minimal_expr>(static_cast<Callable &&>(callable)), *this)(
                transform(as_expr<minimal_expr>(static_cast<Args &&>(args)), *this)...
            );
        }
//]
        // clang-format on

        tuple_t placeholder_args_;
    };


    template<bool Strict, int I, bool IsExprRef>
    struct transform_impl;

    template<
        bool Strict,
        typename Expr,
        typename TransformTuple,
        int I,
        expr_arity Arity,
        typename = void_t<>>
    struct transform_expression_tag;


    // Forward terminals/recurively transform noterminasl; attempted last.

    template<bool IsLvalueRef, bool IsTerminal, bool Strict>
    struct default_transform
    {
        template<typename Expr, typename TransformTuple>
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return static_cast<Expr &&>(expr);
        }
    };

    template<bool IsLvalueRef, bool IsTerminal>
    struct default_transform<IsLvalueRef, IsTerminal, true>
    {
        struct incomplete;

        // If you're getting an error because this function is uncallable,
        // that's by design.  You called yap::transform_strict(expr, xfrom)
        // and one or more subexpression of 'expr' are not callable with any
        // overload in 'xform'.
        template<typename Expr, typename TransformTuple>
        constexpr incomplete operator()(Expr && expr, TransformTuple transforms) const;
    };

    template<
        expr_kind Kind,
        template<expr_kind, class> class ExprTemplate,
        typename OldTuple,
        typename NewTuple>
    constexpr auto make_expr_from_tuple(
        ExprTemplate<Kind, OldTuple> const & expr, NewTuple && tuple)
    {
        return ExprTemplate<Kind, NewTuple>{std::move(tuple)};
    }

    template<expr_kind Kind, typename Expr, typename NewTuple>
    constexpr auto make_expr_from_tuple(Expr const & expr, NewTuple && tuple)
    {
        return minimal_expr<Kind, NewTuple>{std::move(tuple)};
    }

    template<typename Expr, typename Tuple, typename TransformTuple>
    constexpr decltype(auto) transform_nonterminal(
        Expr const & expr, Tuple && tuple, TransformTuple transforms)
    {
        auto transformed_tuple =
            hana::transform(static_cast<Tuple &&>(tuple), [&](auto && element) {
                using element_t = decltype(element);
                auto const kind = remove_cv_ref_t<element_t>::kind;
                ::boost::yap::detail::
                    transform_impl<false, 0, kind == expr_kind::expr_ref>
                        xform;
                return xform(static_cast<element_t &&>(element), transforms);
            });
        auto const kind = remove_cv_ref_t<Expr>::kind;
        return make_expr_from_tuple<kind>(expr, std::move(transformed_tuple));
    }

    template<>
    struct default_transform<true, false, false>
    {
        template<typename Expr, typename TransformTuple>
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return transform_nonterminal(expr, expr.elements, transforms);
        }
    };

    template<>
    struct default_transform<false, false, false>
    {
        template<typename Expr, typename TransformTuple>
        constexpr decltype(auto)
        operator()(Expr && expr, TransformTuple transforms) const
        {
            return transform_nonterminal(
                expr, std::move(expr.elements), transforms);
        }
    };

    // Dispatch to the next transform, or to the default transform if there is
    // no next transform.

    template<
        bool Strict,
        typename Expr,
        typename TransformTuple,
        int I,
        bool NextTransformExists>
    struct next_or_default_transform
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            // Use the next transform.
            constexpr expr_kind kind = remove_cv_ref_t<Expr>::kind;
            return detail::
                transform_impl<Strict, I + 1, kind == expr_kind::expr_ref>{}(
                    static_cast<Expr &&>(expr), transforms);
        }
    };

    template<bool Strict, typename Expr, typename TransformTuple, int I>
    struct next_or_default_transform<Strict, Expr, TransformTuple, I, false>
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            // No next transform exists; use the default transform.
            constexpr expr_kind kind = remove_cv_ref_t<Expr>::kind;
            return default_transform<
                std::is_lvalue_reference<Expr>::value,
                kind == expr_kind::terminal,
                Strict>{}(static_cast<Expr &&>(expr), transforms);
        }
    };

    // Expression-matching; attempted second.

    template<
        bool Strict,
        typename Expr,
        typename TransformTuple,
        int I,
        typename = detail::void_t<>>
    struct transform_expression_expr
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            // No expr-matching succeeded; use the next or default transform.
            return next_or_default_transform<
                Strict,
                Expr,
                TransformTuple,
                I,
                I + 1 < decltype(hana::size(
                            std::declval<TransformTuple>()))::value>{}(
                static_cast<Expr &&>(expr), transforms);
        }
    };

    template<bool Strict, typename Expr, typename TransformTuple, int I>
    struct transform_expression_expr<
        Strict,
        Expr,
        TransformTuple,
        I,
        void_t<decltype((*std::declval<TransformTuple>()[hana::llong<I>{}])(
            std::declval<Expr>()))>>
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return (*transforms[hana::llong<I>{}])(static_cast<Expr &&>(expr));
        }
    };


    // Tag-matching; attempted first.

    template<
        bool Strict,
        typename Expr,
        typename TransformTuple,
        int I,
        expr_arity Arity,
        typename>
    struct transform_expression_tag
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            // No tag-matching succeeded; try expr-matching.
            return transform_expression_expr<Strict, Expr, TransformTuple, I>{}(
                static_cast<Expr &&>(expr), transforms);
        }
    };

    template<typename T>
    constexpr decltype(auto) terminal_value(T && x)
    {
        return value_impl<true>(static_cast<T &&>(x));
    }


    template<bool Strict, typename Expr, typename TransformTuple, int I>
    struct transform_expression_tag<
        Strict,
        Expr,
        TransformTuple,
        I,
        expr_arity::one,
        void_t<decltype((*std::declval<TransformTuple>()[hana::llong<I>{}])(
            expr_tag<remove_cv_ref_t<Expr>::kind>{},
            terminal_value(::boost::yap::value(std::declval<Expr>()))))>>
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return (*transforms[hana::llong<I>{}])(
                expr_tag<remove_cv_ref_t<Expr>::kind>{},
                terminal_value(
                    ::boost::yap::value(static_cast<Expr &&>(expr))));
        }
    };

    template<bool Strict, typename Expr, typename TransformTuple, int I>
    struct transform_expression_tag<
        Strict,
        Expr,
        TransformTuple,
        I,
        expr_arity::two,
        void_t<decltype((*std::declval<TransformTuple>()[hana::llong<I>{}])(
            expr_tag<remove_cv_ref_t<Expr>::kind>{},
            terminal_value(::boost::yap::left(std::declval<Expr>())),
            terminal_value(::boost::yap::right(std::declval<Expr>()))))>>
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return (*transforms[hana::llong<I>{}])(
                expr_tag<remove_cv_ref_t<Expr>::kind>{},
                terminal_value(::boost::yap::left(static_cast<Expr &&>(expr))),
                terminal_value(
                    ::boost::yap::right(static_cast<Expr &&>(expr))));
        }
    };

    template<bool Strict, typename Expr, typename TransformTuple, int I>
    struct transform_expression_tag<
        Strict,
        Expr,
        TransformTuple,
        I,
        expr_arity::three,
        void_t<decltype((*std::declval<TransformTuple>()[hana::llong<I>{}])(
            expr_tag<remove_cv_ref_t<Expr>::kind>{},
            terminal_value(::boost::yap::cond(std::declval<Expr>())),
            terminal_value(::boost::yap::then(std::declval<Expr>())),
            terminal_value(::boost::yap::else_(std::declval<Expr>()))))>>
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return (*transforms[hana::llong<I>{}])(
                expr_tag<remove_cv_ref_t<Expr>::kind>{},
                terminal_value(::boost::yap::cond(static_cast<Expr &&>(expr))),
                terminal_value(::boost::yap::then(static_cast<Expr &&>(expr))),
                terminal_value(
                    ::boost::yap::else_(static_cast<Expr &&>(expr))));
        }
    };

    template<typename Expr, typename Transform>
    struct transform_call_unpacker
    {
        template<long long... I>
        constexpr auto operator()(
            Expr && expr,
            Transform & transform,
            std::integer_sequence<long long, I...>) const
            -> decltype(transform(
                expr_tag<expr_kind::call>{},
                terminal_value(::boost::yap::get(
                    static_cast<Expr &&>(expr), hana::llong_c<I>))...))
        {
            return transform(
                expr_tag<expr_kind::call>{},
                terminal_value(::boost::yap::get(
                    static_cast<Expr &&>(expr), hana::llong_c<I>))...);
        }
    };

    template<typename Expr>
    constexpr auto indices_for(Expr const & expr)
    {
        constexpr long long size = decltype(hana::size(expr.elements))::value;
        return std::make_integer_sequence<long long, size>();
    }

    template<bool Strict, typename Expr, typename TransformTuple, int I>
    struct transform_expression_tag<
        Strict,
        Expr,
        TransformTuple,
        I,
        expr_arity::n,
        void_t<decltype(
            transform_call_unpacker<
                Expr,
                decltype(*std::declval<TransformTuple>()[hana::llong<I>{}])>{}(
                std::declval<Expr>(),
                *std::declval<TransformTuple>()[hana::llong<I>{}],
                indices_for(std::declval<Expr>())))>>
    {
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            using transform_t = decltype(*transforms[hana::llong<I>{}]);
            return transform_call_unpacker<Expr, transform_t>{}(
                static_cast<Expr &&>(expr),
                *transforms[hana::llong<I>{}],
                indices_for(expr));
        }
    };

    template<bool Strict, int I, bool IsExprRef>
    struct transform_impl
    {
        template<typename Expr, typename TransformTuple>
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            constexpr expr_kind kind = detail::remove_cv_ref_t<Expr>::kind;
            return detail::transform_expression_tag<
                Strict,
                Expr,
                TransformTuple,
                I,
                detail::arity_of<kind>()>{}(
                static_cast<Expr &&>(expr), transforms);
        }
    };

    template<bool Strict, int I>
    struct transform_impl<Strict, I, true>
    {
        template<typename Expr, typename TransformTuple>
        constexpr decltype(auto) operator()(Expr && expr, TransformTuple transforms) const
        {
            return detail::transform_impl<Strict, I, false>{}(
                ::boost::yap::deref(static_cast<Expr &&>(expr)), transforms);
        }
    };

}}}

#endif
