// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#define VMA_IMPLEMENTATION
#include "video_core/vulkan_common/vma.h"

#include <codecvt>
#include <cstdio>
#include <cstring>
#include <locale>
#include <map>
#include <set>
#include <sstream>
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <dlfcn.h>
#include <unistd.h>

#include <iostream>
#ifdef ARCHITECTURE_arm64
#include <adrenotools/driver.h>
#endif

#include <android/api-level.h>
#include <android/native_window_jni.h>
#include <common/fs/fs.h>
#include <core/file_sys/patch_manager.h>
#include <core/file_sys/savedata_factory.h>
#include <core/loader/nro.h>
#include <frontend_common/content_manager.h>
#include <jni.h>

#include "common/android/multiplayer/multiplayer.h"
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "frontend_common/play_time_manager.h"
#include "core/constants.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h"
#include "core/frontend/applets/general.h"
#include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
#include "common/android/applets/web_browser.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/hle/service/set/system_settings_server.h"
#include "core/loader/loader.h"
#include "frontend_common/config.h"
#include "frontend_common/firmware_manager.h"
#ifdef ENABLE_UPDATE_CHECKER
#include "frontend_common/update_checker.h"
#endif
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/hid_types.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "jni/native.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/capture.h"
#include "video_core/textures/decoders.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/shader_notify.h"
#include "network/announce_multiplayer_session.h"

#define jconst [[maybe_unused]] const auto
#define jauto [[maybe_unused]] auto

static EmulationSession s_instance;

//Abdroid Multiplayer which can be initialized with parameters
std::unique_ptr<AndroidMultiplayer> multiplayer{nullptr};
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;

//Power Status default values
std::atomic<int> g_battery_percentage = {100};
std::atomic<bool> g_is_charging = {false};
std::atomic<bool> g_has_battery = {true};

// playtime
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;

EmulationSession::EmulationSession() {
    m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
}

EmulationSession& EmulationSession::GetInstance() {
    return s_instance;
}

const Core::System& EmulationSession::System() const {
    return m_system;
}

Core::System& EmulationSession::System() {
    return m_system;
}

FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
    return m_manual_provider.get();
}

InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
    return m_input_subsystem;
}

const EmuWindow_Android& EmulationSession::Window() const {
    return *m_window;
}

EmuWindow_Android& EmulationSession::Window() {
    return *m_window;
}

ANativeWindow* EmulationSession::NativeWindow() const {
    return m_native_window;
}

void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
    m_native_window = native_window;
}

void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
                                           const std::string& custom_driver_dir,
                                           const std::string& custom_driver_name,
                                           const std::string& file_redirect_dir) {
#ifdef ARCHITECTURE_arm64
    void* handle{};
    const char* file_redirect_dir_{};
    int featureFlags{};

    // Enable driver file redirection when renderer debugging is enabled.
    if (Settings::values.renderer_debug && file_redirect_dir.size()) {
        featureFlags |= ADRENOTOOLS_DRIVER_FILE_REDIRECT;
        file_redirect_dir_ = file_redirect_dir.c_str();
    }

    // Try to load a custom driver.
    if (custom_driver_name.size()) {
        handle = adrenotools_open_libvulkan(
            RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
            custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
    }

    // Try to load the system driver.
    if (!handle) {
        handle = adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
                                            nullptr, nullptr, file_redirect_dir_, nullptr);
    }

    m_vulkan_library = std::make_shared<Common::DynamicLibrary>(handle);
#endif
}

bool EmulationSession::IsRunning() const {
    return m_is_running;
}

bool EmulationSession::IsPaused() const {
    return m_is_running && m_is_paused;
}

const Core::PerfStatsResults& EmulationSession::PerfStats() {
    m_perf_stats = m_system.GetAndResetPerfStats();
    return m_perf_stats;
}

int EmulationSession::ShadersBuilding() {
    auto& shader_notify = m_system.GPU().ShaderNotify();
    m_shaders_building = shader_notify.ShadersBuilding();
    return m_shaders_building;
}

void EmulationSession::SurfaceChanged() {
    if (!IsRunning()) {
        return;
    }
    m_window->OnSurfaceChanged(m_native_window);
}

void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) {
    const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::OpenMode::Read);
    if (!file) {
        return;
    }

    if (m_manual_provider->AddEntriesFromContainer(file)) {
        return;
    }

    auto loader = Loader::GetLoader(m_system, file);
    if (!loader) {
        return;
    }

    const auto file_type = loader->GetFileType();
    if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
        return;
    }

    u64 program_id = 0;
    const auto res2 = loader->ReadProgramId(program_id);
    if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
        m_manual_provider->AddEntry(FileSys::TitleType::Application,
                                    FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
                                    program_id, file);
    }
}

void EmulationSession::ConfigureFilesystemProviderFromGameFolder(const std::string& filepath) {
    if (!Settings::values.ext_content_from_game_dirs.GetValue()) {
        return;
    }
    ConfigureFilesystemProvider(filepath);
}

void EmulationSession::InitializeSystem(bool reload) {
    if (!reload) {
        // Initialize logging system
        Common::Log::Initialize();
        Common::Log::SetColorConsoleBackendEnabled(true);
        Common::Log::Start();

        m_input_subsystem.Initialize();
    }

    // Initialize filesystem.
    m_system.SetFilesystem(m_vfs);
    m_system.GetUserChannel().clear();
    m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
    m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
    m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
                                     m_manual_provider.get());
    m_system.GetFileSystemController().CreateFactories(*m_vfs);
}

void EmulationSession::SetAppletId(int applet_id) {
    m_applet_id = applet_id;
    m_system.GetFrontendAppletHolder().SetCurrentAppletId(
        static_cast<Service::AM::AppletId>(m_applet_id));
}

Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath,
                                                               const std::size_t program_index,
                                                               const bool frontend_initiated) {
    std::scoped_lock lock(m_mutex);

    // Create the render window.
    m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library);

    // Initialize system.
    jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>();
    jauto android_webapplet = std::make_unique<Common::Android::WebBrowser::AndroidWebBrowser>();
    m_software_keyboard = android_keyboard.get();
    m_system.SetShuttingDown(false);
    m_system.ApplySettings();
    Settings::LogSettings();
    m_system.HIDCore().ReloadInputDevices();
    m_system.SetFrontendAppletSet({
        nullptr,                     // Amiibo Settings
        nullptr,                     // Controller Selector
        nullptr,                     // Error Display
        nullptr,                     // Mii Editor
        nullptr,                     // Parental Controls
        nullptr,                     // Photo Viewer
        nullptr,                     // Profile Selector
        std::move(android_keyboard), // Software Keyboard
        std::move(android_webapplet),// Web Browser
        nullptr,                     // Net Connect
    });

    // Initialize filesystem.
    ConfigureFilesystemProvider(filepath);

    // Load the ROM.
    Service::AM::FrontendAppletParameters params{
        .applet_id = static_cast<Service::AM::AppletId>(m_applet_id),
        .launch_type = frontend_initiated ? Service::AM::LaunchType::FrontendInitiated
                                          : Service::AM::LaunchType::ApplicationInitiated,
        .program_index = static_cast<s32>(program_index),
    };

    m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params);
    if (m_load_result != Core::SystemResultStatus::Success) {
        return m_load_result;
    }

    // Complete initialization.
    m_system.GPU().Start();
    m_system.GetCpuManager().OnGpuReady();
    m_system.RegisterExitCallback([&] { HaltEmulation(); });

    // Register an ExecuteProgram callback such that Core can execute a sub-program
    m_system.RegisterExecuteProgramCallback([&](std::size_t program_index_) {
        m_next_program_index = program_index_;
        EmulationSession::GetInstance().HaltEmulation();
    });

    OnEmulationStarted();
    return Core::SystemResultStatus::Success;
}

