
{% import "partials/serialize.jinja2" as ser %}
{% import "partials/properties.jinja2" as pr %}
{% import "partials/args.jinja2" as ar %}
#include <vector>
#include <iostream>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/functional/hash.hpp>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <rapidjson/error/en.h>
#include <rapidjson/document.h>

#include "server.hpp"
#include "{{stinger.cpp.enum_header_file}}"
#include "ibrokerconnection.hpp"


constexpr const char {{stinger.cpp.server_class_name}}::NAME[];
constexpr const char {{stinger.cpp.server_class_name}}::INTERFACE_VERSION[];

{{stinger.cpp.server_class_name}}::{{stinger.cpp.server_class_name}}(std::shared_ptr<IBrokerConnection> broker) : _broker(broker) {
    _broker->AddMessageCallback([this](
            const std::string& topic, 
            const std::string& payload, 
            const MqttProperties& mqttProps)
    {
        _receiveMessage(topic, payload, mqttProps);
    });
    {%for method in stinger.methods.values()%}
    _{{method.name | camelCase}}MethodSubscriptionId = _broker->Subscribe("{{method.topic}}", 2);
    {%-endfor%}
    {%for prop_name, prop in stinger.properties.items()%}
    _{{prop_name | lowerCamelCase}}PropertySubscriptionId = _broker->Subscribe("{{prop.update_topic}}", 1);
    {%-endfor%}
}

void {{stinger.cpp.server_class_name}}::_receiveMessage(
        const std::string& topic, 
        const std::string& payload, 
        const MqttProperties& mqttProps)
{
    int subscriptionId = mqttProps.subscriptionId.value_or(-1);
    {%for method_name, method in stinger.methods.items()%}
    {%if not loop.first%}else {%endif%}if ((subscriptionId == _{{method_name | camelCase}}MethodSubscriptionId) || _broker->TopicMatchesSubscription(topic, "{{method.topic}}"))
    {
        std::cout << "Message matched topic {{method.topic}}\n";
        rapidjson::Document doc;
        try {
            if (_{{method_name | camelCase}}Handler)
            {
                rapidjson::ParseResult ok = doc.Parse(payload.c_str());
                if (!ok)
                {
                    //Log("Could not JSON parse {{sig_name}} signal payload.");
                    throw std::runtime_error(rapidjson::GetParseError_En(ok.Code()));
                }

                if (!doc.IsObject()) {
                    throw std::runtime_error("Received payload is not an object");
                }

                _call{{method_name | UpperCamelCase}}Handler(topic, doc, mqttProps.correlationId, mqttProps.responseTopic);
            }
        }
        catch (const boost::bad_lexical_cast&)
        {
            // We couldn't find an integer out of the string in the topic name,
            // so we are dropping the message completely. 
            // TODO: Log this failure
        }
    }
    {%endfor%}
    {%for prop_name, prop in stinger.properties.items()%}
    {%if not loop.first%}else {%endif%}if (subscriptionId == _{{prop_name | lowerCamelCase}}PropertySubscriptionId || _broker->TopicMatchesSubscription(topic, "{{prop.update_topic}}"))
    {
        std::cout << "Message matched topic {{prop.update_topic}}\n";
        _receive{{prop_name | UpperCamelCase}}PropertyUpdate(topic, payload, mqttProps.propertyVersion);
    }
    {%endfor%}
}

{%for sig_name, sig in stinger.signals.items()%}
boost::future<bool> {{stinger.cpp.server_class_name}}::emit{{sig_name | UpperCamelCase}}Signal({%for arg in sig.arg_list%}{{arg.cpp_type}} {{arg.name}}{%if not loop.last%}, {%endif%}{%endfor%})
{
    rapidjson::Document doc;
    doc.SetObject();
    {%-for arg in sig.arg_list%}
    {{ser.addToValue('doc', arg, 'doc.GetAllocator()') | indent}}
    {%-endfor%}
    rapidjson::StringBuffer buf;
    rapidjson::Writer<rapidjson::StringBuffer> writer(buf);
    doc.Accept(writer);
    MqttProperties mqttProps;
    return _broker->Publish("{{sig.topic}}", buf.GetString(), 1, false, mqttProps);
}
{%endfor%}

{%for method_name, method in stinger.methods.items()%}
void {{stinger.cpp.server_class_name}}::register{{method_name | UpperCamelCase}}Handler(std::function<{{method.return_value_cpp_class}}({%for arg in method.arg_list%}{{arg.cpp_type}}{%if not loop.last%}, {%endif%}{%endfor%})> func)
{
    std::cout << "Registered method to handle {{method.topic}}\n";
    _{{method_name | camelCase}}Handler = func;
}
{%endfor%}

