{% from "helper_macros.j2" import print_template_info, print_spdx_line, string_to_enum, enum_to_string, enum_to_string_view %}
{{ print_spdx_line('Apache-2.0') }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}

{{ print_template_info('5') }}

#include <optional>
#include <ostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>

#include <nlohmann/json.hpp>

{% if 'type_headers' in info %}
{% for type_header in info.type_headers %}
#include <{{type_header}}>
{% endfor %}
{% endif %}

using nlohmann::json;

{% if not enums %}
// no enums defined for this interface
{% else %}

// enums of {{ info.interface_name }}

{% for enum in enums %}
{% for namespace in info.namespace %}
namespace {{ namespace }} {
{% endfor %}
enum class {{ enum.enum_type}}
{
    {% for e in enum.enum %}
    {{e}},
    {% endfor %}
};

/// \brief Converts the given {{ enum.enum_type }} \p e to human readable string
/// \returns a string representation of the {{ enum.enum_type }}
inline std::string {{ enum.enum_type | snake_case }}_to_string(const {{ enum.enum_type }}& e) {
    switch (e) {
        {% for e in enum.enum %}
        case {{ enum.enum_type }}::{{ e }}: return "{{e}}";
        {% endfor %}
    }

    throw std::out_of_range("No known string conversion for provided enum of type {{ enum.enum_type }}");
}

/// \brief Converts the given {{ enum.enum_type }} \p e to human readable string
/// \returns a string_view representation of the {{ enum.enum_type }}
inline constexpr std::string_view {{ enum.enum_type | snake_case }}_to_string_view(const {{ enum.enum_type }}& e) {
    switch (e) {
        {% for e in enum.enum %}
        case {{ enum.enum_type }}::{{ e }}: return "{{e}}";
        {% endfor %}
    }

    throw std::out_of_range("No known string conversion for provided enum of type {{ enum.enum_type }}");
}

/// \brief Converts the given std::string \p s to {{ enum.enum_type }}
/// \returns a {{ enum.enum_type }} from a string representation
inline {{ enum.enum_type }} string_to_{{ enum.enum_type | snake_case }}(const std::string& s) {
    {% for e in enum.enum %}
    if (s == "{{e}}") {
        return {{ enum.enum_type }}::{{ e }};
    }
    {% endfor %}

    throw std::out_of_range("Provided string " + s + " could not be converted to enum of type {{ enum.enum_type }}");
}

/// \brief Writes the string representation of the given {{ enum.enum_type }} \p {{ enum.enum_type | snake_case }} to the given output stream \p os
/// \returns an output stream with the {{ enum.enum_type }} written to
inline std::ostream& operator<<(std::ostream& os, const types::{{ info.interface_name }}::{{ enum.enum_type }}& {{ enum.enum_type | snake_case }}) {
    os << types::{{info.interface_name}}::{{ enum.enum_type | snake_case }}_to_string({{ enum.enum_type | snake_case }});
    return os;
}

{% for namespace in info.namespace|reverse %}
} // namespace {{namespace}}
{% endfor %}

{% endfor %}
{% endif%}

