/**
 * @file UDP_broadcast.c
 *
 * @brief UDP broadcast handler
 *  Created on: Nov 6, 2023
 *      Author: joran
 */

// Includes
#include "UDP_broadcast.h"
#define LOGGER_LEVEL_INFO
#include "log.h"

// Global variables

static const char* TAG = "UDP_broadcast"; // Tag used in logs
static owner_details_t udp_owner;
static uint16_t owner_name_x_pos;
static uint16_t owner_name_y_pos;

// Functions
static void udp_broadcast_set_owner_details_mac(void);
static void udp_broadcast_name_to_lcd(void);
static uint8_t udp_broadcast_set_owner_details_name(const char* name);
static uint8_t udp_broadcast_set_owner_details_surname(const char* surname);
static uint8_t udp_broadcast_set_owner_details_reply(const char* reply);
static void udp_broadcast_format_reply(void);
static void udp_receive_callback(void* arg,
                                 struct udp_pcb* connection,
                                 struct pbuf* p,
                                 const ip_addr_t* addr,
                                 u16_t port);

/**
 * @fn uint8_t udp_broadcast_set_owner_details_mac(owner_details_t*)
 * @brief set_owner_details_mac() gets the MAC address from the default netif
 * and sets it in the owner_details_t struct
 */

static void udp_broadcast_set_owner_details_mac(void) {
    for (uint8_t i = 0; i < 6; i++) {
        udp_owner.mac_address[i] = netif_default->hwaddr[i]; // Access the MAC address
    }
}

/**
 * @fn void udp_broadcast_name_to_lcd(void)
 * @brief prints the owner's name with
 * @see UDP_BROADCAST_LCD_NAME_PRE_TEXT in front of it
 */

static void udp_broadcast_name_to_lcd(void){
    char text[UDP_BROADCAST_LCD_TEXT_SIZE];

    memset(text,' ',UDP_BROADCAST_LCD_TEXT_SIZE);    // Fill with spaces
    text[UDP_BROADCAST_LCD_TEXT_SIZE - 1] = '\0';    // Make the last a NULL byte
    lcd_display_text(text, owner_name_x_pos, owner_name_y_pos, LCD_BLACK, LCD_WHITE, LCD_FONT12);

    snprintf(text, UDP_BROADCAST_LCD_TEXT_SIZE, "%s%s",UDP_BROADCAST_LCD_NAME_PRE_TEXT,
                 udp_owner.name);

    lcd_display_text(text, owner_name_x_pos, owner_name_y_pos, LCD_BLACK, LCD_WHITE, LCD_FONT12);
}


/**
 * @fn uint8_t udp_broadcast_set_owner_details_name(owner_details_t*, const char*)
 * @brief set_owner_details_name() sets the owner's name in the owner_details_t struct
 * strncpy is used to copy the function paremeter safely to the owner_details_t's name
 * it also uses the lcd api to display the latest owner's name
 *
 * @param[in] name string containing the owner's name
 * @return setting owner name error
 * - 0: no error occured, name was set
 * - 1: an error occured, name pointer is NULL
 */

static uint8_t udp_broadcast_set_owner_details_name(const char* name) {

    if (name == NULL) {
        LOG_WARN(TAG, "%s: string given is a NULL pointer", __func__);
        return 1;
    }
    LOG_DEBUG(TAG, "set: %s", name);
    strncpy(udp_owner.name, name, sizeof(udp_owner.name) - 1);       // -1: compensate for '\0'

    udp_broadcast_name_to_lcd();
    return 0;
}

/**
 * @fn uint8_t udp_broadcast_set_owner_details_surname(owner_details_t*, const char*)
 * @brief set_owner_details_surname() sets the owner's surname in the owner_details_t struct
 * strncpy is used to copy the function paremeter safely to the owner_details_t's surname
 *
 * @param[in] surname string containing the owner's surname
 * @return setting owner surname error
 * - 0: no error occured, surname was set
 * - 1: an error occured, surname pointer is NULL
 */