void EmulationSession::ShutdownEmulation() {
    std::scoped_lock lock(m_mutex);

    if (m_next_program_index != -1) {
        ChangeProgram(m_next_program_index);
        m_next_program_index = -1;
    }

    m_is_running = false;

    // Unload user input.
    m_system.HIDCore().UnloadInputDevices();

    // Enable all controllers
    m_system.HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All});

    // Shutdown the main emulated process
    if (m_load_result == Core::SystemResultStatus::Success) {
        m_system.DetachDebugger();
        m_system.ShutdownMainProcess();
        m_detached_tasks.WaitForAllTasks();
        m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
        m_window.reset();
        OnEmulationStopped(Core::SystemResultStatus::Success);
        return;
    }

    // Tear down the render window.
    m_window.reset();
}

void EmulationSession::PauseEmulation() {
    std::scoped_lock lock(m_mutex);
    m_system.Pause();
    m_is_paused = true;
}

void EmulationSession::UnPauseEmulation() {
    std::scoped_lock lock(m_mutex);
    m_system.Run();
    m_is_paused = false;
}

void EmulationSession::HaltEmulation() {
    std::scoped_lock lock(m_mutex);
    m_is_running = false;
    m_cv.notify_one();
}

void EmulationSession::RunEmulation() {
    {
        std::scoped_lock lock(m_mutex);
        m_is_running = true;
    }

    // Load the disk shader cache.
    if (Settings::values.use_disk_shader_cache.GetValue()) {
        LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
        m_system.Renderer().ReadRasterizer()->LoadDiskResources(
            m_system.GetApplicationProcessProgramID(), std::stop_token{}, LoadDiskCacheProgress);
        LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
    }

    void(m_system.Run());

    if (m_system.DebuggerEnabled()) {
        m_system.InitializeDebugger();
    }

    while (true) {
        {
            [[maybe_unused]] std::unique_lock lock(m_mutex);
            if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
                              [&]() { return !m_is_running; })) {
                // Emulation halted.
                break;
            }
        }
    }

    // Reset current applet ID.
    m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
}

Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {
    return m_software_keyboard;
}

void EmulationSession::LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress,
                                             int max) {
    JNIEnv* env = Common::Android::GetEnvForThread();
    env->CallStaticVoidMethod(Common::Android::GetDiskCacheProgressClass(),
                              Common::Android::GetDiskCacheLoadProgress(), static_cast<jint>(stage),
                              static_cast<jint>(progress), static_cast<jint>(max));
}

void EmulationSession::OnEmulationStarted() {
    JNIEnv* env = Common::Android::GetEnvForThread();
    env->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(),
                              Common::Android::GetOnEmulationStarted());
}

void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
    JNIEnv* env = Common::Android::GetEnvForThread();
    env->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(),
                              Common::Android::GetOnEmulationStopped(), static_cast<jint>(result));
}

void EmulationSession::ChangeProgram(std::size_t program_index) {
    JNIEnv* env = Common::Android::GetEnvForThread();
    env->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(),
                              Common::Android::GetOnProgramChanged(),
                              static_cast<jint>(program_index));
}

u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) {
    auto program_id_string = Common::Android::GetJString(env, jprogramId);
    try {
        return std::stoull(program_id_string);
    } catch (...) {
        return 0;
    }
}

static Core::SystemResultStatus RunEmulation(const std::string& filepath,
                                             const size_t program_index,
                                             const bool frontend_initiated) {
    LOG_INFO(Frontend, "starting");

    if (filepath.empty()) {
        LOG_CRITICAL(Frontend, "failed to load: filepath empty!");
        return Core::SystemResultStatus::ErrorLoader;
    }

    SCOPE_EXIT {
        EmulationSession::GetInstance().ShutdownEmulation();
    };

    jconst result = EmulationSession::GetInstance().InitializeEmulation(filepath, program_index,
                                                                        frontend_initiated);
    if (result != Core::SystemResultStatus::Success) {
        return result;
    }

    EmulationSession::GetInstance().RunEmulation();

    return Core::SystemResultStatus::Success;
}

namespace {

struct CpuPartInfo {
    u32 vendor;
    u32 part;
    const char* name;
};

constexpr CpuPartInfo s_cpu_list[] = {
    // ARM - 0x41
    {0x41, 0xd01, "Cortex-A32"},
    {0x41, 0xd02, "Cortex-A34"},
    {0x41, 0xd04, "Cortex-A35"},
    {0x41, 0xd03, "Cortex-A53"},
    {0x41, 0xd05, "Cortex-A55"},
    {0x41, 0xd46, "Cortex-A510"},
    {0x41, 0xd80, "Cortex-A520"},
    {0x41, 0xd88, "Cortex-A520AE"},
    {0x41, 0xd07, "Cortex-A57"},
    {0x41, 0xd06, "Cortex-A65"},
    {0x41, 0xd43, "Cortex-A65AE"},
    {0x41, 0xd08, "Cortex-A72"},
    {0x41, 0xd09, "Cortex-A73"},
    {0x41, 0xd0a, "Cortex-A75"},
    {0x41, 0xd0b, "Cortex-A76"},
    {0x41, 0xd0e, "Cortex-A76AE"},
    {0x41, 0xd0d, "Cortex-A77"},
    {0x41, 0xd41, "Cortex-A78"},
    {0x41, 0xd42, "Cortex-A78AE"},
    {0x41, 0xd4b, "Cortex-A78C"},
    {0x41, 0xd47, "Cortex-A710"},
    {0x41, 0xd4d, "Cortex-A715"},
    {0x41, 0xd81, "Cortex-A720"},
    {0x41, 0xd89, "Cortex-A720AE"},
    {0x41, 0xd87, "Cortex-A725"},
    {0x41, 0xd44, "Cortex-X1"},
    {0x41, 0xd4c, "Cortex-X1C"},
    {0x41, 0xd48, "Cortex-X2"},
    {0x41, 0xd4e, "Cortex-X3"},
    {0x41, 0xd82, "Cortex-X4"},
    {0x41, 0xd85, "Cortex-X925"},
    {0x41, 0xd4a, "Neoverse E1"},
    {0x41, 0xd0c, "Neoverse N1"},
    {0x41, 0xd49, "Neoverse N2"},
    {0x41, 0xd8e, "Neoverse N3"},
    {0x41, 0xd40, "Neoverse V1"},
    {0x41, 0xd4f, "Neoverse V2"},
    {0x41, 0xd84, "Neoverse V3"},
    {0x41, 0xd83, "Neoverse V3AE"},
    // Qualcomm - 0x51
    {0x51, 0x201, "Kryo"},
    {0x51, 0x205, "Kryo"},
    {0x51, 0x211, "Kryo"},
    {0x51, 0x800, "Kryo 385 Gold"},
    {0x51, 0x801, "Kryo 385 Silver"},
    {0x51, 0x802, "Kryo 485 Gold"},
    {0x51, 0x803, "Kryo 485 Silver"},
    {0x51, 0x804, "Kryo 680 Prime"},
    {0x51, 0x805, "Kryo 680 Gold"},
    {0x51, 0x06f, "Krait"},
    {0x51, 0xc00, "Falkor"},
    {0x51, 0xc01, "Saphira"},
    {0x51, 0x001, "Oryon"},
};

const char* find_cpu_name(u32 vendor, u32 part) {
    for (const auto& cpu : s_cpu_list) {
        if (cpu.vendor == vendor && cpu.part == part) {
            return cpu.name;
        }
    }
    return nullptr;
}

u64 read_midr_sysfs(u32 cpu_id) {
    char path[128];
    std::snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/regs/identification/midr_el1", cpu_id);

    FILE* f = std::fopen(path, "r");
    if (!f) return 0;

    char value[32];
    if (!std::fgets(value, sizeof(value), f)) {
        std::fclose(f);
        return 0;
    }
    std::fclose(f);

    return std::strtoull(value, nullptr, 16);
}

std::pair<u32, std::string> get_pretty_cpus() {
    std::map<u64, int> core_layout;
    u32 valid_cpus = 0;
    for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) {
        const auto midr = read_midr_sysfs(i);
        if (midr == 0) break;

        valid_cpus++;
        core_layout[midr]++;
    }

