/**
 * @file lv_fs_uefi.c
 *
 */

/*********************
 *      INCLUDES
 *********************/
#include "../../../lvgl.h"

#if LV_USE_FS_UEFI && LV_USE_UEFI

#include "../../drivers/uefi/lv_uefi_private.h"

#include "../../core/lv_global.h"

/*********************
 *      DEFINES
 *********************/
#if LV_FS_UEFI_LETTER == '\0'
    #error "LV_FS_UEFI_LETTER must be set to a valid value"
#else
    #if (LV_FS_UEFI_LETTER < 'A') || (LV_FS_UEFI_LETTER > 'Z')
        #if LV_FS_DEFAULT_DRIVE_LETTER != '\0' /* When using default driver-identifier letter, strict format (X:) is mandatory. */
            #error "LV_FS_UEFI_LETTER must be an upper case ASCII letter"
        #else /*Lean rules for backward compatibility*/
            #warning LV_FS_UEFI_LETTER should be an upper case ASCII letter. \
            Using a slash symbol as driver - identifier letter should be replaced with LV_FS_DEFAULT_DRIVE_LETTER mechanism.
        #endif
    #endif
#endif

/**********************
 *      TYPEDEFS
 **********************/

typedef struct _lv_uefi_fs_file_context_t {
    EFI_FILE_PROTOCOL * interface;
} lv_uefi_fs_file_context_t;

/**********************
 *  STATIC PROTOTYPES
 **********************/

static void lv_fs_drv_uefi_init(lv_fs_drv_t * drv, char fs_drive_letter, EFI_HANDLE fs_handle);
static void lv_fs_drv_uefi_deinit(lv_fs_drv_t * drv);

static void lv_fs_uefi_lvgl_path_to_uefi_path(CHAR16 * path);
static void lv_fs_uefi_uefi_path_to_lvgl_path(CHAR16 * path);
static bool lv_fs_uefi_is_dot_path(CONST CHAR16 * path);
static bool lv_fs_uefi_is_dir(EFI_FILE_PROTOCOL * dir);
static bool lv_fs_uefi_is_file(EFI_FILE_PROTOCOL * file);
static EFI_FILE_INFO * lv_fs_uefi_get_info(EFI_FILE_PROTOCOL * file);

static bool lv_fs_uefi_ready_cb(lv_fs_drv_t * drv);