static uint8_t udp_broadcast_set_owner_details_surname(const char* surname) {
    if (surname == NULL) {
        LOG_WARN(TAG, "%s: string given is a NULL pointer", __func__);
        return 1;
    }
    LOG_DEBUG(TAG, "set: %s", surname);
    strncpy(udp_owner.surname, surname, sizeof(udp_owner.surname)-1);                  // -1: compensate for '\0'
    return 0;
}

/**
 * @fn uint8_t udp_broadcast_set_owner_details_reply(const char*)
 * @brief set_owner_details_reply() sets the UDP reply in the owner_details_t struct
 * strncpy is used to copy the function paremeter safely to the owner_details_t's reply
 *
 * @param[in] reply string used to reply to the UDP broadcast
 * @return setting owner reply error
 * - 0: no error occured, reply was set
 * - 1: an error occured, reply pointer is null
 */

static uint8_t udp_broadcast_set_owner_details_reply(const char* reply) {
    if (reply == NULL) {
        LOG_WARN(TAG, "%s: string given is a NULL pointer", __func__);
        return 1;
    }
    LOG_DEBUG(TAG, "set: %s", reply);
    strncpy(udp_owner.reply, reply, sizeof(udp_owner.reply) - 1);                      // -1: compensate for '\0'
    return 0;
}

/**
 * @fn uint8_t udp_broadcast_format_reply(const owner_details_t*)
 * @brief format_reply() formats all the owner's details into a string
 * it formats a string using the owner's details using snprintf
 * it sets this reply with @see udp_broadcast_set_owner_details_reply()
 */

static void udp_broadcast_format_reply(void) {
    char mac_addr_str[UDP_BROADCAST_MAX_MAC_ADDR_LEN];
    char reply_buf[UDP_BROADCAST_MAX_REPLY_SIZE];

    snprintf(mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", udp_owner.mac_address[0],
             udp_owner.mac_address[1], udp_owner.mac_address[2], udp_owner.mac_address[3], udp_owner.mac_address[4],
             udp_owner.mac_address[5]);

    snprintf(reply_buf, UDP_BROADCAST_MAX_REPLY_SIZE, "%s %s %s %s", mac_addr_str, UDP_BROADCAST_REPLY_MIDDLE_TEXT, udp_owner.surname,
             udp_owner.name);
    udp_broadcast_set_owner_details_reply(reply_buf);
}

/**
 * @fn err_t udp_broadcast_set_owner_details(owner_details_t*, const char*, const char*)
 * @brief set_owner_details() is the interface that can be used in other files
 * to set the owner's details
 * the pointers get checked by the functions that are called in this function
 *
 * @param[in] name string containing the new owner's name
 * @param[in] surname string containing the new owner's surname
 * @return lwIP error code.
 * - ERR_OK. Successful. No error occurred.
 * - ERR_ARG. one or both arguments are NULL pointers
 */
err_t udp_broadcast_set_owner_details(const char* name, const char* surname) {
    if (!udp_broadcast_set_owner_details_name(name) && !udp_broadcast_set_owner_details_surname(surname)) {

        // If both return 0 it's okay
        udp_broadcast_set_owner_details_mac();
        udp_broadcast_format_reply();
        return ERR_OK;
    }
    return ERR_ARG;
}

/**
 * @fn char udp_broadcast_get_owner_details_name*(void)
 * @brief get_owner_details_name() can be used to get the current owner's name
 *
 * @return name of owner
 * this name is set by @see udp_broadcast_set_owner_details_name()
 */

char* udp_broadcast_get_owner_details_name(void) {
    return udp_owner.name;
}

/**
 * @fn char udp_broadcast_get_owner_details_surname*(void)
 * @brief get_owner_details_surname() can be used to get the current owner's surname
 *
 * @return surname of owner
 * this name is set by @see udp_broadcast_set_owner_details_surname()
 */

char* udp_broadcast_get_owner_details_surname(void) {
    return udp_owner.surname;
}

/**
 * @fn char udp_broadcast_get_owner_details_reply*(void)
 * @brief get_owner_details_reply() can be used to get the current UDP reply
 *
 * @return reply for UDP broadcast
 * this reply is formatted by @see format_reply()
 */

char* udp_broadcast_get_owner_details_reply(void) {
    return udp_owner.reply;
}

