
#include <exception>
#include <string>
#include <functional>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>
#include <mosquitto.h>
#include <mqtt_protocol.h>
#include <iostream>

#include "broker.hpp"

using namespace std;

MqttConnection::MqttConnection(const std::string& host, int port, const std::string& clientId)
    : _mosq(NULL), _host(host), _port(port), _clientId(clientId)
{
    boost::mutex::scoped_lock lock(_mutex);

    if (mosquitto_lib_init() != MOSQ_ERR_SUCCESS) {
        throw std::runtime_error("Mosquitto lib init problem");   
    };
    _mosq = mosquitto_new(_clientId.c_str(), false, (void*)this);

    mosquitto_connect_callback_set(_mosq, [](struct mosquitto *mosq, void *user, int i)
    {
        MqttConnection *thisClient = static_cast<MqttConnection*>(user);
        cout << "Connected to " << thisClient->_host << endl;
        boost::mutex::scoped_lock lock(thisClient->_mutex);
        while(!thisClient->_subscriptions.empty())
        {
            auto sub = thisClient->_subscriptions.front();
            cout << "Subscribing to " << sub._topic << endl;
            mosquitto_subscribe(mosq, NULL, sub._topic.c_str(), sub._qos);
            thisClient->_subscriptions.pop();
        }
        while(!thisClient->_msgQueue.empty())
        {
            MqttMessage& msg = thisClient->_msgQueue.front();
            cout << "Sending message to " << msg._topic << endl;
            int mid;
            mosquitto_property *propList = NULL;
            if (msg._optCorrelationId)
            {
                mosquitto_property_add_string(&propList, MQTT_PROP_CORRELATION_DATA, msg._optCorrelationId->c_str());
            }
            if (msg._optResponseTopic)
            {
                mosquitto_property_add_string(&propList, MQTT_PROP_RESPONSE_TOPIC, msg._optResponseTopic->c_str());
            }
            mosquitto_publish_v5(mosq, &mid, msg._topic.c_str(), msg._payload.size(), msg._payload.c_str(), msg._qos, msg._retain, propList);
            mosquitto_property_free_all(&propList);
            thisClient->_sendMessages[mid] = msg._pSentPromise;
            thisClient->_msgQueue.pop();
        }
    });

    mosquitto_message_v5_callback_set(_mosq, [](struct mosquitto *mosq, void *user, const struct mosquitto_message *mmsg, const mosquitto_property *props)
    {
        MqttConnection *thisClient = static_cast<MqttConnection*>(user);
        cout << "Fowarding message (" << mmsg->topic << ") to " << thisClient->_messageCallbacks.size() << " callbacks" << endl;
        std::string topic(mmsg->topic);
        std::string payload(static_cast<char*>(mmsg->payload), mmsg->payloadlen);
        boost::optional<std::string> optCorrelationId;
        boost::optional<std::string> optResponseTopic;
        boost::optional<MethodResultCode> optResultCode;
        const mosquitto_property *prop;
        for (prop = props; prop != NULL; prop = mosquitto_property_next(prop))
        {
            if (mosquitto_property_identifier(prop) == MQTT_PROP_CORRELATION_DATA)
            {
                void *correlation_data;
                uint16_t correlation_data_len;
                if (mosquitto_property_read_binary(prop, MQTT_PROP_CORRELATION_DATA, &correlation_data, &correlation_data_len, false))
                {
                    optCorrelationId = std::string(static_cast<char*>(correlation_data), correlation_data_len);
                }
            }
            else if (mosquitto_property_identifier(prop) == MQTT_PROP_RESPONSE_TOPIC)
            {
                char *responseTopic = NULL;
                if (mosquitto_property_read_string(prop, MQTT_PROP_RESPONSE_TOPIC, &responseTopic, false))
                {
                    optResponseTopic = std::string(responseTopic);
                    free(responseTopic);
                }
            }
            else if (mosquitto_property_identifier(prop) == MQTT_PROP_USER_PROPERTY)
            {
                char *name = NULL;
                char *value = NULL;
                if (mosquitto_property_read_string_pair(prop, MQTT_PROP_USER_PROPERTY, &name, &value, false))
                {
                    if (strcmp(name, "ReturnValue") == 0)
                    {
                        int returnValueInt = boost::lexical_cast<int>(value);
                        optResultCode = static_cast<MethodResultCode>(returnValueInt);
                    }
                    free(name);
                    free(value);
                }
            }
        }
        for (auto& cb : thisClient->_messageCallbacks)
        {
            cout << "Calling callback" << endl;
            cb(topic, payload, optCorrelationId, optResponseTopic, optResultCode);
        }
    });

    mosquitto_publish_callback_set(_mosq, [](struct mosquitto *mosq, void *user, int mid)
    {
        MqttConnection *thisClient = static_cast<MqttConnection*>(user);
        auto found = thisClient->_sendMessages.find(mid);
        if (found != thisClient->_sendMessages.end())
        {
            found->second->set_value(true);
            thisClient->_sendMessages.erase(found);
        }
    });

    Connect();
    mosquitto_loop_start(_mosq);
}

