#include "loader.h"
#include "loader_i.h"
#include <applications.h>
#include <storage/storage.h>
#include <furi_hal.h>
#include <assets_icons.h>

#include <dialogs/dialogs.h>
#include <toolbox/path.h>
#include <flipper_application/flipper_application.h>
#include <loader/firmware_api/firmware_api.h>

#include <momentum/asset_packs.h>

#define TAG "Loader"

#define LOADER_MAGIC_THREAD_VALUE 0xDEADBEEF

// helpers

static const char* loader_find_external_application_by_name(const char* app_name) {
    for(size_t i = 0; i < FLIPPER_EXTERNAL_APPS_COUNT; i++) {
        if(strcmp(FLIPPER_EXTERNAL_APPS[i].name, app_name) == 0) {
            return FLIPPER_EXTERNAL_APPS[i].path;
        }
    }

    for(size_t i = 0; i < FLIPPER_SETTINGS_APPS_COUNT; i++) {
        if(strcmp(FLIPPER_SETTINGS_APPS[i].name, app_name) == 0) {
            return FLIPPER_SETTINGS_APPS[i].path;
        }
    }

    return NULL;
}

// API

static LoaderMessageLoaderStatusResult loader_start_internal(
    Loader* loader,
    const char* name,
    const char* args,
    FuriString* error_message) {
    LoaderMessage message;
    LoaderMessageLoaderStatusResult result;

    message.type = LoaderMessageTypeStartByName;
    message.start.name = name;
    message.start.args = args;
    message.start.error_message = error_message;
    message.api_lock = api_lock_alloc_locked();
    message.status_value = &result;
    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
    api_lock_wait_unlock_and_free(message.api_lock);

    return result;
}

typedef struct {
    const char* error;
    const char* description;
    const char* url;
    const Icon* icon;
} LoaderError;

static const LoaderError err_app_not_found =
    {"App Not Found", "Update firmware or app", "err_01", &I_err_01};
static const LoaderError err_invalid_flie = {"Invalid File", "Update the app", "err_02", &I_err_02};
static const LoaderError err_invalid_manifest =
    {"Invalid Manifest", "Update firmware or app", "err_03", &I_err_03};
static const LoaderError err_missing_imports =
    {"Missing Imports", "Update app or firmware", "err_04", &I_err_04};
static const LoaderError err_hw_target_mismatch =
    {"HW Target\nMismatch", "App not supported", "err_05", &I_err_05};
static const LoaderError err_outdated_app = {"Outdated App", "Update the app", "err_06", &I_err_06};
static const LoaderError err_outdated_firmware =
    {"Outdated\nFirmware", "Update firmware", "err_07", &I_err_07};

static void loader_dialog_prepare_and_show(DialogsApp* dialogs, const LoaderError* err) {
    FuriString* header = furi_string_alloc_printf("Error: %s", err->error);
    FuriString* text =
        furi_string_alloc_printf("%s\nLearn more:\nr.flipper.net/%s", err->description, err->url);
    DialogMessage* message = dialog_message_alloc();

    dialog_message_set_header(message, furi_string_get_cstr(header), 64, 0, AlignCenter, AlignTop);
    dialog_message_set_text(message, furi_string_get_cstr(text), 0, 63, AlignLeft, AlignBottom);
    dialog_message_set_icon(message, err->icon, 128 - 25, 64 - 25);
    dialog_message_show(dialogs, message);

    dialog_message_free(message);
    furi_string_free(header);
    furi_string_free(text);
}

