{% import "partials/deserialize.jinja2" as deser %}
{% import "partials/serialize.jinja2" as ser %}
{% import "partials/args.jinja2" as ar %}
{% import "partials/properties.jinja2" as pr %}

#include <vector>
#include <iostream>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/functional/hash.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <rapidjson/error/en.h>
#include <rapidjson/document.h>

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

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

{{stinger.cpp.client_class_name}}::{{stinger.cpp.client_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 sig_name, sig in stinger.signals.items() %}
    _{{sig_name | lowerCamelCase}}SignalSubscriptionId = _broker->Subscribe("{{sig.topic}}", 2);
    {%-endfor%}
    {%-for method_name, method in stinger.methods.items() %}
    { // Restrict scope
        std::stringstream responseTopicStringStream;
        responseTopicStringStream << boost::format("{{method.response_topic('%1%')}}") % _broker->GetClientId();
        _broker->Subscribe(responseTopicStringStream.str(), 2);
    }
    {%-endfor%}

    {%-for prop_name, prop in stinger.properties.items() %}
    _{{prop_name | lowerCamelCase}}PropertySubscriptionId = _broker->Subscribe("{{prop.value_topic}}", 1);
    {%-endfor%}
}

void {{stinger.cpp.client_class_name}}::_receiveMessage(
        const std::string& topic, 
        const std::string& payload, 
        const MqttProperties& mqttProps)
{
    {%-for sig_name, sig in stinger.signals.items() %}
    if ((mqttProps.subscriptionId && (*mqttProps.subscriptionId == _{{sig_name | lowerCamelCase}}SignalSubscriptionId)) || _broker->TopicMatchesSubscription(topic, "{{sig.topic}}"))
    {
        //Log("Handling {{sig_name}} signal");
        rapidjson::Document doc;
        try {
            if (_{{sig_name | camelCase}}SignalCallbacks.size() > 0)
            {
                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");
                }

                {%for arg in sig.arg_list%}
                {{arg.cpp_temp_type}} temp{{arg.name}};
                { // 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}} = itr->value.Get{{arg.cpp_rapidjson_type}}();
                        {%elif arg.arg_type.name.lower() == 'enum'%}
                        temp{{arg.name}} = static_cast<{{arg.cpp_data_type}}>(itr->value.Get{{arg.cpp_rapidjson_type}}());
                        {%endif%}
                    } else {
                    {%-if arg.optional%}
                        temp{{arg.name}} = boost::none;
                    {%else%}
                        throw std::runtime_error("Received payload doesn't have required value/type");
                    {%endif%}
                    }
                }
                {%endfor%}{# for signal args #}
                std::lock_guard<std::mutex> lock(_{{sig_name | lowerCamelCase}}SignalCallbacksMutex);
                for (const auto& cb : _{{sig_name | lowerCamelCase}}SignalCallbacks)
                {
                    cb({%for arg in sig.arg_list%}temp{{arg.name}}{%if not loop.last%}, {%endif%}{%endfor%});
                }
            }
        }
        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 signals #}

    {%-for method_name, method in stinger.methods.items() %}
    {%if not loop.first%}else {%endif%}if (_broker->TopicMatchesSubscription(topic, "{{method.response_topic('+')}}") && mqttProps.correlationId)
    {
        std::cout << "Matched topic for {{method_name}} response" << std::endl;
        _handle{{method_name|UpperCamelCase}}Response(topic, payload, *mqttProps.correlationId);
    }
    {%-endfor%}{# for methods #}

    {%-for prop_name, prop in stinger.properties.items() %}
    {%if not loop.first%}else {%endif%}if ((mqttProps.subscriptionId && (*mqttProps.subscriptionId == _{{prop_name | lowerCamelCase}}PropertySubscriptionId)) || topic == "{{prop.value_topic}}")
    {
        _receive{{prop_name | UpperCamelCase}}PropertyUpdate(topic, payload, mqttProps.propertyVersion);
    }
    {%-endfor%}{# for properties #}
}


{%-for sig_name, sig in stinger.signals.items() %}
void {{stinger.cpp.client_class_name}}::register{{sig_name | UpperCamelCase}}Callback(const std::function<void({%for arg in sig.arg_list%}{{arg.cpp_type}}{%if not loop.last%}, {%endif%}{%endfor%})>& cb)
{
    std::lock_guard<std::mutex> lock(_{{sig_name | lowerCamelCase}}SignalCallbacksMutex);
    _{{sig_name | lowerCamelCase}}SignalCallbacks.push_back(cb);
}
{%-endfor%}

{%for method_name, method in stinger.methods.items()%}
boost::future<{{method.return_value_cpp_class}}> {{stinger.cpp.client_class_name}}::{{method_name | camelCase}}({%for arg in method.arg_list%}{{arg.cpp_type}} {{arg.name}}{%if not loop.last%}, {%endif%}{%endfor%})
{
    auto correlationId = boost::uuids::random_generator()();
    const std::string correlationIdStr = boost::lexical_cast<std::string>(correlationId);
    _pending{{method_name|UpperCamelCase}}MethodCalls[correlationId] = boost::promise<{{method.return_value_cpp_class}}>();

    rapidjson::Document doc;
    doc.SetObject();

    {%for arg in method.arg_list%}
    {{ser.addToValue('doc', arg, 'doc.GetAllocator()') | indent(4)}}
    {%endfor%}
    rapidjson::StringBuffer buf;
    rapidjson::Writer<rapidjson::StringBuffer> writer(buf);
    doc.Accept(writer);
    std::stringstream responseTopicStringStream;
    responseTopicStringStream << boost::format("{{method.response_topic('%1%')}}") % _broker->GetClientId();
    MqttProperties mqttProps;
    mqttProps.correlationId = correlationIdStr;
    mqttProps.responseTopic = responseTopicStringStream.str();
    mqttProps.returnCode = MethodReturnCode::SUCCESS;
    _broker->Publish("{{method.topic}}", buf.GetString(), 2, false, mqttProps);

    return _pending{{method_name|UpperCamelCase}}MethodCalls[correlationId].get_future();
}

void {{stinger.cpp.client_class_name}}::_handle{{method_name|UpperCamelCase}}Response(
        const std::string& topic, 
        const std::string& payload, 
        const std::string &correlationId) 
{
    std::cout << "In response handler for " << topic << " with correlationId=" << correlationId << std::endl;
    {%if method.return_value_type is not false%}
    rapidjson::Document doc;
    rapidjson::ParseResult ok = doc.Parse(payload.c_str());
    if (!ok)
    {
        //Log("Could not JSON parse {{method_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");
    }
    {%endif%}
    boost::uuids::uuid correlationIdUuid = boost::lexical_cast<boost::uuids::uuid>(correlationId);
    auto promiseItr = _pending{{method_name|UpperCamelCase}}MethodCalls.find(correlationIdUuid);
    if (promiseItr != _pending{{method_name|UpperCamelCase}}MethodCalls.end())
    {
        {%macro unpack_rv(arg)-%}
        rapidjson::Value::ConstMemberIterator {{arg.name}}Itr = doc.FindMember("{{arg.name}}");
        {%if arg.arg_type.name.lower() == 'primitive'-%}
        {{arg.cpp_type}} {{arg.name}} = {{arg.name}}Itr->value.Get{{arg.cpp_rapidjson_type}}();
        {%elif arg.arg_type.name.lower() == 'enum'-%}
        {{arg.cpp_type}} {{arg.name}} = static_cast<{{arg.cpp_type}}>({{arg.name}}Itr->value.GetInt());
        {%endif-%}
        {%endmacro-%}
        {%if method.return_value_type == 'struct'-%}
        // Response has multiple values.
        {%for item in method.return_value%}
        {{unpack_rv(item)}}
        {%-endfor%}
        {{method.return_value_cpp_class}} returnValue { //initializer list
        {%for item in method.return_value%}
            {{item.name}}{%if not loop.last%},{%endif%}
        {%-endfor%}
        };
        promiseItr->second.set_value(returnValue);
        {%-elif method.return_value_type is false %}
        // There are no values in the response.
        promiseItr->second.set_value();
        {%-else%}
        // Response has a single value.
        {{unpack_rv(method.return_value)}}
        promiseItr->second.set_value({{method.return_value_property_name}});
        {%-endif%}
    }

    std::cout << "End of response handler for " << topic << std::endl;
}
{%endfor%}{# end for each method #}
{%for prop_name, prop in stinger.properties.items()%}
void {{stinger.cpp.client_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()) {
        throw std::runtime_error("Received {{prop_name}} payload is not an object");
    }
    {{prop.name | UpperCamelCase}}Property tempValue;
    {{deser.deserialize('tempValue', prop.arg_list, 'doc') | indent(4)}}

    { // Scope lock
        std::lock_guard<std::mutex> lock(_{{prop_name | lowerCamelCase}}PropertyMutex);
        _{{prop_name | lowerCamelCase}}Property = tempValue;
        _last{{prop_name | UpperCamelCase}}PropertyVersion = optPropertyVersion ? *optPropertyVersion : -1;
    }
    // Notify all registered callbacks.
    { // Scope lock
        std::lock_guard<std::mutex> lock(_{{prop_name | lowerCamelCase}}PropertyCallbacksMutex);
        for (const auto& cb : _{{prop_name | lowerCamelCase}}PropertyCallbacks)
        {
            // Don't need a mutex since we're using tempValue.
            {%if prop.arg_list | length > 1 %}
            cb({%for arg in prop.arg_list %}tempValue.{{arg.name}}{%if not loop.last%}, {%endif%}{%endfor%});
            {%else %}
            cb(tempValue);
            {%endif %}
        }
    }
}

{{pr.get_property_definition(stinger.cpp.client_class_name, prop_name, prop)}}

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

{% if not prop.read_only %}
boost::future<bool> {{stinger.cpp.client_class_name}}::update{{prop_name | UpperCamelCase}}Property({{ar.methodParams(prop.arg_list)}}) const
{

    rapidjson::Document doc;
    doc.SetObject();
    {%for item in prop.arg_list %}
    {{ser.addToValue('doc', item, 'doc.GetAllocator()') | indent(4)}}
    {%endfor %}
    rapidjson::StringBuffer buf;
    rapidjson::Writer<rapidjson::StringBuffer> writer(buf);
    doc.Accept(writer);
    MqttProperties mqttProps;
    return _broker->Publish("{{prop.update_topic}}", buf.GetString(), 1, false, mqttProps);

}
{%endif%}
{% endfor %} {# for properties #}