    std::string cpus;

    if (!core_layout.empty()) {
        const CpuPartInfo* lowest_part = nullptr;
        u32 lowest_part_id = 0xFFFFFFFF;

        for (const auto& [midr, count] : core_layout) {
            const auto vendor = (midr >> 24) & 0xff;
            const auto part = (midr >> 4) & 0xfff;

            if (!cpus.empty()) cpus += " + ";
            cpus += fmt::format("{}x {}", count, find_cpu_name(vendor, part));
        }
    }

    return {valid_cpus, cpus};
}

std::string get_arm_cpu_name() {
    std::map<u64, int> core_layout;
    for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) {
        const auto midr = read_midr_sysfs(i);
        if (midr == 0) break;

        core_layout[midr]++;
    }

    if (!core_layout.empty()) {
        const CpuPartInfo* lowest_part = nullptr;
        u32 lowest_part_id = 0xFFFFFFFF;

        for (const auto& [midr, count] : core_layout) {
            const auto vendor = (midr >> 24) & 0xff;
            const auto part = (midr >> 4) & 0xfff;

            for (const auto& cpu : s_cpu_list) {
                if (cpu.vendor == vendor && cpu.part == part) {
                    if (cpu.part < lowest_part_id) {
                        lowest_part_id = cpu.part;
                        lowest_part = &cpu;
                    }
                    break;
                }
            }
        }

        if (lowest_part) {
            return lowest_part->name;
        }
    }

    FILE* f = std::fopen("/proc/cpuinfo", "r");
    if (!f) return "";

    char buf[512];
    std::string result;

    auto trim = [](std::string& s) {
        const auto start = s.find_first_not_of(" \t\r\n");
        const auto end = s.find_last_not_of(" \t\r\n");
        s = (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
    };

    while (std::fgets(buf, sizeof(buf), f)) {
        std::string line(buf);
        if (line.find("Hardware") == 0) {
            auto pos = line.find(':');
            if (pos != std::string::npos) {
                result = line.substr(pos + 1);
                trim(result);
                break;
            }
        }
    }
    std::fclose(f);

    if (!result.empty()) {
        std::transform(result.begin(), result.end(), result.begin(), ::tolower);
    }

    return result;
}

const char* fallback_cpu_detection() {
    static std::string s_result = []() -> std::string {
        std::string result = get_arm_cpu_name();
        if (result.empty()) {
            return "Cortex-A34";
        }
        return result;
    }();

    return s_result.c_str();
}

} // namespace

extern "C" {

void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject instance,
                                                          [[maybe_unused]] jobject surf) {
    EmulationSession::GetInstance().SetNativeWindow(ANativeWindow_fromSurface(env, surf));
    EmulationSession::GetInstance().SurfaceChanged();
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) {
    if (auto* native_window = EmulationSession::GetInstance().NativeWindow(); native_window) {
        ANativeWindow_release(native_window);
    }
    EmulationSession::GetInstance().SetNativeWindow(nullptr);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance,
                                                           [[maybe_unused]] jstring j_directory) {
    Common::FS::SetAppDirectory(Common::Android::GetJString(env, j_directory));
}

int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
                                                            jstring j_file, jobject jcallback) {
    auto jlambdaClass = env->GetObjectClass(jcallback);
    auto jlambdaInvokeMethod = env->GetMethodID(
        jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
        auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
                                                   Common::Android::ToJDouble(env, max),
                                                   Common::Android::ToJDouble(env, progress));
        return Common::Android::GetJBoolean(env, jwasCancelled);
    };

    return static_cast<int>(
        ContentManager::InstallNSP(EmulationSession::GetInstance().System(),
                                   *EmulationSession::GetInstance().System().GetFilesystem(),
                                   Common::Android::GetJString(env, j_file), callback));
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
                                                                      jstring jprogramId,
                                                                      jstring jupdatePath) {
    u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
    std::string updatePath = Common::Android::GetJString(env, jupdatePath);
    std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
        EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
            updatePath, FileSys::OpenMode::Read));
    for (const auto& item : nsp->GetNCAs()) {
        for (const auto& nca_details : item.second) {
            if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
                auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL;
                if (update_id == program_id) {
                    return true;
                }
            }
        }
    }
    return false;
}

void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env,
                                                                       [[maybe_unused]] jclass clazz,
                                                                       jstring hook_lib_dir,
                                                                       jstring custom_driver_dir,
                                                                       jstring custom_driver_name,
                                                                       jstring file_redirect_dir) {
    // Log active Freedreno environment variables
    const char* tu_debug = getenv("TU_DEBUG");
    const char* fd_debug = getenv("FD_MESA_DEBUG");
    const char* ir3_debug = getenv("IR3_SHADER_DEBUG");
    const char* fd_rd_dump = getenv("FD_RD_DUMP");
    const char* tu_breadcrumbs = getenv("TU_BREADCRUMBS");

    if (tu_debug || fd_debug || ir3_debug || fd_rd_dump || tu_breadcrumbs) {
        LOG_INFO(Frontend, "[Freedreno] Initializing GPU driver with configuration:");
        if (tu_debug) LOG_INFO(Frontend, "[Freedreno]   TU_DEBUG={}", tu_debug);
        if (fd_debug) LOG_INFO(Frontend, "[Freedreno]   FD_MESA_DEBUG={}", fd_debug);
        if (ir3_debug) LOG_INFO(Frontend, "[Freedreno]   IR3_SHADER_DEBUG={}", ir3_debug);
        if (fd_rd_dump) LOG_INFO(Frontend, "[Freedreno]   FD_RD_DUMP={}", fd_rd_dump);
        if (tu_breadcrumbs) LOG_INFO(Frontend, "[Freedreno]   TU_BREADCRUMBS={}", tu_breadcrumbs);
    }

    EmulationSession::GetInstance().InitializeGpuDriver(
        Common::Android::GetJString(env, hook_lib_dir),
        Common::Android::GetJString(env, custom_driver_dir),
        Common::Android::GetJString(env, custom_driver_name),
        Common::Android::GetJString(env, file_redirect_dir));
}

[[maybe_unused]] static bool CheckKgslPresent() {
    constexpr auto KgslPath{"/dev/kgsl-3d0"};

    return access(KgslPath, F_OK) == 0;
}

[[maybe_unused]] bool SupportsCustomDriver() {
    return android_get_device_api_level() >= 28 && CheckKgslPresent();
}

jboolean JNICALL Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_supportsCustomDriverLoading(
    JNIEnv* env, jobject instance) {
#ifdef ARCHITECTURE_arm64
    // If the KGSL device exists custom drivers can be loaded using adrenotools
    return SupportsCustomDriver();
#else
    return false;
#endif
}

jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(
    JNIEnv* env, jobject j_obj, jobject j_surf, jstring j_hook_lib_dir) {
#ifdef ARCHITECTURE_arm64
    const char* file_redirect_dir_{};
    int featureFlags{};
    std::string hook_lib_dir = Common::Android::GetJString(env, j_hook_lib_dir);
    auto handle = adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
                                             nullptr, nullptr, file_redirect_dir_, nullptr);
    auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
    InputCommon::InputSubsystem input_subsystem;
    auto window =
        std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);

    Vulkan::vk::InstanceDispatch dld;
    Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
        *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);

    auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());

    auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);

    auto driver_version = device.GetDriverVersion();
    auto version_string =
        fmt::format("{}.{}.{}", VK_API_VERSION_MAJOR(driver_version),
                    VK_API_VERSION_MINOR(driver_version), VK_API_VERSION_PATCH(driver_version));
    auto driver_name = device.GetDriverName();