static void loader_show_gui_error(
    LoaderMessageLoaderStatusResult status,
    const char* name,
    FuriString* error_message) {
    furi_check(name);
    DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
    DialogMessage* message = dialog_message_alloc();

    if(status.value == LoaderStatusErrorUnknownApp &&
       loader_find_external_application_by_name(name) != NULL) {
        // Special case for external apps
        const char* header = NULL;
        const char* text = NULL;
        Storage* storage = furi_record_open(RECORD_STORAGE);
        if(storage_sd_status(storage) == FSE_OK) {
            header = "Update needed";
            text = "Reinstall firmware\nto run this app";
        } else {
            header = "SD card needed";
            text = "Install SD card\nto run this app";
        }
        furi_record_close(RECORD_STORAGE);
        dialog_message_set_header(message, header, 64, 3, AlignCenter, AlignTop);
        dialog_message_set_icon(message, &I_WarningDolphinFlip_45x42, 83, 22);
        dialog_message_set_text(message, text, 3, 26, AlignLeft, AlignTop);
        dialog_message_show(dialogs, message);
    } else if(status.value == LoaderStatusErrorUnknownApp) {
        loader_dialog_prepare_and_show(dialogs, &err_app_not_found);
    } else if(status.value == LoaderStatusErrorInternal) {
        // TODO FL-3522: we have many places where we can emit a double start, ex: desktop, menu
        // so i prefer to not show LoaderStatusErrorAppStarted error message for now
        switch(status.error) {
        case LoaderStatusErrorInvalidFile:
            loader_dialog_prepare_and_show(dialogs, &err_invalid_flie);
            break;
        case LoaderStatusErrorInvalidManifest:
            loader_dialog_prepare_and_show(dialogs, &err_invalid_manifest);
            break;
        case LoaderStatusErrorMissingImports:
            loader_dialog_prepare_and_show(dialogs, &err_missing_imports);
            break;
        case LoaderStatusErrorHWMismatch:
            loader_dialog_prepare_and_show(dialogs, &err_hw_target_mismatch);
            break;
        case LoaderStatusErrorOutdatedApp:
            loader_dialog_prepare_and_show(dialogs, &err_outdated_app);
            break;
        case LoaderStatusErrorOutdatedFirmware:
            loader_dialog_prepare_and_show(dialogs, &err_outdated_firmware);
            break;
        case LoaderStatusErrorOutOfMemory:
            dialog_message_set_header(
                message, "Error: Out of Memory", 64, 0, AlignCenter, AlignTop);
            dialog_message_set_text(
                message,
                "Not enough RAM to run the\napp. Please reboot the device",
                64,
                13,
                AlignCenter,
                AlignTop);
            dialog_message_set_buttons(message, NULL, NULL, "Reboot");
            if(dialog_message_show(dialogs, message) == DialogMessageButtonRight) {
                furi_hal_power_reset();
            }
            break;
        default:
            // Generic error
            dialog_message_set_header(message, "Error", 64, 0, AlignCenter, AlignTop);

            furi_string_replace(error_message, "/ext/apps/", "");
            furi_string_replace(error_message, ", ", "\n");
            furi_string_replace(error_message, ": ", "\n");

            dialog_message_set_text(
                message, furi_string_get_cstr(error_message), 64, 35, AlignCenter, AlignCenter);
            dialog_message_show(dialogs, message);
            break;
        }
    }

    dialog_message_free(message);
    furi_record_close(RECORD_DIALOGS);
}

static void loader_generic_synchronous_request(Loader* loader, LoaderMessage* message) {
    furi_check(loader);
    message->api_lock = api_lock_alloc_locked();
    furi_message_queue_put(loader->queue, message, FuriWaitForever);
    api_lock_wait_unlock_and_free(message->api_lock);
}

LoaderStatus
    loader_start(Loader* loader, const char* name, const char* args, FuriString* error_message) {
    furi_check(loader);
    furi_check(name);

    LoaderMessageLoaderStatusResult result =
        loader_start_internal(loader, name, args, error_message);
    return result.value;
}

LoaderStatus loader_start_with_gui_error(Loader* loader, const char* name, const char* args) {
    furi_check(loader);
    furi_check(name);

    FuriString* error_message = furi_string_alloc();
    LoaderMessageLoaderStatusResult result =
        loader_start_internal(loader, name, args, error_message);
    loader_show_gui_error(result, name, error_message);
    furi_string_free(error_message);
    return result.value;
}

void loader_start_detached_with_gui_error(Loader* loader, const char* name, const char* args) {
    furi_check(loader);
    furi_check(name);

    LoaderMessage message = {
        .type = LoaderMessageTypeStartByNameDetachedWithGuiError,
        .start.name = strdup(name),
        .start.args = args ? strdup(args) : NULL,
    };
    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
}

bool loader_lock(Loader* loader) {
    LoaderMessageBoolResult result;
    LoaderMessage message = {
        .type = LoaderMessageTypeLock,
        .bool_value = &result,
    };
    loader_generic_synchronous_request(loader, &message);
    return result.value;
}

