// 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_PRINT_HPP_INCLUDED
#define BOOST_YAP_PRINT_HPP_INCLUDED

#include <boost/yap/algorithm_fwd.hpp>

#include <boost/hana/for_each.hpp>
#include <boost/type_index.hpp>
#include <iostream>


namespace boost { namespace yap {

    /** Returns the <code>char const *</code> string for the spelling of the
        C++ operator associated with \a kind.  It returns the special values
        "ref" and "term" for the non-operator kinds
        <code>expr_kind::expr_ref</code> amd <code>expr_kind::terminal</code>,
        respectively.*/
    inline constexpr char const * op_string(expr_kind kind)
    {
        switch (kind) {
        case expr_kind::expr_ref: return "ref";

        case expr_kind::terminal: return "term";

        case expr_kind::unary_plus: return "+";
        case expr_kind::negate: return "-";
        case expr_kind::dereference: return "*";
        case expr_kind::complement: return "~";
        case expr_kind::address_of: return "&";
        case expr_kind::logical_not: return "!";
        case expr_kind::pre_inc: return "++";
        case expr_kind::pre_dec: return "--";
        case expr_kind::post_inc: return "++(int)";
        case expr_kind::post_dec: return "--(int)";

        case expr_kind::shift_left: return "<<";
        case expr_kind::shift_right: return ">>";
        case expr_kind::multiplies: return "*";
        case expr_kind::divides: return "/";
        case expr_kind::modulus: return "%";
        case expr_kind::plus: return "+";
        case expr_kind::minus: return "-";
        case expr_kind::less: return "<";
        case expr_kind::greater: return ">";
        case expr_kind::less_equal: return "<=";
        case expr_kind::greater_equal: return ">=";
        case expr_kind::equal_to: return "==";
        case expr_kind::not_equal_to: return "!=";
        case expr_kind::logical_or: return "||";
        case expr_kind::logical_and: return "&&";
        case expr_kind::bitwise_and: return "&";
        case expr_kind::bitwise_or: return "|";
        case expr_kind::bitwise_xor: return "^";
        case expr_kind::comma: return ",";
        case expr_kind::mem_ptr: return "->*";
        case expr_kind::assign: return "=";
        case expr_kind::shift_left_assign: return "<<=";
        case expr_kind::shift_right_assign: return ">>=";
        case expr_kind::multiplies_assign: return "*=";
        case expr_kind::divides_assign: return "/=";
        case expr_kind::modulus_assign: return "%=";
        case expr_kind::plus_assign: return "+=";
        case expr_kind::minus_assign: return "-=";
        case expr_kind::bitwise_and_assign: return "&=";
        case expr_kind::bitwise_or_assign: return "|=";
        case expr_kind::bitwise_xor_assign: return "^=";
        case expr_kind::subscript: return "[]";

        case expr_kind::if_else: return "?:";

        case expr_kind::call: return "()";

        default: return "** ERROR: UNKNOWN OPERATOR! **";
        }
    }

    namespace detail {

        inline std::ostream & print_kind(std::ostream & os, expr_kind kind)
        {
            return os << op_string(kind);
        }

        template<typename T, typename = void_t<>>
        struct printer
        {
            std::ostream & operator()(std::ostream & os, T const &)
            {
                return os << "<<unprintable-value>>";
            }
        };

        template<typename T>
        struct printer<
            T,
            void_t<decltype(
                std::declval<std::ostream &>() << std::declval<T const &>())>>
        {
            std::ostream & operator()(std::ostream & os, T const & x)
            {
                return os << x;
            }
        };

        template<typename T>
        inline std::ostream & print_value(std::ostream & os, T const & x)
        {
            return printer<T>{}(os, x);
        }

        template<long long I>
        inline std::ostream & print_value(std::ostream & os, hana::llong<I>)
        {
            return os << I << "_p";
        }

        template<typename T>
        std::ostream & print_type(std::ostream & os, hana::tuple<T> const &)
        {
            os << typeindex::type_id<T>().pretty_name();
            if (std::is_const<T>::value)
                os << " const";
            if (std::is_volatile<T>::value)
                os << " volatile";
            if (std::is_lvalue_reference<T>::value)
                os << " &";
            if (std::is_rvalue_reference<T>::value)
                os << " &&";
            return os;
        }

        template<typename T>
        bool is_const_expr_ref(T const &)
        {
            return false;
        }
        template<typename T, template<expr_kind, class> class expr_template>
        bool is_const_expr_ref(
            expr_template<expr_kind::expr_ref, hana::tuple<T const *>> const &)
        {
            return true;
        }

#ifdef BOOST_NO_CONSTEXPR_IF