/**
 * @fn void udp_broadcast_check_function(const char[])
 * @brief checks what the UDP datagram asked to do
 * and processes the datagram if it was not @see UDP_QUESTION1
 *
 * @param[in] data the datagram received on port 64000
 *
 * @return checked
 * - 0: a function was found and processed if necessary
 * - 1: datagram didn't have a known function
 */

static uint8_t udp_broadcast_check_function(const char data[UDP_BROADCAST_MAX_DATA_SIZE]) {
    char func[UDP_BROADCAST_MAX_FUNC_LEN];
    char buffer[UDP_BROADCAST_MAX_NAME_SIZE];
    uint8_t enders[UDP_BROADCAST_MAX_COLON_COMMA_COUNT];
    uint8_t counter = 0;
    uint8_t data_len = strlen(data);

    if (strcmp(data, UDP_BROADCAST_UDP_QUESTION1) == 0) {
        return 0;
    }

    memset(func, 0, sizeof(func));
    memset(buffer, 0, sizeof(buffer));

    memcpy(func,data,UDP_BROADCAST_MAX_FUNC_LEN - 1);
    if (strcmp(func, "func1:") != 0) {
        LOG_WARN(TAG, "%s: datagram does not contain function that's currently available", __func__);
        return 1;
    }
    for (uint8_t i = 0; i < data_len && counter < UDP_BROADCAST_MAX_COLON_COMMA_COUNT; i++) {
        if ((data[i] == ',' || data[i] == ':')) {
            enders[counter] = i;
            counter++;
        }
    }
    if (enders[2] - enders[1] < UDP_BROADCAST_MAX_NAME_SIZE + 2 && data_len - enders[3] < UDP_BROADCAST_MAX_NAME_SIZE + 2
            && strncmp(data + enders[0], ":name", 5) == 0 && strncmp(data + enders[2], ", surname", 9) == 0) {

        counter = 0;
        for (uint8_t i = enders[1] + 2; i < enders[2]; i++) {
            buffer[counter] = data[i];
            counter++;
        }
        if (buffer[0]=='\0') {
            strncpy(buffer, "name", sizeof(buffer) - 1);          // -1: compensate for '\0'
        }
        LOG_INFO(TAG, "new owner name:%s", buffer);
        udp_broadcast_set_owner_details_name(buffer);
        memset(buffer, 0, sizeof(buffer));
        counter = 0;
        for (uint8_t i = enders[3] + 2; i < data_len; i++) {
            buffer[counter] = data[i];
            counter++;
        }
        if (buffer[0]=='\0') {
            strncpy(buffer, "default", sizeof(buffer) - 1);       // -1: compensate for '\0'
        }
        LOG_INFO(TAG, "new owner surname:%s", buffer);
        udp_broadcast_set_owner_details_surname(buffer);
        udp_broadcast_format_reply();
        return 0;
    }
    LOG_WARN(TAG,"%s: function didn't receive the right formatting", __func__);
    return 1;
}

/**
 * @fn void udp_receive_callback(void*, struct udp_pcb*, struct pbuf*, const ip_addr_t*, u16_t)
 * @brief udp_receive_callback() callback function for when a UDP packet has been received.
 * it compares the data to a set string @see UDP_QUESTION1, if it's the same it sends the reply string,
 * @see reply_str, back to the client
 * if it was not @see UDP_QUESTION1, it checks what function was called with @see udp_broadcast_check_function()
 *
 * @param[in] arg a pointer to some user-defined data or context
 * @param[in] connection  UDP PCB to be bound with a local address ipaddr and port.
 * @param[in] p  packet buffer it holds the incoming UDP packet data, including its payload and length
 * @param[in] addr  ip_addr_t structure that contains the IP address of the sender of the UDP packet
 * @param[in] port  the source port number of the sender's UDP packet
 */