void loader_unlock(Loader* loader) {
    furi_check(loader);

    LoaderMessage message;
    message.type = LoaderMessageTypeUnlock;

    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
}

bool loader_is_locked(Loader* loader) {
    LoaderMessageBoolResult result;
    LoaderMessage message = {
        .type = LoaderMessageTypeIsLocked,
        .bool_value = &result,
    };
    loader_generic_synchronous_request(loader, &message);
    return result.value;
}

void loader_show_menu(Loader* loader) {
    furi_check(loader);

    LoaderMessage message;
    message.type = LoaderMessageTypeShowMenu;

    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
}

void loader_show_settings(Loader* loader) {
    furi_check(loader);

    LoaderMessage message;
    message.type = LoaderMessageTypeShowSettings;

    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
}

FuriPubSub* loader_get_pubsub(Loader* loader) {
    furi_check(loader);
    // it's safe to return pubsub without locking
    // because it's never freed and loader is never exited
    // also the loader instance cannot be obtained until the pubsub is created
    return loader->pubsub;
}

bool loader_signal(Loader* loader, uint32_t signal, void* arg) {
    LoaderMessageBoolResult result;
    LoaderMessage message = {
        .type = LoaderMessageTypeSignal,
        .signal.signal = signal,
        .signal.arg = arg,
        .bool_value = &result,
    };
    loader_generic_synchronous_request(loader, &message);
    return result.value;
}

bool loader_get_application_name(Loader* loader, FuriString* name) {
    LoaderMessageBoolResult result;
    LoaderMessage message = {
        .type = LoaderMessageTypeGetApplicationName,
        .application_name = name,
        .bool_value = &result,
    };
    loader_generic_synchronous_request(loader, &message);
    return result.value;
}

bool loader_get_application_launch_path(Loader* loader, FuriString* name) {
    LoaderMessageBoolResult result;
    LoaderMessage message = {
        .type = LoaderMessageTypeGetApplicationLaunchPath,
        .application_name = name,
        .bool_value = &result,
    };
    loader_generic_synchronous_request(loader, &message);
    return result.value;
}

void loader_enqueue_launch(
    Loader* loader,
    const char* name,
    const char* args,
    LoaderDeferredLaunchFlag flags) {
    LoaderMessage message = {
        .type = LoaderMessageTypeEnqueueLaunch,
        .defer_start =
            {
                .name_or_path = strdup(name),
                .args = args ? strdup(args) : NULL,
                .flags = flags,
            },
    };
    loader_generic_synchronous_request(loader, &message);
}

void loader_clear_launch_queue(Loader* loader) {
    LoaderMessage message = {
        .type = LoaderMessageTypeClearLaunchQueue,
    };
    loader_generic_synchronous_request(loader, &message);
}

// callbacks

static void loader_menu_closed_callback(void* context) {
    Loader* loader = context;
    LoaderMessage message;
    message.type = LoaderMessageTypeMenuClosed;
    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
}

static void loader_applications_closed_callback(void* context) {
    Loader* loader = context;
    LoaderMessage message;
    message.type = LoaderMessageTypeApplicationsClosed;
    furi_message_queue_put(loader->queue, &message, FuriWaitForever);
}

static void
    loader_thread_state_callback(FuriThread* thread, FuriThreadState thread_state, void* context) {
    UNUSED(thread);
    furi_assert(context);

    if(thread_state == FuriThreadStateStopped) {
        Loader* loader = context;

        LoaderMessage message;
        message.type = LoaderMessageTypeAppClosed;
        furi_message_queue_put(loader->queue, &message, FuriWaitForever);
    }
}

// implementation

static Loader* loader_alloc(void) {
    Loader* loader = malloc(sizeof(Loader));
    loader->pubsub = furi_pubsub_alloc();
    loader->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage));
    loader->gui = furi_record_open(RECORD_GUI);
    loader->view_holder = view_holder_alloc();
    loader->loading = loading_alloc();
    view_holder_attach_to_gui(loader->view_holder, loader->gui);
    return loader;
}

static FlipperInternalApplication const* loader_find_application_by_name_in_list(
    const char* name,
    const FlipperInternalApplication* list,
    const uint32_t n_apps) {
    for(size_t i = 0; i < n_apps; i++) {
        if((strcmp(name, list[i].name) == 0) || (strcmp(name, list[i].appid) == 0)) {
            return &list[i];
        }
    }
    return NULL;
}

