// named_write.hpp

// Boost Logging library
//
// Author: John Torjo, www.torjo.com
//
// Copyright (C) 2007 John Torjo (see www.torjo.com for email)
//
// 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)
//
// See http://www.boost.org for updates, documentation, and revision history.
// See http://www.torjo.com/log2/ for more details

#pragma once

#include <pika/config.hpp>
#include <pika/logging/format/destinations.hpp>
#include <pika/logging/format/formatters.hpp>

#include <cstddef>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

namespace pika::util::logging::detail {

    template <typename T>
    struct named
    {
        std::string name;
        T value;
    };

    template <typename C, typename S>
    typename C::iterator find_named(C& c, S const& name)
    {
        for (auto iter = c.begin(), end = c.end(); iter != end; ++iter)
        {
            if (iter->name == name) return iter;
        }
        return c.end();
    }

    /// @brief Allows you to contain multiple formatters,
    /// and specify a %spacer between them. You have a %spacer string, and within it,
    /// you can escape your contained formatters.
    ///
    /// @code
    /// #include <pika/logging/format/formatter/named.hpp>
    /// @endcode
    ///
    /// This allows you:
    /// - to hold multiple formatters
    /// - each formatter is given a name, when being added
    /// - you have a %spacer string, which contains what is to be prepended or
    /// appended to the string (by default, prepended)
    /// - a formatter is escaped with @c '\%' chars, like this @c "%name%"
    /// - if you want to write the @c '\%', just double it,
    /// like this: <tt>"this %% gets written"</tt>
    ///
    /// Example:
    ///
    /// @code
    /// #define L_ PIKA_LOG_USE_LOG_IF_FILTER(g_l(), g_log_filter()->is_enabled() )
    ///
    /// g_l()->writer().add_formatter( formatter::named("[%index%] %time% (T%thread%) ")
    ///         .add( "index", formatter::idx())
    ///         .add( "thread", formatter::thread_id())
    ///         .add( "time", formatter::time("$mm")) );
    /// @endcode
    ///
    /// Assuming you'd use the above in code
    /// @code
    /// int i = 1;
    /// L_ << "this is so cool " << i++;
    /// L_ << "this is so cool again " << i++;
    /// @endcode
    ///
    /// You could have an output like this:
    ///
    /// @code
    /// [1] 53 (T3536) this is so cool 1
    /// [2] 54 (T3536) this is so cool again 2
    /// @endcode

    struct named_formatters
    {
        PIKA_NON_COPYABLE(named_formatters);

        using ptr_type = std::unique_ptr<formatter::manipulator>;

        named_formatters() = default;

        named_formatters& string(std::string const& str)
        {
            format_string = str;
            compute_write_steps();
            return *this;
        }

        void add(std::string const& name, ptr_type p)
        {
            auto iter = find_named(formatters, name);
            if (iter != formatters.end())
                iter->value = PIKA_MOVE(p);
            else
                formatters.push_back(named<ptr_type>{name, PIKA_MOVE(p)});
            compute_write_steps();
        }

        // NOLINTBEGIN(bugprone-easily-swappable-parameters)
        void configure(std::string const& name, std::string const& configure_str)
        // NOLINTEND(bugprone-easily-swappable-parameters)
        {
            auto iter = find_named(formatters, name);
            if (iter != formatters.end()) iter->value->configure(configure_str);
        }

        void operator()(std::stringstream& out, message const& msg) const
        {
            for (auto const& step : write_steps)
            {
                out << step.prefix;
                if (step.fmt)
                {
                    if (step.fmt == (formatter::manipulator*) -1)
                        out << msg;
                    else
                        (*step.fmt)(out);
                }
            }
        }

    private:
        // recomputes the write steps - note that this takes place after
        // each operation for instance, the user might have first set the
        // string and later added the formatters
        PIKA_EXPORT void compute_write_steps();

    private:
        struct write_step
        {
            write_step(std::string const& prefix_, formatter::manipulator* fmt_)
              : prefix(prefix_)
              , fmt(fmt_)
            {
            }
            std::string prefix;
            // could be null - in case formatter not found by name, or it's
            // the last step
            formatter::manipulator* fmt;
        };

        std::vector<named<ptr_type>> formatters;
        std::vector<write_step> write_steps;
        std::string format_string;
    };

