#include "application_assets.h"
#include <toolbox/path.h>
#include <storage/storage_i.h>

// #define ELF_ASSETS_DEBUG_LOG 1

#ifndef ELF_ASSETS_DEBUG_LOG
#undef FURI_LOG_D
#define FURI_LOG_D(...)
#undef FURI_LOG_E
#define FURI_LOG_E(...)
#endif

#define FLIPPER_APPLICATION_ASSETS_MAGIC 0x4F4C5A44
#define FLIPPER_APPLICATION_ASSETS_VERSION 1
#define FLIPPER_APPLICATION_ASSETS_SIGNATURE_FILENAME ".assets.signature"

#define BUFFER_SIZE 512

#define TAG "FapAssets"

typedef struct FURI_PACKED {
    uint32_t magic;
    uint32_t version;
    uint32_t dirs_count;
    uint32_t files_count;
} FlipperApplicationAssetsHeader;

typedef enum {
    AssetsSignatureResultEqual,
    AssetsSignatureResultNotEqual,
    AssetsSignatureResultError,
} AssetsSignatureResult;

static FuriString* flipper_application_assets_alloc_app_full_path(FuriString* app_name) {
    furi_assert(app_name);
    FuriString* full_path = furi_string_alloc_set(APPS_ASSETS_PATH "/");
    furi_string_cat(full_path, app_name);
    return full_path;
}

static FuriString* flipper_application_assets_alloc_signature_file_path(FuriString* app_name) {
    furi_assert(app_name);
    FuriString* signature_file_path = flipper_application_assets_alloc_app_full_path(app_name);
    furi_string_cat(signature_file_path, "/" FLIPPER_APPLICATION_ASSETS_SIGNATURE_FILENAME);

    return signature_file_path;
}

static uint8_t* flipper_application_assets_alloc_and_load_data(File* file, size_t* size) {
    furi_assert(file);

    uint8_t* data = NULL;
    uint32_t length = 0;

    // read data length
    if(storage_file_read(file, &length, sizeof(length)) != sizeof(length)) {
        return NULL;
    }

    data = malloc(length);

    // read data
    if(storage_file_read(file, (void*)data, length) != length) {
        free((void*)data);
        return NULL;
    }

    if(size != NULL) {
        *size = length;
    }

    return data;
}