static const FlipperInternalApplication* loader_find_application_by_name(const char* name) {
    const struct {
        const FlipperInternalApplication* list;
        const uint32_t count;
    } lists[] = {
        {FLIPPER_APPS, FLIPPER_APPS_COUNT},
        {FLIPPER_SYSTEM_APPS, FLIPPER_SYSTEM_APPS_COUNT},
        {FLIPPER_DEBUG_APPS, FLIPPER_DEBUG_APPS_COUNT},
    };

    for(size_t i = 0; i < COUNT_OF(lists); i++) {
        const FlipperInternalApplication* application =
            loader_find_application_by_name_in_list(name, lists[i].list, lists[i].count);
        if(application) {
            return application;
        }
    }

    return NULL;
}

static void loader_start_app_thread(Loader* loader, FlipperApplicationFlag flags) {
    // setup heap trace
    FuriHalRtcHeapTrackMode mode = furi_hal_rtc_get_heap_track_mode();
    if(mode > FuriHalRtcHeapTrackModeNone) {
        furi_thread_enable_heap_trace(loader->app.thread);
    } else {
        furi_thread_disable_heap_trace(loader->app.thread);
    }

    // setup insomnia
    if(!(flags & FlipperApplicationFlagInsomniaSafe)) {
        furi_hal_power_insomnia_enter();
        loader->app.insomniac = true;
    } else {
        loader->app.insomniac = false;
    }

    // setup thread state callbacks
    furi_thread_set_state_context(loader->app.thread, loader);
    furi_thread_set_state_callback(loader->app.thread, loader_thread_state_callback);

    // start app thread
    furi_thread_start(loader->app.thread);
}

static void loader_start_internal_app(
    Loader* loader,
    const FlipperInternalApplication* app,
    const char* args) {
    FURI_LOG_I(TAG, "Starting %s", app->name);
    if(app->flags & FlipperApplicationFlagUnloadAssetPacks) {
        loader->app.unloaded_asset_packs = true;
        asset_packs_free();
    } else {
        loader->app.unloaded_asset_packs = false;
    }

    // store args
    furi_assert(loader->app.args == NULL);
    if(args && strlen(args) > 0) {
        loader->app.args = strdup(args);
    }

    loader->app.thread =
        furi_thread_alloc_ex(app->name, app->stack_size, app->app, loader->app.args);
    furi_thread_set_appid(loader->app.thread, app->appid);

    loader_start_app_thread(loader, app->flags);
}

static void loader_log_status_error(
    LoaderStatus status,
    FuriString* error_message,
    const char* format,
    va_list args) {
    if(error_message) {
        furi_string_vprintf(error_message, format, args);
        FURI_LOG_E(TAG, "Status [%d]: %s", status, furi_string_get_cstr(error_message));
    } else {
        FURI_LOG_E(TAG, "Status [%d]", status);
    }
}

static LoaderStatus loader_make_status_error(
    LoaderStatus status,
    FuriString* error_message,
    const char* format,
    ...) {
    va_list args;
    va_start(args, format);
    loader_log_status_error(status, error_message, format, args);
    va_end(args);
    return status;
}

static LoaderStatus loader_make_success_status(FuriString* error_message) {
    if(error_message) {
        furi_string_set(error_message, "App started");
    }

    return LoaderStatusOk;
}

static LoaderStatusError
    loader_status_error_from_preload_status(FlipperApplicationPreloadStatus status) {
    switch(status) {
    case FlipperApplicationPreloadStatusInvalidFile:
        return LoaderStatusErrorInvalidFile;
    case FlipperApplicationPreloadStatusNotEnoughMemory:
        return LoaderStatusErrorOutOfMemory;
    case FlipperApplicationPreloadStatusInvalidManifest:
        return LoaderStatusErrorInvalidManifest;
    case FlipperApplicationPreloadStatusApiTooOld:
        return LoaderStatusErrorOutdatedApp;
    case FlipperApplicationPreloadStatusApiTooNew:
        return LoaderStatusErrorOutdatedFirmware;
    case FlipperApplicationPreloadStatusTargetMismatch:
        return LoaderStatusErrorHWMismatch;
    default:
        return LoaderStatusErrorUnknown;
    }
}

