/**
 * @file tftp.c
 * @brief tftp server
 * @author Sander S.
 */

#define LOGGER_LEVEL_ALL
#include "tftp.h"

#define VIRT_INDEX_TXT 0
#define VIRT_IMAGE_BMP 1
#define VIRT_IMAGE_GIF 2
#define VIRT_TEXT_TXT  3
#define MAX_VIRT_FILES 4

#define IMAGE_BUFFER_SIZE 81920

static const char* TAG = "tftp_server";

static tftp_custom_file_t virt_file[] = {{.name = "index.txt", .data = NULL, .len = 0, .offset = 0},
                                         {.name = "virtImage.bmp", .data = NULL, .len = 0, .offset = 0},
                                         {.name = "virtImage.gif", .data = NULL, .len = 0, .offset = 0},
                                         {.name = "virtText.txt", .data = NULL, .len = 0, .offset = 0}};

static int str_cat_str(char* dest, size_t dest_size, const char* src) {
    size_t dest_len = strlen(dest);
    size_t src_len = strlen(src);
    if (dest_len + src_len > dest_size) {
        return -1;
    }
    memcpy(dest + dest_len, src, src_len);
    return 0;
}

static int str_cat(char* dest, size_t dest_size, char c) {
    size_t dest_len = strlen(dest);
    if (dest_len + 1 > dest_size) {
        return -1;
    }
    dest[dest_len] = c;
    dest[dest_len + 1] = '\0';
    return 0;
}

/**
 * @brief tftp custom file functions to set the offset and read the data
 * @param[in,out] handle Custom file handles
 * @param[in] offset The offset to set
 * @param[in] whence The origin of the offset
 */
void tftp_custom_fseek(tftp_custom_file_t* handle, size_t offset, int whence) {
    switch (whence) {
        case SEEK_SET:
            handle->offset = offset;
            break;
        case SEEK_CUR:
            handle->offset += offset;
            break;
        case SEEK_END:
            break;
    }
    if (handle->offset > handle->len) {
        handle->offset = handle->len;
    }
}

/**
 * @brief tftp custom file functions to read the data
 *        auto rolling over the offset
 *        if the bytes to read is bigger than the remaining bytes
 *        it will read the remaining bytes and set the bytes to 0
 * @param[out] buf The buffer to write the data to
 * @param[in] bytes The number of bytes to read
 * @param[in,out] handle Custom file handles
 */
size_t tftp_custom_fread(void* buf, size_t bytes, tftp_custom_file_t* handle) {
    if (handle->offset + bytes > handle->len) {
        bytes = handle->len - handle->offset;
    }
    memcpy(buf, handle->data + handle->offset, bytes);
    handle->offset += bytes;
    ((char*)buf)[bytes] = '\0';
    if (handle->offset > handle->len) {
        bytes = 0;
    }
    return bytes;
}

/**
 * @brief tftp custom file functions to write the data
 *        auto rolling over the offset
 *
 * @param buf The buffer to write the data to
 * @param bytes The number of bytes to write
 * @param handle The handle to the file to write to
 * @return The number of bytes written
 */
size_t tftp_custom_fwrite(const void* buf, size_t bytes, tftp_custom_file_t* handle) {
    if (handle->offset + bytes > handle->len) {
        bytes = handle->len - handle->offset;
    }
    memcpy(handle->data + handle->offset, buf, bytes);
    handle->offset += bytes;
    if (handle->offset > handle->len) {
        bytes = 0;
    }
    return bytes;
}

/**
 * @brief tftp helper functions
 */

/**
 * @brief This function is called when a file is opened
 *        It should return a handle to the file or NULL if the file does not exist
 *        The handle contains a ptr to or the actual file data or a virtual file
 *
 * @param fname The name of the file to open
 * @param mode 	Mode string from TFTP RFC
 * @param write Flag indicating read (0) or write (!= 0) access
 * @return void* File handle supplied to other functions
 */