{% if not types %}
// no types defined for this interface
{% else %}
// types of {{ info.interface_name }}
{% for namespace in info.namespace %}
namespace {{ namespace }} {
{% endfor %}

{% for parsed_type in types %}
{% if parsed_type.properties|length > 0 %}
struct {{ parsed_type.name }} {
{% for property in parsed_type.properties %}
    {# Constraints will be checked by the framework #}
    {{ 'std::optional<' if not property.required -}}
    {{ property.type -}}
    {{ '>' if not property.required -}}
    {{ ' ' + property.name + ';' }} ///< {{ property.info.description }}
{% endfor %}

    /// \brief Conversion from a given {{ parsed_type.name }} \p k to a given json object \p j
    friend void to_json(json& j, const {{ parsed_type.name }}& k) {
        // the required parts of the type
        {% if parsed_type.properties|selectattr('required')|list|length %}
        j = json{
        {%- endif %}
        {%- for property in parsed_type.properties %}
        {%- if property.required +%}
            {"{{property.name}}",
            {%- if property.enum %} {{ enum_to_string_view(property.type) }}(k.{{ property.name }})
            {%- else %}
                {%- if property.type == 'DateTime' %} k.{{property.name}}.to_rfc3339()
                {%- else %} k.{{property.name}}
                {%- endif %}
            {%- endif %}},
        {%- endif %}
        {%- endfor %}
        {% if not parsed_type.properties|selectattr('required')|list|length %}
        j = json ({});
        {%- else +%}
        };
        {%- endif %}

        // the optional parts of the type
        {% for property in parsed_type.properties %}
        {% if not property.required %}
        if (k.{{property.name}}) {
                    {% if property.type.startswith('std::vector<') %}
                    {%- if parsed_type.properties|selectattr('required')|list|length %}
            j.emplace("{{property.name}}", *k.{{property.name}});
                    {%- else %}
        {#only optional keys in json#}
        {#TODO: add key to json when there are no required keys but multiple optional keys#}
        {# FIXME: this is never generated?#}
            if (j.size() == 0) {
                j = json{{'{{"'+property.name+'", json::array()}};'}}
            } else {
                j.emplace("{{property.name}}", *k.{{property.name}});
            }
        {% endif %}
        {% else %}
        {%- if property.enum %}
        j.emplace("{{property.name}}", {{ enum_to_string_view(property.type) }}(*k.{{ property.name }}));
            {%- else %}
                {%- if property.type == 'DateTime' %}
            j.emplace("{{property.name}}", k.{{property.name}}->to_rfc3339());
                {%- else %}
            j.emplace("{{property.name}}", *k.{{property.name}});
                {%- endif %}
            {%- endif %}
        {% endif %}

        }
        {% endif %}
        {% endfor %}
    }

    /// \brief Conversion from a given json object \p j to a given {{ parsed_type.name }} \p k
    friend void from_json(const json& j, {{ parsed_type.name }}& k) {
        // the required parts of the type
        {% for property in parsed_type.properties %}
        {% if property.required %}
        {% if property.type.startswith('std::vector<') %}
        k.{{property.name}} = std::move(j.at("{{property.name}}").get<{{property.type}}>());
        {% else %}
        k.{{property.name}} =
            {%- if property.enum %} {{ string_to_enum(property.type) }}(j.at("{{property.name}}"))
            {%- else %}
                {%- if property.type == 'DateTime' %} DateTime(std::string(j.at("{{property.name}}")));
                {%- else %} j.at("{{property.name}}")
                {%- endif %}
            {%- endif %};
        {% endif %}
        {% endif %}
        {%- endfor %}

        // the optional parts of the type
        auto it = j.end();
        {% for property in parsed_type.properties %}
            {% if not property.required %}
        it = it = j.find("{{property.name}}");
        if (it != j.end()) {
                {% if property.type.startswith('std::vector<') %}
            k.{{property.name}} = j.at("{{property.name}}").get<{{property.type}}>();
                {% else %}
                    {%- if property.enum %}
            k.{{property.name}} = {{ string_to_enum(property.type) }}(it->get<std::string>());
                    {%- else %}
            k.{{property.name}} = it->get<{{property.type}}>();
                    {% endif %}
                {% endif %}
        }
            {% endif %}
        {% endfor %}
    }

    /// \brief Compares objects of type {{ parsed_type.name }} for equality
    friend constexpr bool operator==(const {{ parsed_type.name }}& k, const {{ parsed_type.name }}& l) {
        {%- for var, tuple in [('k.', 'lhs'), ('l.', 'rhs')] +%}
        const auto& {{ tuple }}_tuple = std::tie(
        {%- for property in parsed_type.properties +%}
            {{ var }}{{ property.name }}
            {%- if not loop.last %},
            {%- endif %}
        {%- endfor +%}
        );
        {%- endfor +%}
        return lhs_tuple == rhs_tuple;
    }

    /// \brief Compares objects of type {{ parsed_type.name }} for inequality
    friend constexpr bool operator!=(const {{ parsed_type.name }}& k, const {{ parsed_type.name }}& l) {
        return not operator==(k, l);
    }

    /// \brief Writes the string representation of the given {{ parsed_type.name }} \p k to the given output stream \p os
    /// \returns an output stream with the {{ parsed_type.name }} written to
    friend std::ostream& operator<<(std::ostream& os, const {{ parsed_type.name }}& k) {
        os << json(k).dump(4);
        return os;
    }

};
{% endif %}
{% endfor %}
{% for namespace in info.namespace|reverse %}
} // namespace {{namespace}}
{% endfor %}
{% endif %}


#endif // {{ info.hpp_guard }}