static LoaderStatusError
    loader_status_error_from_load_status(FlipperApplicationLoadStatus status) {
    switch(status) {
    case FlipperApplicationLoadStatusMissingImports:
        return LoaderStatusErrorMissingImports;
    default:
        return LoaderStatusErrorUnknown;
    }
}

static LoaderMessageLoaderStatusResult loader_start_external_app(
    Loader* loader,
    Storage* storage,
    const char* path,
    const char* args,
    FuriString* error_message) {
    LoaderMessageLoaderStatusResult result;
    result.value = loader_make_success_status(error_message);
    result.error = LoaderStatusErrorUnknown;

    do {
        loader->app.fap = flipper_application_alloc(storage, firmware_api_interface);
        size_t start = furi_get_tick();

        FURI_LOG_I(TAG, "Loading %s", path);

        // Calling preload will load whole FAP file, so need to preload manifest first to
        // get flags value, unload asset packs if requested by flags, then preload whole FAP
        FlipperApplicationFlag flags = FlipperApplicationFlagDefault;
        FlipperApplicationPreloadStatus preload_res =
            flipper_application_preload_manifest(loader->app.fap, path);
        loader->app.unloaded_asset_packs = false;
        if(preload_res != FlipperApplicationPreloadStatusInvalidFile &&
           preload_res != FlipperApplicationPreloadStatusInvalidManifest) {
            flags = flipper_application_get_manifest(loader->app.fap)->flags;
            if(flags & FlipperApplicationFlagUnloadAssetPacks) {
                loader->app.unloaded_asset_packs = true;
                asset_packs_free();
            }
            preload_res = flipper_application_preload(loader->app.fap, path);
        }

        bool api_mismatch = false;
        if(preload_res == FlipperApplicationPreloadStatusApiTooOld ||
           preload_res == FlipperApplicationPreloadStatusApiTooNew) {
            api_mismatch = true;
        } else if(preload_res != FlipperApplicationPreloadStatusSuccess) {
        api_mismatch_bypass_failed:
            const char* err_msg = flipper_application_preload_status_to_string(preload_res);
            result.value = loader_make_status_error(
                LoaderStatusErrorInternal, error_message, "Preload failed, %s: %s", path, err_msg);
            result.error = loader_status_error_from_preload_status(preload_res);
            break;
        }

        FURI_LOG_I(TAG, "Mapping");
        FlipperApplicationLoadStatus load_status =
            flipper_application_map_to_memory(loader->app.fap);
        FURI_LOG_I(TAG, "Loaded in %zums", (size_t)(furi_get_tick() - start));
        if(load_status != FlipperApplicationLoadStatusSuccess) {
            if(api_mismatch) goto api_mismatch_bypass_failed;
            const char* err_msg = flipper_application_load_status_to_string(load_status);
            result.value = loader_make_status_error(
                LoaderStatusErrorInternal, error_message, "Load failed, %s: %s", path, err_msg);
            result.error = loader_status_error_from_load_status(load_status);
            break;
        } else if(api_mismatch) {
            // Successful map, but found api mismatch -> warn user
            const FlipperApplicationManifest* manifest =
                flipper_application_get_manifest(loader->app.fap);

            bool app_newer = preload_res == FlipperApplicationPreloadStatusApiTooNew;
            const char* header = app_newer ? "App Too New" : "App Too Old";
            char text[63];
            snprintf(
                text,
                sizeof(text),
                "APP:%i %c FW:%i\nThis app might not work\nContinue anyways?",
                manifest->base.api_version.major,
                app_newer ? '>' : '<',
                firmware_api_interface->api_version_major);

            DialogMessage* message = dialog_message_alloc();
            dialog_message_set_header(message, header, 64, 0, AlignCenter, AlignTop);
            dialog_message_set_buttons(message, "Cancel", NULL, "Continue");
            dialog_message_set_text(message, text, 64, 32, AlignCenter, AlignCenter);
            DialogMessageButton res =
                dialog_message_show(furi_record_open(RECORD_DIALOGS), message);
            dialog_message_free(message);
            furi_record_close(RECORD_DIALOGS);
            if(res != DialogMessageButtonRight) {
                const char* err_msg = flipper_application_preload_status_to_string(preload_res);
                result.value = loader_make_status_error(
                    LoaderStatusErrorApiMismatchCanceled, // Not LoaderStatusErrorInternal since it would show another popup
                    error_message,
                    "Preload failed, %s: %s",
                    path,
                    err_msg);
                result.error = loader_status_error_from_preload_status(preload_res);
                break;
            }
        }

        FURI_LOG_I(TAG, "Starting app");

        if(flipper_application_is_plugin(loader->app.fap)) {
            result.value = loader_make_status_error(
                LoaderStatusErrorInternal, error_message, "Plugin %s is not runnable", path);
            break;
        }

        loader->app.thread = flipper_application_alloc_thread(loader->app.fap, args);
        FuriString* app_name = furi_string_alloc();
        path_extract_filename_no_ext(path, app_name);
        furi_thread_set_appid(loader->app.thread, furi_string_get_cstr(app_name));
        furi_string_free(app_name);

        /* This flag is set by the debugger - to break on app start */
        if(furi_hal_debug_is_gdb_session_active()) {
            FURI_LOG_W(TAG, "Triggering BP for debugger");
            /* After hitting this, you can set breakpoints in your .fap's code
             * Note that you have to toggle breakpoints that were set before */
            __asm volatile("bkpt 0");
        }

        loader_start_app_thread(loader, flags);
    } while(0);

    if(result.value != LoaderStatusOk) {
        flipper_application_free(loader->app.fap);
        loader->app.fap = NULL;
        LoaderEvent event;
        event.type = LoaderEventTypeApplicationLoadFailed;
        furi_pubsub_publish(loader->pubsub, &event);
        if(loader->app.unloaded_asset_packs) {
            asset_packs_init();
        }
    }

    return result;
}