static void * lv_fs_uefi_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode);
static lv_fs_res_t lv_fs_uefi_close_cb(lv_fs_drv_t * drv, void * file_p);
static lv_fs_res_t lv_fs_uefi_read_cb(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br);
static lv_fs_res_t lv_fs_uefi_write_cb(lv_fs_drv_t * drv, void * file_p, const void * buf, uint32_t btw, uint32_t * bw);
static lv_fs_res_t lv_fs_uefi_seek_cb(lv_fs_drv_t * drv, void * file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t lv_fs_uefi_tell_cb(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p);

static void * lv_fs_uefi_dir_open_cb(lv_fs_drv_t * drv, const char * path);
static lv_fs_res_t lv_fs_uefi_dir_read_cb(lv_fs_drv_t * drv, void * rddir_p, char * fn, uint32_t fn_len);
static lv_fs_res_t lv_fs_uefi_dir_close_cb(lv_fs_drv_t * drv, void * rddir_p);

/**********************
 *  STATIC VARIABLES
 **********************/

static EFI_GUID _uefi_guid_simple_file_system = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
static EFI_GUID _uefi_guid_loaded_image = EFI_LOADED_IMAGE_PROTOCOL_GUID;
static EFI_GUID _uefi_guid_file_info = EFI_FILE_INFO_ID;

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

/**
 * Register a driver for the File system interface
 */
void lv_fs_uefi_init(void)
{
    EFI_LOADED_IMAGE_PROTOCOL * interface_loaded_image = NULL;
    EFI_HANDLE fs_handle = NULL;

    /*---------------------------------------------------
     * Register the file system interface in LVGL
     *--------------------------------------------------*/

    interface_loaded_image = lv_uefi_protocol_open(gLvEfiImageHandle, &_uefi_guid_loaded_image);
    LV_ASSERT_NULL(interface_loaded_image);

    fs_handle = interface_loaded_image->DeviceHandle;

    if(fs_handle == NULL) return;

    lv_uefi_protocol_close(gLvEfiImageHandle, &_uefi_guid_loaded_image);

    /*Add a simple driver to open images*/
    lv_fs_drv_t * fs_drv_p = &(LV_GLOBAL_DEFAULT()->uefi_fs_drv);
    lv_fs_drv_uefi_init(fs_drv_p, LV_FS_UEFI_LETTER, fs_handle);

    lv_fs_drv_register(fs_drv_p);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

static bool lv_fs_uefi_ready_cb(lv_fs_drv_t * drv)
{
    EFI_HANDLE fs_handle = (EFI_HANDLE)drv->user_data;

    return fs_handle != NULL;
}

static void * lv_fs_uefi_open_cb(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode)
{
    EFI_STATUS status;
    EFI_FILE_PROTOCOL * fs_root = NULL;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL * fs_interface = NULL;
    CHAR16 path_ucs2[LV_FS_MAX_PATH_LENGTH + 1];
    lv_uefi_fs_file_context_t * file_ctx = NULL;
    UINT64 uefi_mode = 0;

    EFI_HANDLE fs_handle = (EFI_HANDLE)drv->user_data;

    fs_interface = lv_uefi_protocol_open(fs_handle, &_uefi_guid_simple_file_system);
    if(fs_interface == NULL) {
        LV_LOG_WARN("[lv_uefi] Unable to open file system protocol.");
        goto error;
    }

    status = fs_interface->OpenVolume(fs_interface, &fs_root);
    if(status != EFI_SUCCESS) {
        LV_LOG_WARN("[lv_uefi] Unable to open file system root.");
        goto error;
    }

    if(lv_uefi_ascii_to_ucs2(path, path_ucs2, LV_FS_MAX_PATH_LENGTH + 1) == 0) {
        LV_LOG_WARN("[lv_uefi] Unable to convert the ASCII path into an UCS-2 path.");
        goto error;
    }

    lv_fs_uefi_lvgl_path_to_uefi_path(path_ucs2);

    file_ctx = lv_calloc(1, sizeof(lv_uefi_fs_file_context_t));
    LV_ASSERT_MALLOC(file_ctx);

    if(mode == LV_FS_MODE_WR) {
        uefi_mode = EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE;
    }
    else {
        uefi_mode = EFI_FILE_MODE_READ;
    }

    status = fs_root->Open(
                 fs_root,
                 &file_ctx->interface,
                 path_ucs2,
                 uefi_mode,
                 0);
    if(status != EFI_SUCCESS) {
        LV_LOG_WARN("[lv_uefi] Unable to open file '%s'.", path);
        goto error;
    }

    if(!lv_fs_uefi_is_file(file_ctx->interface)) {
        goto error;
    }

    goto finish;

error:
    if(file_ctx != NULL) {
        if(file_ctx->interface != NULL) file_ctx->interface->Close(file_ctx->interface);
        lv_free(file_ctx);
        file_ctx = NULL;
    }

finish:
    if(fs_interface != NULL) lv_uefi_protocol_close(fs_handle, &_uefi_guid_simple_file_system);
    if(fs_root != NULL) fs_root->Close(fs_root);

    return file_ctx;
}

static lv_fs_res_t lv_fs_uefi_close_cb(lv_fs_drv_t * drv, void * file_p)
{
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)file_p;

    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    status = file_ctx->interface->Close(file_ctx->interface);
    if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

    lv_free(file_ctx);

    return LV_FS_RES_OK;
}

static lv_fs_res_t lv_fs_uefi_read_cb(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br)
{
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)file_p;
    UINTN buf_size = btr;

    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    status = file_ctx->interface->Read(
                     file_ctx->interface,
                     &buf_size,
                     buf);
    if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

    *br = (uint32_t) buf_size;

    return LV_FS_RES_OK;
}

