#include <array>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <poll.h>
#include <signal.h>
#include <sys/eventfd.h>
#include <sys/socket.h>
#include <unistd.h>

#include <MessageFrame.h>

/**
 * @brief       Read data.
 *
 * @param[in]   fd      File descriptor to read.
 */
void readData(int fd) {}

/**
 * @brief       Write data.
 *
 * @param[in]   fd      File descriptor to write.
 */
void writeData(int fd) {}

/**
 * @brief       Entery.
 *
 * @param[in]   argc    Count of arguments.
 * @param[in]   argv    Valuse of arguments.
 *
 * @return      Exit code.
 */
int main(int argc, char *argv[])
{
    // Get arguments.
    if (argc != 4) {
        ::std::cerr << "Illegal arguments." << ::std::endl;
        return -1;
    }

    ::std::string   dmd3aIP = argv[1];
    ::std::uint16_t targetPort
        = static_cast<::std::uint16_t>(::std::strtoul(argv[2], nullptr, 10));
    ::std::uint16_t listenPort
        = static_cast<::std::uint16_t>(::std::strtoul(argv[3], nullptr, 10));

    // Create socket.
    int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        int err = errno;
        ::std::cerr << "Failed to create socket: " << ::strerror(err);
        return -1;
    }
    ::std::unique_ptr<int, void (*)(int *)> autoCloseFd(&fd,
                                                        [](int *fd) -> void {
                                                            ::close(*fd);
                                                            *fd = -1;
                                                        });

    // Bind.
    ::sockaddr_in bindAddress;
    ::memset(&bindAddress, 0, sizeof(bindAddress));
    bindAddress.sin_family      = AF_INET;
    bindAddress.sin_addr.s_addr = INADDR_ANY;
    bindAddress.sin_port        = listenPort;
    auto bindResult = ::bind(fd, reinterpret_cast<::sockaddr *>(&bindAddress),
                             sizeof(bindAddress));
    if (bindResult < 0) {
        int err = errno;
        ::std::cerr << "Failed to bind listen port: " << ::strerror(err);
        return -1;
    }

    // Connect.
    ::sockaddr_in targetAddress;
    ::memset(&targetAddress, 0, sizeof(targetAddress));
    targetAddress.sin_family = AF_INET;
    if (! ::inet_pton(AF_INET, dmd3aIP.c_str(), &targetAddress.sin_addr)) {
        ::std::cerr << "Illegal DMD3A IP address \"" << dmd3aIP << "\".";
        return -1;
    }
    targetAddress.sin_port = targetPort;
    auto connectResult
        = ::connect(AF_INET, reinterpret_cast<::sockaddr *>(&targetAddress),
                    sizeof(targetPort));
    if (connectResult < 0) {
        int err = errno;
        ::std::cerr << "Failed to connect to DMD3A port: " << ::strerror(err);
        return -1;
    }

    // Reuse.
    int opt = 1;
    ::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    opt = 1;
    ::setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

    // Create quit event.
    static int quitEvent = -1;
    quitEvent            = ::eventfd(0, 0);
    if (quitEvent < 0) {
        int err = errno;
        ::std::cerr << "Failed to create quit event: " << ::strerror(err);
        return -1;
    }
    ::std::unique_ptr<int, void (*)(int *)> autoCloseQuitEvent(
        &quitEvent, [](int *fd) -> void {
            ::close(*fd);
            *fd = -1;
        });

    // Handle signal.
    auto onSignal = [](int) -> void {
        ::std::uint64_t v = 1;
        ::write(quitEvent, &v, sizeof(v));
    };
    signal(SIGINT, onSignal);
    signal(SIGTERM, onSignal);
    signal(SIGHUP, onSignal);

    // Main loop.
    auto nextTimestamp = ::std::chrono::steady_clock::now();
    while (true) {
        auto now = ::std::chrono::steady_clock::now();
        if (now - nextTimestamp > ::std::chrono::seconds(1)) {
            // Send.
            writeData(fd);
            nextTimestamp += ::std::chrono::seconds(1);
            if (now > nextTimestamp) {
                nextTimestamp = now + ::std::chrono::seconds(1);
            }
        }

        // Wait for data.
        ::std::array<::pollfd, 2> fds {
            ::pollfd {.fd = fd, .events = POLLIN, .revents = 0},
            ::pollfd {.fd = quitEvent, .events = POLLIN, .revents = 0}};
        auto pollResult
            = ::poll(fds.data(), fds.size(),
                     ::std::chrono::duration_cast<::std::chrono::milliseconds>(
                         nextTimestamp - ::std::chrono::steady_clock::now())
                         .count());
        if (pollResult > 0) {
            for (auto &status : fds) {
                if (status.revents & POLLIN) {
                    if (status.fd == fd) {
                        ::readData(fd);
                    } else if (status.fd == quitEvent) {
                        ::std::cout << "Quit event received." << ::std::endl;
                        break;
                    }
                }
            }
        }
    }

    return 0;
}
