#include "WHISPER/Messages/Message.hpp"
#include <WHISPER/InternalUDPListener.hpp>
#include <memory>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>

namespace WHISPER {

InternalUDPListener::InternalUDPListener(std::uint16_t port)
{
    port_ = checkPort(port);
}

InternalUDPListener::~InternalUDPListener()
{
    unsubscribe(WHISPER::MsgTopics::MANAGEMENT);
    while (subscribedTopics.size() > 0) {
    unsubscribe(subscribedTopics.front());
    }

    if (listening_ == true) {
    stop();
    }
    receiverSocket_->close();
}

void InternalUDPListener::connect(std::shared_ptr<threadSafeQueue<WHISPER::Message>> receiver)
{
    receiverQueue_ = receiver;

    if (receiverSocket_ == nullptr) 
    {
        ctx = zmq::context_t(2);
        receiverSocket_ = std::make_shared<zmq::socket_t>(ctx,zmq::socket_type::dish);

        std::string portAsString = std::to_string(port_);
        receiverSocket_->bind("udp://*:"+portAsString);  
          ///used to set a custom time out to the socket 
        receiverSocket_->set(zmq::sockopt::rcvtimeo,100); 
    }
    subscribe(WHISPER::MsgTopics::MANAGEMENT);
   
    start();
}

void InternalUDPListener::listen()
{
    listening_ = true;

    while (stoplistening_ == false) 
    {
    zmq::recv_result_t res;
    zmq::message_t msg;   

    ///res is the size of the received message or error; cant figure out what value is set when ther is an error
    res = receiverSocket_->recv(msg,zmq::recv_flags::none);
    // std::string message = msg.to_string();            
    WHISPER::Message receivedMessage(msg.to_string());

    receiverQueue_->addElement(receivedMessage);

    }
    listening_ = false;
}
void InternalUDPListener::start()
{
    if (receiverSocket_ != nullptr)   
    {
        if (listening_ != true) 
        {
            stoplistening_ = false;
            listeningThread = std::thread(&InternalUDPListener::listen,this);
            // while (listening_ != true)
            // {
            // }
        }
    }
}

void InternalUDPListener::stop()
{
    stoplistening_ = true;
    if (listeningThread.joinable()) 
    {
        listeningThread.join();
    }

}

void InternalUDPListener::subscribe(WHISPER::MsgTopics topic)
{
    subscribedTopics.push_back(topic);
    receiverSocket_->join(WHISPER::MsgTopicsMap[topic].c_str());

}

void InternalUDPListener::unsubscribe(WHISPER::MsgTopics topic)
{
    try 
    {
        receiverSocket_->leave(WHISPER::MsgTopicsMap[topic].c_str());
        for (auto it = subscribedTopics.begin(); it != subscribedTopics.end(); )
        {
            if (*it  == topic)
            {   
                it = subscribedTopics.erase(it);
            }else 
            {
                it++;
            }
        }
    } catch(const std::exception& e) 
    {
            LOG_S(ERROR)<<e.what();
    }
}

std::uint16_t InternalUDPListener::checkPort(std::uint16_t port)
    {
        std::uint16_t localPort = port;

        struct sockaddr_in local;
        socklen_t localLen;
        int sockfd; 
        // Creating socket file descriptor 
        if ( (sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) 
        { 
            perror("socket creation failed"); 
            exit(EXIT_FAILURE); 
        } 

        local.sin_family    = AF_INET; // IPv4 
        local.sin_addr.s_addr = INADDR_ANY; 
        local.sin_port = htons(port); 
        int err = bind(sockfd, (const struct sockaddr *)&local, sizeof(local));

        if (err == -1 && errno == EADDRINUSE)
        {
            // set port to auto on binding
            local.sin_port = 0;
            err = bind(sockfd, (const struct sockaddr *) &local, sizeof(local));

        
            if(err == -1)
            {
                throw std::runtime_error(std::string(__PRETTY_FUNCTION__) + std::string("could not bind port to socket "));
            }

            // if we could bind to a port we have to get the port value from the socket
            memset((char *) &local, 0, sizeof(local));
            localLen=sizeof(local);
            err=getsockname(sockfd, (struct sockaddr *) &local, &localLen);
            if(err == -1)
            {
                throw std::runtime_error(std::string(__PRETTY_FUNCTION__) + std::string("could not get port nr errno=") + std::to_string(errno));
            }

            localPort=ntohs(local.sin_port);
            if (localPort==0)
            {
                err=getsockname(sockfd, (struct sockaddr *) &local, &localLen);
                if(err == -1)
                {
                    throw std::runtime_error(std::string(__PRETTY_FUNCTION__) + std::string("could not get port nr "));
                }
                localPort=ntohs(local.sin_port);
            }
        }
        else if(err == -1)
        {
         throw std::runtime_error(std::string(__PRETTY_FUNCTION__) + std::string("could not bind port to socket"));
        }


        close(sockfd); 

        return localPort;
    }



}