    /// @brief Allows you to contain multiple destinations,
    /// give each such destination a name.
    /// Then, at run-time, you can specify a format string which will specify which
    /// destinations to be called, and on what order.
    ///
    /// This allows you:
    /// - to hold multiple destinations
    /// - each destination is given a name, when being added.
    /// The name <b>must not</b> contain spaces and must not start with '+'/'-' signs
    /// - you have a %format string, which contains what destinations to be called,
    /// and on which order
    ///
    /// The %format string contains destination names, separated by space.
    ///
    /// When a message is written to this destination,
    /// I parse the format string. When a name is encountered, if there's a destination
    /// corresponding to this name, I will call it.
    ///
    /// Example:
    ///
    /// @code
    /// g_l()->writer().add_destination(
    ///     destination::named_destinations("cout out debug")
    ///         .add( "cout", destination::cout())
    ///         .add( "debug", destination::dbg_window() )
    ///         .add( "out", destination::file("out.txt"))
    ///      );
    /// @endcode
    ///
    /// In the above code, we'll write to 3 destinations, in the following order:
    /// - first, to the console
    /// - second, to the out.txt file
    /// - third, to the debug window
    ///
    ///
    ///
    /// @section If you deal with config files
    ///
    /// As an extra feature:
    /// - if a name starts with '-' is ignored
    /// - if a name starts with '+', is included.
    ///
    /// This is useful if you want to set this format string in a config file.
    /// The good thing is that this way you can easily turn on/off
    /// certain destinations, while seeing all the available destinations as well.
    ///
    /// Example: \n <tt>+out_file -debug_window +console</tt> \n
    /// In the above example, I know that the available destinations are @c out_file,
    /// @c debug_window and @c console, but I'm not writing to @c debug_window.

    struct named_destinations
    {
        PIKA_NON_COPYABLE(named_destinations);

        using ptr_type = std::unique_ptr<destination::manipulator>;

        named_destinations() = default;

        named_destinations& string(std::string const& str)
        {
            format_string = str;
            compute_write_steps();
            return *this;
        }

        void add(std::string const& name, ptr_type p)
        {
            auto iter = find_named(destinations, name);
            if (iter != destinations.end())
                iter->value = PIKA_MOVE(p);
            else
                destinations.push_back(named<ptr_type>{name, PIKA_MOVE(p)});
            compute_write_steps();
        }

        // NOLINTBEGIN(bugprone-easily-swappable-parameters)
        void configure(std::string const& name, std::string const& configure_str)
        // NOLINTEND(bugprone-easily-swappable-parameters)
        {
            auto iter = find_named(destinations, name);
            if (iter != destinations.end()) iter->value->configure(configure_str);
        }

        void operator()(const message& msg) const
        {
            for (auto const& step : write_steps) (*step)(msg);
        }

    private:
        // recomputes the write steps - note that this takes place after
        // each operation for instance, the user might have first set the
        // string and later added the formatters
        PIKA_EXPORT void compute_write_steps();

    private:
        std::vector<named<ptr_type>> destinations;
        std::vector<destination::manipulator*> write_steps;
        std::string format_string;
    };

}    // namespace pika::util::logging::detail

namespace pika::util::logging::writer {