void* tftp_open(const char* fname, const char* mode, uint8_t write) {
    LOG_INFO(TAG, "Opening %s", fname);

    UNUSED(mode);

    if (strcmp(fname, virt_file[VIRT_INDEX_TXT].name) == 0 && write == TFTP_READ) {
        tftp_custom_fseek(&virt_file[VIRT_INDEX_TXT], 0, SEEK_SET);
        return &virt_file[0];
    } else if (strcmp(fname, virt_file[VIRT_IMAGE_BMP].name) == 0 && write != TFTP_READ) {
        return &virt_file[VIRT_IMAGE_BMP];
    } else if (strcmp(fname, virt_file[VIRT_IMAGE_GIF].name) == 0 && write != TFTP_READ) {
        return &virt_file[VIRT_IMAGE_GIF];
    } else if (strcmp(fname, virt_file[VIRT_TEXT_TXT].name) == 0 && write != TFTP_READ) {
        return &virt_file[VIRT_TEXT_TXT];
    }

    return fopen(fname, write ? "wb" : "rb");
}

/**
 * @brief This function is called when a file is closed
 *
 * @param handle The handle to the file to close
 */
void tftp_close(void* handle) {
    LOG_INFO(TAG, "closing file");
    if (handle == NULL) {
        LOG_CRIT(TAG, "handle is null");
        return;
    }

    if (handle == &virt_file[VIRT_IMAGE_BMP]) {
        lcd_clear_images();
        lcd_clear_text();
        lcd_draw_bmp_img((uint8_t*)virt_file[VIRT_IMAGE_BMP].data, 0, 0);
    }

    if (handle == &virt_file[VIRT_IMAGE_GIF]) {
        lcd_clear_images();
        lcd_clear_text();
        lcd_draw_gif((uint8_t*)virt_file[VIRT_IMAGE_GIF].data, virt_file[VIRT_IMAGE_GIF].offset, 0, 0);
    }

    if (handle == &virt_file[VIRT_TEXT_TXT]) {
        lcd_clear_images();
        lcd_clear_text();
        lcd_display_text((uint8_t*)virt_file[VIRT_TEXT_TXT].data, 0, 0, LCD_COLOR_WHITE, LCD_TRANSPARENT, LCD_FONT16);
    }

    if (handle == &virt_file[VIRT_INDEX_TXT] || handle == &virt_file[VIRT_IMAGE_BMP]
        || handle == &virt_file[VIRT_IMAGE_GIF] || handle == &virt_file[VIRT_TEXT_TXT]) {
        ((tftp_custom_file_t*)handle)->offset = 0;
        return;
    }

    fclose((FILE*)handle);
}

/**
 * @brief This function is called when a file is read
 *        The virtual files are filtered out first
 *        then the file is trying to get read from the llfs
 *
 * @param handle File handle returned by open()
 * @param buf Target buffer to copy read data to
 * @param bytes Number of bytes to copy to buf
 * @return int >= 0: Success; < 0: Error
 */
int tftp_read(void* handle, void* buf, int bytes) {
    int ret = 0;
    LOG_INFO(TAG, "reading file");
    if (handle == NULL) {
        LOG_CRIT(TAG, "handle is null");
        return -1;
    }
    FILE* file = (FILE*)handle;

    if ((tftp_custom_file_t*)file == &virt_file[VIRT_INDEX_TXT]) {
        ret = (int)tftp_custom_fread(buf, (size_t)bytes, (tftp_custom_file_t*)file);
        return ret;
    } else if ((tftp_custom_file_t*)file == &virt_file[VIRT_IMAGE_BMP]) {
        LOG_CRIT(TAG, "Exception: Trying to read a write only file");
        return -1;
    } else if ((tftp_custom_file_t*)file == &virt_file[VIRT_IMAGE_GIF]) {
        LOG_CRIT(TAG, "Exception: Trying to read a write only file");
        return -1;
    } else if ((tftp_custom_file_t*)file == &virt_file[VIRT_TEXT_TXT]) {
        LOG_CRIT(TAG, "Exception: Trying to read a write only file");
        return -1;
    }

    ret = (int)fread(buf, sizeof(uint8_t), (size_t)bytes, file);
    if (ret <= 0) {
        return -1;
    }
    return ret;
}

/**
 * @brief This function is called when a file is written
 *
 * @param handle File handle returned by open()
 * @param p PBUF adjusted such that payload pointer points to the beginning of write data.
 * In other words, TFTP headers are stripped off.
 * @return int >= 0: Success; < 0: Error
 */