static void udp_receive_callback(void* arg,
                                 struct udp_pcb* connection,
                                 struct pbuf* p,
                                 const ip_addr_t* addr,
                                 u16_t port) {
    struct pbuf* p_data;
    size_t len;
    char* pc;
    char data[UDP_BROADCAST_MAX_DATA_SIZE];
    char source_ip_str[16];

    memset(data, 0, sizeof(data));

    ipaddr_ntoa_r(addr, source_ip_str, sizeof(source_ip_str)); // Convert the source IP address to a string

    if (p == NULL) {
        LOG_WARN(TAG, "%s: input buffer was a NULL pointer", __func__);
        return;
    }
    pc = (char*)p->payload;
    len = p->tot_len;
    if (len >= UDP_BROADCAST_MAX_DATA_SIZE) {    // >= : only if it's smaller to compensate for '\0'
        LOG_WARN(TAG, "%s: input buffer was bigger than or was max size %d", __func__, UDP_BROADCAST_MAX_DATA_SIZE);
        return;
    }

    p_data = pbuf_alloc(PBUF_TRANSPORT, sizeof(udp_owner.reply), PBUF_RAM);
    if (p_data == NULL) {
        LOG_WARN(TAG, "%s: unable to allocate data buffer for reply", __func__);
        goto defer;
    }
    for (size_t i = 0; i < len; i++) {
        data[i] = pc[i];
    }

    LOG_INFO(TAG, "%s: received data from %s at port: %d: %s", __func__, source_ip_str, port, data);
    LOG_INFO(TAG, "%s: checking which function was called", __func__);

    if(!udp_broadcast_check_function(data)){    // Should return 0 to reply
        p_data->payload = udp_owner.reply;
        p_data->len = strlen(udp_owner.reply);
        p_data->tot_len = strlen(udp_owner.reply);
        udp_sendto(connection, p_data, addr, 64000);    // QT app listens on port 64000
        LOG_INFO(TAG, "%s: tried to reply to %s at port: %d: %s", __func__, source_ip_str, 64000, udp_owner.reply);
    }


defer:
    pbuf_free(p);
    pbuf_free(p_data);
}

/**
 * @fn err_t udp_broadcast_init(void)
 * @brief init_UDP_server() initialises the UDP connection so that it listens for all traffic on
 * port 6400
 * it makes a udp_pcb, binds it to port 64000 and initializes the callback function for when data is received
 *
 * @return lwIP error code.
 * - ERR_OK. Successful. No error occurred.
 * - ERR_USE. The specified ipaddr and port are already bound to by another UDP PCB.
 * - ERR_MEM. udp pcb couldn't be created
 */

err_t udp_broadcast_connection_init(void) {
    struct udp_pcb* connection;
    err_t err;

    LOG_INFO(TAG, "%s: initializing UDP server", __func__);
    connection = udp_new();
    if (connection == NULL) {
        LOG_WARN(TAG, "%s: Initializing UDP server failed, connection is null", __func__);
        return ERR_MEM;
    }
    err = udp_bind(connection, IP_ANY_TYPE, 64000);
    if (err != ERR_OK) {
        LOG_WARN(TAG, "%s: Initializing UDP server failed, err not ok", __func__);
        udp_remove(connection);
        return err;
    }
    udp_recv(connection, udp_receive_callback, NULL);
    LOG_INFO(TAG, "%s: Initializing UDP server successful, callback running", __func__);
    return err;
}



/**
 * @fn err_t udp_broadcast_init()
 * @brief udp_broadcast_init() initializes the owner's variables and calls upon @see udp_broadcast_connection_init()
 *
 * @return lwIP error code.
 * - ERR_OK. Successful. No error occurred.
 * - ERR_USE. The specified ipaddr and port are already bound to by another UDP PCB.
 * - ERR_MEM. udp pcb couldn't be created
 *
 * - ERR_ARG. one or both arguments of udp_broadcast_set_owner_details() are NULL pointers
 */
err_t udp_broadcast_init(uint16_t x_pos, uint16_t y_pos) {
    owner_name_x_pos = x_pos;
    owner_name_y_pos = y_pos;
    if(udp_broadcast_set_owner_details("name", "default") != ERR_OK){
        LOG_WARN(TAG, "%s: don't give NULL pointers as arguments for the owner's details", __func__);
        return ERR_ARG;
    }
    return udp_broadcast_connection_init();
}
