// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

#include "common/archives.h"
#include "common/file_util.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/frontend/input.h"
#include "core/hle/applets/applet.h"
#include "core/hle/applets/erreula.h"
#include "core/hle/applets/mii_selector.h"
#include "core/hle/applets/mint.h"
#include "core/hle/applets/swkbd.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/apt/applet_manager.h"
#include "core/hle/service/apt/errors.h"
#include "core/hle/service/apt/ns.h"
#include "core/hle/service/cfg/cfg.h"
#include "core/hle/service/gsp/gsp_gpu.h"
#include "video_core/utils.h"

SERVICE_CONSTRUCT_IMPL(Service::APT::AppletManager)

namespace Service::APT {

/// The interval at which the home button update callback will be called, 16.6ms
static constexpr u64 button_update_interval_us = 16666;
/// The interval at which the HLE Applet update callback will be called, 16.6ms.
static constexpr u64 hle_applet_update_interval_us = 16666;

struct AppletTitleData {
    // There are two possible applet ids for each applet.
    std::array<AppletId, 2> applet_ids;

    // There's a specific TitleId per region for each applet.
    static constexpr std::size_t NumRegions = 7;
    std::array<u64, NumRegions> title_ids;
    std::array<u64, NumRegions> n3ds_title_ids = {0, 0, 0, 0, 0, 0, 0};
};

static constexpr std::size_t NumApplets = 29;
static constexpr std::array<AppletTitleData, NumApplets> applet_titleids = {{
    {{AppletId::HomeMenu, AppletId::None},
     {0x4003000008202, 0x4003000008F02, 0x4003000009802, 0x4003000008202, 0x400300000A102,
      0x400300000A902, 0x400300000B102}},
    {{AppletId::AlternateMenu, AppletId::None},
     {0x4003000008102, 0x4003000008102, 0x4003000008102, 0x4003000008102, 0x4003000008102,
      0x4003000008102, 0x4003000008102}},
    {{AppletId::Camera, AppletId::None},
     {0x4003000008402, 0x4003000009002, 0x4003000009902, 0x4003000008402, 0x400300000A202,
      0x400300000AA02, 0x400300000B202}},
    {{AppletId::FriendList, AppletId::None},
     {0x4003000008D02, 0x4003000009602, 0x4003000009F02, 0x4003000008D02, 0x400300000A702,
      0x400300000AF02, 0x400300000B702}},
    {{AppletId::GameNotes, AppletId::None},
     {0x4003000008702, 0x4003000009302, 0x4003000009C02, 0x4003000008702, 0x400300000A502,
      0x400300000AD02, 0x400300000B502}},
    {{AppletId::InternetBrowser, AppletId::None},
     {0x4003000008802, 0x4003000009402, 0x4003000009D02, 0x4003000008802, 0x400300000A602,
      0x400300000AE02, 0x400300000B602},
     {0x4003020008802, 0x4003020009402, 0x4003020009D02, 0x4003020008802, 0, 0x400302000AE02, 0}},
    {{AppletId::InstructionManual, AppletId::None},
     {0x4003000008602, 0x4003000009202, 0x4003000009B02, 0x4003000008602, 0x400300000A402,
      0x400300000AC02, 0x400300000B402}},
    {{AppletId::Notifications, AppletId::None},
     {0x4003000008E02, 0x4003000009702, 0x400300000A002, 0x4003000008E02, 0x400300000A802,
      0x400300000B002, 0x400300000B802}},
    {{AppletId::Miiverse, AppletId::None},
     {0x400300000BC02, 0x400300000BD02, 0x400300000BE02, 0x400300000BC02, 0x4003000009E02,
      0x4003000009502, 0x400300000B902}},
    // These values obtained from an older NS dump firmware 4.5
    {{AppletId::MiiversePost, AppletId::None},
     {0x400300000BA02, 0x400300000BA02, 0x400300000BA02, 0x400300000BA02, 0x400300000BA02,
      0x400300000BA02, 0x400300000BA02}},
    // {AppletId::MiiversePost, AppletId::None, 0x4003000008302, 0x4003000008B02, 0x400300000BA02,
    //  0x4003000008302, 0x0, 0x0, 0x0},
    {{AppletId::AmiiboSettings, AppletId::None},
     {0x4003000009502, 0x4003000009E02, 0x400300000B902, 0x4003000009502, 0x0, 0x4003000008C02,
      0x400300000BF02}},
    {{AppletId::SoftwareKeyboard1, AppletId::SoftwareKeyboard2},
     {0x400300000C002, 0x400300000C802, 0x400300000D002, 0x400300000C002, 0x400300000D802,
      0x400300000DE02, 0x400300000E402}},
    {{AppletId::Ed1, AppletId::Ed2},
     {0x400300000C102, 0x400300000C902, 0x400300000D102, 0x400300000C102, 0x400300000D902,
      0x400300000DF02, 0x400300000E502}},
    {{AppletId::PnoteApp, AppletId::PnoteApp2},
     {0x400300000C302, 0x400300000CB02, 0x400300000D302, 0x400300000C302, 0x400300000DB02,
      0x400300000E102, 0x400300000E702}},
    {{AppletId::SnoteApp, AppletId::SnoteApp2},
     {0x400300000C402, 0x400300000CC02, 0x400300000D402, 0x400300000C402, 0x400300000DC02,
      0x400300000E202, 0x400300000E802}},
    {{AppletId::Error, AppletId::Error2},
     {0x400300000C502, 0x400300000C502, 0x400300000C502, 0x400300000C502, 0x400300000CF02,
      0x400300000CF02, 0x400300000CF02}},
    {{AppletId::Mint, AppletId::Mint2},
     {0x400300000C602, 0x400300000CE02, 0x400300000D602, 0x400300000C602, 0x400300000DD02,
      0x400300000E302, 0x400300000E902}},
    {{AppletId::Extrapad, AppletId::Extrapad2},
     {0x400300000CD02, 0x400300000CD02, 0x400300000CD02, 0x400300000CD02, 0x400300000D502,
      0x400300000D502, 0x400300000D502}},
    {{AppletId::Memolib, AppletId::Memolib2},
     {0x400300000F602, 0x400300000F602, 0x400300000F602, 0x400300000F602, 0x400300000F602,
      0x400300000F602, 0x400300000F602}},
    // TODO(Subv): Fill in the rest of the titleids
}};

static u64 GetTitleIdForApplet(AppletId id, u32 region_value) {
    ASSERT_MSG(id != AppletId::None, "Invalid applet id");

    auto itr = std::find_if(applet_titleids.begin(), applet_titleids.end(),
                            [id](const AppletTitleData& data) {
                                return data.applet_ids[0] == id || data.applet_ids[1] == id;
                            });

    ASSERT_MSG(itr != applet_titleids.end(), "Unknown applet id 0x{:#05X}", id);

    auto n3ds_title_id = itr->n3ds_title_ids[region_value];
    if (n3ds_title_id != 0 && Settings::values.is_new_3ds.GetValue()) {
        return n3ds_title_id;
    }
    return itr->title_ids[region_value];
}

static bool IsSystemAppletId(AppletId applet_id) {
    return (static_cast<u32>(applet_id) & static_cast<u32>(AppletId::AnySystemApplet)) != 0;
}

static bool IsApplicationAppletId(AppletId applet_id) {
    return (static_cast<u32>(applet_id) & static_cast<u32>(AppletId::Application)) != 0;
}

AppletManager::AppletSlot AppletManager::GetAppletSlotFromId(AppletId id) {
    if (id == AppletId::Application) {
        if (GetAppletSlot(AppletSlot::Application)->applet_id != AppletId::None)
            return AppletSlot::Application;

        return AppletSlot::Error;
    }

    if (id == AppletId::AnySystemApplet) {
        if (GetAppletSlot(AppletSlot::SystemApplet)->applet_id != AppletId::None)
            return AppletSlot::SystemApplet;

        // The Home Menu is also a system applet, but it lives in its own slot to be able to run
        // concurrently with other system applets.
        if (GetAppletSlot(AppletSlot::HomeMenu)->applet_id != AppletId::None)
            return AppletSlot::HomeMenu;

        return AppletSlot::Error;
    }

    if (id == AppletId::AnyLibraryApplet || id == AppletId::AnySysLibraryApplet) {
        auto slot_data = GetAppletSlot(AppletSlot::LibraryApplet);
        if (slot_data->applet_id == AppletId::None)
            return AppletSlot::Error;

        auto applet_pos = slot_data->attributes.applet_pos.Value();
        if ((id == AppletId::AnyLibraryApplet && applet_pos == AppletPos::Library) ||
            (id == AppletId::AnySysLibraryApplet && applet_pos == AppletPos::SysLibrary))
            return AppletSlot::LibraryApplet;

        return AppletSlot::Error;
    }

    if (id == AppletId::HomeMenu || id == AppletId::AlternateMenu) {
        if (GetAppletSlot(AppletSlot::HomeMenu)->applet_id != AppletId::None)
            return AppletSlot::HomeMenu;

        return AppletSlot::Error;
    }

    for (std::size_t slot = 0; slot < applet_slots.size(); ++slot) {
        if (applet_slots[slot].applet_id == id) {
            return static_cast<AppletSlot>(slot);
        }
    }

    return AppletSlot::Error;
}

AppletManager::AppletSlot AppletManager::GetAppletSlotFromAttributes(AppletAttributes attributes) {
    // Mapping from AppletPos to AppletSlot
    static constexpr std::array<AppletSlot, 6> applet_position_slots = {
        AppletSlot::Application,   AppletSlot::LibraryApplet, AppletSlot::SystemApplet,
        AppletSlot::LibraryApplet, AppletSlot::Error,         AppletSlot::LibraryApplet};

    auto applet_pos_value = static_cast<u32>(attributes.applet_pos.Value());
    if (applet_pos_value >= applet_position_slots.size())
        return AppletSlot::Error;

    auto slot = applet_position_slots[applet_pos_value];
    if (slot == AppletSlot::Error)
        return AppletSlot::Error;

    // The Home Menu is a system applet, however, it has its own applet slot so that it can run
    // concurrently with other system applets.
    if (slot == AppletSlot::SystemApplet && attributes.is_home_menu)
        return AppletSlot::HomeMenu;

    return slot;
}

AppletManager::AppletSlot AppletManager::GetAppletSlotFromPos(AppletPos pos) {
    AppletId applet_id;
    switch (pos) {
    case AppletPos::Application:
        applet_id = AppletId::Application;
        break;
    case AppletPos::Library:
        applet_id = AppletId::AnyLibraryApplet;
        break;
    case AppletPos::System:
        applet_id = AppletId::AnySystemApplet;
        break;
    case AppletPos::SysLibrary:
        applet_id = AppletId::AnySysLibraryApplet;
        break;
    default:
        return AppletSlot::Error;
    }
    return GetAppletSlotFromId(applet_id);
}

void AppletManager::CancelAndSendParameter(const MessageParameter& parameter) {
    LOG_DEBUG(
        Service_APT, "Sending parameter from {:03X} to {:03X} with signal {:08X} and size {:08X}",
        parameter.sender_id, parameter.destination_id, parameter.signal, parameter.buffer.size());

    // If the applet is being HLEd, send directly to the applet.
    const auto applet = hle_applets[parameter.destination_id];
    if (applet != nullptr) {
        applet->ReceiveParameter(parameter);
    } else {
        // Otherwise, send the parameter the LLE way.
        next_parameter = parameter;

        if (parameter.signal == SignalType::RequestForSysApplet) {
            // APT handles RequestForSysApplet messages itself.
            LOG_DEBUG(Service_APT, "Replying to RequestForSysApplet from {:03X}",
                      parameter.sender_id);

            if (parameter.buffer.size() >= sizeof(CaptureBufferInfo)) {
                SetCaptureInfo(parameter.buffer);
                CaptureFrameBuffers();
            }

            next_parameter->sender_id = parameter.destination_id;
            next_parameter->destination_id = parameter.sender_id;
            next_parameter->signal = SignalType::Response;
            next_parameter->buffer.clear();
            next_parameter->object = nullptr;
        } else if (IsSystemAppletId(parameter.sender_id) &&
                   IsApplicationAppletId(parameter.destination_id) && parameter.object) {
            // When a message is sent from a system applet to an application, APT
            // replaces its object with the zero handle.
            next_parameter->object = nullptr;
        }

        // Signal the event to let the receiver know that a new parameter is ready to be read
        auto slot = GetAppletSlotFromId(next_parameter->destination_id);
        if (slot != AppletSlot::Error) {
            GetAppletSlot(slot)->parameter_event->Signal();
        } else {
            LOG_DEBUG(Service_APT, "No applet was registered with ID {:03X}",
                      next_parameter->destination_id);
        }
    }
}

Result AppletManager::SendParameter(const MessageParameter& parameter) {
    // A new parameter can not be sent if the previous one hasn't been consumed yet
    if (next_parameter) {
        LOG_WARNING(Service_APT, "Parameter from {:03X} to {:03X} blocked by pending parameter.",
                    parameter.sender_id, parameter.destination_id);
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    CancelAndSendParameter(parameter);
    return ResultSuccess;
}

ResultVal<MessageParameter> AppletManager::GlanceParameter(AppletId app_id) {
    if (!next_parameter) {
        return Result(ErrorDescription::NoData, ErrorModule::Applet, ErrorSummary::InvalidState,
                      ErrorLevel::Status);
    }

    if (next_parameter->destination_id != app_id) {
        return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                      ErrorLevel::Status);
    }

    auto parameter = *next_parameter;

    // Note: The NS module always clears the DSPSleep and DSPWakeup signals even in GlanceParameter.
    if (next_parameter->signal == SignalType::DspSleep ||
        next_parameter->signal == SignalType::DspWakeup) {
        next_parameter = {};
    }

    return parameter;
}

ResultVal<MessageParameter> AppletManager::ReceiveParameter(AppletId app_id) {
    auto result = GlanceParameter(app_id);
    if (result.Succeeded()) {
        LOG_DEBUG(Service_APT,
                  "Received parameter from {:03X} to {:03X} with signal {:08X} and size {:08X}",
                  result->sender_id, result->destination_id, result->signal, result->buffer.size());

        // Clear the parameter
        next_parameter = {};
    }
    return result;
}

bool AppletManager::CancelParameter(bool check_sender, AppletId sender_appid, bool check_receiver,
                                    AppletId receiver_appid) {
    auto cancellation_success =
        next_parameter && (!check_sender || next_parameter->sender_id == sender_appid) &&
        (!check_receiver || next_parameter->destination_id == receiver_appid);

    if (cancellation_success)
        next_parameter = {};

    return cancellation_success;
}

ResultVal<AppletManager::GetLockHandleResult> AppletManager::GetLockHandle(
    AppletAttributes attributes) {
    auto corrected_attributes = attributes;
    if (attributes.applet_pos == AppletPos::Library ||
        attributes.applet_pos == AppletPos::SysLibrary ||
        attributes.applet_pos == AppletPos::AutoLibrary) {
        auto corrected_pos = last_library_launcher_slot == AppletSlot::Application
                                 ? AppletPos::Library
                                 : AppletPos::SysLibrary;
        corrected_attributes.applet_pos.Assign(corrected_pos);
        LOG_DEBUG(Service_APT, "Corrected applet attributes from {:08X} to {:08X}", attributes.raw,
                  corrected_attributes.raw);
    }

    return GetLockHandleResult{corrected_attributes, 0, lock};
}

ResultVal<AppletManager::InitializeResult> AppletManager::Initialize(AppletId app_id,
                                                                     AppletAttributes attributes) {
    auto slot = GetAppletSlotFromAttributes(attributes);
    // Note: The real NS service does not check if the attributes value is valid before accessing
    // the data in the array
    ASSERT_MSG(slot != AppletSlot::Error, "Invalid application attributes");

    auto slot_data = GetAppletSlot(slot);
    if (slot_data->registered) {
        LOG_WARNING(Service_APT, "Applet attempted to register in occupied slot {:02X}", slot);
        return Result(ErrorDescription::AlreadyExists, ErrorModule::Applet,
                      ErrorSummary::InvalidState, ErrorLevel::Status);
    }

    LOG_DEBUG(Service_APT, "Initializing applet with ID {:03X} and attributes {:08X}.", app_id,
              attributes.raw);
    slot_data->applet_id = static_cast<AppletId>(app_id);
    // Note: In the real console the title id of a given applet slot is set by the APT module when
    // calling StartApplication.
    slot_data->title_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
    slot_data->attributes.raw = attributes.raw;

    // Applications need to receive a Wakeup signal to actually start up, this signal is usually
    // sent by the Home Menu after starting the app by way of APT::WakeupApplication. However,
    // if nothing is running yet the signal should be sent by APT::Initialize itself.
    if (active_slot == AppletSlot::Error) {
        active_slot = slot;

        // APT automatically calls enable on the first registered applet.
        Enable(attributes);

        // Wake up the applet.
        SendParameter({
            .sender_id = AppletId::None,
            .destination_id = app_id,
            .signal = SignalType::Wakeup,
        });
    }

    return InitializeResult{slot_data->notification_event, slot_data->parameter_event};
}

Result AppletManager::Enable(AppletAttributes attributes) {
    auto slot = GetAppletSlotFromAttributes(attributes);
    if (slot == AppletSlot::Error) {
        LOG_WARNING(Service_APT,
                    "Attempted to register with attributes {:08X}, but could not find slot.",
                    attributes.raw);
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    LOG_DEBUG(Service_APT, "Registering applet with attributes {:08X}.", attributes.raw);
    auto slot_data = GetAppletSlot(slot);
    slot_data->registered = true;

    if (slot_data->applet_id != AppletId::None &&
        slot_data->attributes.applet_pos == AppletPos::System &&
        slot_data->attributes.is_home_menu) {
        slot_data->attributes.raw |= attributes.raw;
        LOG_DEBUG(Service_APT, "Updated home menu attributes to {:08X}.",
                  slot_data->attributes.raw);
    }

    // Send any outstanding parameters to the newly-registered application
    if (delayed_parameter && delayed_parameter->destination_id == slot_data->applet_id) {
        // TODO: Real APT would loop trying to send the parameter until it succeeds,
        // essentially waiting for existing parameters to be delivered.
        CancelAndSendParameter(*delayed_parameter);
        delayed_parameter.reset();
    }

    return ResultSuccess;
}

Result AppletManager::Finalize(AppletId app_id) {
    auto slot = GetAppletSlotFromId(app_id);
    if (slot == AppletSlot::Error) {
        return {ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                ErrorLevel::Status};
    }

    auto slot_data = GetAppletSlot(slot);
    slot_data->Reset();

    auto inactive = active_slot == AppletSlot::Error;
    if (!inactive) {
        auto active_slot_data = GetAppletSlot(active_slot);
        inactive = active_slot_data->applet_id == AppletId::None ||
                   active_slot_data->attributes.applet_pos.Value() == AppletPos::Invalid;
    }

    if (inactive) {
        active_slot = GetAppletSlotFromPos(AppletPos::System);
    }

    return ResultSuccess;
}

u32 AppletManager::CountRegisteredApplet() {
    return static_cast<u32>(std::count_if(applet_slots.begin(), applet_slots.end(),
                                          [](auto& slot_data) { return slot_data.registered; }));
}

bool AppletManager::IsRegistered(AppletId app_id) {
    auto slot = GetAppletSlotFromId(app_id);
    return slot != AppletSlot::Error && GetAppletSlot(slot)->registered;
}

ResultVal<AppletAttributes> AppletManager::GetAttribute(AppletId app_id) {
    auto slot = GetAppletSlotFromId(app_id);
    if (slot == AppletSlot::Error) {
        return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                      ErrorLevel::Status);
    }

    auto slot_data = GetAppletSlot(slot);
    if (!slot_data->registered) {
        return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                      ErrorLevel::Status);
    }