int tftp_write(void* handle, struct pbuf* p) {
    LOG_INFO(TAG, "Writing file");
    tftp_custom_file_t* file = (tftp_custom_file_t*)handle;
    if (file == &virt_file[VIRT_IMAGE_BMP] || file == &virt_file[VIRT_IMAGE_GIF] || file == &virt_file[VIRT_TEXT_TXT]) {
        return (int)tftp_custom_fwrite(p->payload, (size_t)(p->len), file);
    }
    return -1;
}

/**
 * @brief This function creates the file list for index.txt
 */
void init_index(void) {
    size_t len = 0;
    // Add len of the virt files to the size
    for (int i = 0; i < MAX_VIRT_FILES; i++) {
        len += strlen(virt_file[i].name) + 1;
    }
    void* mem = NULL;   // Pointer for internal use by the llfs library
    llfs_file_t* file;
    while ((file = llfs_next_file(&mem, NULL)) != NULL) {
        len += strlen(file->name) + 1;
    }
    len++; // +1 for the \0
    virt_file[VIRT_INDEX_TXT].data = calloc(len, sizeof(char));
    if (virt_file[VIRT_INDEX_TXT].data == NULL) {
        LOG_FATAL(TAG, "Could not allocate memory for index.txt");
        return;
    }
    virt_file[VIRT_INDEX_TXT].len = len;

    for (int i = 0; i < MAX_VIRT_FILES; i++) {
        str_cat_str(virt_file[VIRT_INDEX_TXT].data, len, virt_file[i].name);
        str_cat(virt_file[VIRT_INDEX_TXT].data, len, '\n');
    }

    mem = NULL;
    file = NULL;

    while ((file = llfs_next_file(&mem, NULL)) != NULL) {
        str_cat_str(virt_file[VIRT_INDEX_TXT].data, len, file->name);
        str_cat(virt_file[VIRT_INDEX_TXT].data, len, '\n');
    }
}

struct tftp_context tftpContext_s = {.open = tftp_open, .close = tftp_close, .read = tftp_read, .write = tftp_write};

/**
 * @brief Initialize tftp server
 */
void tftp_server_init(void) {
    LOG_INFO(TAG, "Initializing tftp server");
    // init the index.txt virt_file
    init_index();
    LOG_DEBUG(TAG, "index.txt: %s", virt_file[VIRT_INDEX_TXT].data);
    // init the virtImage.raw virt_file with 80kb of ram
    virt_file[VIRT_IMAGE_BMP].data = calloc(IMAGE_BUFFER_SIZE, sizeof(char));
    if (virt_file[VIRT_IMAGE_BMP].data == NULL) {
        LOG_FATAL(TAG, "Could not allocate memory for virtImage.bmp/virtImage.gif");
        return;
    }
    virt_file[VIRT_IMAGE_BMP].len = IMAGE_BUFFER_SIZE;

    virt_file[VIRT_IMAGE_GIF].data = virt_file[VIRT_IMAGE_BMP].data;
    virt_file[VIRT_IMAGE_GIF].len = virt_file[VIRT_IMAGE_BMP].len;

    virt_file[VIRT_TEXT_TXT].data = virt_file[VIRT_IMAGE_BMP].data;
    virt_file[VIRT_TEXT_TXT].len = virt_file[VIRT_IMAGE_BMP].len;

    // Init the tftp server
    if (tftp_init(&tftpContext_s) != ERR_OK) {
        LOG_FATAL(TAG, "Could not initialize tftp server");
        tftp_server_deinit();
        return;
    }
    LOG_INFO(TAG, "tftp server initialized successfully");
}

void tftp_server_deinit(void) {
    LOG_INFO(TAG, "Deinitializing tftp server");
    tftp_cleanup();
    LOG_INFO(TAG, "tftp server deinitialized successfully");
    free(virt_file[VIRT_INDEX_TXT].data);
    virt_file[VIRT_INDEX_TXT].data = NULL;
    virt_file[VIRT_INDEX_TXT].len = 0;
    virt_file[VIRT_INDEX_TXT].offset = 0;
    free(virt_file[VIRT_IMAGE_BMP].data);
    virt_file[VIRT_IMAGE_BMP].data = NULL;
    virt_file[VIRT_IMAGE_BMP].len = 0;

    virt_file[VIRT_IMAGE_GIF].data = NULL;
    virt_file[VIRT_IMAGE_GIF].len = 0;

    virt_file[VIRT_TEXT_TXT].data = NULL;
    virt_file[VIRT_TEXT_TXT].len = 0;
}