    /// @brief Composed of a named formatter and a named destinations.
    /// Thus, you can specify the formatting and destinations as strings
    ///
    /// @code
    /// #include <pika/logging/format/named_write.hpp>
    /// @endcode
    ///
    ///
    /// Contains a very easy interface for using @ref manipulator "formatters and destinations":
    /// - at construction, specify 2 params: the %formatter string and the destinations string
    ///
    /// Setting the @ref manipulator "formatters and destinations" to
    /// write to is extremely simple:
    ///
    /// @code
    /// // Set the formatters (first param) and destinatins (second step) in one step
    /// g_l()->writer().write("%time%($hh:$mm.$ss.$mili) [%idx%] |\n",
    /// "cout file(out.txt) debug");
    ///
    /// // set the formatter(s)
    /// g_l()->writer().format("%time%($hh:$mm.$ss.$mili) [%idx%] |\n");
    ///
    /// // set the destination(s)
    /// g_l()->writer().destination("cout file(out.txt) debug");
    /// @endcode
    ///
    ///
    /// @section format_string_syntax The syntax of the format string
    ///
    /// - The format string specifies how the message is to be logged
    /// - Every formatter is escaped using <tt>%</tt><em>fmt</em><tt>%</tt>
    ///   - Available formatters:
    ///     - <tt>"%idx%"</tt> - writes the index of the message (formatter::idx)
    ///     - <tt>"%time%"</tt> - writes the time (formatter::high_precision_time)
    ///     - <tt>"%thread_id%"</tt> - writes the thread id (formatter::thread_id)
    ///     - if you want to write @c "%", double it, like this: @c "%%"
    /// - @c "|" is used to specify the original message. What is before it,
    /// is prepended to the message, what is after, is appended to the message
    /// - If a formatter is configurable, append @em (params) to it
    ///   - For now, only @c "%time%" is configurable. For instance,
    ///   @c "%time%($hh:$mm.$ss.$mili)" writes time like @c "21:14.24.674"
    ///
    /// Example:
    /// @code
    /// "%time%($hh:$mm.$ss.$mili) [%idx%] |\n"
    /// @endcode
    ///
    /// The output can look like:
    ///
    /// @code
    /// 21:03.17.243 [1] this is so cool
    /// 21:03.17.243 [2] first error
    /// 21:03.17.243 [3] hello, world
    /// @endcode
    ///
    ///
    /// @section dest_string_syntax The syntax of the destinations string
    ///
    /// - The syntax of the destination string specifies where the message is to be logged
    ///   - Every destination is specified by name
    ///   - Separate destinations by space (' ')
    /// - Available destinations
    ///   - <tt>"cout"</tt> - writes to std::cout (destination::cout)
    ///   - <tt>"cerr"</tt> - writes to std::cerr (destination::cerr)
    ///   - <tt>"debug"</tt> - writes to the debug window: OutputDebugString in Windows,
    ///   console on Linux (destination::dbg_window)
    ///   - <tt>"file"</tt> - writes to a file (destination::file)
    /// - If a destination is configurable, append @em (params) to it
    ///   - Right now, @c "file" is configurable
    ///     - Append <tt>(</tt><em>filename</em><tt>)</tt> to them to specify the file name.
    ///     Example: @c "file(out.txt)" will write to the out.txt file
    ///
    /// Examples:
    /// - <tt>"file(out.txt) cout"</tt> - will write to a file called out.txt and to cout
    /// - <tt>"cout debug"</tt> - will write to cout and debug window (see above)
    ///
    /// @note
    /// If you want to output to 2 files, don't use "file(one.txt) file(two.txt)".
    /// This will just configure "file" twice, ending up with writing only to "two.txt" file.
    ///
    /// @param format_write_ the underlying format writer

    struct named_write
    {
        PIKA_EXPORT named_write();

        /// @brief sets the format string: what should be before,
        /// and what after the original message, separated by "|"

        /// Example: \n
        /// "[%idx%] |\n" - this writes "[%idx%] " before the message,
        /// and "\n" after the message

        /// If "|" is not present, the whole message is prepended to the message
        void format(std::string const& format_str)
        {
            m_format_str = format_str;
            configure_formatter(format_str);
        };

        /// @brief sets the destinations string - where should logged messages
        /// be outputted
        void destination(std::string const& destination_str)
        {
            m_destination_str = destination_str;
            configure_destination(destination_str);
        }

        /// @brief Specifies the formats and destinations in one step
        // NOLINTBEGIN(bugprone-easily-swappable-parameters)
        void write(std::string const& format_str, std::string const& destination_str)
        // NOLINTEND(bugprone-easily-swappable-parameters)
        {
            format(format_str);
            destination(destination_str);
        }

        void operator()(message const& msg) const
        {
            std::stringstream out;
            m_format(out, msg);

#if defined(PIKA_COMPUTE_HOST_CODE)
            message formatted(PIKA_MOVE(out));
            m_destination(formatted);
#endif
        }

        /// @brief Replaces a formatter from the named formatter.
        ///
        /// You can use this, for instance, when you want to share
        /// a formatter between multiple named writers.
        template <typename Formatter>
        void set_formatter(std::string const& name, Formatter fmt)
        {
            m_format.add(name, detail::named_formatters::ptr_type(new Formatter(fmt)));
        }

        template <typename Formatter, typename... Args>
        void set_formatter(std::string const& name, Args&&... args)
        {
            m_format.add(name, Formatter::make(PIKA_FORWARD(Args, args)...));
        }

        /// @brief Replaces a destination from the named destination.
        ///
        /// You can use this, for instance, when you want to share a
        /// destination between multiple named writers.
        template <typename Destination>
        void set_destination(std::string const& name, Destination dest)
        {
            m_destination.add(name, detail::named_destinations::ptr_type(new Destination(dest)));
        }

        template <typename Destination, typename... Args>
        void set_destination(std::string const& name, Args&&... args)
        {
            m_destination.add(name, Destination::make(PIKA_FORWARD(Args, args)...));
        }

    private:
        PIKA_EXPORT void configure_formatter(std::string const& format);
        PIKA_EXPORT void configure_destination(std::string const& format);

    private:
        detail::named_formatters m_format;
        detail::named_destinations m_destination;

        std::string m_format_str;
        std::string m_destination_str;
    };

}    // namespace pika::util::logging::writer
