#pragma once

#include "additional_data.hpp"
#include "elog_entry.hpp"

#include <phosphor-logging/log.hpp>
#include <sdeventplus/event.hpp>
#include <sdeventplus/source/event.hpp>

#include <queue>
#include <tuple>

namespace openpower::pels
{

/**
 * @class EventLogger
 *
 * This class handles creating OpenBMC event logs (and thus PELs) from
 * within the PEL extension code.
 *
 * The function to actually create the event log is passed in via the
 * constructor so that different functions can be used when testing.
 *
 * To create the event log, call log() with the appropriate arguments
 * and the log will be created as soon as the flow gets back to the event
 * loop.  If the queue isn't empty after a log is created, the next
 * one will be scheduled to be created from the event loop again.
 *
 * This class does not allow new events to be added while inside the
 * creation function, because if the code added an event log every time
 * it tried to create one, it would do so infinitely.
 */
class EventLogger
{
  public:
    using ADMap = std::map<std::string, std::string>;
    using LogFunction = std::function<void(
        const std::string&, phosphor::logging::Entry::Level, const ADMap&)>;

    static constexpr size_t msgPos = 0;
    static constexpr size_t levelPos = 1;
    static constexpr size_t adPos = 2;
    using EventEntry = std::tuple<std::string, phosphor::logging::Entry::Level,
                                  AdditionalData>;

    EventLogger() = delete;
    ~EventLogger() = default;
    EventLogger(const EventLogger&) = delete;
    EventLogger& operator=(const EventLogger&) = delete;
    EventLogger(EventLogger&&) = delete;
    EventLogger& operator=(EventLogger&&) = delete;

    /**
     * @brief Constructor
     *
     * @param[in] creator - The function to use to create the event log
     */
    explicit EventLogger(LogFunction creator) :
        _event(sdeventplus::Event::get_default()), _creator(creator)
    {}

    /**
     * @brief Adds an event to the queue so that it will be created
     *        as soon as the code makes it back to the event loop.
     *
     * Won't add it to the queue if already inside the create()
     * callback.
     *
     * @param[in] message - The message property of the event log
     * @param[in] severity - The severity level of the event log
     * @param[in] ad - The additional data property of the event log
     */
    void log(const std::string& message,
             phosphor::logging::Entry::Level severity, const AdditionalData& ad)
    {
        if (!_inEventCreation)
        {
            _eventsToCreate.emplace(message, severity, ad);

            if (!_eventSource)
            {
                scheduleCreate();
            }
        }
        else
        {
            phosphor::logging::log<phosphor::logging::level::INFO>(
                "Already in event create callback, skipping new create",
                phosphor::logging::entry("ERROR_NAME=%s", message.c_str()));
        }
    }

    /**
     * @brief Returns the event log queue size.
     *
     * @return size_t - The queue size
     */
    size_t queueSize() const
    {
        return _eventsToCreate.size();
    }

    /**
     * @brief Schedules the create() function to run using the
     *        'defer' sd_event source.
     */
    void scheduleCreate()
    {
        _eventSource = std::make_unique<sdeventplus::source::Defer>(
            _event, std::bind(std::mem_fn(&EventLogger::create), this,
                              std::placeholders::_1));
    }

  private:
    /**
     * @brief Creates an event log and schedules the next one if
     *        there is one.
     *
     * This gets called from the event loop by the sd_event code.
     *
     * @param[in] source - The event source object used
     */
    void create(sdeventplus::source::EventBase& /*source*/)
    {
        _eventSource.reset();

        if (_eventsToCreate.empty())
        {
            return;
        }

        auto event = _eventsToCreate.front();
        _eventsToCreate.pop();

        _inEventCreation = true;

        try
        {
            _creator(std::get<msgPos>(event), std::get<levelPos>(event),
                     std::get<adPos>(event).getData());
        }
        catch (const std::exception& e)
        {
            phosphor::logging::log<phosphor::logging::level::ERR>(
                "EventLogger's create function threw an exception",
                phosphor::logging::entry("ERROR=%s", e.what()));
        }

        _inEventCreation = false;

        if (!_eventsToCreate.empty())
        {
            scheduleCreate();
        }
    }

    /**
     * @brief The sd_event object.
     */
    sdeventplus::Event _event;

    /**
     * @brief The user supplied function to create the event log.
     */
    LogFunction _creator;

    /**
     * @brief Keeps track of if an event is currently being created.
     *
     * Guards against creating new events while creating events.
     */
    bool _inEventCreation = false;

    /**
     * @brief The event source object used for scheduling.
     */
    std::unique_ptr<sdeventplus::source::Defer> _eventSource;

    /**
     * @brief The queue of event logs to create.
     */
    std::queue<EventEntry> _eventsToCreate;
};

} // namespace openpower::pels