// process messages

static void loader_do_menu_show(Loader* loader, bool settings_only) {
    if(!loader->loader_menu) {
        loader->loader_menu =
            loader_menu_alloc(loader_menu_closed_callback, loader, settings_only);
    }
}

static void loader_do_menu_closed(Loader* loader) {
    if(loader->loader_menu) {
        loader_menu_free(loader->loader_menu);
        loader->loader_menu = NULL;
    }
}

static void loader_do_applications_show(Loader* loader) {
    if(!loader->loader_applications) {
        loader->loader_applications =
            loader_applications_alloc(loader_applications_closed_callback, loader);
    }
}

static void loader_do_applications_closed(Loader* loader) {
    if(loader->loader_applications) {
        loader_applications_free(loader->loader_applications);
        loader->loader_applications = NULL;
    }
}

static bool loader_do_is_locked(Loader* loader) {
    return loader->app.thread != NULL;
}

static LoaderMessageLoaderStatusResult loader_do_start_by_name(
    Loader* loader,
    const char* name,
    const char* args,
    FuriString* error_message) {
    LoaderMessageLoaderStatusResult status;
    status.value = loader_make_success_status(error_message);
    status.error = LoaderStatusErrorUnknown;

    if(name == NULL) return status;

    do {
        // check lock
        if(loader_do_is_locked(loader)) {
            if(loader->app.thread == (FuriThread*)LOADER_MAGIC_THREAD_VALUE) {
                status.value = loader_make_status_error(
                    LoaderStatusErrorAppStarted, error_message, "Loader is locked");
            } else {
                const char* current_thread_name =
                    furi_thread_get_name(furi_thread_get_id(loader->app.thread));

                status.value = loader_make_status_error(
                    LoaderStatusErrorAppStarted,
                    error_message,
                    "Loader is locked, please close the \"%s\" first",
                    current_thread_name);
            }
            break;
        }

        // check Applications
        if(strcmp(name, LOADER_APPLICATIONS_NAME) == 0) {
            loader_do_applications_show(loader);
            status.value = loader_make_success_status(error_message);
            break;
        }

        LoaderEvent event;
        event.type = LoaderEventTypeApplicationBeforeLoad;
        furi_pubsub_publish(loader->pubsub, &event);

        // Translate app names (mainly for RPC)
        if(!strncmp(name, "Bad USB", strlen("Bad USB"))) {
            name = "Bad KB";
        }

        // check internal apps
        {
            const FlipperInternalApplication* app = loader_find_application_by_name(name);
            if(app) {
                loader_start_internal_app(loader, app, args);
                status.value = loader_make_success_status(error_message);
                break;
            }
        }

        // check External Applications
        {
            const char* path = loader_find_external_application_by_name(name);
            if(path) {
                name = path;
            }
        }

        // check Faps
        {
            Storage* storage = furi_record_open(RECORD_STORAGE);
            if(storage_file_exists(storage, name)) {
                status = loader_start_external_app(loader, storage, name, args, error_message);
                furi_record_close(RECORD_STORAGE);
                break;
            }
            furi_record_close(RECORD_STORAGE);
        }

        status.value = loader_make_status_error(
            LoaderStatusErrorUnknownApp, error_message, "Application \"%s\" not found", name);
    } while(false);

    if(status.value == LoaderStatusOk) {
        loader->app.launch_path = furi_string_alloc_set_str(name);
    }

    return status;
}

