/**
 * @file llfs.c
 * @brief Linked List Filesystem implementation (llfs)
 * @author Lorenz C.
 * @version 0.1.1
 */

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#define LOGGER_LEVEL_WARN
#include "log.h"
#include "llfs.h"

/**
 * @brief The maximum number of files that can be opened concurrently using the POSIX API
 */
#define POSIX_MAX_FILES 10

extern struct llfs_data_file* llfs_root;
const char* TAG = "llfs";
static size_t file_count = 0;   // Cache for the number of files in the filesystem
static FILE* file_table[POSIX_MAX_FILES];

static int new_file_table_entry(void);
static int free_file_table_entry(int file_id);
static FILE* file_id_to_stream(int file_id);
static uint8_t file_ext_cmp(const char* filename, const char* ext);

int8_t llfs_init(void) {
    LOG_DEBUG(TAG, "Initializing llfs");

    // Initialize the file table
    for (int i = 0; i < POSIX_MAX_FILES; i++) {
        file_table[i] = NULL;
    }

    // Add stdin, stdout and stderr to the file table
    file_table[STDIN_FILENO] = stdin;
    file_table[STDOUT_FILENO] = stdout;
    file_table[STDERR_FILENO] = stderr;

    // Calculate the number of files in the filesystem and cache it
    const struct llfs_data_file* file = llfs_root;
    file_count = 0;
    while (file != NULL) {
        file_count++;
        file = file->next;
    }

    return 0;
}

size_t llfs_file_list(llfs_file_t* file_list, size_t max_files, char* filter) {
    size_t count = 0;                              // Number of files found
    const struct llfs_data_file* file = llfs_root; // Pointer to the current file

    if (filter != NULL) {
        LOG_DEBUG(TAG, "Filtering files with filter: %s", filter);

        if (filter[0] == '*') {
            filter++;
        }
    }

    // Iterate over all files in the filesystem
    while (file != NULL && count < max_files) {
        // Filter out files with a filename that does not match the filter
        if (filter != NULL) {
            if (!file_ext_cmp(file->name, filter)) {
                file = file->next;
                continue;
            }
        }

        // Add the file to the file list
        file_list[count].data = file->data;
        file_list[count].name = file->name;
        file_list[count].len = file->len;

        // Move to the next file
        count++;
        file = file->next;
    }

    LOG_DEBUG(TAG, "Files found: %d", count);
    return count;
}

llfs_file_t* llfs_file_open(const char* name) {
    const struct llfs_data_file* file = llfs_root;

    LOG_DEBUG(TAG, "Opening file: %s", name);

    while (file != NULL) {
        if (strcmp(file->name, name) == 0) {
            LOG_DEBUG(TAG, "File found: %s, size: %d", file->name, file->len);
            return (llfs_file_t*)file;
        }
        file = file->next;
    }

    LOG_DEBUG(TAG, "File not found: %s", name);
    return NULL;
}

llfs_file_t* llfs_next_file(void** mem, char* filter) {
    struct llfs_data_file* prev_file = (struct llfs_data_file*)*mem;
    uint8_t filter_ok = 0;

    if (prev_file == NULL) {
        prev_file = llfs_root;
    } else {
        prev_file = (struct llfs_data_file*)prev_file->next;
    }

    // If a filter is specified, only return files that match the filter
    if (filter != NULL) {
        LOG_DEBUG(TAG, "Filtering files with filter: %s", filter);

        // Remove the '*' from the filter
        if (filter[0] == '*') {
            filter++;
        }

        while (prev_file != NULL) {
            if (file_ext_cmp(prev_file->name, filter)) {
                filter_ok = 1;
                break;
            }
            prev_file = (struct llfs_data_file*)prev_file->next;
        }
    }

    *mem = (void*)prev_file;
    return (llfs_file_t*)prev_file;
}

size_t llfs_file_count(void) {
    return file_count;
}

/**
 * @brief Newlib open implementation
 *
 * @param path
 * @param flags
 * @param mode
 * @return
 */
int _open(char* path, int flags, int mode) {
    int file_id;
    FILE* stream;
    errno = 0;
    llfs_file_t* llfs_file;

    // Add a new entry to the file table
    file_id = new_file_table_entry();
    if (file_id < 0) {
        LOG_WARN(TAG, "Failed to add new file table entry. %s", strerror(errno));
        return -1;
    }

    // Get the stream associated with the file id
    stream = file_id_to_stream(file_id);
    if (stream == NULL) {
        LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno));
        free_file_table_entry(file_id);
        return -1;
    }

    // Get the file from the llfs filesystem
    llfs_file = llfs_file_open(path);
    if (llfs_file == NULL) {
        LOG_DEBUG(TAG, "Failed to open file: %s", path);
        free_file_table_entry(file_id);
        return -1;
    }

    // Initialize the stream
    stream->_cookie = (void*)llfs_file;
    stream->_offset = 0;
    stream->_flags = __SRD;

    return file_id;
}

/**
 * @brief Newlib close implementation
 *
 * @param file_id
 * @return
 */
int _close(int file_id) {
    FILE* stream;

    // Get the stream associated with the file id
    stream = file_id_to_stream(file_id);
    if (stream == NULL) {
        LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno));
        return -1;
    }

    // Remove the entry from the file table
    if (free_file_table_entry(file_id) < 0) {
        LOG_WARN(TAG, "Failed to remove file table entry. %s", strerror(errno));
        return -1;
    }

    return 0;
}

/**
 * @brief Newlib read implementation
 *
 * @param file_id
 * @param ptr
 * @param len
 * @return
 */
