#pragma once

#include <cstddef>
#include <memory>
#include <string>
#include <stdexcept>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>

#include "net/async_fd.hpp"
#include "net/connection_client.hpp"
#include "net/ioqueue.hpp"
#include "net/socketaddress.hpp"
#include "net/sock_address_factory.hpp"
#include "net/udp_packet.hpp"

#include <iostream>

namespace rmrf::net {

/**
 * This class provides a UDP server / client wrapper. Please note that it is packet based and thus not based
 * on connection client on purpose.
 * @class udp_client
 * @author leondietrich
 * @date 04/12/21
 * @file udp_client.hpp
 * @brief A UDP socket wrapper class
 */
class udp_client : public connection_client, std::enable_shared_from_this<udp_client> {
private:
    int send_flags = MSG_DONTWAIT;
    bool client_is_bound = false;

public:

    udp_client(auto_fd&& socket_fd, const socketaddr& own_address_) :
        connection_client{std::forward<auto_fd>(socket_fd), socketaddr{}, nullptr}
    {
        this->own_address = own_address_;
    }

    virtual ~udp_client() {}

    /**
     * This method copies the content of data on the write queue and schedules its transmission to destination.
     * It needs to know the packet size of the udp packet, provided by the template.
     * @brief Send a udp packet
     * @param destination The destination to send it to
     * @param data The data to be send
     */
    template<size_t packet_size>
    inline void send_packet(const socketaddr& destination, const udp_packet<packet_size>& data) {
        this->write_data(std::forward<iorecord>(iorecord{data.raw(), data.size(), destination}));
    }

    /**
     * Use this method in order to register your callback function that should be
     * called when the client got data to process. If this method is called for the first
     * time it binds the socket and will throw an error if it is not possible.
     * @param cb The callback function to register [void(iorecord& data)]
     */
    virtual void set_incomming_data_callback(const incomming_data_cb& cb) {
        connection_client::set_incomming_data_callback(cb);
        if(cb && !client_is_bound) {
            if (auto error = bind(this->net_socket.get(), this->own_address.ptr(), this->own_address.size()); error != 0) {
                throw netio_exception("Failed to bind UDP socket: " + std::to_string(error));
            }
            this->client_is_bound = true;
        }
    }

    /**
     * Enable or disable UDP confirm mode. Disabled by default. If enabled every
     * device handling the send packet shall report if the path was viable or not. This
     * is useful for debugging but may negatively impact performance.
     * @brief Enable or disable UDP confirm mode
     * @param enabled If set to true the UDP confirm mode will be activated.
     */
    void enable_confirm_mode(bool enabled) {
        if (enabled) {
            this->send_flags |= MSG_CONFIRM;
        } else {
            this->send_flags &= ~MSG_CONFIRM;
        }
    };
    
protected:

    virtual void read_from_socket(::ev::io& w) {
        sockaddr_storage source_addr_raw{};
        socklen_t srcaddr_len = sizeof(source_addr_raw);
        
        uint8_t buffer[1024];
        auto read_bytes_or_error = recvfrom(w.fd, buffer, sizeof(buffer), 0, (sockaddr*) &source_addr_raw, &srcaddr_len);
        if (read_bytes_or_error < 0) {
            throw netio_exception("Failed to read UDP packet. err:" + std::to_string(read_bytes_or_error));
        }

        const socketaddr source_address{source_addr_raw};
        // The case below is safe baecause we checked it earlyier to be greater or equal to zero
        this->in_data_cb(iorecord{buffer, (size_t) read_bytes_or_error, source_address});
    }

    virtual ssize_t push_write_queue(::ev::io& w, iorecord& buffer) {
        const auto dest_addr = buffer.get_address();
        const ssize_t written = sendto(w.fd, buffer.ptr(), buffer.size(), this->send_flags, dest_addr.ptr(), dest_addr.size());
        return written;
    }
};

}