static bool loader_do_lock(Loader* loader) {
    if(loader->app.thread) {
        return false;
    }

    loader->app.thread = (FuriThread*)LOADER_MAGIC_THREAD_VALUE;
    return true;
}

static void loader_do_unlock(Loader* loader) {
    furi_check(loader->app.thread == (FuriThread*)LOADER_MAGIC_THREAD_VALUE);
    loader->app.thread = NULL;
}

static void loader_do_emit_queue_empty_event(Loader* loader) {
    if(loader_do_is_locked(loader)) return;
    FURI_LOG_I(TAG, "Launch queue empty");
    LoaderEvent event;
    event.type = LoaderEventTypeNoMoreAppsInQueue;
    furi_pubsub_publish(loader->pubsub, &event);
}

static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record);

static void loader_do_next_deferred_launch_if_available(Loader* loader) {
    LoaderDeferredLaunchRecord record;
    if(loader_queue_pop(&loader->launch_queue, &record)) {
        loader_do_deferred_launch(loader, &record);
        loader_queue_item_clear(&record);
    } else {
        loader_do_emit_queue_empty_event(loader);
    }
}

static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record) {
    furi_assert(loader);
    furi_assert(record);

    bool is_successful = false;
    FuriString* error_message = furi_string_alloc();
    view_holder_set_view(loader->view_holder, loading_get_view(loader->loading));
    view_holder_send_to_front(loader->view_holder);

    do {
        const char* app_name_str = record->name_or_path;
        const char* app_args = record->args;
        FURI_LOG_I(TAG, "Deferred launch: %s", app_name_str);

        LoaderMessageLoaderStatusResult result =
            loader_do_start_by_name(loader, app_name_str, app_args, error_message);
        if(result.value == LoaderStatusOk) {
            is_successful = true;
            break;
        }

        if(record->flags & LoaderDeferredLaunchFlagGui)
            loader_show_gui_error(result, app_name_str, error_message);

        loader_do_next_deferred_launch_if_available(loader);
    } while(false);

    if(!loader->loader_menu) {
        view_holder_set_view(loader->view_holder, NULL);
    }
    furi_string_free(error_message);
    return is_successful;
}

static void loader_do_app_closed(Loader* loader) {
    furi_assert(loader->app.thread);

    furi_thread_join(loader->app.thread);
    FURI_LOG_I(TAG, "App returned: %li", furi_thread_get_return_code(loader->app.thread));

    if(loader->app.args) {
        free(loader->app.args);
        loader->app.args = NULL;
    }

    if(loader->app.insomniac) {
        furi_hal_power_insomnia_exit();
    }

    if(loader->app.fap) {
        flipper_application_free(loader->app.fap);
        loader->app.fap = NULL;
        loader->app.thread = NULL;
    } else {
        furi_thread_free(loader->app.thread);
        loader->app.thread = NULL;
    }

    furi_string_free(loader->app.launch_path);

    FURI_LOG_I(TAG, "Application stopped. Free heap: %zu", memmgr_get_free_heap());

    LoaderEvent event;
    event.type = LoaderEventTypeApplicationStopped;
    furi_pubsub_publish(loader->pubsub, &event);
    if(loader->app.unloaded_asset_packs) {
        asset_packs_init();
    }

    loader_do_next_deferred_launch_if_available(loader);
}

static bool loader_is_application_running(Loader* loader) {
    FuriThread* app_thread = loader->app.thread;
    return app_thread && (app_thread != (FuriThread*)LOADER_MAGIC_THREAD_VALUE);
}

static bool loader_do_signal(Loader* loader, uint32_t signal, void* arg) {
    if(loader_is_application_running(loader)) {
        return furi_thread_signal(loader->app.thread, signal, arg);
    }

    return false;
}