size_t _read(int file_id, char* ptr, int len) {
    FILE* stream;
    llfs_file_t* llfs_file;
    size_t bytes_read;

    // Read from stdin, stdout and stderr is not supported
    if (file_id == STDIN_FILENO || file_id == STDOUT_FILENO || file_id == STDERR_FILENO) {
        LOG_DEBUG(TAG, "Trying to read from stdin, stdout or stderr: %d", file_id);
        return -1;
    }

    // Get the stream associated with the file id
    stream = file_id_to_stream(file_id);
    if (stream == NULL) {
        LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno));
        return -1;
    }

    // Get the file from the llfs filesystem associated with the stream
    llfs_file = (llfs_file_t*)stream->_cookie;
    if (llfs_file == NULL) {
        LOG_WARN(TAG, "Failed to get llfs file associated with stream: %d", file_id);
        return -1;
    }

    // Calculate the number of bytes to read (limited by the file size)
    bytes_read = llfs_file->len - stream->_offset;
    if (bytes_read > len) {
        bytes_read = len;
    } else if (bytes_read == 0) { // End of file
        stream->_flags |= __SEOF;
        return 0;
    }

    // Copy the data over to the dst buffer
    memcpy(ptr, llfs_file->data + stream->_offset, bytes_read);

    stream->_offset += (off_t)bytes_read;
    return bytes_read;
}

/**
 * @brief Newlib isatty implementation
 *
 * @param file
 * @return 1 if the file is stdin, stdout or stderr, 0 otherwise
 */
int isatty(int file) {
    if (file == STDIN_FILENO || file == STDOUT_FILENO || file == STDERR_FILENO) {
        return 1;
    }

    return 0;
}

/**
 * @brief Newlib lseek implementation
 *
 * @param file
 * @param ptr
 * @param dir
 * @return
 */
off_t _lseek(int file, int ptr, int dir) {
    FILE* stream;

    if (file == STDIN_FILENO || file == STDOUT_FILENO || file == STDERR_FILENO) {
        LOG_DEBUG(TAG, "Trying to seek stdin, stdout or stderr: %d", file);
        return -1;
    }

    stream = file_id_to_stream(file);
    if (stream == NULL) {
        LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno));
        return -1;
    }

    switch (dir) {
        case SEEK_SET:
            stream->_offset = ptr;
            break;
        case SEEK_CUR:
            stream->_offset += ptr;
            break;
        case SEEK_END:
            stream->_offset = (off_t)((llfs_file_t*)stream->_cookie)->len + ptr;
            break;
        default:
            LOG_WARN(TAG, "Invalid seek direction: %d", dir);
            return -1;
    }

    return stream->_offset;
}

/**
 * @brief Newlib fstat implementation
 *
 * @param[in] file
 * @param[out] st
 * @return
 */
int _fstat(int file, struct stat* st) {
    FILE* stream;
    llfs_file_t *llfs_file;

    // Check if the file is stdin, stdout or stderr
    if (file == STDIN_FILENO || file == STDOUT_FILENO || file == STDERR_FILENO) {
        st->st_mode = S_IFCHR; // Character special file
        return 0;
    }

    // Get the stream associated with the file id
    stream = file_id_to_stream(file);
    if (stream == NULL) {
        LOG_WARN(TAG, "Failed to get file table entry. %s", strerror(errno));
        return -1;
    }

    // Get the file from the llfs filesystem associated with the stream
    llfs_file = (llfs_file_t*)stream->_cookie;
    if (llfs_file == NULL) {
        LOG_WARN(TAG, "Failed to get llfs file associated with stream: %d", file);
        return -1;
    }

    st->st_mode = S_IFREG;  // Regular file
    st->st_size = (off_t)llfs_file->len;
    return 0;
}

/**
 * @brief Create a new entry in the file table
 *
 * @return The file id or -1 if an error occurred. See errno for more information.
 */
static int new_file_table_entry(void) {
    FILE* stream;

    // Try to find an empty entry in the file table
    for (int i = 0; i < POSIX_MAX_FILES; i++) {
        if (file_table[i] == NULL) {
            stream = (FILE*)malloc(sizeof(FILE));
            if (stream == NULL) {
                errno = ENOMEM; // Out of memory
                return -1;
            }
            file_table[i] = stream;
            return i;
        }
    }

    // No empty entry found
    errno = ENFILE; // Too many open files
    return -1;
}

/**
 * @brief Remove an entry from the file table
 *
 * @param[in] file_id The file id to remove
 * @return 0 if successful, -1 if an error occurred. See errno for more information.
 */
static int free_file_table_entry(int file_id) {
    // Check if the file id is valid
    if (file_id < 0 || file_id >= POSIX_MAX_FILES) {
        errno = EBADF; // Bad file number
        return -1;
    }

    // Remove the entry from the file table
    free(file_table[file_id]);
    file_table[file_id] = NULL;

    return 0;
}

/**
 * @brief Get the stream associated with a file id
 *
 * @param[in] file_id The file id to get the stream for
 * @return The stream or NULL if an error occurred. See errno for more information.
 */
static FILE* file_id_to_stream(int file_id) {
    if (file_id < 0 || file_id >= POSIX_MAX_FILES) {
        errno = EBADF; // Bad file number
        return NULL;
    }
    return file_table[file_id];
}

/**
 * @brief Check if a filename ends with a given extension
 *
 * @param[in] filename The filename to check
 * @param[in] ext The extension to check for
 * @return 1 if the filename ends with the extension, 0 otherwise
 */
static uint8_t file_ext_cmp(const char* const filename, const char* const ext) {
    uint8_t ext_len = strlen(ext);
    uint8_t filename_len = strlen(filename);

    if (filename_len < ext_len) {
        return 0;
    }

    // Compare backwards
    for (uint8_t i = 0; i < ext_len; i++) {
        if (filename[filename_len - i] != ext[ext_len - i]) {
            return 0;
        }
    }
    return 1;
}
