#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>

constexpr ::std::size_t MTU = 1024;

template<typename MessageType>
using MessagePtr = ::std::unique_ptr<MessageType, void (*)(MessageType *)>;

#define releaseMessage(MessageType)                          \
    [](MessageType *message) -> void {                       \
        if (message != nullptr) {                            \
            ASN_STRUCT_FREE(asn_DEF_##MessageType, message); \
        }                                                    \
    }

/**
 * @brief       Print data.
 */
::std::ostream &operator<<(::std::ostream &stream, const MessageFrame &message)
{
    auto flags = stream.flags();
    auto fill  = stream.fill();
    auto width = stream.width();

    stream << "MessageFrame{";
    ::std::unique_ptr<char[]> buffer;
    ::std::size_t             bufferSize = 1024;
    while (true) {
        buffer = ::std::unique_ptr<char[]>(new char[bufferSize]);
        ::asn_enc_rval_t encodeResult = asn_encode_to_buffer(
            nullptr, ATS_NONSTANDARD_PLAINTEXT, &asn_DEF_MessageFrame,
            reinterpret_cast<const void *>(&message), buffer.get(), bufferSize);
        if (encodeResult.encoded < 0) {
            stream << "ENCODE_ERROR";
        } else if (encodeResult.encoded <= bufferSize) {
            stream.write(buffer.get(), encodeResult.encoded);
            break;
        } else {
            bufferSize *= 2;
        }
    }
    stream << "}";

    stream.flags(flags);
    stream.fill(fill);
    stream.width(width);

    return stream;
}

/**
 * @brief   Make BSM message.
 *
 * @return  Fake message made.
 */
MessagePtr<MessageFrame> makeBSMMessage()
{
    MessagePtr<MessageFrame> ret(
        reinterpret_cast<MessageFrame *>(::calloc(1, sizeof(MessageFrame))),
        releaseMessage(MessageFrame));

    ret->present            = MessageFrame_PR::MessageFrame_PR_bsmFrame;
    ::asn_dec_rval_t result = ::asn_decode(
        nullptr, ATS_RANDOM, &::asn_DEF_BasicSafetyMessage,
        reinterpret_cast<void **>(&ret->choice.bsmFrame), nullptr, 0);
    if (result.code != RC_OK) {
        ::std::cerr << "Failed to generate BSM message." << ::std::endl;
        return MessagePtr<MessageFrame>(nullptr, releaseMessage(MessageFrame));
    }

    return ret;
}

/**
 * @brief   Make map message.
 *
 * @return  Fake message made.
 */
MessagePtr<MessageFrame> makeMapMessage()
{
    MessagePtr<MessageFrame> ret(
        reinterpret_cast<MessageFrame *>(::calloc(1, sizeof(MessageFrame))),
        releaseMessage(MessageFrame));

    ret->present            = MessageFrame_PR::MessageFrame_PR_mapFrame;
    ::asn_dec_rval_t result = ::asn_decode(
        nullptr, ATS_RANDOM, &::asn_DEF_MapData,
        reinterpret_cast<void **>(&ret->choice.mapFrame), nullptr, 0);
    if (result.code != RC_OK) {
        ::std::cerr << "Failed to generate Map message." << ::std::endl;
        return MessagePtr<MessageFrame>(nullptr, releaseMessage(MessageFrame));
    }

    return ret;
}

/**
 * @brief   Make spat message.
 *
 * @return  Fake message made.
 */
MessagePtr<MessageFrame> makeSPATMessage()
{
    MessagePtr<MessageFrame> ret(
        reinterpret_cast<MessageFrame *>(::calloc(1, sizeof(MessageFrame))),
        releaseMessage(MessageFrame));

    ret->present            = MessageFrame_PR::MessageFrame_PR_spatFrame;
    ::asn_dec_rval_t result = ::asn_decode(
        nullptr, ATS_RANDOM, &::asn_DEF_SPAT,
        reinterpret_cast<void **>(&ret->choice.spatFrame), nullptr, 0);
    if (result.code != RC_OK) {
        ::std::cerr << "Failed to generate SPAT message." << ::std::endl;
        return MessagePtr<MessageFrame>(nullptr, releaseMessage(MessageFrame));
    }

    return ret;
}

/**
 * @brief       Read data.
 *
 * @param[in]   fd      File descriptor to read.
 */
void readData(int fd)
{
    // Receive data.
    char      buffer[MTU];
    ::ssize_t sizeRead = ::recv(fd, buffer, sizeof(buffer), 0);
    if (sizeRead < 0) {
        int err = errno;
        ::std::cerr << "Failed to receive data: " << ::strerror(err);
        return;
    }

    // Decode data.
    MessageFrame    *message = nullptr;
    ::asn_dec_rval_t result  = ::asn_decode(
        nullptr, ATS_UNALIGNED_CANONICAL_PER, &::asn_DEF_MessageFrame,
        reinterpret_cast<void **>(&message), buffer, sizeRead);
    if (result.code != RC_OK) {
        ::std::cerr << "Decode error." << ::std::endl;
        return;
    }
    MessagePtr<MessageFrame> autoRelease(message, releaseMessage(MessageFrame));

    // Print data.
    ::std::cout << ::std::chrono::duration_cast<::std::chrono::seconds>(
                       ::std::chrono::system_clock::now().time_since_epoch())
                       .count()
                << " Data received: " << *message << ::std::endl;
}

/**
 * @brief       Write data.
 *
 * @param[in]   fd      File descriptor to write.
 */
void writeData(int fd)
{
    MessagePtr<MessageFrame> messages[] {makeBSMMessage(), makeMapMessage(),
                                         makeSPATMessage()};
    for (auto &message : messages) {
        if (message == nullptr) {
            continue;
        }
        char             buffer[MTU];
        ::asn_enc_rval_t encodeResult = asn_encode_to_buffer(
            nullptr, ATS_UNALIGNED_CANONICAL_PER, &asn_DEF_MessageFrame,
            reinterpret_cast<void *>(message.get()), buffer, sizeof(buffer));

        if (encodeResult.encoded > 0) {
            ssize_t sizeSent = ::send(fd, buffer, encodeResult.encoded, 0);
            if (sizeSent <= 0) {
                ::std::cout
                    << ::std::chrono::duration_cast<::std::chrono::seconds>(
                           ::std::chrono::system_clock::now()
                               .time_since_epoch())
                           .count()
                    << " Data sent: " << *message << ::std::endl;
            } else {
                int err = errno;
                ::std::cerr << "Failed to send data: " << ::strerror(err);
                return;
            }
        } else {
            ::std::cerr << "Encode error." << ::std::endl;
        }
    }
}

/**
 * @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;
}