        template<expr_kind Kind>
        struct print_impl
        {
            template<typename Expr>
            std::ostream & operator()(
                std::ostream & os,
                Expr const & expr,
                int indent,
                char const * indent_str,
                bool is_ref = false,
                bool is_const_ref = false)
            {
                for (int i = 0; i < indent; ++i) {
                    os << indent_str;
                }

                os << "expr<";
                ::boost::yap::detail::print_kind(os, Expr::kind);
                os << ">";
                if (is_const_ref)
                    os << " const &";
                else if (is_ref)
                    os << " &";
                os << "\n";
                hana::for_each(
                    expr.elements,
                    [&os, indent, indent_str](auto const & element) {
                        using element_type = decltype(element);
                        constexpr expr_kind kind =
                            detail::remove_cv_ref_t<element_type>::kind;
                        print_impl<kind>{}(os, element, indent + 1, indent_str);
                    });

                return os;
            }
        };

        template<>
        struct print_impl<expr_kind::expr_ref>
        {
            template<typename Expr>
            std::ostream & operator()(
                std::ostream & os,
                Expr const & expr,
                int indent,
                char const * indent_str,
                bool is_ref = false,
                bool is_const_ref = false)
            {
                using ref_type = decltype(::boost::yap::deref(expr));
                constexpr expr_kind ref_kind =
                    detail::remove_cv_ref_t<ref_type>::kind;
                print_impl<ref_kind>{}(
                    os,
                    ::boost::yap::deref(expr),
                    indent,
                    indent_str,
                    true,
                    ::boost::yap::detail::is_const_expr_ref(expr));
                return os;
            }
        };

        template<>
        struct print_impl<expr_kind::terminal>
        {
            template<typename Expr>
            std::ostream & operator()(
                std::ostream & os,
                Expr const & expr,
                int indent,
                char const * indent_str,
                bool is_ref = false,
                bool is_const_ref = false)
            {
                for (int i = 0; i < indent; ++i) {
                    os << indent_str;
                }

                os << "term<";
                ::boost::yap::detail::print_type(os, expr.elements);
                os << ">[=";
                ::boost::yap::detail::print_value(
                    os, ::boost::yap::value(expr));
                os << "]";
                if (is_const_ref)
                    os << " const &";
                else if (is_ref)
                    os << " &";
                os << "\n";

                return os;
            }
        };

#else

        template<typename Expr>
        std::ostream & print_impl(
            std::ostream & os,
            Expr const & expr,
            int indent,
            char const * indent_str,
            bool is_ref = false,
            bool is_const_ref = false)
        {
            if constexpr (Expr::kind == expr_kind::expr_ref) {
                print_impl(
                    os,
                    ::boost::yap::deref(expr),
                    indent,
                    indent_str,
                    true,
                    ::boost::yap::detail::is_const_expr_ref(expr));
            } else {
                for (int i = 0; i < indent; ++i) {
                    os << indent_str;
                }

                if constexpr (Expr::kind == expr_kind::terminal) {
                    os << "term<";
                    ::boost::yap::detail::print_type(os, expr.elements);
                    os << ">[=";
                    ::boost::yap::detail::print_value(
                        os, ::boost::yap::value(expr));
                    os << "]";
                    if (is_const_ref)
                        os << " const &";
                    else if (is_ref)
                        os << " &";
                    os << "\n";
                } else {
                    os << "expr<";
                    ::boost::yap::detail::print_kind(os, Expr::kind);
                    os << ">";
                    if (is_const_ref)
                        os << " const &";
                    else if (is_ref)
                        os << " &";
                    os << "\n";
                    hana::for_each(
                        expr.elements,
                        [&os, indent, indent_str](auto const & element) {
                            ::boost::yap::detail::print_impl(
                                os, element, indent + 1, indent_str);
                        });
                }
            }

            return os;
        }

#endif // BOOST_NO_CONSTEXPR_IF
    }

    /** Prints expression \a expr to stream \a os.  Returns \a os. */
    template<typename Expr>
    std::ostream & print(std::ostream & os, Expr const & expr)
    {
#ifdef BOOST_NO_CONSTEXPR_IF
        return detail::print_impl<detail::remove_cv_ref_t<Expr>::kind>{}(
            os, expr, 0, "    ");
#else
        return detail::print_impl(os, expr, 0, "    ");
#endif
    }

}}

#endif