static bool flipper_application_assets_process_files(
    Storage* storage,
    File* file,
    FuriString* app_name,
    uint32_t files_count) {
    furi_assert(storage);
    furi_assert(file);
    furi_assert(app_name);

    UNUSED(storage);

    bool success = false;
    uint32_t length = 0;
    char* path = NULL;
    FuriString* file_path = furi_string_alloc();
    File* destination = storage_file_alloc(storage);

    FuriString* full_path = flipper_application_assets_alloc_app_full_path(app_name);

    for(uint32_t i = 0; i < files_count; i++) {
        path = (char*)flipper_application_assets_alloc_and_load_data(file, NULL);

        if(path == NULL) {
            break;
        }

        // read file size
        if(storage_file_read(file, &length, sizeof(length)) != sizeof(length)) {
            break;
        }

        furi_string_set(file_path, full_path);
        furi_string_cat(file_path, "/");
        furi_string_cat(file_path, path);

        if(!storage_file_open(
               destination, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
            FURI_LOG_E(TAG, "Can't create file: %s", furi_string_get_cstr(file_path));
            break;
        }

        // copy data to file
        if(!storage_file_copy_to_file(file, destination, length)) {
            FURI_LOG_E(TAG, "Can't copy data to file: %s", furi_string_get_cstr(file_path));
            break;
        }

        storage_file_close(destination);

        free(path);
        path = NULL;

        if(i == files_count - 1) {
            success = true;
        }
    }

    if(path != NULL) {
        free(path);
    }

    storage_file_free(destination);
    furi_string_free(file_path);

    return success;
}

static bool flipper_application_assets_process_dirs(
    Storage* storage,
    File* file,
    FuriString* app_name,
    uint32_t dirs_count) {
    furi_assert(storage);
    furi_assert(file);
    furi_assert(app_name);

    bool success = false;
    FuriString* full_path = flipper_application_assets_alloc_app_full_path(app_name);

    do {
        FuriString* dir_path = furi_string_alloc();
        char* path = NULL;

        for(uint32_t i = 0; i < dirs_count; i++) {
            path = (char*)flipper_application_assets_alloc_and_load_data(file, NULL);

            if(path == NULL) {
                break;
            }

            furi_string_set(dir_path, full_path);
            furi_string_cat(dir_path, "/");
            furi_string_cat(dir_path, path);

            if(!storage_simply_mkdir(storage, furi_string_get_cstr(dir_path))) {
                FURI_LOG_E(TAG, "Can't create directory: %s", furi_string_get_cstr(dir_path));
                break;
            }

            free(path);
            path = NULL;

            if(i == dirs_count - 1) {
                success = true;
            }
        }

        if(path != NULL) {
            free(path);
        }

        furi_string_free(dir_path);
    } while(false);

    furi_string_free(full_path);

    return success;
}

static AssetsSignatureResult flipper_application_assets_process_signature(
    Storage* storage,
    File* file,
    FuriString* app_name,
    uint8_t** signature_data,
    size_t* signature_data_size) {
    furi_assert(storage);
    furi_assert(file);
    furi_assert(app_name);
    furi_assert(signature_data);
    furi_assert(signature_data_size);

    AssetsSignatureResult result = AssetsSignatureResultError;
    File* signature_file = storage_file_alloc(storage);
    FuriString* signature_file_path =
        flipper_application_assets_alloc_signature_file_path(app_name);

    do {
        // read signature
        *signature_data =
            flipper_application_assets_alloc_and_load_data(file, signature_data_size);

        if(*signature_data == NULL) { //-V547
            FURI_LOG_E(TAG, "Can't read signature");
            break;
        }

        result = AssetsSignatureResultNotEqual;

        if(!storage_file_open(
               signature_file,
               furi_string_get_cstr(signature_file_path),
               FSAM_READ_WRITE,
               FSOM_OPEN_EXISTING)) {
            FURI_LOG_E(TAG, "Can't open signature file");
            break;
        }

        size_t signature_size = storage_file_size(signature_file);
        uint8_t* signature_file_data = malloc(signature_size);
        if(storage_file_read(signature_file, signature_file_data, signature_size) !=
           signature_size) {
            FURI_LOG_E(TAG, "Can't read signature file");
            free(signature_file_data);
            break;
        }

        if(memcmp(*signature_data, signature_file_data, signature_size) == 0) {
            FURI_LOG_D(TAG, "Assets signature is equal");
            result = AssetsSignatureResultEqual;
        }

        free(signature_file_data);
    } while(0);

    storage_file_free(signature_file);
    furi_string_free(signature_file_path);

    return result;
}

bool flipper_application_assets_load(File* file, const char* elf_path, size_t offset, size_t size) {
    UNUSED(size);
    furi_assert(file);
    furi_assert(elf_path);
    FlipperApplicationAssetsHeader header;
    bool result = false;
    Storage* storage = furi_record_open(RECORD_STORAGE);
    uint8_t* signature_data = NULL;
    size_t signature_data_size = 0;
    FuriString* app_name = furi_string_alloc();
    path_extract_filename_no_ext(elf_path, app_name);

    FURI_LOG_D(TAG, "Loading assets for %s", furi_string_get_cstr(app_name));

    FuriString* full_path = flipper_application_assets_alloc_app_full_path(app_name);

    do {
        if(!storage_file_seek(file, offset, true)) {
            break;
        }

        // read header
        if(storage_file_read(file, &header, sizeof(header)) != sizeof(header)) {
            break;
        }

        if(header.magic != FLIPPER_APPLICATION_ASSETS_MAGIC) {
            break;
        }

        if(header.version != FLIPPER_APPLICATION_ASSETS_VERSION) {
            break;
        }

        // process signature
        AssetsSignatureResult signature_result = flipper_application_assets_process_signature(
            storage, file, app_name, &signature_data, &signature_data_size);

        if(signature_result == AssetsSignatureResultError) {
            FURI_LOG_E(TAG, "Assets signature error");
            break;
        } else if(signature_result == AssetsSignatureResultEqual) {
            FURI_LOG_D(TAG, "Assets signature equal, skip loading");
            result = true;
            break;
        } else {
            FURI_LOG_D(TAG, "Assets signature not equal, loading");

            // remove old assets
            FuriString* full_path = flipper_application_assets_alloc_app_full_path(app_name);
            storage_simply_remove_recursive(storage, furi_string_get_cstr(full_path));
            furi_string_free(full_path);

            FURI_LOG_D(TAG, "Assets removed");
        }

        if(!storage_simply_mkdir(storage, APPS_ASSETS_PATH)) {
            break;
        }

        if(!storage_simply_mkdir(storage, furi_string_get_cstr(full_path))) {
            break;
        }

        // process directories
        if(header.dirs_count &&
           !flipper_application_assets_process_dirs(storage, file, app_name, header.dirs_count)) {
            break;
        }

        // process files
        if(header.files_count && !flipper_application_assets_process_files(
                                     storage, file, app_name, header.files_count)) {
            break;
        }

        // write signature
        FuriString* signature_file_path =
            flipper_application_assets_alloc_signature_file_path(app_name);
        File* signature_file = storage_file_alloc(storage);

        if(storage_file_open(
               signature_file,
               furi_string_get_cstr(signature_file_path),
               FSAM_WRITE,
               FSOM_CREATE_ALWAYS)) {
            storage_file_write(signature_file, signature_data, signature_data_size);
        }

        storage_file_free(signature_file);
        furi_string_free(signature_file_path);

        result = true;
    } while(false);

    if(signature_data != NULL) {
        free(signature_data);
    }

    furi_record_close(RECORD_STORAGE);
    furi_string_free(full_path);
    furi_string_free(app_name);

    FURI_LOG_D(TAG, "Assets loading %s", result ? "success" : "failed");

    return result;
}