MqttConnection::~MqttConnection()
{
    boost::mutex::scoped_lock lock(_mutex);
    mosquitto_loop_stop(_mosq, true);
    mosquitto_disconnect(_mosq);
    mosquitto_destroy(_mosq);
    mosquitto_lib_cleanup();

}

void MqttConnection::Connect()
{
    mosquitto_int_option(_mosq, MOSQ_OPT_PROTOCOL_VERSION, MQTT_PROTOCOL_V5);
    mosquitto_connect(_mosq, _host.c_str(), _port, 120);
}

boost::future<bool> MqttConnection::Publish(
        const std::string& topic, 
        const std::string& payload, 
        unsigned qos, 
        bool retain, 
        boost::optional<std::string> optCorrelationId, 
        boost::optional<std::string> optResponseTopic,
        boost::optional<MethodResultCode> optResultCode)
{
    int mid;
    mosquitto_property *propList = NULL;
    mosquitto_property_add_string(&propList, MQTT_PROP_CONTENT_TYPE, "application/json");
    if (optCorrelationId)
    {
        mosquitto_property_add_binary(&propList, MQTT_PROP_CORRELATION_DATA, (void*)optCorrelationId->c_str(), optCorrelationId->size());
    }
    if (optResponseTopic)
    {
        mosquitto_property_add_string(&propList, MQTT_PROP_RESPONSE_TOPIC, optResponseTopic->c_str());
    }
    if (optResultCode)
    {
        std::string resultCodeStr = std::to_string(static_cast<int>(*optResultCode));
        mosquitto_property_add_string_pair(&propList, MQTT_PROP_USER_PROPERTY, "ReturnValue", resultCodeStr.c_str());
    }
    int rc = mosquitto_publish_v5(_mosq, &mid, topic.c_str(), payload.size(), payload.c_str(), qos, retain, propList);
    mosquitto_property_free_all(&propList);
    if (rc == MOSQ_ERR_NO_CONN)
    {
        std::cout << "Delayed published queued to: " << topic << std::endl;
        MqttConnection::MqttMessage msg(topic, payload, qos, retain, optCorrelationId, optResponseTopic);
        auto future = msg.getFuture();
        boost::mutex::scoped_lock lock(_mutex);
        _msgQueue.push(std::move(msg));
        return future;
    }
    else if (rc == MOSQ_ERR_SUCCESS)
    {
        std::cout << "Published to: " << topic << " | " << payload << std::endl;
        auto pPromise = std::make_shared<boost::promise<bool>>();
        auto future = pPromise->get_future();
        boost::mutex::scoped_lock lock(_mutex);
        _sendMessages[mid] = std::move(pPromise);
        return future;
    }
    else
    {
        std::cout << "RC " << rc << std::endl;
    }
    throw std::runtime_error("Unhandled rc");
}

void MqttConnection::Subscribe(const std::string& topic, int qos)
{
    std::cout << "Subscribing to " << topic << endl;
    int rc = mosquitto_subscribe(_mosq, NULL, topic.c_str(), qos);
    if (rc == MOSQ_ERR_NO_CONN)
    {
        MqttConnection::MqttSubscription sub(topic, qos);
        boost::mutex::scoped_lock lock(_mutex);
        _subscriptions.push(sub);
    }
    else if (rc == MOSQ_ERR_SUCCESS)
    {
        std::cout << "Subscribed to " << topic << std::endl;
    }
}

void MqttConnection::AddMessageCallback(
        const std::function<void(const std::string&, const std::string&, const boost::optional<std::string>, const boost::optional<std::string>, const boost::optional<MethodResultCode>)>& cb)
{
    boost::mutex::scoped_lock lock(_mutex);
    _messageCallbacks.push_back(cb);
    cout << "Message callback set" << endl;
}

bool MqttConnection::TopicMatchesSubscription(const std::string& topic, const std::string& subscr) const
{
    bool result;
    int rc = mosquitto_topic_matches_sub(subscr.c_str(), topic.c_str(), &result);
    if (rc != MOSQ_ERR_SUCCESS)
    {
        throw std::runtime_error("Mosquitto error");
    }
    return result;
}

std::string MqttConnection::GetClientId() const
{
    return _clientId;
}

{%for broker in stinger.brokers.values()%}
{{broker.class_name}}::{{broker.class_name}}({%if broker.hostname is none%}const std::string& host, int port, {%endif%}const std::string& clientId)
    :  MqttConnection({%if broker.hostname is none%}host{%else%}"{{broker.hostname}}"{%endif%}, {%if broker.port is none%}port{%else%}{{broker.port}}{%endif%}, clientId)
{

}

{%endfor%}