#else
    auto driver_version = "1.0.0";
    auto version_string = "1.1.0"; //Assume lowest Vulkan level
    auto driver_name = "generic";
#endif
    jobjectArray j_driver_info = env->NewObjectArray(2, Common::Android::GetStringClass(), Common::Android::ToJString(env, version_string));
    env->SetObjectArrayElement(j_driver_info, 1, Common::Android::ToJString(env, driver_name));
    return j_driver_info;
}

jstring Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getGpuModel(JNIEnv *env, jobject j_obj, jobject j_surf, jstring j_hook_lib_dir) {
#ifdef ARCHITECTURE_arm64
    const char* file_redirect_dir_{};
    int featureFlags{};
    std::string hook_lib_dir = Common::Android::GetJString(env, j_hook_lib_dir);
    auto handle = adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
                                             nullptr, nullptr, file_redirect_dir_, nullptr);
    auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
    InputCommon::InputSubsystem input_subsystem;
    auto window =
            std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);

    Vulkan::vk::InstanceDispatch dld;
    Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
            *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);

    auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());

    auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);

    const std::string model_name{device.GetModelName()};

    window.release();

    return Common::Android::ToJString(env, model_name);
#else
    return Common::Android::ToJString(env, "no-info");
#endif
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadKeys(JNIEnv* env, jclass clazz) {
    Core::Crypto::KeyManager::Instance().ReloadKeys();
    return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().AreKeysLoaded());
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_unpauseEmulation(JNIEnv* env, jclass clazz) {
    EmulationSession::GetInstance().UnPauseEmulation();
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_pauseEmulation(JNIEnv* env, jclass clazz) {
    EmulationSession::GetInstance().PauseEmulation();
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_stopEmulation(JNIEnv* env, jclass clazz) {
    EmulationSession::GetInstance().HaltEmulation();
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning(JNIEnv* env, jclass clazz) {
    return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass clazz) {
    return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
}

jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) {
    using namespace VideoCore::Capture;

    if (!EmulationSession::GetInstance().IsRunning()) {
        return env->NewByteArray(0);
    }

    const auto tiled = EmulationSession::GetInstance().System().GPU().GetAppletCaptureBuffer();
    if (tiled.size() < TiledSize) {
        return env->NewByteArray(0);
    }

    std::vector<u8> linear(LinearWidth * LinearHeight * BytesPerPixel);
    Tegra::Texture::UnswizzleTexture(linear, tiled, BytesPerPixel, LinearWidth, LinearHeight,
                                     LinearDepth, BlockHeight, BlockDepth);

    auto buffer = env->NewByteArray(static_cast<jsize>(linear.size()));
    if (!buffer) {
        return env->NewByteArray(0);
    }

    env->SetByteArrayRegion(buffer, 0, static_cast<jsize>(linear.size()),
                            reinterpret_cast<const jbyte*>(linear.data()));
    return buffer;
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureWidth(JNIEnv* env, jclass clazz) {
    return static_cast<jint>(VideoCore::Capture::LinearWidth);
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureHeight(JNIEnv* env, jclass clazz) {
    return static_cast<jint>(VideoCore::Capture::LinearHeight);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
                                                            jboolean reload) {
    // Initialize the emulated system.
    if (!reload) {
        EmulationSession::GetInstance().System().Initialize();
    }
    EmulationSession::GetInstance().InitializeSystem(reload);
}

jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {
    jdoubleArray j_stats = env->NewDoubleArray(4);

    if (EmulationSession::GetInstance().IsRunning()) {
        jconst results = EmulationSession::GetInstance().PerfStats();

        // Converting the structure into an array makes it easier to pass it to the frontend
        double stats[4] = {results.system_fps, results.average_game_fps, results.frametime,
                           results.emulation_speed};

        env->SetDoubleArrayRegion(j_stats, 0, 4, stats);
    }

    return j_stats;
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getShadersBuilding(JNIEnv* env, jclass clazz) {
    jint j_shaders = 0;

    if (EmulationSession::GetInstance().IsRunning()) {
        j_shaders = EmulationSession::GetInstance().ShadersBuilding();
    }

    return j_shaders;
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) {
    if (Settings::IsNceEnabled()) {
        return Common::Android::ToJString(env, "NCE");
    }

    return Common::Android::ToJString(env, "JIT");
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject jobj) {
    return Common::Android::ToJString(
        env, EmulationSession::GetInstance().System().GPU().Renderer().GetDeviceVendor());
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject /*jobj*/) {
    get_arm_cpu_name();
    constexpr const char* CPUINFO_PATH = "/proc/cpuinfo";

    auto trim = [](std::string& s) {
        const auto start = s.find_first_not_of(" \t\r\n");
        const auto end = s.find_last_not_of(" \t\r\n");
        s = (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
    };

    auto to_lower = [](std::string s) {
        for (auto& c : s) c = std::tolower(c);
        return s;
    };

    try {
        std::string result;
        std::pair<u32, std::string> pretty_cpus = get_pretty_cpus();
        u32 threads = pretty_cpus.first;
        std::string cpus = pretty_cpus.second;

        fmt::format_to(std::back_inserter(result), "CPUs: {}\n{} Threads",
                       cpus, threads);

        FILE* f = std::fopen(CPUINFO_PATH, "r");
        if (!f) return Common::Android::ToJString(env, result);

        char buf[512];

        if (f) {
            std::set<std::string> feature_set;
            while (std::fgets(buf, sizeof(buf), f)) {
                std::string line(buf);
                if (line.find("Features") == 0 || line.find("features") == 0) {
                    auto pos = line.find(':');
                    if (pos != std::string::npos) {
                        std::string feat_line = line.substr(pos + 1);
                        trim(feat_line);

                        std::istringstream iss(feat_line);
                        std::string feature;
                        while (iss >> feature) {
                            feature_set.insert(to_lower(feature));
                        }
                    }
                }
            }
            std::fclose(f);

            bool has_neon = feature_set.count("neon") || feature_set.count("asimd");
            bool has_fp = feature_set.count("fp") || feature_set.count("vfp");
            bool has_sve = feature_set.count("sve");
            bool has_sve2 = feature_set.count("sve2");
            bool has_crypto = feature_set.count("aes") || feature_set.count("sha1") ||
                             feature_set.count("sha2") || feature_set.count("pmull");
            bool has_dotprod = feature_set.count("asimddp") || feature_set.count("dotprod");
            bool has_i8mm = feature_set.count("i8mm");
            bool has_bf16 = feature_set.count("bf16");
            bool has_atomics = feature_set.count("atomics") || feature_set.count("lse");

            std::string features;
            if (has_neon || has_fp) {
                features += "NEON";
                if (has_dotprod) features += "+DP";
                if (has_i8mm) features += "+I8MM";
                if (has_bf16) features += "+BF16";
            }

            if (has_sve) {
                if (!features.empty()) features += " | ";
                features += "SVE";
                if (has_sve2) features += "2";
            }

            if (has_crypto) {
                if (!features.empty()) features += " | ";
                features += "Crypto";
            }

            if (has_atomics) {
                if (!features.empty()) features += " | ";
                features += "LSE";
            }

            if (!features.empty()) {
                result += "\nFeatures: " + features;
            }
        }

        fmt::format_to(std::back_inserter(result), "\nLLVM CPU: {}", fallback_cpu_detection());

        return Common::Android::ToJString(env, result);
    } catch (...) {
        return Common::Android::ToJString(env, "Unknown");
    }
}


namespace {
constexpr u32 VENDOR_QUALCOMM = 0x5143;
constexpr u32 VENDOR_ARM = 0x13B5;

VkPhysicalDeviceProperties GetVulkanDeviceProperties() {
    Common::DynamicLibrary library;
    if (!library.Open("libvulkan.so")) {
        return {};
    }

    Vulkan::vk::InstanceDispatch dld;
    // TODO: warn the user that Vulkan is unavailable rather than hard crash
    const auto instance = Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_1);
    const auto physical_devices = instance.EnumeratePhysicalDevices();
    if (physical_devices.empty()) {
        return {};
    }

    const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
    return physical_device.GetProperties();
}
} // namespace

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanDriverVersion(JNIEnv* env, jobject jobj) {
    try {
        const auto props = GetVulkanDeviceProperties();
        if (props.deviceID == 0) {
            return Common::Android::ToJString(env, "N/A");
        }

        const u32 driver_version = props.driverVersion;
        const u32 vendor_id = props.vendorID;

        if (driver_version == 0) {
            return Common::Android::ToJString(env, "N/A");
        }

        std::string version_str;

        if (vendor_id == VENDOR_QUALCOMM) {
            const u32 major = (driver_version >> 24) << 2;
            const u32 minor = (driver_version >> 12) & 0xFFF;
            const u32 patch = driver_version & 0xFFF;
            version_str = fmt::format("{}.{}.{}", major, minor, patch);
        }
        else if (vendor_id == VENDOR_ARM) {
            u32 major = VK_API_VERSION_MAJOR(driver_version);
            u32 minor = VK_API_VERSION_MINOR(driver_version);
            u32 patch = VK_API_VERSION_PATCH(driver_version);

            // ARM custom encoding for newer drivers
            if (major > 10) {
                major = (driver_version >> 22) & 0x3FF;
                minor = (driver_version >> 12) & 0x3FF;
                patch = driver_version & 0xFFF;
            }
            version_str = fmt::format("{}.{}.{}", major, minor, patch);
        }
        // Standard Vulkan version encoding for other vendors
        else {
            const u32 major = VK_API_VERSION_MAJOR(driver_version);
            const u32 minor = VK_API_VERSION_MINOR(driver_version);
            const u32 patch = VK_API_VERSION_PATCH(driver_version);
            version_str = fmt::format("{}.{}.{}", major, minor, patch);
        }

        return Common::Android::ToJString(env, version_str);
    } catch (...) {
        return Common::Android::ToJString(env, "N/A");
    }
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanApiVersion(JNIEnv* env, jobject jobj) {
    try {
        const auto props = GetVulkanDeviceProperties();
        if (props.deviceID == 0) {
            return Common::Android::ToJString(env, "N/A");
        }

        const u32 api_version = props.apiVersion;
        const u32 major = VK_API_VERSION_MAJOR(api_version);
        const u32 minor = VK_API_VERSION_MINOR(api_version);
        const u32 patch = VK_API_VERSION_PATCH(api_version);
        const u32 variant = VK_API_VERSION_VARIANT(api_version);

        // Include variant if non-zero (rare on Android)
        const std::string version_str = variant > 0
            ? fmt::format("{}.{}.{}.{}", variant, major, minor, patch)
            : fmt::format("{}.{}.{}", major, minor, patch);

        return Common::Android::ToJString(env, version_str);
    } catch (...) {
        return Common::Android::ToJString(env, "N/A");
    }
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) {
    const auto props = GetVulkanDeviceProperties();
    if (props.deviceID == 0) {
        return Common::Android::ToJString(env, "Unknown");
    }

    return Common::Android::ToJString(env, props.deviceName);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
    EmulationSession::GetInstance().System().ApplySettings();
    EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
    Settings::LogSettings();
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobject jobj, jint index) {
    return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
    if (enabled) {
        Settings::values.use_speed_limit.SetValue(true);
        Settings::SetSpeedMode(Settings::SpeedMode::Turbo);
    } else {
        Settings::SetSpeedMode(Settings::SpeedMode::Standard);
    }
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_setSlowSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
    if (enabled) {
        Settings::values.use_speed_limit.SetValue(true);
        Settings::SetSpeedMode(Settings::SpeedMode::Slow);
    } else {
        Settings::SetSpeedMode(Settings::SpeedMode::Standard);
    }
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_setStandardSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
    Settings::values.use_speed_limit.SetValue(enabled);
    if (enabled) {
        Settings::SetSpeedMode(Settings::SpeedMode::Standard);
    }
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isTurboMode(JNIEnv *env, jobject jobj) {
    return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Turbo;
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isSlowMode(JNIEnv *env, jobject jobj) {
    return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Slow;
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_run(JNIEnv* env, jobject jobj, jstring j_path,
                                               jint j_program_index,
                                               jboolean j_frontend_initiated) {
    const std::string path = Common::Android::GetJString(env, j_path);

    const Core::SystemResultStatus result{
        RunEmulation(path, j_program_index, j_frontend_initiated)};
    if (result != Core::SystemResultStatus::Success) {
        env->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(),
                                  Common::Android::GetExitEmulationActivity(),
                                  static_cast<int>(result));
    }
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_logDeviceInfo(JNIEnv* env, jclass clazz) {
    LOG_INFO(Frontend, "eden Version: {}-{}", Common::g_scm_branch, Common::g_scm_desc);
    LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardText(JNIEnv* env, jclass clazz,
                                                                    jstring j_text) {
    const std::u16string input = Common::UTF8ToUTF16(Common::Android::GetJString(env, j_text));
    EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env, jclass clazz,
                                                                     jint j_key_code) {
    EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env,
                                                                        jobject instance) {
    const auto nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir);
    auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory(
        Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read);

    const auto user_id = EmulationSession::GetInstance().System().GetProfileManager().GetUser(
        static_cast<std::size_t>(0));
    ASSERT(user_id);

    const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
        {}, vfs_nand_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, 1,
        user_id->AsU128(), 0);

    const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
    if (!Common::FS::CreateParentDirs(full_path)) {
        LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory");
    }
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerInit(JNIEnv* env, jobject obj) {
    // for some reason the full user directory isnt initialized in Android, so we need to create it
    const auto play_time_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir);
    if (!Common::FS::IsDir(play_time_dir) && !Common::FS::CreateDir(play_time_dir))
        LOG_WARNING(Frontend, "Failed to create play time directory");

    play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStart(JNIEnv* env, jobject obj) {
    if (play_time_manager) {
        play_time_manager->SetProgramId(EmulationSession::GetInstance().System().GetApplicationProcessProgramID());
        play_time_manager->Start();
    }
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStop(JNIEnv* env, jobject obj) {
    if (play_time_manager)
        play_time_manager->Stop();
}

jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetPlayTime(JNIEnv* env, jobject obj, jstring jprogramId) {
    if (play_time_manager) {
        u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
        return play_time_manager->GetPlayTime(program_id);
    }
    return 0UL;
}

jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetCurrentTitleId(JNIEnv* env,
                                                                             jobject obj) {
    return EmulationSession::GetInstance().System().GetApplicationProcessProgramID();
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerResetProgramPlayTime(JNIEnv* env, jobject obj,
                                                                jstring jprogramId) {
    if (play_time_manager) {
        u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
        play_time_manager->ResetProgramPlayTime(program_id);
    }
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerSetPlayTime(JNIEnv* env, jobject obj,
                                                                jstring jprogramId, jlong playTimeSeconds) {
    if (play_time_manager) {
        u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
        play_time_manager->SetPlayTime(program_id, u64(playTimeSeconds));
    }
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz,
                                                                  jlong jid) {
    auto bis_system =
        EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
    if (!bis_system) {
        return Common::Android::ToJString(env, "");
    }

    auto applet_nca =
        bis_system->GetEntry(static_cast<u64>(jid), FileSys::ContentRecordType::Program);
    if (!applet_nca) {
        return Common::Android::ToJString(env, "");
    }

    return Common::Android::ToJString(env, applet_nca->GetFullPath());
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentAppletId(JNIEnv* env, jclass clazz,
                                                              jint jappletId) {
    EmulationSession::GetInstance().SetAppletId(jappletId);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass clazz,
                                                          jint jcabinetMode) {
    EmulationSession::GetInstance().System().GetFrontendAppletHolder().SetCabinetMode(
        static_cast<Service::NFP::CabinetMode>(jcabinetMode));
}

bool isFirmwarePresent() {
    return FirmwareManager::CheckFirmwarePresence(EmulationSession::GetInstance().System());
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, jclass clazz) {
    return isFirmwarePresent();
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_firmwareVersion(JNIEnv* env, jclass clazz) {
    const auto pair = FirmwareManager::GetFirmwareVersion(EmulationSession::GetInstance().System());
    const auto firmware_data = pair.first;
    const auto result = pair.second;

    if (result.IsError() || !isFirmwarePresent()) {
        return Common::Android::ToJString(env, "N/A");
    }

    const std::string display_version(firmware_data.display_version.data());
    const std::string display_title(firmware_data.display_title.data());

    LOG_INFO(Frontend, "Installed firmware: {}", display_title);

    return Common::Android::ToJString(env, display_version);
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_gameRequiresFirmware(JNIEnv* env, jclass clazz, jstring jprogramId) {
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);

    return FirmwareManager::GameRequiresFirmware(program_id);
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz, jstring jpath, jstring jext) {
    const auto path = Common::Android::GetJString(env, jpath);
    const auto ext = Common::Android::GetJString(env, jext);

    return static_cast<int>(FirmwareManager::InstallKeys(path, ext));
}

jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
                                                                     jstring jpath,
                                                                     jstring jprogramId) {
    const auto path = Common::Android::GetJString(env, jpath);
    const auto vFile =
        Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
    if (vFile == nullptr) {
        return nullptr;
    }

    auto& system = EmulationSession::GetInstance().System();
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
    const FileSys::PatchManager pm{program_id, system.GetFileSystemController(),
                                   system.GetContentProvider()};
    const auto loader = Loader::GetLoader(system, vFile);

    FileSys::VirtualFile update_raw;
    loader->ReadUpdateRaw(update_raw);

    auto patches = pm.GetPatches(update_raw);
    jobjectArray jpatchArray =
        env->NewObjectArray(patches.size(), Common::Android::GetPatchClass(), nullptr);
    int i = 0;
    for (const auto& patch : patches) {
        jobject jpatch = env->NewObject(
            Common::Android::GetPatchClass(), Common::Android::GetPatchConstructor(), patch.enabled,
            Common::Android::ToJString(env, patch.name),
            Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
            Common::Android::ToJString(env, std::to_string(patch.program_id)),
            Common::Android::ToJString(env, std::to_string(patch.title_id)),
            static_cast<jlong>(patch.numeric_version), static_cast<jint>(patch.source));
        env->SetObjectArrayElement(jpatchArray, i, jpatch);
        ++i;
    }
    return jpatchArray;
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj,
                                                        jstring jprogramId) {
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
    ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(),
                                 program_id);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj,
                                                     jstring jprogramId) {
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
    ContentManager::RemoveAllDLC(EmulationSession::GetInstance().System(), program_id);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId,
                                                     jstring jname) {
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
    ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(),
                              program_id, Common::Android::GetJString(env, jname));
}

jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyInstalledContents(JNIEnv* env,
                                                                           jobject jobj,
                                                                           jobject jcallback) {
    auto jlambdaClass = env->GetObjectClass(jcallback);
    auto jlambdaInvokeMethod = env->GetMethodID(
        jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
        auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
                                                   Common::Android::ToJDouble(env, max),
                                                   Common::Android::ToJDouble(env, progress));
        return Common::Android::GetJBoolean(env, jwasCancelled);
    };

    auto& session = EmulationSession::GetInstance();
    std::vector<std::string> result = ContentManager::VerifyInstalledContents(
        session.System(), *session.GetContentProvider(), callback);
    jobjectArray jresult = env->NewObjectArray(result.size(), Common::Android::GetStringClass(),
                                               Common::Android::ToJString(env, ""));
    for (size_t i = 0; i < result.size(); ++i) {
        env->SetObjectArrayElement(jresult, i, Common::Android::ToJString(env, result[i]));
    }
    return jresult;
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyGameContents(JNIEnv* env, jobject jobj,
                                                              jstring jpath, jobject jcallback) {
    auto jlambdaClass = env->GetObjectClass(jcallback);
    auto jlambdaInvokeMethod = env->GetMethodID(
        jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
        auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
                                                   Common::Android::ToJDouble(env, max),
                                                   Common::Android::ToJDouble(env, progress));
        return Common::Android::GetJBoolean(env, jwasCancelled);
    };
    auto& session = EmulationSession::GetInstance();
    return static_cast<jint>(ContentManager::VerifyGameContents(
        session.System(), Common::Android::GetJString(env, jpath), callback));
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
                                                          jstring jprogramId) {
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
    if (program_id == 0) {
        return Common::Android::ToJString(env, "");
    }

    auto& system = EmulationSession::GetInstance().System();

    Service::Account::ProfileManager manager;
    // TODO: Pass in a selected user once we get the relevant UI working
    const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
    ASSERT(user_id);

    const auto saveDir = Common::FS::GetEdenPath(Common::FS::EdenPath::SaveDir);
    auto vfsSaveDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(saveDir),
                                                            FileSys::OpenMode::Read);

    const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
        {}, vfsSaveDir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, program_id,
        user_id->AsU128(), 0);
    return Common::Android::ToJString(env, user_save_data_path);
}

jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultProfileSaveDataRoot(JNIEnv* env,
                                                                            jobject jobj,
                                                                            jboolean jfuture) {
    Service::Account::ProfileManager manager;
    // TODO: Pass in a selected user once we get the relevant UI working
    const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
    ASSERT(user_id);

    const auto user_save_data_root =
        FileSys::SaveDataFactory::GetUserGameSaveDataRoot(user_id->AsU128(), jfuture);
    return Common::Android::ToJString(env, user_save_data_root);
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj,
                                                                       jstring jpath) {
    EmulationSession::GetInstance().ConfigureFilesystemProvider(
        Common::Android::GetJString(env, jpath));
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_addGameFolderFileToFilesystemProvider(
    JNIEnv* env, jobject jobj, jstring jpath) {
    EmulationSession::GetInstance().ConfigureFilesystemProviderFromGameFolder(
        Common::Android::GetJString(env, jpath));
}

void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
    EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
}

jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobject jobj) {
    auto& system = EmulationSession::GetInstance().System();
    system.GetFileSystemController().CreateFactories(*system.GetFilesystem());
    return ContentManager::AreKeysPresent();
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getVirtualAmiiboState(JNIEnv* env, jobject jobj) {
    if (!EmulationSession::GetInstance().IsRunning()) {
        return static_cast<jint>(InputCommon::VirtualAmiibo::State::Disabled);
    }

    auto* virtual_amiibo =
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo();
    if (virtual_amiibo == nullptr) {
        return static_cast<jint>(InputCommon::VirtualAmiibo::State::Disabled);
    }

    return static_cast<jint>(virtual_amiibo->GetCurrentState());
}

jint Java_org_yuzu_yuzu_1emu_NativeLibrary_loadAmiibo(JNIEnv* env, jobject jobj,
                                                      jbyteArray jdata) {
    if (!EmulationSession::GetInstance().IsRunning() || jdata == nullptr) {
        return static_cast<jint>(InputCommon::VirtualAmiibo::Info::WrongDeviceState);
    }

    auto* virtual_amiibo =
        EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo();
    if (virtual_amiibo == nullptr) {
        return static_cast<jint>(InputCommon::VirtualAmiibo::Info::Unknown);
    }

    const jsize length = env->GetArrayLength(jdata);
    std::vector<u8> bytes(static_cast<std::size_t>(length));
    if (length > 0) {
        env->GetByteArrayRegion(jdata, 0, length,
                                reinterpret_cast<jbyte*>(bytes.data()));
    }

    const auto info =
        virtual_amiibo->LoadAmiibo(std::span<u8>(bytes.data(), bytes.size()));
    return static_cast<jint>(info);
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer(
        JNIEnv* env, [[maybe_unused]] jobject obj) {
    if (multiplayer) {
        return;
    }

    announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();

    multiplayer = std::make_unique<AndroidMultiplayer>(s_instance.System(), announce_multiplayer_session);
    multiplayer->NetworkInit();
}

JNIEXPORT jobjectArray JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetPublicRooms(
        JNIEnv *env, [[maybe_unused]] jobject obj) {
    return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetPublicRooms());
}

JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayCreateRoom(
        JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
        jstring username, jstring preferredGameName, jlong preferredGameId, jstring password,
        jstring room_name, jint max_players, jboolean isPublic) {
    return static_cast<jint>(
            multiplayer->NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
                              Common::Android::GetJString(env, username), Common::Android::GetJString(env, preferredGameName),
                              preferredGameId,Common::Android::GetJString(env, password),
                              Common::Android::GetJString(env, room_name), max_players, isPublic));
}

JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayJoinRoom(
        JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
        jstring username, jstring password) {
    return static_cast<jint>(
            multiplayer->NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port,
                            Common::Android::GetJString(env, username), Common::Android::GetJString(env, password)));
}

JNIEXPORT jobjectArray JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayRoomInfo(
        JNIEnv* env, [[maybe_unused]] jobject obj) {
    return Common::Android::ToJStringArray(env, multiplayer->NetPlayRoomInfo());
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsJoined(
        [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    return multiplayer->NetPlayIsJoined();
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsHostedRoom(
        [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    return multiplayer->NetPlayIsHostedRoom();
}

JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlaySendMessage(
        JNIEnv* env, [[maybe_unused]] jobject obj, jstring msg) {
    multiplayer->NetPlaySendMessage(Common::Android::GetJString(env, msg));
}

JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayKickUser(
        JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
    multiplayer->NetPlayKickUser(Common::Android::GetJString(env, username));
}

JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayLeaveRoom(
        [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    multiplayer->NetPlayLeaveRoom();
}

JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsModerator(
        [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
    return multiplayer->NetPlayIsModerator();
}

JNIEXPORT jobjectArray JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetBanList(
        JNIEnv* env, [[maybe_unused]] jobject obj) {
    return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetBanList());
}

JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayBanUser(
        JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
    multiplayer->NetPlayBanUser(Common::Android::GetJString(env, username));
}

JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayUnbanUser(
        JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
    multiplayer->NetPlayUnbanUser(Common::Android::GetJString(env, username));
}

JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updatePowerState(
        JNIEnv* env,
        jobject,
        jint percentage,
        jboolean isCharging,
        jboolean hasBattery) {

    g_battery_percentage.store(percentage, std::memory_order_relaxed);
    g_is_charging.store(isCharging, std::memory_order_relaxed);
    g_has_battery.store(hasBattery, std::memory_order_relaxed);
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isUpdateCheckerEnabled(
        JNIEnv* env,
        jobject obj) {
#ifdef ENABLE_UPDATE_CHECKER
    return JNI_TRUE;
#else
    return JNI_FALSE;
#endif
    }

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isNightlyBuild(
    JNIEnv* env,
    jobject obj) {
#ifdef NIGHTLY_BUILD
    return JNI_TRUE;
#else
    return JNI_FALSE;
#endif
}

#ifdef ENABLE_UPDATE_CHECKER


JNIEXPORT jobject JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate(
        JNIEnv* env,
        jobject obj) {
    std::optional<Common::Net::Release> release = UpdateChecker::GetUpdate();
    if (!release) return nullptr;

    const std::string tag = release->tag;
    const std::string title = release->title;
    const std::string body = release->body;
    const std::string url = release->html_url;

    // Android *should* only ever define a single asset.
    // If not, something has gone wrong, but the Kotlin side can handle it.
    const auto assets = release->GetPlatformAssets();

    jclass updateResultClass = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$UpdateResult");
    if (!updateResultClass) {
        LOG_ERROR(Frontend, "Could not find UpdateResult class");
        return nullptr;
    }

    jmethodID updateResultCtor = env->GetMethodID(updateResultClass, "<init>", "()V");

    if (!updateResultCtor) {
        LOG_ERROR(Frontend, "Could not find UpdateResult ctor");
        env->DeleteLocalRef(updateResultClass);
        return nullptr;
    }

    jmethodID setTag = env->GetMethodID(updateResultClass, "setTag", "(Ljava/lang/String;)V");
    jmethodID setTitle = env->GetMethodID(updateResultClass, "setTitle", "(Ljava/lang/String;)V");
    jmethodID setBody = env->GetMethodID(updateResultClass, "setBody", "(Ljava/lang/String;)V");
    jmethodID setUrl = env->GetMethodID(updateResultClass, "setUrl", "(Ljava/lang/String;)V");
    jmethodID addAsset = env->GetMethodID(updateResultClass, "addAsset", "(Ljava/lang/String;)V");

    jobject updateResult = env->NewObject(updateResultClass, updateResultCtor);

    LOG_DEBUG(Frontend, "Tag: {}", tag);
    LOG_DEBUG(Frontend, "Title: {}", title);
    LOG_DEBUG(Frontend, "Body: {}", body);
    LOG_DEBUG(Frontend, "Url: {}", url);

    const auto jtag = env->NewStringUTF(tag.c_str());
    const auto jtitle = env->NewStringUTF(title.c_str());
    const auto jbody = env->NewStringUTF(body.c_str());
    const auto jurl = env->NewStringUTF(url.c_str());

    env->CallVoidMethod(updateResult, setTag, jtag);
    env->CallVoidMethod(updateResult, setTitle, jtitle);
    env->CallVoidMethod(updateResult, setBody, jbody);
    env->CallVoidMethod(updateResult, setUrl, jurl);

    // TODO(crueter): Handling for multiple assets?
    // Maybe another data class x(
    for (const Common::Net::Asset &a : assets) {
        const auto jaurl = env->NewStringUTF(a.path.c_str());
        env->CallVoidMethod(updateResult, addAsset, jaurl);
        env->DeleteLocalRef(jaurl);
    }

    env->DeleteLocalRef(jtag);
    env->DeleteLocalRef(jtitle);
    env->DeleteLocalRef(jbody);
    env->DeleteLocalRef(jurl);

    env->DeleteLocalRef(updateResultClass);

    return updateResult;
}

#endif

JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    return env->NewStringUTF(Common::g_build_version);
}

JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getAllUsers(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();

    manager.ResetUserSaveFile();

    if (manager.GetUserCount() == 0) {
        manager.CreateNewUser(Common::UUID::MakeRandom(), "Eden");
        manager.WriteUserSaveFile();
    }

    const auto& users = manager.GetAllUsers();

    jclass string_class = env->FindClass("java/lang/String");
    if (!string_class) {
        return env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
    }

    jsize valid_count = 0;
    for (const auto& user : users) {
        if (user.IsValid()) {
            valid_count++;
        }
    }

    jobjectArray result = env->NewObjectArray(valid_count, string_class, nullptr);
    if (!result) {
        return env->NewObjectArray(0, string_class, nullptr);
    }

    // fill array sequentially with only valid users
    jsize array_index = 0;
    for (const auto& user : users) {
        if (user.IsValid()) {
            jstring uuid_str = env->NewStringUTF(user.FormattedString().c_str());
            if (uuid_str) {
                env->SetObjectArrayElement(result, array_index++, uuid_str);
            }
        }
    }

    return result;
}

JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserUsername(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto uuid = Common::UUID{uuid_string};

    Service::Account::ProfileBase profile{};
    if (!manager.GetProfileBase(uuid, profile)) {
        jstring result = env->NewStringUTF("");
        return result ? result : env->NewStringUTF("");
    }

    const auto text = Common::StringFromFixedZeroTerminatedBuffer(
        reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
    jstring result = env->NewStringUTF(text.c_str());
    return result ? result : env->NewStringUTF("");
}

JNIEXPORT jlong JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserCount(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    return static_cast<jlong>(manager.GetUserCount());
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_canCreateUser(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    return manager.CanSystemRegisterUser();
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_createUser(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid,
        jstring jusername) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto username = Common::Android::GetJString(env, jusername);
    const auto uuid = Common::UUID{uuid_string};

    const auto result = manager.CreateNewUser(uuid, username);
    if (result.IsSuccess()) {
        manager.WriteUserSaveFile();
        return true;
    }
    return false;
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updateUserUsername(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid,
        jstring jusername) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto username = Common::Android::GetJString(env, jusername);
    const auto uuid = Common::UUID{uuid_string};

    Service::Account::ProfileBase profile{};
    if (!manager.GetProfileBase(uuid, profile)) {
        return false;
    }

    std::fill(profile.username.begin(), profile.username.end(), '\0');
    std::copy(username.begin(), username.end(), profile.username.begin());

    if (manager.SetProfileBase(uuid, profile)) {
        manager.WriteUserSaveFile();
        return true;
    }
    return false;
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUser(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto uuid = Common::UUID{uuid_string};

    const auto user_index = manager.GetUserIndex(uuid);
    if (!user_index) {
        return false;
    }

    if (Settings::values.current_user.GetValue() == static_cast<s32>(*user_index)) {
        Settings::values.current_user = 0;
    }

    if (manager.RemoveUser(uuid)) {
        manager.WriteUserSaveFile();
        manager.ResetUserSaveFile();
        return true;
    }
    return false;
}

JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getCurrentUser(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    const auto user_id = manager.GetUser(Settings::values.current_user.GetValue());
    if (!user_id) {
        jstring result = env->NewStringUTF("");
        return result ? result : env->NewStringUTF("");
    }
    jstring result = env->NewStringUTF(user_id->FormattedString().c_str());
    return result ? result : env->NewStringUTF("");
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentUser(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto uuid = Common::UUID{uuid_string};

    const auto index = manager.GetUserIndex(uuid);
    if (index) {
        Settings::values.current_user = static_cast<s32>(*index);
        return true;
    }
    return false;
}

JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserImagePath(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid) {
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto uuid = Common::UUID{uuid_string};

    const auto path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
        fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());

    jstring result = Common::Android::ToJString(env, Common::FS::PathToUTF8String(path));
    return result ? result : env->NewStringUTF("");
}

JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_saveUserImage(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jstring juuid,
        jstring jimagePath) {
    const auto uuid_string = Common::Android::GetJString(env, juuid);
    const auto uuid = Common::UUID{uuid_string};
    const auto image_source = Common::Android::GetJString(env, jimagePath);

    const auto dest_path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
        fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());

    const auto dest_dir = dest_path.parent_path();
    if (!Common::FS::CreateDirs(dest_dir)) {
        return false;
    }

    try {
        std::filesystem::copy_file(image_source, dest_path,
            std::filesystem::copy_options::overwrite_existing);
        return true;
    } catch (const std::filesystem::filesystem_error& e) {
        LOG_ERROR(Common_Filesystem, "Failed to copy image file: {}", e.what());
        return false;
    }
}

JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadProfiles(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
    manager.ResetUserSaveFile();

    // create a default user if non exist
    if (manager.GetUserCount() == 0) {
        manager.CreateNewUser(Common::UUID::MakeRandom(), "Eden");
        manager.WriteUserSaveFile();
    }

    LOG_INFO(Service_ACC, "Profile manager reloaded, user count: {}", manager.GetUserCount());
}

//  for firmware avatar images
static std::vector<uint8_t> DecompressYaz0(const FileSys::VirtualFile& file) {
    if (!file) {
        return std::vector<uint8_t>();
    }

    uint32_t magic{};
    file->ReadObject(&magic, 0);
    if (magic != Common::MakeMagic('Y', 'a', 'z', '0')) {
        return std::vector<uint8_t>();
    }

    uint32_t decoded_length{};
    file->ReadObject(&decoded_length, 4);
    decoded_length = Common::swap32(decoded_length);

    std::size_t input_size = file->GetSize() - 16;
    std::vector<uint8_t> input(input_size);
    file->ReadBytes(input.data(), input_size, 16);

    uint32_t input_offset{};
    uint32_t output_offset{};
    std::vector<uint8_t> output(decoded_length);

    uint16_t mask{};
    uint8_t header{};

    while (output_offset < decoded_length) {
        if ((mask >>= 1) == 0) {
            if (input_offset >= input.size()) break;
            header = input[input_offset++];
            mask = 0x80;
        }

        if ((header & mask) != 0) {
            if (output_offset >= output.size() || input_offset >= input.size()) {
                break;
            }
            output[output_offset++] = input[input_offset++];
        } else {
            if (input_offset + 1 >= input.size()) break;
            uint8_t byte1 = input[input_offset++];
            uint8_t byte2 = input[input_offset++];

            uint32_t dist = ((byte1 & 0xF) << 8) | byte2;
            uint32_t position = output_offset - (dist + 1);

            uint32_t length = byte1 >> 4;
            if (length == 0) {
                if (input_offset >= input.size()) break;
                length = static_cast<uint32_t>(input[input_offset++]) + 0x12;
            } else {
                length += 2;
            }

            for (uint32_t i = 0; i < length && output_offset < decoded_length; ++i) {
                output[output_offset++] = output[position++];
            }
        }
    }

    return output;
}

static FileSys::VirtualDir GetFirmwareAvatarDirectory() {
    constexpr u64 AvatarImageDataId = 0x010000000000080AULL;

    auto* bis_system = EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
    if (!bis_system) {
        return nullptr;
    }

    const auto nca = bis_system->GetEntry(AvatarImageDataId, FileSys::ContentRecordType::Data);
    if (!nca) {
        return nullptr;
    }

    const auto romfs = nca->GetRomFS();
    if (!romfs) {
        return nullptr;
    }

    const auto extracted = FileSys::ExtractRomFS(romfs);
    if (!extracted) {
        return nullptr;
    }

    return extracted->GetSubdirectory("chara");
}

JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getFirmwareAvatarCount(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    const auto chara_dir = GetFirmwareAvatarDirectory();
    if (!chara_dir) {
        return 0;
    }

    int count = 0;
    for (const auto& item : chara_dir->GetFiles()) {
        if (item->GetExtension() == "szs") {
            count++;
        }
    }
    return count;
}

JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getFirmwareAvatarImage(
        JNIEnv* env,
        [[maybe_unused]] jobject obj,
        jint index) {
    const auto chara_dir = GetFirmwareAvatarDirectory();
    if (!chara_dir) {
        return nullptr;
    }

    int current_index = 0;
    for (const auto& item : chara_dir->GetFiles()) {
        if (item->GetExtension() != "szs") {
            continue;
        }

        if (current_index == index) {
            auto image_data = DecompressYaz0(item);
            if (image_data.empty()) {
                return nullptr;
            }

            jbyteArray result = env->NewByteArray(image_data.size());
            if (result) {
                env->SetByteArrayRegion(result, 0, image_data.size(),
                    reinterpret_cast<const jbyte*>(image_data.data()));
            }
            return result;
        }
        current_index++;
    }

    return nullptr;
}

JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultAccountBackupJpeg(
        JNIEnv* env,
        [[maybe_unused]] jobject obj) {
    jbyteArray result = env->NewByteArray(Core::Constants::ACCOUNT_BACKUP_JPEG.size());
    if (result) {
        env->SetByteArrayRegion(result, 0, Core::Constants::ACCOUNT_BACKUP_JPEG.size(),
            reinterpret_cast<const jbyte*>(Core::Constants::ACCOUNT_BACKUP_JPEG.data()));
    }
    return result;
}

} // extern "C"
