
#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 boost::optional<std::string> optCorrelationId, const boost::optional<std::string> unusedRespTopic, const boost::optional<MethodResultCode> optResultCode)
    {
        _receiveMessage(topic, payload, optCorrelationId, optResultCode);
    });

    {%-for sig_name, sig in stinger.signals.items() %}
    _broker->Subscribe("{{sig.topic}}", 1);
    {%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%}
}

void {{stinger.cpp.client_class_name}}::_receiveMessage(
        const std::string& topic, 
        const std::string& payload, 
        const boost::optional<std::string> optCorrelationId, 
        const boost::optional<MethodResultCode> optResultCode)
{
    {%-for sig_name, sig in stinger.signals.items() %}
    if (_broker->TopicMatchesSubscription(topic, "{{sig.topic}}"))
    {
        //Log("Handling {{sig_name}} signal");
        rapidjson::Document doc;
        try {
            if (_{{sig_name | camelCase}}Callback)
            {
                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%}

                _{{sig_name | camelCase}}Callback({%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 method_name, method in stinger.methods.items() %}
    {%if not loop.first%}else {%endif%}if (_broker->TopicMatchesSubscription(topic, "{{method.response_topic('+')}}") && optCorrelationId)
    {
        std::cout << "Matched topic for {{method_name}} response" << std::endl;
        _handle{{method_name|UpperCamelCase}}Response(topic, payload, *optCorrelationId);
    }
    {%-endfor%}
}


{%-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)
{
    _{{sig_name | camelCase}}Callback = 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();

    {%-macro addToDoc(arg)%}
    {%-if arg.arg_type.name.lower() == 'primitive'%}
    {%-if arg.optional %}if ({{arg.name}}) {%endif-%}
    {%-if arg.type.name.lower() == 'string'%}
    { // restrict scope
        rapidjson::Value tempStringValue;
        tempStringValue.SetString({{arg.name}}{%-if arg.optional %}->{%else%}.{%endif-%}c_str(), {{arg.name}}.size(), doc.GetAllocator());
        doc.AddMember("{{arg.name}}", tempStringValue, doc.GetAllocator());
    }
    {%-else%}
    doc.AddMember("{{arg.name}}", {%-if arg.optional %}*{%endif-%}{{arg.name}}, doc.GetAllocator());
    {%-endif%}
    {%elif arg.arg_type.name.lower() == 'enum'%}
    doc.AddMember("{{arg.name}}", static_cast<int>({{arg.name}}), doc.GetAllocator());
    {%-endif-%}
    {%endmacro%}
    {%for arg in method.arg_list%}
    {{addToDoc(arg)}}
    {%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();
    _broker->Publish("{{method.topic}}", buf.GetString(), 2, false, correlationIdStr, responseTopicStringStream.str(), MethodResultCode::SUCCESS);

    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;
    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");
    }

    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%}