static lv_fs_res_t lv_fs_uefi_write_cb(lv_fs_drv_t * drv, void * file_p, const void * buf, uint32_t btw, uint32_t * bw)
{
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)file_p;
    UINTN buf_size = btw;

    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    status = file_ctx->interface->Write(
                     file_ctx->interface,
                     &buf_size,
                     (VOID *)buf);
    if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

    file_ctx->interface->Flush(file_ctx->interface);

    *bw = (uint32_t) buf_size;

    return LV_FS_RES_OK;
}

static lv_fs_res_t lv_fs_uefi_seek_cb(lv_fs_drv_t * drv, void * file_p, uint32_t pos, lv_fs_whence_t whence)
{
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)file_p;
    UINT64 new_pos;

    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    if(whence == LV_FS_SEEK_END) {
        status = file_ctx->interface->SetPosition(
                         file_ctx->interface,
                         UINT64_MAX);
        if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

        status = file_ctx->interface->GetPosition(
                         file_ctx->interface,
                         &new_pos);
        if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

        if(new_pos < pos) {
            new_pos = 0;
        }
        else {
            new_pos -= pos;
        }
    }
    else if(whence == LV_FS_SEEK_SET) {
        new_pos = pos;
    }
    else if(whence == LV_FS_SEEK_CUR) {
        status = file_ctx->interface->GetPosition(
                         file_ctx->interface,
                         &new_pos);
        if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

        new_pos += pos;
    }
    else {
        return LV_FS_RES_INV_PARAM;
    }

    status = file_ctx->interface->SetPosition(
                     file_ctx->interface,
                     new_pos);
    if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

    return LV_FS_RES_OK;
}

static lv_fs_res_t lv_fs_uefi_tell_cb(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p)
{
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)file_p;
    UINT64 pos;

    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    status = file_ctx->interface->GetPosition(
                     file_ctx->interface,
                     &pos);
    if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

    if(pos > UINT32_MAX) return LV_FS_RES_UNKNOWN;

    *pos_p = (uint32_t) pos;

    return LV_FS_RES_OK;
}

static void * lv_fs_uefi_dir_open_cb(lv_fs_drv_t * drv, const char * path)
{
    EFI_STATUS status;
    EFI_FILE_PROTOCOL * fs_root = NULL;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL * fs_interface = NULL;
    CHAR16 path_ucs2[LV_FS_MAX_PATH_LENGTH + 1];
    lv_uefi_fs_file_context_t * file_ctx = NULL;
    UINT64 mode = 0;
    UINT64 attributes = 0;

    EFI_HANDLE fs_handle = (EFI_HANDLE)drv->user_data;

    fs_interface = lv_uefi_protocol_open(fs_handle, &_uefi_guid_simple_file_system);
    if(fs_interface == NULL) {
        LV_LOG_WARN("[lv_uefi] Unable to open file system protocol.");
        goto error;
    }

    status = fs_interface->OpenVolume(fs_interface, &fs_root);
    if(status != EFI_SUCCESS) {
        LV_LOG_WARN("[lv_uefi] Unable to open file system root.");
        goto error;
    }

    if(path != NULL && path[0] != '\0') {
        if(lv_uefi_ascii_to_ucs2(path, path_ucs2, LV_FS_MAX_PATH_LENGTH + 1) == 0) {
            LV_LOG_WARN("[lv_uefi] Unable to convert the ASCII path into an UCS-2 path.");
            goto error;
        }
    }
    else {
        path_ucs2[0] = '\\';
        path_ucs2[1] = '\0';
    }

    lv_fs_uefi_lvgl_path_to_uefi_path(path_ucs2);

    file_ctx = lv_calloc(1, sizeof(lv_uefi_fs_file_context_t));
    LV_ASSERT_MALLOC(file_ctx);

    mode = EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE;
    attributes = EFI_FILE_DIRECTORY;

    status = fs_root->Open(
                 fs_root,
                 &file_ctx->interface,
                 path_ucs2,
                 mode,
                 attributes);
    if(status != EFI_SUCCESS) {
        LV_LOG_WARN("[lv_uefi] Unable to open directory '%s'.", path);
        goto error;
    }

    if(!lv_fs_uefi_is_dir(file_ctx->interface)) {
        goto error;
    }

    goto finish;

error:
    if(file_ctx != NULL) {
        if(file_ctx->interface != NULL) {
            file_ctx->interface->Close(file_ctx->interface);
        }
        lv_free(file_ctx);
        file_ctx = NULL;
    }

finish:
    if(fs_interface != NULL) lv_uefi_protocol_close(fs_handle, &_uefi_guid_simple_file_system);
    if(fs_root != NULL) fs_root->Close(fs_root);

    return file_ctx;
}

