/* Copyright (c) 2023 SCANLAB GmbH */

#if defined(_WIN32)
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #pragma comment(lib, "Ws2_32.lib")
using socklen_t = int;
#elif defined(__linux__)
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
using SOCKET = int;
constexpr SOCKET INVALID_SOCKET = -1;
    #define closesocket close
#endif

#include "rtc6_rif_wrapper.h"
#include "RTC6impl.hpp" // Only required for configuring the card

#include <stdexcept>

using namespace rtc6_rif;

// RTC6eth IP address, static or assigned by a DHCP server.
constexpr const char* CLIENT_ADDRESS = "192.168.250.5";
// UDPexcl as defined in eth_set_port_numbers, eth_get_port_numbers
constexpr uint16_t CLIENT_PORT = 63750;

// Create a derived class from NetworkAdapter and provide platform specific implementations of send/recv/select
class MyNet : public NetworkAdapter
{
public:
    MyNet(const char* ip, uint16_t port);
    ~MyNet();

    int32_t send(const char* buf, size_t len) override;
    int32_t recv(char* buf, size_t len) override;
    int32_t select(long timeout_us) override;

private:
    SOCKET socket_ = 0;
    sockaddr_in client_addr_ = { 0 };
};


MyNet::MyNet(const char* ip, uint16_t port)
{
    // Init socket
    socket_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (socket_ == INVALID_SOCKET)
    {
        throw std::runtime_error{ "Failed to create socket" };
    }

    // Init client addr
    client_addr_.sin_family = AF_INET;
    client_addr_.sin_port = htons(port);
    inet_pton(AF_INET, ip, &client_addr_.sin_addr.s_addr);
}

MyNet::~MyNet()
{
    closesocket(socket_);
}

int32_t MyNet::send(const char* buf, size_t len)
{
    const int res = ::sendto(socket_, buf, len, 0, reinterpret_cast<sockaddr*>(&client_addr_), sizeof(client_addr_));
    if (res == len)
    {
        return 0;
    }

    // Custom error handling (return value can be retrieved by get_network_error())
    return -1;
}

int32_t MyNet::recv(char* buf, size_t len)
{
    socklen_t fromlen = sizeof(client_addr_);
    const int res = ::recvfrom(socket_, buf, len, 0, reinterpret_cast<sockaddr*>(&client_addr_), &fromlen) > 0;
    if (res > 0)
    {
        return 0;
    }

    // Custom error handling (return value can be retrieved by get_network_error())
    return -2;
}

int32_t MyNet::select(long timeout_us)
{
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(socket_, &fds);
    struct timeval tv;
    tv.tv_sec = timeout_us / 1000000;
    tv.tv_usec = timeout_us % 1000000;

    const int res = ::select(socket_ + 1, &fds, NULL, NULL, &tv);
    if (res >= 0)
    {
        return res;
    }

    // Custom error handling (return value can be retrieved by get_network_error())
    return -3;
}


bool prepareCard()
{
    // Prerequisites: BIOS >= 38 (Rev. 1.16.0)

    const uint32_t init = init_rtc6_dll();
    if (init > 1) // 1 = no PCIe card is ok
    {
        return false;
    }

    const uint32_t card = eth_assign_card_ip(eth_convert_string_to_ip(CLIENT_ADDRESS), 0);
    const uint32_t loadProgram = n_load_program_file(card, nullptr);
    const uint32_t select = select_rtc(card);

    if (loadProgram != 0 || select != card)
    {
        return false;
    }

    // Load all correction files that will be used by the application
    const uint32_t loadCorrection = load_correction_file("D3_1to1.ct5", 1, 3);
    if (loadCorrection != 0)
    {
        return false;
    }

    // IMPORTANT: This is the only boot command that has to be specified for remote interface to work.
    eth_boot_dcmd();
    eth_set_remote_tgm_format(1);

    // OPTIONAL: Setup any other control commands to be executed during Standalone boot.
    eth_boot_dcmd();
    select_cor_table(1, 0);

    // Without eth_boot_timeout the card will wait between boot phase 1 and 2 for an external /START to continue
    eth_boot_dcmd();
    eth_boot_timeout(1);

    const uint32_t error = get_error();
    if (error != 0)
    {
        return false;
    }

    set_eth_boot_control(0);
    const uint32_t store = store_program(3);
    if (store != 0)
    {
        return false;
    }

    set_eth_boot_control(1);
    release_rtc(card);
    free_rtc6_dll();

    // Power cycle the card. After it finished booting it will be ready for remote interface control.
    // Note: From this point on normal communication using the DLL is not possible, unless:
    // * You call wrapper command disable_remote_interface()
    // * You interrupt Standalone boot by calling load_program_file (during boot sequence)

    return true;
}


void example1()
{
    // Create connection to RTC6eth
    RTC rtc(std::make_unique<MyNet>(CLIENT_ADDRESS, CLIENT_PORT));

    // Call wrapper API like RTC6DLL
    rtc.goto_xyz(0, 0, 0);

    rtc.set_start_list_1();
    rtc.jump_abs_3d(100, 200, 300);
    rtc.set_end_of_list();
    rtc.execute_list_1();

    uint32_t status, pos;
    rtc.get_status(status, pos);

    const int32_t x = rtc.get_value(7);
    const int32_t y = rtc.get_value(8);
    const int32_t z = rtc.get_value(9);
}


int main()
{
    // Setup card for remote interface, this has to be done only once.
    // It has to be done on a computer that can use the RTC6DLL.
    // Alternatively use RTC6conf to enable remote interface and remove prepareCard.
    if (!prepareCard())
    {
        return 1;
    }

#ifdef _WIN32
    // Init winsock
    WORD wVersionRequested = MAKEWORD(2, 2);
    WSADATA wsaData;
    const int result = WSAStartup(wVersionRequested, &wsaData);
    if (result != 0)
    {
        throw std::runtime_error{ "WSAStartup failed" };
    }

    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
    {
        throw std::runtime_error{ "Invalid winsock version" };
    }
#endif

    example1();

#ifdef _WIN32
    WSACleanup();
#endif

    return 0;
}