static bool loader_do_get_application_name(Loader* loader, FuriString* name) {
    if(loader_is_application_running(loader)) {
        furi_string_set(name, furi_thread_get_name(furi_thread_get_id(loader->app.thread)));
        return true;
    }

    return false;
}

static bool loader_do_get_application_launch_path(Loader* loader, FuriString* path) {
    if(loader_is_application_running(loader)) {
        furi_string_set(path, loader->app.launch_path);
        return true;
    }

    return false;
}

// app

int32_t loader_srv(void* p) {
    UNUSED(p);
    Loader* loader = loader_alloc();
    furi_record_create(RECORD_LOADER, loader);

    FURI_LOG_I(TAG, "Executing system start hooks");
    for(size_t i = 0; i < FLIPPER_ON_SYSTEM_START_COUNT; i++) {
        FLIPPER_ON_SYSTEM_START[i]();
    }

    if((furi_hal_rtc_get_boot_mode() == FuriHalRtcBootModeNormal) && FLIPPER_AUTORUN_APP_NAME &&
       strlen(FLIPPER_AUTORUN_APP_NAME)) {
        FURI_LOG_I(TAG, "Starting autorun app: %s", FLIPPER_AUTORUN_APP_NAME);
        loader_do_start_by_name(loader, FLIPPER_AUTORUN_APP_NAME, NULL, NULL);
    }

    LoaderMessage message;
    while(true) {
        if(furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) {
            switch(message.type) {
            case LoaderMessageTypeStartByName: {
                LoaderMessageLoaderStatusResult status = loader_do_start_by_name(
                    loader,
                    message.start.name,
                    message.start.args,
                    message.start.error_message); //-V595
                *(message.status_value) = status;
                if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader);
                api_lock_unlock(message.api_lock);
                break;
            }
            case LoaderMessageTypeStartByNameDetachedWithGuiError: {
                FuriString* error_message = furi_string_alloc();
                LoaderMessageLoaderStatusResult status = loader_do_start_by_name(
                    loader, message.start.name, message.start.args, error_message); //-V595
                loader_show_gui_error(status, message.start.name, error_message);
                if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader);
                if(message.start.name) free((void*)message.start.name);
                if(message.start.args) free((void*)message.start.args);
                furi_string_free(error_message);
                break;
            }
            case LoaderMessageTypeShowMenu:
                loader_do_menu_show(loader, false);
                break;
            case LoaderMessageTypeShowSettings:
                loader_do_menu_show(loader, true);
                break;
            case LoaderMessageTypeMenuClosed:
                loader_do_menu_closed(loader);
                break;
            case LoaderMessageTypeIsLocked:
                message.bool_value->value = loader_do_is_locked(loader);
                api_lock_unlock(message.api_lock);
                break;
            case LoaderMessageTypeAppClosed:
                loader_do_app_closed(loader);
                break;
            case LoaderMessageTypeLock:
                message.bool_value->value = loader_do_lock(loader);
                api_lock_unlock(message.api_lock);
                break;
            case LoaderMessageTypeUnlock:
                loader_do_unlock(loader);
                break;
            case LoaderMessageTypeApplicationsClosed:
                loader_do_applications_closed(loader);
                break;
            case LoaderMessageTypeSignal:
                message.bool_value->value =
                    loader_do_signal(loader, message.signal.signal, message.signal.arg);
                api_lock_unlock(message.api_lock);
                break;
            case LoaderMessageTypeGetApplicationName:
                message.bool_value->value =
                    loader_do_get_application_name(loader, message.application_name);
                api_lock_unlock(message.api_lock);
                break;
            case LoaderMessageTypeGetApplicationLaunchPath:
                message.bool_value->value =
                    loader_do_get_application_launch_path(loader, message.application_name);
                api_lock_unlock(message.api_lock);
                break;
            case LoaderMessageTypeEnqueueLaunch:
                furi_check(loader_queue_push(&loader->launch_queue, &message.defer_start));
                api_lock_unlock(message.api_lock);
                break;
            case LoaderMessageTypeClearLaunchQueue:
                loader_queue_clear(&loader->launch_queue);
                api_lock_unlock(message.api_lock);
                break;
            }
        }
    }

    return 0;
}