static lv_fs_res_t lv_fs_uefi_dir_read_cb(lv_fs_drv_t * drv, void * rddir_p, char * fn, uint32_t fn_len)
{
    lv_fs_res_t return_code;
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)rddir_p;

    EFI_FILE_INFO * info = NULL;
    UINTN size;

    CONST CHAR16 * fn_ucs2;

    if(fn == NULL || fn_len == 0) return LV_FS_RES_INV_PARAM;
    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    // skip . and ..
    do {
        if(info != NULL) lv_free(info);
        info = NULL;
        size = 0;
        status = file_ctx->interface->Read(
                         file_ctx->interface,
                         &size,
                         info);
        if(status == EFI_SUCCESS && size == 0) {
            return_code = LV_FS_RES_OK;
            *fn = '\0';
            goto finish;
        }
        else if(status != EFI_BUFFER_TOO_SMALL) {
            return_code = LV_FS_RES_NOT_EX;
            goto error;
        }

        info = lv_calloc(1, size);
        LV_ASSERT_MALLOC(info);

        status = file_ctx->interface->Read(
                         file_ctx->interface,
                         &size,
                         info);
        if(status != EFI_SUCCESS) {
            return_code = LV_FS_RES_HW_ERR;
            goto error;
        }
    } while(lv_fs_uefi_is_dot_path(info->FileName));

    lv_fs_uefi_uefi_path_to_lvgl_path(info->FileName);

    // skip leading \ and /
    for(fn_ucs2 = info->FileName; *fn_ucs2 != L'\0'; fn_ucs2++) {
        if(*fn_ucs2 != L'\\' && *fn_ucs2 != L'/') {
            break;
        }
    }

    if((info->Attribute & EFI_FILE_DIRECTORY) != 0) {
        if(fn_len == 0) {
            return_code = LV_FS_RES_UNKNOWN;
            goto error;
        }
        fn[0] = '/';
        fn++;
        fn_len--;
    }

    if(lv_uefi_ucs2_to_ascii(fn_ucs2, fn, fn_len) == 0) {
        LV_LOG_WARN("[lv_uefi] Unable to convert the UCS-2 path into an ascii path.");
        return_code = LV_FS_RES_UNKNOWN;
        goto error;
    }

    return_code = LV_FS_RES_OK;
    goto finish;

error:

finish:
    if(info) lv_free(info);

    return return_code;
}

static lv_fs_res_t lv_fs_uefi_dir_close_cb(lv_fs_drv_t * drv, void * rddir_p)
{
    EFI_STATUS status;
    lv_uefi_fs_file_context_t * file_ctx = (lv_uefi_fs_file_context_t *)rddir_p;

    if(file_ctx == NULL || file_ctx->interface == NULL) return LV_FS_RES_INV_PARAM;

    status = file_ctx->interface->Close(file_ctx->interface);
    if(status != EFI_SUCCESS) return LV_FS_RES_HW_ERR;

    lv_free(file_ctx);

    return LV_FS_RES_OK;
}