    return slot_data->attributes;
}

ResultVal<Notification> AppletManager::InquireNotification(AppletId app_id) {
    auto slot = GetAppletSlotFromId(app_id);
    if (slot != AppletSlot::Error) {
        auto slot_data = GetAppletSlot(slot);
        if (slot_data->registered) {
            auto notification = slot_data->notification;
            slot_data->notification = Notification::None;
            return notification;
        }
    }

    return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                  ErrorLevel::Status);
}

Result AppletManager::SendNotification(Notification notification) {
    if (active_slot != AppletSlot::Error) {
        const auto slot_data = GetAppletSlot(active_slot);
        if (slot_data->registered) {
            slot_data->notification = notification;
            slot_data->notification_event->Signal();
            return ResultSuccess;
        }
    }

    return {ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
            ErrorLevel::Status};
}

void AppletManager::SendNotificationToAll(Notification notification) {
    for (auto& slot_data : applet_slots) {
        if (slot_data.registered) {
            slot_data.notification = notification;
            slot_data.notification_event->Signal();
        }
    }
}

Result AppletManager::CreateHLEApplet(AppletId id, AppletId parent, bool preload) {
    switch (id) {
    case AppletId::SoftwareKeyboard1:
    case AppletId::SoftwareKeyboard2:
        hle_applets[id] = std::make_shared<HLE::Applets::SoftwareKeyboard>(
            system, id, parent, preload, shared_from_this());
        break;
    case AppletId::Ed1:
    case AppletId::Ed2:
        hle_applets[id] = std::make_shared<HLE::Applets::MiiSelector>(system, id, parent, preload,
                                                                      shared_from_this());
        break;
    case AppletId::Error:
    case AppletId::Error2:
        hle_applets[id] = std::make_shared<HLE::Applets::ErrEula>(system, id, parent, preload,
                                                                  shared_from_this());
        break;
    case AppletId::Mint:
    case AppletId::Mint2:
        hle_applets[id] =
            std::make_shared<HLE::Applets::Mint>(system, id, parent, preload, shared_from_this());
        break;
    default:
        LOG_ERROR(Service_APT, "Could not create applet {}", id);
        // TODO(Subv): Find the right error code
        return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotSupported,
                      ErrorLevel::Permanent);
    }

    AppletAttributes attributes;
    attributes.applet_pos.Assign(AppletPos::AutoLibrary);
    attributes.is_home_menu.Assign(false);
    const auto lock_handle_data = GetLockHandle(attributes);

    Initialize(id, lock_handle_data->corrected_attributes);
    Enable(lock_handle_data->corrected_attributes);
    if (preload) {
        FinishPreloadingLibraryApplet(id);
    }

    // Schedule the update event
    system.CoreTiming().ScheduleEvent(usToCycles(hle_applet_update_interval_us),
                                      hle_applet_update_event, static_cast<u64>(id));
    return ResultSuccess;
}

