#pragma once

#include <rapidjson/document.h>
#include <unordered_set>
#include <string>
#include <chrono>


namespace gelfcpp
{

namespace detail
{
struct DocumentAccessor;
}

/**
 * \brief A single GELF message, with write-only access to its fields.
 *
 * \note This class only supports GELF version 1.1 and treats deprecated standard fields as normal additional fields.
 *
 * Currently the following standard fields are handled: <code>version, host, short_message, full_message, timestamp, level</code>.
 * Standard fields are included "as-is". All other fields are prefixed with "_".
 */
class GelfMessage
{
    friend struct detail::DocumentAccessor;

    struct FieldSetter
    {
        GelfMessage& owner;
        std::string field;

        template<typename T>
        void operator=(T&& value)
        {
            owner.SetField(field, std::forward<T>(value));
        }
    };

public:
    GelfMessage() :
            doc_(rapidjson::kObjectType)
    {
        SetField("version", "1.1");
    }

    /**
     * \brief Quick accessor for <code>short_message</code> field.
     *
     * Behaves like \code SetField("short_message", message). \endcode
     *
     * \param message message
     */
    void SetMessage(const std::string& message)
    {
        SetField("short_message", message);
    }

    /**
     * \brief Quick accessor for <code>full_message</code> field.
     *
     * Behaves like \code SetField("full_message", message). \endcode
     *
     * \param message message
     */
    void SetFullMessage(const std::string& message)
    {
        SetField("full_message", message);
    }

    /**
     * \brief Quick accessor for <code>host</code> field.
     *
     * Behaves like \code SetField("host", message). \endcode
     *
     * \param host hostname
     */
    void SetHost(const std::string& host)
    {
        SetField("host", host);
    }

    /**
     * \brief Quick accessor for <code>timestamp</code> field.
     *
     * Behaves like \code SetField("timestamp", message). \endcode
     *
     * \param timestamp timestamp
     */
    void SetTimestamp(double timestamp)
    {
        SetField("timestamp", timestamp);
    }

#ifndef GELFCPP_DOXYGEN_RUNNING

    template<typename T>
    auto SetField(const std::string& name, T value) -> std::enable_if_t<std::is_arithmetic<T>::value && !std::is_same<T, bool>::value>
    {
        rapidjson::Value json(std::forward<T>(value));
        SetField(name, std::move(json));
    }

    void SetField(const std::string& name, bool value)
    {
        SetField(name, value ? 1 : 0);
    }

    void SetField(const std::string& name, const char* value)
    {
        rapidjson::Value json(value, doc_.GetAllocator());
        SetField(name, std::move(json));
    }

    void SetField(const std::string& name, const std::string& value)
    {
        rapidjson::Value json(value, doc_.GetAllocator());
        SetField(name, std::move(json));
    }

#else
    /**
     * \brief Adds a field to the message.
     *
     * \note If a field was already assigned the old value is overwritten.
     *
     * \tparam T value type, supported: <code>bool, integral, floating point, strings</code>
     * \param name field name, the "_" prefix is added automatically
     * \param value field value of any supported type
     */
    template<typename T> void SetField(const std::string& name, T value) {}
#endif

    /**
     * \brief Adds a field to the message.
     *
     * This behaves like SetField(const std::string&, T) but allows map/array like syntax.
     *
     * Example usage:
     * \code{.cpp}
     * GelfMessage message;
     * message["field_name"] = field_value;
     * \endcode
     *
     * \param field field name, the "_" prefix is added automatically
     * \return wrapper to allow assignment, see the usage example.
     */
    FieldSetter operator[](const std::string& field)
    {
        return { *this, field };
    }

private:
    void SetField(const std::string& field, rapidjson::Value&& value)
    {
        static const std::unordered_set<std::string> BUILDIN_FIELDS{ "version", "host", "short_message", "full_message", "timestamp", "level" };

        rapidjson::Value key;
        if (BUILDIN_FIELDS.find(field) != BUILDIN_FIELDS.end())
            key.SetString(field.c_str(), static_cast<rapidjson::SizeType>(field.size()), doc_.GetAllocator());
        else
            key.SetString(("_" + field).c_str(), static_cast<rapidjson::SizeType>(field.size() + 1), doc_.GetAllocator());

        auto find = doc_.FindMember(key);
        if (find != doc_.MemberEnd())
            find->value = std::move(value);
        else
            doc_.AddMember(std::move(key), std::move(value), doc_.GetAllocator());
    }

private:
    rapidjson::Document doc_;
};

}