static void lv_fs_drv_uefi_init(lv_fs_drv_t * drv, char fs_drive_letter, EFI_HANDLE fs_handle)
{
    LV_ASSERT_NULL(drv);
    LV_ASSERT_NULL(fs_handle);

    lv_fs_drv_init(drv);

    drv->letter = fs_drive_letter;
    drv->cache_size = 0;

    drv->ready_cb = lv_fs_uefi_ready_cb;
    drv->open_cb = lv_fs_uefi_open_cb;
    drv->close_cb = lv_fs_uefi_close_cb;
    drv->read_cb = lv_fs_uefi_read_cb;
    drv->write_cb = lv_fs_uefi_write_cb;
    drv->seek_cb = lv_fs_uefi_seek_cb;
    drv->tell_cb = lv_fs_uefi_tell_cb;

    drv->dir_open_cb = lv_fs_uefi_dir_open_cb;
    drv->dir_read_cb = lv_fs_uefi_dir_read_cb;
    drv->dir_close_cb = lv_fs_uefi_dir_close_cb;

    drv->user_data = (void *) fs_handle;
}

static void lv_fs_drv_uefi_deinit(lv_fs_drv_t * drv)
{
    LV_ASSERT_NULL(drv);
    drv->user_data = NULL;
}

static void lv_fs_uefi_lvgl_path_to_uefi_path(CHAR16 * path)
{
    if(path == NULL) return;

    for(; *path != '\0'; path++) {
        if(*path == L'/') *path = L'\\';
    }
}

static void lv_fs_uefi_uefi_path_to_lvgl_path(CHAR16 * path)
{
    if(path == NULL) return;

    for(; *path != '\0'; path++) {
        if(*path == L'\\') *path = L'/';
    }
}

static bool lv_fs_uefi_is_dot_path(CONST CHAR16 * path)
{
    if(path == NULL) return FALSE;

    if(path[0] == L'.' && path[1] == L'\0') return TRUE;
    if(path[0] == L'.' && path[1] == L'.' && path[2] == L'\0') return TRUE;

    return FALSE;
}

static bool lv_fs_uefi_is_dir(EFI_FILE_PROTOCOL * dir)
{
    UINT64 attributes;

    if(dir == NULL) return FALSE;

    EFI_FILE_INFO * info = lv_fs_uefi_get_info(dir);
    if(info == NULL) return FALSE;

    attributes = info->Attribute;
    lv_free(info);

    return (attributes & EFI_FILE_DIRECTORY) != 0;
}

static bool lv_fs_uefi_is_file(EFI_FILE_PROTOCOL * file)
{
    UINT64 attributes;

    if(file == NULL) return FALSE;

    EFI_FILE_INFO * info = lv_fs_uefi_get_info(file);
    if(info == NULL) return FALSE;

    attributes = info->Attribute;
    lv_free(info);

    return (attributes & EFI_FILE_DIRECTORY) == 0;
}

static EFI_FILE_INFO * lv_fs_uefi_get_info(EFI_FILE_PROTOCOL * file)
{
    EFI_STATUS status;
    EFI_FILE_INFO * info = NULL;
    UINTN size = 0;

    status = file->GetInfo(file, &_uefi_guid_file_info, &size, info);
    if(status != EFI_BUFFER_TOO_SMALL) return NULL;

    info = lv_calloc(1, size);
    LV_ASSERT_MALLOC(info);

    status = file->GetInfo(file, &_uefi_guid_file_info, &size, info);
    if(status != EFI_SUCCESS) {
        lv_free(info);
        return NULL;
    }

    return info;
}

#else /* LV_FS_UEFI_LETTER == 0*/

#if defined(LV_FS_UEFI_LETTER) && LV_FS_UEFI_LETTER != '\0'
    #warning "LV_FS_UEFI is not enabled but LV_FS_UEFI_LETTER is set"
#endif

#endif