Result AppletManager::PrepareToStartLibraryApplet(AppletId applet_id) {
    // The real APT service returns an error if there's a pending APT parameter when this function
    // is called.
    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    if (GetAppletSlot(AppletSlot::LibraryApplet)->registered) {
        return {ErrorDescription::AlreadyExists, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    last_library_launcher_slot = active_slot;
    last_prepared_library_applet = applet_id;

    capture_buffer_info.reset();

    if (Settings::values.lle_applets) {
        auto cfg = Service::CFG::GetModule(system);
        auto process = NS::LaunchTitle(FS::MediaType::NAND,
                                       GetTitleIdForApplet(applet_id, cfg->GetRegionValue()));
        if (process) {
            return ResultSuccess;
        }
    }

    // If we weren't able to load the native applet title, try to fallback to an HLE implementation.
    if (hle_applets[applet_id] != nullptr) {
        LOG_WARNING(Service_APT, "Applet has already been started id={:03X}", applet_id);
        return ResultSuccess;
    } else {
        auto parent = GetAppletSlotId(last_library_launcher_slot);
        LOG_DEBUG(Service_APT, "Creating HLE applet {:03X} with parent {:03X}", applet_id, parent);
        return CreateHLEApplet(applet_id, parent, false);
    }
}

Result AppletManager::PreloadLibraryApplet(AppletId applet_id) {
    if (GetAppletSlot(AppletSlot::LibraryApplet)->registered) {
        return {ErrorDescription::AlreadyExists, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    last_library_launcher_slot = active_slot;
    last_prepared_library_applet = applet_id;

    if (Settings::values.lle_applets) {
        auto cfg = Service::CFG::GetModule(system);
        auto process = NS::LaunchTitle(FS::MediaType::NAND,
                                       GetTitleIdForApplet(applet_id, cfg->GetRegionValue()));
        if (process) {
            return ResultSuccess;
        }
    }

    // If we weren't able to load the native applet title, try to fallback to an HLE implementation.
    if (hle_applets[applet_id] != nullptr) {
        LOG_WARNING(Service_APT, "Applet has already been started id={:08X}", applet_id);
        return ResultSuccess;
    } else {
        auto parent = GetAppletSlotId(last_library_launcher_slot);
        LOG_DEBUG(Service_APT, "Creating HLE applet {:03X} with parent {:03X}", applet_id, parent);
        return CreateHLEApplet(applet_id, parent, true);
    }
}

Result AppletManager::FinishPreloadingLibraryApplet(AppletId applet_id) {
    // TODO(Subv): This function should fail depending on the applet preparation state.
    GetAppletSlot(AppletSlot::LibraryApplet)->loaded = true;
    return ResultSuccess;
}

Result AppletManager::StartLibraryApplet(AppletId applet_id, std::shared_ptr<Kernel::Object> object,
                                         const std::vector<u8>& buffer) {
    active_slot = AppletSlot::LibraryApplet;

    auto send_res = SendParameter({
        .sender_id = GetAppletSlotId(last_library_launcher_slot),
        .destination_id = applet_id,
        .signal = SignalType::Wakeup,
        .object = std::move(object),
        .buffer = buffer,
    });
    if (send_res.IsError()) {
        active_slot = last_library_launcher_slot;
        return send_res;
    }

    return ResultSuccess;
}

Result AppletManager::PrepareToCloseLibraryApplet(bool not_pause, bool exiting, bool jump_home) {
    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    if (!not_pause)
        library_applet_closing_command = SignalType::WakeupByPause;
    else if (jump_home)
        library_applet_closing_command = SignalType::WakeupToJumpHome;
    else if (exiting)
        library_applet_closing_command = SignalType::WakeupByCancel;
    else
        library_applet_closing_command = SignalType::WakeupByExit;

    return ResultSuccess;
}

Result AppletManager::CloseLibraryApplet(std::shared_ptr<Kernel::Object> object,
                                         const std::vector<u8>& buffer) {
    auto slot = GetAppletSlot(AppletSlot::LibraryApplet);
    auto destination_id = GetAppletSlotId(last_library_launcher_slot);

    active_slot = last_library_launcher_slot;

    MessageParameter param = {
        .sender_id = slot->applet_id,
        .destination_id = destination_id,
        .signal = library_applet_closing_command,
        .object = std::move(object),
        .buffer = buffer,
    };

    if (library_applet_closing_command != SignalType::WakeupByPause) {
        CancelAndSendParameter(param);
        // TODO: Terminate the running applet title
        slot->Reset();
    } else {
        SendParameter(param);
    }

    return ResultSuccess;
}

Result AppletManager::CancelLibraryApplet(bool app_exiting) {
    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    auto slot = GetAppletSlot(AppletSlot::LibraryApplet);
    if (!slot->registered) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    return SendParameter({
        .sender_id = GetAppletSlotId(last_library_launcher_slot),
        .destination_id = slot->applet_id,
        .signal = SignalType::WakeupByCancel,
    });
}

Result AppletManager::SendDspSleep(AppletId from_applet_id,
                                   std::shared_ptr<Kernel::Object> object) {
    auto lib_slot = GetAppletSlotFromPos(AppletPos::Library);
    auto lib_app_id =
        lib_slot != AppletSlot::Error ? GetAppletSlot(lib_slot)->applet_id : AppletId::None;
    if (from_applet_id == lib_app_id) {
        SendParameter({
            .sender_id = from_applet_id,
            .destination_id = AppletId::Application,
            .signal = SignalType::DspSleep,
            .object = std::move(object),
        });
        return ResultSuccess;
    }

    auto sys_lib_slot = GetAppletSlotFromPos(AppletPos::SysLibrary);
    auto sys_lib_app_id =
        sys_lib_slot != AppletSlot::Error ? GetAppletSlot(sys_lib_slot)->applet_id : AppletId::None;
    if (from_applet_id == sys_lib_app_id) {
        auto sys_slot = GetAppletSlotFromPos(AppletPos::System);
        auto sys_app_id =
            sys_slot != AppletSlot::Error ? GetAppletSlot(sys_slot)->applet_id : AppletId::None;
        SendParameter({
            .sender_id = from_applet_id,
            .destination_id = sys_app_id,
            .signal = SignalType::DspSleep,
            .object = std::move(object),
        });
        return ResultSuccess;
    }

    return ResultSuccess;
}

Result AppletManager::SendDspWakeUp(AppletId from_applet_id,
                                    std::shared_ptr<Kernel::Object> object) {
    auto lib_slot = GetAppletSlotFromPos(AppletPos::Library);
    auto lib_app_id =
        lib_slot != AppletSlot::Error ? GetAppletSlot(lib_slot)->applet_id : AppletId::None;
    if (from_applet_id == lib_app_id) {
        SendParameter({
            .sender_id = from_applet_id,
            .destination_id = AppletId::Application,
            .signal = SignalType::DspSleep,
            .object = std::move(object),
        });
    } else {
        auto sys_lib_slot = GetAppletSlotFromPos(AppletPos::SysLibrary);
        auto sys_lib_app_id = sys_lib_slot != AppletSlot::Error
                                  ? GetAppletSlot(sys_lib_slot)->applet_id
                                  : AppletId::None;
        if (from_applet_id == sys_lib_app_id) {
            auto sys_slot = GetAppletSlotFromPos(AppletPos::System);
            auto sys_app_id =
                sys_slot != AppletSlot::Error ? GetAppletSlot(sys_slot)->applet_id : AppletId::None;
            SendParameter({
                .sender_id = from_applet_id,
                .destination_id = sys_app_id,
                .signal = SignalType::DspSleep,
                .object = std::move(object),
            });
        }
    }

    return ResultSuccess;
}

Result AppletManager::PrepareToStartSystemApplet(AppletId applet_id) {
    // The real APT service returns an error if there's a pending APT parameter when this function
    // is called.
    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    last_system_launcher_slot = active_slot;
    return ResultSuccess;
}

Result AppletManager::StartSystemApplet(AppletId applet_id, std::shared_ptr<Kernel::Object> object,
                                        const std::vector<u8>& buffer) {
    auto source_applet_id = AppletId::Application;
    if (last_system_launcher_slot != AppletSlot::Error) {
        const auto launcher_slot_data = GetAppletSlot(last_system_launcher_slot);
        source_applet_id = launcher_slot_data->applet_id;

        // APT generally clears and terminates the caller of StartSystemApplet. This helps in
        // situations such as a system applet launching another system applet, which would
        // otherwise deadlock.
        // TODO: In real APT, the check for AppletSlot::Application does not exist; there is
        // TODO: something wrong with our implementation somewhere that makes this necessary.
        // TODO: Otherwise, games that attempt to launch system applets will be cleared and
        // TODO: emulation will crash.
        if (!launcher_slot_data->registered ||
            (last_system_launcher_slot != AppletSlot::Application &&
             !launcher_slot_data->attributes.no_exit_on_system_applet)) {
            launcher_slot_data->Reset();
            // TODO: Implement launcher process termination.
        }
    }

    // If a system applet is not already registered, it is started by APT.
    const auto slot_id =
        applet_id == AppletId::HomeMenu ? AppletSlot::HomeMenu : AppletSlot::SystemApplet;
    if (!GetAppletSlot(slot_id)->registered) {
        auto cfg = Service::CFG::GetModule(system);
        auto process = NS::LaunchTitle(FS::MediaType::NAND,
                                       GetTitleIdForApplet(applet_id, cfg->GetRegionValue()));
        if (!process) {
            // TODO: Find the right error code.
            return {ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotSupported,
                    ErrorLevel::Permanent};
        }
    }

    active_slot = slot_id;

    SendApplicationParameterAfterRegistration({
        .sender_id = source_applet_id,
        .destination_id = applet_id,
        .signal = SignalType::Wakeup,
        .object = std::move(object),
        .buffer = buffer,
    });

    return ResultSuccess;
}

Result AppletManager::PrepareToCloseSystemApplet() {
    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    return ResultSuccess;
}

Result AppletManager::CloseSystemApplet(std::shared_ptr<Kernel::Object> object,
                                        const std::vector<u8>& buffer) {
    ASSERT_MSG(active_slot == AppletSlot::HomeMenu || active_slot == AppletSlot::SystemApplet,
               "Attempting to close a system applet from a non-system applet.");

    auto slot = GetAppletSlot(active_slot);
    auto closed_applet_id = slot->applet_id;

    active_slot = last_system_launcher_slot;
    slot->Reset();

    if (ordered_to_close_sys_applet) {
        ordered_to_close_sys_applet = false;

        active_slot = AppletSlot::Application;
        CancelAndSendParameter({
            .sender_id = closed_applet_id,
            .destination_id = AppletId::Application,
            .signal = SignalType::WakeupByExit,
            .object = std::move(object),
            .buffer = buffer,
        });
    }

    // TODO: Terminate the running applet title
    return ResultSuccess;
}

Result AppletManager::OrderToCloseSystemApplet() {
    if (active_slot == AppletSlot::Error) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    auto active_slot_data = GetAppletSlot(active_slot);
    if (active_slot_data->applet_id == AppletId::None ||
        active_slot_data->attributes.applet_pos != AppletPos::Application) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    auto system_slot = GetAppletSlotFromPos(AppletPos::System);
    if (system_slot == AppletSlot::Error) {
        return {ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                ErrorLevel::Status};
    }

    auto system_slot_data = GetAppletSlot(system_slot);
    if (!system_slot_data->registered) {
        return {ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                ErrorLevel::Status};
    }

    ordered_to_close_sys_applet = true;
    active_slot = system_slot;

    SendParameter({
        .sender_id = AppletId::Application,
        .destination_id = system_slot_data->applet_id,
        .signal = SignalType::WakeupByCancel,
    });

    return ResultSuccess;
}

Result AppletManager::PrepareToJumpToHomeMenu() {
    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    last_jump_to_home_slot = active_slot;

    capture_buffer_info.reset();

    if (last_jump_to_home_slot == AppletSlot::Application) {
        EnsureHomeMenuLoaded();
    }

    return ResultSuccess;
}

Result AppletManager::JumpToHomeMenu(std::shared_ptr<Kernel::Object> object,
                                     const std::vector<u8>& buffer) {
    if (last_jump_to_home_slot != AppletSlot::Error) {
        auto slot_data = GetAppletSlot(last_jump_to_home_slot);
        if (slot_data->applet_id != AppletId::None) {
            MessageParameter param;
            param.object = std::move(object);
            param.buffer = buffer;

            switch (slot_data->attributes.applet_pos) {
            case AppletPos::Application:
                active_slot = AppletSlot::HomeMenu;

                param.destination_id = AppletId::HomeMenu;
                param.sender_id = AppletId::Application;
                param.signal = SignalType::WakeupByPause;
                SendParameter(param);
                break;
            case AppletPos::Library:
                param.destination_id = slot_data->applet_id;
                param.sender_id = slot_data->applet_id;
                param.signal = SignalType::WakeupByCancel;
                SendParameter(param);
                break;
            case AppletPos::System:
                if (slot_data->attributes.is_home_menu) {
                    param.destination_id = slot_data->applet_id;
                    param.sender_id = slot_data->applet_id;
                    param.signal = SignalType::WakeupToJumpHome;
                    SendParameter(param);
                }
                break;
            case AppletPos::SysLibrary: {
                const auto system_slot_data = GetAppletSlot(AppletSlot::SystemApplet);
                param.destination_id = slot_data->applet_id;
                param.sender_id = slot_data->applet_id;
                param.signal = system_slot_data->registered ? SignalType::WakeupByCancel
                                                            : SignalType::WakeupToJumpHome;
                SendParameter(param);
                break;
            }
            default:
                break;
            }
        }
    }

    return ResultSuccess;
}

Result AppletManager::PrepareToLeaveHomeMenu() {
    if (!GetAppletSlot(AppletSlot::Application)->registered) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    return ResultSuccess;
}

Result AppletManager::LeaveHomeMenu(std::shared_ptr<Kernel::Object> object,
                                    const std::vector<u8>& buffer) {
    active_slot = AppletSlot::Application;

    SendParameter({
        .sender_id = AppletId::HomeMenu,
        .destination_id = AppletId::Application,
        .signal = SignalType::WakeupByPause,
        .object = std::move(object),
        .buffer = buffer,
    });

    return ResultSuccess;
}

Result AppletManager::OrderToCloseApplication() {
    if (active_slot == AppletSlot::Error) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    auto active_slot_data = GetAppletSlot(active_slot);
    if (active_slot_data->applet_id == AppletId::None ||
        active_slot_data->attributes.applet_pos != AppletPos::System) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    ordered_to_close_application = true;
    active_slot = AppletSlot::Application;

    SendParameter({
        .sender_id = AppletId::HomeMenu,
        .destination_id = AppletId::Application,
        .signal = SignalType::WakeupByCancel,
    });

    return ResultSuccess;
}

Result AppletManager::PrepareToCloseApplication(bool return_to_sys) {
    if (active_slot == AppletSlot::Error) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    auto active_slot_data = GetAppletSlot(active_slot);
    if (active_slot_data->applet_id == AppletId::None ||
        active_slot_data->attributes.applet_pos != AppletPos::Application) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    auto system_slot_data = GetAppletSlot(AppletSlot::SystemApplet);
    auto home_menu_slot_data = GetAppletSlot(AppletSlot::HomeMenu);

    if (!application_cancelled && return_to_sys) {
        // TODO: Left side of the OR also includes "&& !power_button_clicked", but this isn't
        // implemented yet.
        if (!ordered_to_close_application || !system_slot_data->registered) {
            application_close_target = AppletSlot::HomeMenu;
        } else {
            application_close_target = AppletSlot::SystemApplet;
        }
    } else {
        application_close_target = AppletSlot::Error;
    }

    if (application_close_target != AppletSlot::HomeMenu && !system_slot_data->registered &&
        !home_menu_slot_data->registered) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    if (next_parameter) {
        return {ErrCodes::ParameterPresent, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    if (application_close_target == AppletSlot::HomeMenu) {
        // Real APT would make sure home menu is loaded here. However, this is only really
        // needed if the home menu wasn't loaded in the first place. Since we want to
        // preserve normal behavior when the user loaded the game directly without going
        // through home menu, we skip this. Then, later we just close to the game list
        // when the application finishes closing.
        // EnsureHomeMenuLoaded();
    }

    return ResultSuccess;
}

Result AppletManager::CloseApplication(std::shared_ptr<Kernel::Object> object,
                                       const std::vector<u8>& buffer) {
    ordered_to_close_application = false;
    application_cancelled = false;

    GetAppletSlot(AppletSlot::Application)->Reset();

    if (application_close_target != AppletSlot::Error) {
        // If exiting to the home menu and it is not loaded, exit to game list.
        if (application_close_target == AppletSlot::HomeMenu &&
            !GetAppletSlot(application_close_target)->registered) {
            system.RequestShutdown();
        } else {
            active_slot = application_close_target;

            CancelAndSendParameter({
                .sender_id = AppletId::Application,
                .destination_id = GetAppletSlot(application_close_target)->applet_id,
                .signal = SignalType::WakeupByExit,
                .object = std::move(object),
                .buffer = buffer,
            });
        }
    }

    // TODO: Terminate the application process.
    return ResultSuccess;
}

ResultVal<AppletManager::AppletManInfo> AppletManager::GetAppletManInfo(
    AppletPos requested_applet_pos) {
    auto active_applet_pos = AppletPos::Invalid;
    auto active_applet_id = AppletId::None;
    if (active_slot != AppletSlot::Error) {
        auto active_slot_data = GetAppletSlot(active_slot);
        if (active_slot_data->applet_id != AppletId::None) {
            active_applet_pos = active_slot_data->attributes.applet_pos;
            active_applet_id = active_slot_data->applet_id;
        }
    }

    auto requested_applet_id = AppletId::None;
    auto requested_slot = GetAppletSlotFromPos(requested_applet_pos);
    if (requested_slot != AppletSlot::Error) {
        auto requested_slot_data = GetAppletSlot(requested_slot);
        if (requested_slot_data->registered) {
            requested_applet_id = requested_slot_data->applet_id;
        }
    }

    return AppletManInfo{
        .active_applet_pos = active_applet_pos,
        .requested_applet_id = requested_applet_id,
        .home_menu_applet_id = AppletId::HomeMenu,
        .active_applet_id = active_applet_id,
    };
}

ResultVal<AppletManager::AppletInfo> AppletManager::GetAppletInfo(AppletId app_id) {
    auto slot = GetAppletSlotFromId(app_id);
    if (slot == AppletSlot::Error) {
        return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                      ErrorLevel::Status);
    }

    auto slot_data = GetAppletSlot(slot);
    if (!slot_data->registered) {
        return Result(ErrorDescription::NotFound, ErrorModule::Applet, ErrorSummary::NotFound,
                      ErrorLevel::Status);
    }

    auto media_type = Service::AM::GetTitleMediaType(slot_data->title_id);
    return AppletInfo{
        .title_id = slot_data->title_id,
        .media_type = media_type,
        .registered = slot_data->registered,
        .loaded = slot_data->loaded,
        .attributes = slot_data->attributes.raw,
    };
}

ResultVal<Service::FS::MediaType> AppletManager::Unknown54(u32 in_param) {
    auto slot_data = GetAppletSlot(AppletSlot::Application);
    if (slot_data->applet_id == AppletId::None) {
        return Result{ErrCodes::AppNotRunning, ErrorModule::Applet, ErrorSummary::InvalidState,
                      ErrorLevel::Permanent};
    }

    if (in_param >= 0x80) {
        // TODO: Add error description name when the parameter is known.
        return Result{10, ErrorModule::Applet, ErrorSummary::InvalidArgument, ErrorLevel::Usage};
    }

    // TODO: Figure out what this logic is actually for.
    auto check_target =
        in_param >= 0x40 ? Service::FS::MediaType::GameCard : Service::FS::MediaType::SDMC;
    auto check_update = in_param == 0x01 || in_param == 0x42;

    auto app_media_type = Service::AM::GetTitleMediaType(slot_data->title_id);
    auto app_update_media_type =
        Service::AM::GetTitleMediaType(Service::AM::GetTitleUpdateId(slot_data->title_id));
    if (app_media_type == check_target || (check_update && app_update_media_type == check_target)) {
        return Service::FS::MediaType::SDMC;
    } else {
        return Service::FS::MediaType::NAND;
    }
}

TargetPlatform AppletManager::GetTargetPlatform() {
    if (Settings::values.is_new_3ds.GetValue() && !new_3ds_mode_blocked) {
        return TargetPlatform::New3ds;
    } else {
        return TargetPlatform::Old3ds;
    }
}

ApplicationRunningMode AppletManager::GetApplicationRunningMode() {
    auto slot_data = GetAppletSlot(AppletSlot::Application);
    if (slot_data->applet_id == AppletId::None) {
        return ApplicationRunningMode::NoApplication;
    }

    // APT checks whether the system is a New 3DS and the 804MHz CPU speed is enabled to determine
    // the result.
    auto new_3ds_mode = GetTargetPlatform() == TargetPlatform::New3ds &&
                        system.Kernel().GetNew3dsHwCapabilities().enable_804MHz_cpu;
    if (slot_data->registered) {
        return new_3ds_mode ? ApplicationRunningMode::New3dsRegistered
                            : ApplicationRunningMode::Old3dsRegistered;
    } else {
        return new_3ds_mode ? ApplicationRunningMode::New3dsUnregistered
                            : ApplicationRunningMode::Old3dsUnregistered;
    }
}

Result AppletManager::PrepareToDoApplicationJump(u64 title_id, FS::MediaType media_type,
                                                 ApplicationJumpFlags flags) {
    // A running application can not launch another application directly because the applet state
    // for the Application slot is already in use. The way this is implemented in hardware is to
    // launch the Home Menu and tell it to launch our desired application.

    ASSERT_MSG(flags != ApplicationJumpFlags::UseStoredParameters,
               "Unimplemented application jump flags 1");

    // Save the title data to send it to the Home Menu when DoApplicationJump is called.
    auto application_slot_data = GetAppletSlot(AppletSlot::Application);
    app_jump_parameters.current_title_id = application_slot_data->title_id;
    app_jump_parameters.current_media_type =
        Service::AM::GetTitleMediaType(application_slot_data->title_id);
    if (flags == ApplicationJumpFlags::UseCurrentParameters) {
        app_jump_parameters.next_title_id = app_jump_parameters.current_title_id;
        app_jump_parameters.next_media_type = app_jump_parameters.current_media_type;
    } else {
        app_jump_parameters.next_title_id = title_id;
        app_jump_parameters.next_media_type = media_type;
    }
    app_jump_parameters.flags = flags;

    // Note: The real console uses the Home Menu to perform the application jump, therefore the menu
    // needs to be running. The real APT module starts the Home Menu here if it's not already
    // running, we don't have to do this. See `EnsureHomeMenuLoaded` for launching the Home Menu.
    return ResultSuccess;
}

Result AppletManager::DoApplicationJump(const DeliverArg& arg) {
    // Note: The real console uses the Home Menu to perform the application jump, it goes
    // OldApplication->Home Menu->NewApplication. We do not need to use the Home Menu to do this so
    // we launch the new application directly. In the real APT service, the Home Menu must be
    // running to do this, otherwise error 0xC8A0CFF0 is returned.

    auto application_slot_data = GetAppletSlot(AppletSlot::Application);
    auto title_id = application_slot_data->title_id;
    application_slot_data->Reset();

    // Set the delivery parameters.
    deliver_arg = arg;
    if (app_jump_parameters.flags != ApplicationJumpFlags::UseCurrentParameters) {
        // The source program ID is not updated when using flags 0x2.
        deliver_arg->source_program_id = title_id;
    }

    if (GetAppletSlot(AppletSlot::HomeMenu)->registered) {
        // If the home menu is running, use it to jump to the next application.
        // The home menu will call GetProgramIdOnApplicationJump and
        // PrepareToStartApplication/StartApplication to launch the title.
        active_slot = AppletSlot::HomeMenu;
        SendParameter({
            .sender_id = AppletId::Application,
            .destination_id = AppletId::HomeMenu,
            .signal = SignalType::WakeupToLaunchApplication,
        });

        // TODO: APT terminates the application here, usually it will exit itself properly though.
        return ResultSuccess;
    } else {
        // Otherwise, work around the missing home menu by launching the title directly.

        // TODO: The emulator does not support terminating the old process immediately.
        // We could call TerminateProcess but references to the process are still held elsewhere,
        // preventing clean up. This code is left commented for when this is implemented, for now we
        // cannot use NS as the old process resources would interfere with the new ones.
        /*
        auto process =
            NS::LaunchTitle(app_jump_parameters.next_media_type, app_jump_parameters.next_title_id);
        if (!process) {
            LOG_CRITICAL(Service_APT, "Failed to launch title during application jump, exiting.");
            system.RequestShutdown();
        }
        return ResultSuccess;
        */

        NS::RebootToTitle(system, app_jump_parameters.next_media_type,
                          app_jump_parameters.next_title_id);
        return ResultSuccess;
    }
}

Result AppletManager::PrepareToStartApplication(u64 title_id, FS::MediaType media_type) {
    if (active_slot == AppletSlot::Error ||
        GetAppletSlot(active_slot)->attributes.applet_pos != AppletPos::System) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    // TODO(Subv): This should return 0xc8a0cff0 if the applet preparation state is already set

    if (GetAppletSlot(AppletSlot::Application)->registered) {
        return {ErrorDescription::AlreadyExists, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    ASSERT_MSG(!app_start_parameters,
               "Trying to prepare an application when another is already prepared");

    app_start_parameters.emplace();
    app_start_parameters->next_title_id = title_id;
    app_start_parameters->next_media_type = media_type;

    capture_buffer_info.reset();

    return ResultSuccess;
}

Result AppletManager::StartApplication(const std::vector<u8>& parameter,
                                       const std::vector<u8>& hmac, bool paused) {
    // The delivery argument is always unconditionally set.
    deliver_arg.emplace(DeliverArg{parameter, hmac});

    // Note: APT first checks if we can launch the application via AM::CheckDemoLaunchRights and
    // returns 0xc8a12403 if we can't. We intentionally do not implement that check.

    // TODO(Subv): The APT service performs several checks here related to the exheader flags of the
    // process we're launching and other things like title id blacklists. We do not yet implement
    // any of that.

    // TODO(Subv): The real APT service doesn't seem to check whether the titleid to launch is set
    // or not, it either launches NATIVE_FIRM if some internal state is set, or fails when calling
    // PM::LaunchTitle. We should research more about that.
    ASSERT_MSG(app_start_parameters, "Trying to start an application without preparing it first.");

    active_slot = AppletSlot::Application;

    // Launch the title directly.
    auto process =
        NS::LaunchTitle(app_start_parameters->next_media_type, app_start_parameters->next_title_id);
    if (!process) {
        LOG_CRITICAL(Service_APT, "Failed to launch title during application start, exiting.");
        system.RequestShutdown();
    }

    app_start_parameters.reset();

    if (!paused) {
        return WakeupApplication(nullptr, {});
    }

    return ResultSuccess;
}

Result AppletManager::WakeupApplication(std::shared_ptr<Kernel::Object> object,
                                        const std::vector<u8>& buffer) {
    // Send a Wakeup signal via the apt parameter to the application once it registers itself.
    // The real APT service does this by spin waiting on another thread until the application is
    // registered.
    SendApplicationParameterAfterRegistration({
        .sender_id = AppletId::HomeMenu,
        .destination_id = AppletId::Application,
        .signal = SignalType::Wakeup,
        .object = std::move(object),
        .buffer = buffer,
    });

    return ResultSuccess;
}

Result AppletManager::CancelApplication() {
    auto application_slot_data = GetAppletSlot(AppletSlot::Application);
    if (application_slot_data->applet_id == AppletId::None) {
        return {ErrCodes::InvalidAppletSlot, ErrorModule::Applet, ErrorSummary::InvalidState,
                ErrorLevel::Status};
    }

    application_cancelled = true;

    SendApplicationParameterAfterRegistration({
        .sender_id = active_slot != AppletSlot::Error ? GetAppletSlot(active_slot)->applet_id
                                                      : AppletId::None,
        .destination_id = AppletId::Application,
        .signal = SignalType::WakeupByCancel,
    });

    return ResultSuccess;
}

void AppletManager::SendApplicationParameterAfterRegistration(const MessageParameter& parameter) {
    auto slot = GetAppletSlotFromId(parameter.destination_id);

    // If the application is already registered, immediately send the parameter
    if (slot != AppletSlot::Error && GetAppletSlot(slot)->registered) {
        CancelAndSendParameter(parameter);
        return;
    }

    // Otherwise queue it until the Application calls APT::Enable
    delayed_parameter = parameter;
}

void AppletManager::EnsureHomeMenuLoaded() {
    // TODO(Subv): The real APT service sends signal 12 (WakeupByCancel) to the currently running
    // System applet, waits for it to finish, and then launches the Home Menu.
    ASSERT_MSG(!GetAppletSlot(AppletSlot::SystemApplet)->registered,
               "A system applet is already running");

    if (GetAppletSlot(AppletSlot::HomeMenu)->registered) {
        // The Home Menu is already running.
        return;
    }

    auto cfg = Service::CFG::GetModule(system);
    auto menu_title_id = GetTitleIdForApplet(AppletId::HomeMenu, cfg->GetRegionValue());
    auto process = NS::LaunchTitle(FS::MediaType::NAND, menu_title_id);
    if (!process) {
        LOG_WARNING(Service_APT,
                    "The Home Menu failed to launch, application jumping will not work.");
    }
}

static u32 GetDisplayBufferModePixelSize(DisplayBufferMode mode) {
    switch (mode) {
    // NOTE: APT does in fact use pixel size 3 for R8G8B8A8 captures.
    case DisplayBufferMode::R8G8B8A8:
    case DisplayBufferMode::R8G8B8:
        return 3;
    case DisplayBufferMode::R5G6B5:
    case DisplayBufferMode::R5G5B5A1:
    case DisplayBufferMode::R4G4B4A4:
        return 2;
    case DisplayBufferMode::Unimportable:
        return 0;
    default:
        UNREACHABLE_MSG("Unknown display buffer mode {}", mode);
        return 0;
    }
}

static void CaptureFrameBuffer(Core::System& system, u32 capture_offset, VAddr src, u32 height,
                               DisplayBufferMode mode) {
    const auto bpp = GetDisplayBufferModePixelSize(mode);
    if (bpp == 0) {
        return;
    }

    system.Memory().RasterizerFlushVirtualRegion(src, GSP::FRAMEBUFFER_WIDTH * height * bpp,
                                                 Memory::FlushMode::Flush);

    // Address in VRAM that APT copies framebuffer captures to.
    constexpr VAddr screen_capture_base_vaddr = Memory::VRAM_VADDR + 0x500000;
    const auto dst_vaddr = screen_capture_base_vaddr + capture_offset;
    auto dst_ptr = system.Memory().GetPointer(dst_vaddr);
    if (!dst_ptr) {
        LOG_ERROR(Service_APT,
                  "Could not retrieve framebuffer capture destination buffer, skipping screen.");
        return;
    }

    const auto src_ptr = system.Memory().GetPointer(src);
    if (!src_ptr) {
        LOG_ERROR(Service_APT,
                  "Could not retrieve framebuffer capture source buffer, skipping screen.");
        return;
    }

    for (u32 y = 0; y < height; y++) {
        for (u32 x = 0; x < GSP::FRAMEBUFFER_WIDTH; x++) {
            const auto dst_offset = VideoCore::GetMortonOffset(x, y, bpp) +
                                    (y & ~7) * GSP::FRAMEBUFFER_WIDTH_POW2 * bpp;
            const auto src_offset = bpp * (GSP::FRAMEBUFFER_WIDTH * y + x);
            std::memcpy(dst_ptr + dst_offset, src_ptr + src_offset, bpp);
        }
    }

    system.Memory().RasterizerFlushVirtualRegion(
        dst_vaddr, GSP::FRAMEBUFFER_WIDTH_POW2 * height * bpp, Memory::FlushMode::Invalidate);
}

void AppletManager::CaptureFrameBuffers() {
    CaptureFrameBuffer(system, capture_info->bottom_screen_left_offset,
                       GSP::FRAMEBUFFER_SAVE_AREA_BOTTOM, GSP::BOTTOM_FRAMEBUFFER_HEIGHT,
                       capture_info->bottom_screen_format);
    CaptureFrameBuffer(system, capture_info->top_screen_left_offset,
                       GSP::FRAMEBUFFER_SAVE_AREA_TOP_LEFT, GSP::TOP_FRAMEBUFFER_HEIGHT,
                       capture_info->top_screen_format);
    if (capture_info->is_3d) {
        CaptureFrameBuffer(system, capture_info->top_screen_right_offset,
                           GSP::FRAMEBUFFER_SAVE_AREA_TOP_RIGHT, GSP::TOP_FRAMEBUFFER_HEIGHT,
                           capture_info->top_screen_format);
    }
}

void AppletManager::LoadInputDevices() {
    home_button = Input::CreateDevice<Input::ButtonDevice>(
        Settings::values.current_input_profile.buttons[Settings::NativeButton::Home]);
    power_button = Input::CreateDevice<Input::ButtonDevice>(
        Settings::values.current_input_profile.buttons[Settings::NativeButton::Power]);
}

/// Handles updating the current Applet every time it's called.
void AppletManager::HLEAppletUpdateEvent(std::uintptr_t user_data, s64 cycles_late) {
    const auto id = static_cast<AppletId>(user_data);
    const auto applet = hle_applets[id];
    if (applet == nullptr) {
        // Dead applet, exit event loop.
        LOG_WARNING(Service_APT, "Attempted to update missing applet id={:03X}", id);
        return;
    }

    if (applet->IsActive()) {
        applet->Update();
    }

    // If the applet is still running after the last update, reschedule the event
    if (applet->IsRunning()) {
        system.CoreTiming().ScheduleEvent(usToCycles(hle_applet_update_interval_us) - cycles_late,
                                          hle_applet_update_event, user_data);
    } else {
        // Otherwise the applet has terminated, in which case we should clean it up
        hle_applets[id] = nullptr;
    }
}

void AppletManager::ButtonUpdateEvent(std::uintptr_t user_data, s64 cycles_late) {
    if (is_device_reload_pending.exchange(false)) {
        LoadInputDevices();
    }

    // NOTE: We technically do support loading and jumping to home menu even if it isn't
    // initially registered. However since the home menu suspend is not bug-free, we don't
    // want normal users who didn't launch the home menu accidentally pressing the home
    // button binding and freezing their game, so for now, gate it to only environments
    // where the home menu was already loaded by the user (last condition).

    if (GetAppletSlot(AppletSlot::HomeMenu)->registered) {
        const bool home_state = home_button->GetStatus();
        if (home_state && !last_home_button_state) {
            SendNotification(Notification::HomeButtonSingle);
        }
        last_home_button_state = home_state;

        const bool power_state = power_button->GetStatus();
        if (power_state && !last_power_button_state) {
            SendNotificationToAll(Notification::PowerButtonClick);
        }
        last_power_button_state = power_state;
    }

    // Reschedule recurrent event
    system.CoreTiming().ScheduleEvent(usToCycles(button_update_interval_us) - cycles_late,
                                      button_update_event);
}

AppletManager::AppletManager(Core::System& system) : system(system) {
    lock = system.Kernel().CreateMutex(false, "APT_U:Lock");
    for (std::size_t slot = 0; slot < applet_slots.size(); ++slot) {
        auto& slot_data = applet_slots[slot];
        slot_data.slot = static_cast<AppletSlot>(slot);
        slot_data.applet_id = AppletId::None;
        slot_data.attributes.raw = 0;
        slot_data.registered = false;
        slot_data.loaded = false;
        slot_data.notification_event =
            system.Kernel().CreateEvent(Kernel::ResetType::OneShot, "APT:Notification");
        slot_data.parameter_event =
            system.Kernel().CreateEvent(Kernel::ResetType::OneShot, "APT:Parameter");
    }
    hle_applet_update_event = system.CoreTiming().RegisterEvent(
        "HLE Applet Update Event", [this](std::uintptr_t user_data, s64 cycles_late) {
            HLEAppletUpdateEvent(user_data, cycles_late);
        });
    button_update_event = system.CoreTiming().RegisterEvent(
        "APT Button Update Event", [this](std::uintptr_t user_data, s64 cycles_late) {
            ButtonUpdateEvent(user_data, cycles_late);
        });
    system.CoreTiming().ScheduleEvent(usToCycles(button_update_interval_us), button_update_event);
}

AppletManager::~AppletManager() {
    system.CoreTiming().RemoveEvent(hle_applet_update_event);
    system.CoreTiming().RemoveEvent(button_update_event);
}

void AppletManager::ReloadInputDevices() {
    is_device_reload_pending.store(true);
}

} // namespace Service::APT