{%for method_name, method in stinger.methods.items()%}
void {{stinger.cpp.server_class_name}}::_call{{method_name | UpperCamelCase}}Handler(
        const std::string& topic, 
        const rapidjson::Document& doc, 
        const boost::optional<std::string> optCorrelationId,
        const boost::optional<std::string> optResponseTopic) const
{
    std::cout << "Handling call to {{method_name}}\n";
    if (_{{method_name | camelCase}}Handler) {
        {%for arg in method.arg_list%}
        {{arg.cpp_temp_type}} temp{{arg.name|UpperCamelCase}};
        { // Scoping
            rapidjson::Value::ConstMemberIterator itr = doc.FindMember("{{arg.name}}");
            if (itr != doc.MemberEnd() && itr->value.Is{{arg.cpp_rapidjson_type}}()) {
                {%if arg.arg_type.name.lower() == 'primitive'%}
                temp{{arg.name|UpperCamelCase}} = itr->value.Get{{arg.cpp_rapidjson_type}}();
                {%elif arg.arg_type.name.lower() == 'enum'%}
                temp{{arg.name|UpperCamelCase}} = static_cast<{{arg.cpp_type}}>(itr->value.Get{{arg.cpp_rapidjson_type}}());
                {%endif%}
            } else {
                throw std::runtime_error("Received payload doesn't have required value/type");
            }
        }
        {%endfor%}

        {%if method.return_value_type is not false%}{{method.return_value_cpp_class}} ret = {%endif%}_{{method_name | camelCase}}Handler({%for arg in method.arg_list%}temp{{arg.name|UpperCamelCase}}{%if not loop.last%}, {%endif%}{%endfor%});

        if (optResponseTopic)
        {
            rapidjson::Document responseJson;
            responseJson.SetObject();
            
            {%macro add_arg_to_json(arg, value) %}
            // add the {{arg.name}} (a/n {{arg.arg_type.name}}) to the json
            rapidjson::Value returnValue{{arg.name|UpperCamelCase}};
            {%-if arg.arg_type.name.lower() == 'primitive' %}
            {%-if arg.type.name.lower() == 'string' %}
            returnValue{{arg.name|UpperCamelCase}}.SetString({{value}}.c_str(), {{value}}.size(), responseJson.GetAllocator());
            {%-else%}
            returnValue{{arg.name|UpperCamelCase}}.Set{{method.return_value.cpp_rapidjson_type | UpperCamelCase}}({{value}});
            {%-endif%} {# value type string/primitive #}
            {%-elif arg.arg_type.name.lower() == 'enum'%}
            returnValue{{arg.name|UpperCamelCase}}.SetInt(static_cast<int>({{value}}));
            {%-endif%} {# value or enum#}
            responseJson.AddMember("{{arg.name}}", returnValue{{arg.name|UpperCamelCase}}, responseJson.GetAllocator());
            {%endmacro%}
            {%-if method.return_value_type == 'struct'%}
            // Return type is a struct of values that need added to json
            {%for member in method.return_value-%}
            {{add_arg_to_json(member, "ret."+member.name)}}
            {%endfor%}
            {%-elif method.return_value_type is not false%}
            // Return type is a single value
            {{add_arg_to_json(method.return_value, "ret")}}
            {%-endif%}
            rapidjson::StringBuffer buf;
            rapidjson::Writer<rapidjson::StringBuffer> writer(buf);
            responseJson.Accept(writer);
            MqttProperties mqttProps;
            mqttProps.correlationId = optCorrelationId;
            mqttProps.returnCode = MethodReturnCode::SUCCESS;
            _broker->Publish(*optResponseTopic, buf.GetString(), 2, false, mqttProps);
        }
    }
}
{%endfor%}

{%for prop_name, prop in stinger.properties.items()%}
{{pr.get_property_definition(stinger.cpp.server_class_name, prop_name, prop)}}

{{pr.callback_registration_definition(stinger.cpp.server_class_name, prop_name, prop)}}

void {{stinger.cpp.server_class_name}}::update{{prop_name | UpperCamelCase}}Property({{ar.methodParams(prop.arg_list)}})
{
    { // Scope lock
    std::lock_guard<std::mutex> lock(_{{prop_name | lowerCamelCase}}PropertyMutex);
        {%if prop.arg_list|length == 1 -%}
        _{{prop_name | lowerCamelCase}}Property = {{prop.arg_list[0].name}};
        {%else-%}
        _{{prop_name | lowerCamelCase}}Property = {{prop_name | UpperCamelCase}}Property{ {%for arg in prop.arg_list %}{{arg.name}}{%if not loop.last%}, {%endif%}{%endfor%} };
        {%endif-%}
        _last{{prop_name | UpperCamelCase}}PropertyVersion++;
    }
    { // Scope lock
        std::lock_guard<std::mutex> lock(_{{prop_name | lowerCamelCase}}PropertyCallbacksMutex);
        for (const auto& cb : _{{prop_name | lowerCamelCase}}PropertyCallbacks)
        {
            cb({{ar.callParams(prop.arg_list)}});
        }
    }
    republish{{prop_name | UpperCamelCase}}Property();
}

void {{stinger.cpp.server_class_name}}::republish{{prop_name | UpperCamelCase}}Property() const
{
    std::lock_guard<std::mutex> lock(_{{prop_name | lowerCamelCase}}PropertyMutex);
    rapidjson::Document doc;
    if (_{{prop_name | lowerCamelCase}}Property)
    {
        doc.SetObject();
        {%if prop.arg_list|length == 1 -%}
        {%if prop.arg_list[0].arg_type.name.lower() == 'primitive' and prop.arg_list[0].type.name.lower() == 'string' -%}
        rapidjson::Value tempStringValue;
        tempStringValue.SetString(_{{prop_name | lowerCamelCase}}Property->c_str(), _{{prop_name | lowerCamelCase}}Property->size(), doc.GetAllocator());
        doc.AddMember("{{prop.arg_list[0].name}}", tempStringValue, doc.GetAllocator());
        {%else%} {# not string primitive #}
        doc.AddMember("{{prop.arg_list[0].name}}", *_{{prop_name | lowerCamelCase}}Property, doc.GetAllocator());
        {%endif%} {# primitive string or other primitive #}
        {%else%} {# complex type #}
        _{{prop_name | lowerCamelCase}}Property->AddToRapidJsonObject(doc, doc.GetAllocator());
        {%endif%}
    }
    else
    {
        doc.SetNull();
    }
    
    rapidjson::StringBuffer buf;
    rapidjson::Writer<rapidjson::StringBuffer> writer(buf);
    doc.Accept(writer);
    MqttProperties mqttProps;
    mqttProps.propertyVersion = _last{{prop_name | UpperCamelCase}}PropertyVersion;
    _broker->Publish("{{prop.value_topic}}", buf.GetString(), 1, false, mqttProps);
}

void {{stinger.cpp.server_class_name}}::_receive{{prop_name | UpperCamelCase}}PropertyUpdate(const std::string& topic, const std::string& payload, boost::optional<int> optPropertyVersion)
{
    rapidjson::Document doc;
    rapidjson::ParseResult ok = doc.Parse(payload.c_str());
    if (!ok)
    {
        //Log("Could not JSON parse {{prop_name}} property update payload.");
        throw std::runtime_error(rapidjson::GetParseError_En(ok.Code()));
    }

    if (!doc.IsObject() && !doc.IsNull()) {
        throw std::runtime_error("Received {{prop_name}} payload is not an object or null");
    }

    // TODO: Check _last{{prop_name | UpperCamelCase}}PropertyVersion against optPropertyVersion and
    // reject the update if it's older than what we have.

    {%if prop.arg_list | length == 1%}
    {% set arg = prop.arg_list[0] %}
    {{arg.cpp_temp_type}} temp{{arg.name|UpperCamelCase}};
    rapidjson::Value::ConstMemberIterator itr = doc.FindMember("{{arg.name}}");
    if (itr != doc.MemberEnd() && itr->value.Is{{arg.cpp_rapidjson_type}}()) {
        {%if arg.arg_type.name.lower() == 'primitive'%}
        temp{{arg.name|UpperCamelCase}} = itr->value.Get{{arg.cpp_rapidjson_type}}();
        {%elif arg.arg_type.name.lower() == 'enum'%}
        temp{{arg.name|UpperCamelCase}} = static_cast<{{arg.cpp_type}}>(itr->value.Get{{arg.cpp_rapidjson_type}}());
        {%endif%}
    } else {
        throw std::runtime_error("Received payload doesn't have required value/type");
    }

    {%else%}
    // Deserialize {{prop.arg_list|length}} values into struct.
    {{pr.prop_value_type(prop)}} tempValue = {{pr.prop_value_type(prop)}}::FromRapidJsonObject(doc);
    {%endif%}

    { // Scope lock
        std::lock_guard<std::mutex> lock(_{{prop_name | lowerCamelCase}}PropertyMutex);
        _{{prop_name | lowerCamelCase}}Property = {%if prop.arg_list|length == 1 %}temp{{arg.name|UpperCamelCase}};{%else%}tempValue;{%endif%}
        _last{{prop_name | UpperCamelCase}}PropertyVersion++;
    }
    republish{{prop_name | UpperCamelCase}}Property();
}
{%endfor%}