#include <ui/key_database.hpp>

#include "platform/file_formats/cia.hpp"
#include "platform/file_formats/ncch.hpp"
#include "processes/pxi_fs.hpp"
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1

#include "session.hpp"
#include "os.hpp"

#include "framework/logging.hpp"
#include "framework/meta_tools.hpp"
#include "framework/ranges.hpp"
#include "framework/settings.hpp"
#include <framework/profiler.hpp>

#include <SDL.h>
#include <SDL_vulkan.h>

#include <spdlog/spdlog.h>
#include <spdlog/sinks/null_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>

#include <boost/program_options.hpp>
#include <boost/endian/arithmetic.hpp>

#include <range/v3/algorithm/any_of.hpp>
#include <range/v3/algorithm/equal.hpp>
#include <range/v3/algorithm/none_of.hpp>

#include <chrono>
#include <codecvt>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <numeric>
#include <thread>

#include <vulkan/vulkan.hpp>

void InstallCIA(std::filesystem::path, spdlog::logger&, const KeyDatabase&, HLE::PXI::FS::FileContext&, HLE::PXI::FS::File&);

using boost::endian::big_uint32_t;

static const char* app_name = "Mikage";

#include "sdl_vulkan_display.hpp"

std::mutex g_vulkan_queue_mutex; // TODO: Turn into a proper interface

namespace bpo = boost::program_options;

static std::vector<std::pair<int16_t, int16_t>> g_samples;
static std::mutex g_sample_mutex;
void TeakraAudioCallback(std::array<int16_t, 2> samples) {
    std::unique_lock lock(g_sample_mutex);
//    g_samples.push_back(std::pair{samples[0], samples[1]});
}


#ifdef __ANDROID__ // TODO: Consider enabling this elsewhere, too!
PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXTptr;
PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXTptr;

extern "C" {
VkResult vkCreateDebugReportCallbackEXT(VkInstance_T* a, VkDebugReportCallbackCreateInfoEXT const* b, VkAllocationCallbacks const* c, VkDebugReportCallbackEXT_T** d) {
return vkCreateDebugReportCallbackEXTptr(a, b, c, d);
}

void vkDestroyDebugReportCallbackEXT(VkInstance_T* a, VkDebugReportCallbackEXT_T* b, VkAllocationCallbacks const* c) {
vkDestroyDebugReportCallbackEXTptr(a, b, c);
}
}

static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(
                                   VkDebugReportFlagsEXT msgFlags,
                                   VkDebugReportObjectTypeEXT objType,
                                   uint64_t srcObject, size_t location,
                                   int32_t msgCode, const char * pLayerPrefix,
                                   const char * pMsg, void * pUserData )
{
   if (msgFlags & VK_DEBUG_REPORT_ERROR_BIT_EXT) {
       __android_log_print(ANDROID_LOG_ERROR,
                           "MikageValidation",
                           "ERROR: [%s] Code %i : %s",
                           pLayerPrefix, msgCode, pMsg);
   } else if (msgFlags & VK_DEBUG_REPORT_WARNING_BIT_EXT) {
       __android_log_print(ANDROID_LOG_WARN,
                           "MikageValidation",
                           "WARNING: [%s] Code %i : %s",
                           pLayerPrefix, msgCode, pMsg);
   } else if (msgFlags & VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT) {
       __android_log_print(ANDROID_LOG_WARN,
                           "MikageValidation",
                           "PERFORMANCE WARNING: [%s] Code %i : %s",
                           pLayerPrefix, msgCode, pMsg);
   } else if (msgFlags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) {
       __android_log_print(ANDROID_LOG_INFO,
                           "MikageValidation", "INFO: [%s] Code %i : %s",
                           pLayerPrefix, msgCode, pMsg);
   } else if (msgFlags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) {
       __android_log_print(ANDROID_LOG_VERBOSE,
                           "MikageValidation", "DEBUG: [%s] Code %i : %s",
                           pLayerPrefix, msgCode, pMsg);
   }

   // Returning false tells the layer not to stop when the event occurs, so
   // they see the same behavior with and without validation layers enabled.
   return VK_FALSE;
}
#endif

static volatile int wait_debugger = 0;

namespace Settings {

inline std::istream& operator>>(std::istream& is, CPUEngine& engine) {
    std::string str;
    is >> str;
    if (str == "narmive") {
        engine = CPUEngine::NARMive;
    } else {
        is.setstate(std::ios_base::failbit);
    }
    return is;
}

inline std::ostream& operator<<(std::ostream& os, CPUEngine engine) {
    switch (engine) {
    case CPUEngine::NARMive:
        return (os << "narmive");
    }
}

inline std::istream& operator>>(std::istream& is, ShaderEngine& engine) {
    std::string str;
    is >> str;
    if (str == "interpreter") {
        engine = ShaderEngine::Interpreter;
    } else if (str == "bytecode") {
        engine = ShaderEngine::Bytecode;
    } else if (str == "glsl") {
        engine = ShaderEngine::GLSL;
    } else {
        is.setstate(std::ios_base::failbit);
    }
    return is;
}

inline std::ostream& operator<<(std::ostream& os, ShaderEngine engine) {
    switch (engine) {
    case ShaderEngine::Interpreter:
        return (os << "interpreter");

    case ShaderEngine::Bytecode:
        return (os << "bytecode");

    case ShaderEngine::GLSL:
        return (os << "glsl");
    }
}

}

namespace CustomEvents {
enum {
    CirclePadPosition = SDL_USEREVENT
};
}

int main(int argc, char* argv[]) {
    while (wait_debugger) {

    }

    Settings::Settings settings;

    bool enable_logging;
    bool bootstrap_nand;
    {
        std::string filename;
        bool enable_debugging;

        bpo::options_description desc("Allowed options");
        desc.add_options()
            ("help", "produce help message")
            ("input", bpo::value<std::string>(&filename), "Input file to load")
            ("launch_menu", bpo::bool_switch(), "Launch Home Menu from NAND on startup")
            ("debug", bpo::bool_switch(&enable_debugging), "Connect to GDB upon startup")
            ("dump_frames", bpo::bool_switch(), "Dump frame data")
            // TODO: Instead read this from the game image... or add support for software reboots!
            ("appmemtype", bpo::value<unsigned>()->default_value(0), "APPMEMTYPE for configuration memory")
            ("attach_to_process", bpo::value<unsigned>(), "Process to attach the debugger (if enabled) to during startup")
            ("render_on_cpu", bpo::bool_switch(), "Render 3D graphics in software")
            ("shader_engine", bpo::value<Settings::ShaderEngine>()->default_value(Settings::ShaderEngineTag::default_value()), "Select which Shader engine to use (interpreter, bytecode, or glsl)")
            ("enable_logging", bpo::bool_switch(&enable_logging), "Enable logging (slow!)")
            ("bootstrap_nand", bpo::bool_switch(&bootstrap_nand), "Bootstrap NAND from game update partition")
            ("enable_audio", bpo::bool_switch(), "Enable audio emulation (slow!)")
            ;

        boost::program_options::positional_options_description p;
        p.add("input", -1);

        bpo::variables_map vm;
        try {
            bpo::store(bpo::command_line_parser(argc, argv).options(desc).positional(p).run(), vm);

            if (!vm.count("input") && !vm["launch_menu"].as<bool>())
                throw bpo::required_option("input or launch_menu"); // TODO: Better string?

            if (vm["debug"].as<bool>() || vm.count("attach_to_process")) {
                if (!vm["debug"].as<bool>() || !vm.count("attach_to_process")) {
                    throw std::runtime_error("Cannot use either of --debug or --attach_to_process without specifying the other");
                }
            }

            bpo::notify(vm);
        } catch (bpo::required_option& e) {
            std::cerr << "ERROR: " << e.what() << std::endl;
            return 1;
        } catch (bpo::invalid_command_line_syntax& e) {
            std::cerr << "ERROR, invalid command line syntax: " << e.what() << std::endl;
            return 1;
        } catch (bpo::multiple_occurrences& e) {
            std::cerr << "ERROR: " << e.what() << std::endl;
            return 1;
        }

        if (vm.count("help")) {
            std::cout << desc << std::endl;
            return 0;
        }

        settings.set<Settings::DumpFrames>(vm["dump_frames"].as<bool>());
        settings.set<Settings::BootToHomeMenu>(vm["launch_menu"].as<bool>());
        if (enable_debugging) {
            settings.set<Settings::ConnectToDebugger>(true);
            settings.set<Settings::AttachToProcessOnStartup>(vm["attach_to_process"].as<unsigned>());
        }
        settings.set<Settings::AppMemType>(vm["appmemtype"].as<unsigned>());
        settings.set<Settings::CPUEngineTag>(Settings::CPUEngine::NARMive);
        if (vm["render_on_cpu"].as<bool>()) {
            settings.set<Settings::RendererTag>(Settings::Renderer::Software);
            if (!vm.count("shader_engine")) {
                // Switch default from GLSL to the bytecode processor
                settings.set<Settings::ShaderEngineTag>(Settings::ShaderEngine::Bytecode);
            }
        }
        settings.set<Settings::ShaderEngineTag>(vm["shader_engine"].as<Settings::ShaderEngine>());
        if (settings.get<Settings::RendererTag>() == Settings::Renderer::Software && settings.get<Settings::ShaderEngineTag>() == Settings::ShaderEngine::GLSL) {
            std::cerr << "ERROR: Cannot use GLSL shader engine with software rendering" << std::endl;
            std::exit(1);
        }

        settings.set<Settings::EnableAudioEmulation>(vm["enable_audio"].as<bool>());

        if (vm.count("input")) {
            Settings::InitialApplicationTag::HostFile file{vm["input"].as<std::string>()};
            settings.set<Settings::InitialApplicationTag>({file});
        }
    }

    auto log_manager = Meta::invoke([enable_logging]() {
        spdlog::sink_ptr logging_sink;
        if (!enable_logging) {
            logging_sink = std::make_shared<spdlog::sinks::null_sink_mt>();
        } else {
            logging_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
        }
        return std::make_unique<LogManager>(logging_sink);
    });
    auto frontend_logger = log_manager->RegisterLogger("FRONTEND");

    auto keydb = LoadKeyDatabase(*frontend_logger, "./aes_keys.txt");

if (bootstrap_nand) // Experimental system bootstrapper
    try {
        // TODO: Replicate the functionality of ns:s's CardUpdateInitialize on boot:
        //       Compare the title version of CVer in emulated NAND against the title
        //       version in the TMD of the CVer CIA in the game update partition.

        auto gamecard = LoadGameCard(*frontend_logger, settings);
        auto update_partition = gamecard->GetPartitionFromId(Loader::NCSDPartitionId::UpdateData);
        if (!update_partition) {
            throw std::runtime_error("Couldn't find update partition");
        }

        auto file_context = HLE::PXI::FS::FileContext { *frontend_logger };
        auto [result] = (*update_partition)->Open(file_context, false);
        if (result != HLE::OS::RESULT_OK) {
            throw std::runtime_error("Failed to open update partition");
        }

        // Open RomFS for update partition
        auto romfs = HLE::PXI::FS::NCCHOpenExeFSSection(*frontend_logger, file_context, keydb, std::move(*update_partition), 0, {});

        FileFormat::RomFSLevel3Header level3_header;
        const uint32_t lv3_offset = 0x1000;

        std::tie(result) = romfs->Open(file_context, false);
        uint32_t bytes_read;
        std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset, sizeof(level3_header), HLE::PXI::FS::FileBufferInHostMemory(level3_header));
        if (result != HLE::OS::RESULT_OK) {
            throw std::runtime_error("Failed to read update partition RomFS");
        }

        for (uint32_t metadata_offset = 0; metadata_offset < level3_header.file_metadata_size;) {
            FileFormat::RomFSFileMetadata file_metadata;
            std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset + level3_header.file_metadata_offset + metadata_offset, sizeof(file_metadata), HLE::PXI::FS::FileBufferInHostMemory(file_metadata));
            if (result != HLE::OS::RESULT_OK) {
                throw std::runtime_error("Failed to read file metadata");
            }
            metadata_offset += sizeof(file_metadata);

            std::u16string filename;
            filename.resize((file_metadata.name_size + 1) / 2);
            std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset + level3_header.file_metadata_offset + metadata_offset, file_metadata.name_size, HLE::PXI::FS::FileBufferInHostMemory(filename.data(), file_metadata.name_size));
            if (result != HLE::OS::RESULT_OK) {
                throw std::runtime_error("Failed to read filename from metadata");
            }

            std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> conversion;
            std::string filename2 { conversion.to_bytes(filename) };

            fprintf(stderr, "FOUND FILENAME: %s\n", filename2.c_str());

            if (filename2.ends_with(".cia")) {
                auto cia_file = std::make_unique<HLE::PXI::FS::FileView>(std::move(romfs), lv3_offset + level3_header.file_data_offset + file_metadata.data_offset, file_metadata.data_size);
                FileFormat::CIAHeader cia_header;
                std::tie(result, bytes_read) = cia_file->Read(file_context, 0, sizeof(cia_header), HLE::PXI::FS::FileBufferInHostMemory(cia_header));
                if (result != HLE::OS::RESULT_OK) {
                    throw std::runtime_error("Failed to read file data");
                }

                fprintf(stderr, "CIA header size: %#x\n", cia_header.header_size);

                std::filesystem::path content_dir = "./data";
                InstallCIA(std::move(content_dir), *frontend_logger, keydb, file_context, *cia_file);

                romfs = cia_file->ReleaseParentAndClose();
            }

            // Align filename size to 4 bytes
            metadata_offset += (file_metadata.name_size + 3) & ~3;
        }

        // Initial system setup requires title 0004001000022400 to be present.
        // Normally, this is the Camera app, which isn't bundled in game update
        // partitions. Since the setup only reads the ExeFS icon that aren't
        // specific to the Camera app (likely to perform general checks), we
        // use the Download Play app as a drop-in replacement if needed.
        if (!std::filesystem::exists("./data/00040010/00022400")) {
            std::filesystem::copy("./data/00040010/00022100", "./data/00040010/00022400", std::filesystem::copy_options::recursive);
        }

        std::filesystem::create_directories("./data/ro/sys");
        std::filesystem::create_directories("./data/rw/sys");
        std::filesystem::create_directories("./data/twlp");

        // Create dummy HWCAL0, required for cfg module to work without running initial system setup first
        char zero[128]{};
        {
            std::ofstream hwcal0("./data/ro/sys/HWCAL0.dat");
            const auto size = 2512;
            for (unsigned i = 0; i < size / sizeof(zero); ++i) {
                hwcal0.write(zero, sizeof(zero));
            }
            hwcal0.write(zero, size % sizeof(zero));
        }

        // Set up dummy rw/sys/SecureInfo_A with region EU
        // TODO: Let the user select the region
        {
            std::ofstream info("./data/rw/sys/SecureInfo_A");
            info.write(zero, sizeof(zero));
            info.write(zero, sizeof(zero));
            char region = 2; // Europe
            info.write(&region, 1);
            info.write(zero, 0x10);
        }

        // TODO: Set up shared font
        // TODO: Set up GPIO
    } catch (std::runtime_error&) {
        throw;
    }

    // Initialize SDL
//    const unsigned window_width = 800, window_height = 960;
//    const unsigned window_width = 720, window_height = 864;
    const unsigned window_width = 400, window_height = 480;
//    const unsigned window_width = 800, window_height = 480;

    // TODO: Fix indentation... but we do want this scope so that all cleanup is finished by the time we print the final log message
    {
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        frontend_logger->critical("Failed to initialize SDL: {}", SDL_GetError());
        return 1;
    }
#ifdef TRACY_ENABLE
// SDL's "SDLAudioP2" thread creates unintelligible back traces in profiles for some reason
#define DISABLE_AUDIO
#error "TEST"
#endif
#define DISABLE_AUDIO // TODO: Drop.
#ifndef DISABLE_AUDIO
    if (SDL_Init(SDL_INIT_AUDIO) != 0) {
        frontend_logger->critical("Failed s to initialize SDL: {}", SDL_GetError());
        return 1;
    }
#endif

//exit(1);
    g_samples.reserve(32768);

#ifndef DISABLE_AUDIO
                static std::ofstream ofs("samples.raw", std::ios_base::binary);
                static std::ofstream ofs2("samples2.raw", std::ios_base::binary);
                static std::vector<uint16_t> data;
                static std::vector<uint16_t> data2;
    SDL_AudioCallback abc = [](void*, uint8_t* stream, int num_bytes) {
        static uint32_t index = 0;

        std::unique_lock lock(g_sample_mutex);
        if (g_samples.empty()) {
            g_samples.push_back(std::pair<int16_t, int16_t>(0, 0));
        }

// TODO: Also handle the emulator running too fast!
        auto num_ready_samples = std::min<uint32_t>(g_samples.size(), num_bytes / 4);

//fprintf(stderr, "Copying %d/%d samples\n", num_ready_samples, num_bytes / 4);

        if (num_ready_samples > 1) {
            g_samples.erase(g_samples.begin());
            --num_ready_samples;
        }

        for (uint32_t sample = 0; sample < num_bytes / 4; ++sample) {
            auto& data = g_samples[sample * num_ready_samples / (num_bytes / 4)];
            memcpy(stream + sample * 4, &data, sizeof(data));
            index++;

        }
//        for (uint32_t sample = 0; sample < num_ready_samples; ++sample) {
//            auto& data = g_samples[sample];
//            ofs.write((char*)&data, sizeof(data) / 2);
//            static int index =0;
//            index++;
//            if ((index % 1000) == 0) {
//                ofs.flush();
//            }
//        }

        // Always leave at least 1 sample for interpolation
        g_samples.erase(g_samples.begin(), g_samples.begin() + (num_ready_samples - 1));

//        for (uint32_t sample = 0; sample < (unsigned)num_bytes / 4; ++sample) {
//            int16_t data = sinf(440.f * 6.28 * index / 32728.f/* 44100.f*/) * 16384.f * (0.1f + (1.0 + sinf(index / 100000.f)) / 2);
//            memcpy(stream + sample * 4, &data, sizeof(data));
//            data = sinf(440.f * 6.28 * index / 32728.f/* 44100.f*/) * 16384.f * (1.1f - (1.0 + sinf(index / 100000.f)) / 2);
//            memcpy(stream + sample * 4 + sizeof(data), &data, sizeof(data));
//            ++index;
//        }
    };

    SDL_AudioSpec desired_audio_spec {
        .freq = 32728,
        .format = AUDIO_S16LSB,
        .channels = 2,
        .silence = 0, // filled by SDL
        .samples = 4096, // TODO?
        .padding = 0,
        .size = 0, // filled by SDL
        // NOTE: Consider using SDL_QueueAudio as an alternative
        .callback = abc,
        .userdata = nullptr
    } ;
    SDL_AudioSpec audio_spec;
//        SDL_AUDIO_DRIVER_DISK
//SDL_AUDIO_DRIVER_DISK;
    fmt::print(stderr, "Opening SDL audio device\n");
    auto device_id = SDL_OpenAudioDevice(nullptr, false, &desired_audio_spec, &audio_spec, 0 /* no allowed changes; let SDL auto-convert */);
    if (device_id == 0) {
        throw std::runtime_error("Failed to open SDL audio device");
    }

    fmt::print(stderr, "Opened SDL audio device with {} samples\n", audio_spec.freq);
//    exit(1);

    // TODO: Setup audio callback

    SDL_PauseAudioDevice(device_id, false);
#endif

    // TODO: For some reason, SDL picks RGB565 as the surface format for us... these hints don't help, either :(
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

    const bool enable_fullscreen = false;
//    const bool enable_fullscreen = true;
    auto sdl_window_flags = enable_fullscreen ? (SDL_WINDOW_VULKAN | SDL_WINDOW_FULLSCREEN_DESKTOP) : SDL_WINDOW_VULKAN;
    std::unique_ptr<SDL_Window, void(*)(SDL_Window*)> window(SDL_CreateWindow(app_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, sdl_window_flags), SDL_DestroyWindow);
    if (!window) {
        throw std::runtime_error("Failed to create window");
    }

    int actual_window_width, actual_window_height;
    SDL_GetWindowSize(&*window, &actual_window_width, &actual_window_height);

    std::array<Layout, SDLVulkanDisplay::FrameData::num_screen_ids> layouts;
    if (enable_fullscreen) {
        // Stretch top screen image by an integral factor; hide the bottom screen
        // TODO: Allow for unhiding the bottom screen!
        unsigned factor = std::min(actual_window_width / 400, actual_window_height / 480);
        unsigned usable_window_width = factor * 400;
        unsigned usable_window_height = factor * 240;
        // NOTE: For recording, make sure the screen positions are aligned by 16
        layouts = {{
//            { true, 0/*(actual_window_width - usable_window_width) / 2*/, 0, static_cast<uint32_t>(usable_window_width), static_cast<uint32_t>(usable_window_width * 240 / 400) },
//            { false, 0, 0, 0, 0 },
////            { false, 0, 0, 0, 0 },
//            { true, static_cast<uint32_t>(usable_window_width) * 40 / 400, (actual_window_height - usable_window_height), static_cast<uint32_t>(usable_window_width) * 320 / 400, static_cast<uint32_t>(usable_window_width * 240 / 400) },
            { true, 28, /*40*/ 180, 400 * 3, 240 * 3 },
            { false, 0, 0, 0, 0 },
//            { false, 0, 0, 0, 0 },
//            { true, 1920 - 640 - 35 + 3, 1080 - 300 - 480 + 4, 640, 480 }, // vertically-centered bottom screen
            { true, 1920 - 640 - 28 + 3, 180, 640, 480 }, // top-aligned bottom screen
        }};
    } else {
        unsigned usable_window_width = actual_window_width;
        unsigned usable_window_height = actual_window_height;
        unsigned bottom_width = usable_window_width * 320 / 400;
        unsigned bottom_height = bottom_width * 240 / 320;
        if (bottom_height * 2 > (unsigned)usable_window_height) {
            bottom_height = usable_window_height / 2;
            bottom_width = bottom_height * 320 / 240;
            usable_window_width = bottom_width * 400 / 320;
        }
        layouts = {{
            { true, (actual_window_width - usable_window_width) / 2, 0, static_cast<uint32_t>(usable_window_width), static_cast<uint32_t>(bottom_width * 240 / 320) },
            { false, 0, 0, 0, 0 },
            { true, (actual_window_width - bottom_width) / 2, static_cast<uint32_t>(bottom_width * 240 / 320), static_cast<uint32_t>(bottom_width), static_cast<uint32_t>(bottom_width * 240 / 320) },
//            { true, 0, 0, 400, 240 },
//            { false, 0, 0, 0, 0 },
//            { true, 400, 120, 320, 240 },
        }};
    }

    if (settings.get<Settings::DumpFrames>()) {
        throw std::runtime_error("Frame dumping not implemented");
    }


    auto display = std::make_unique<SDLVulkanDisplay>(frontend_logger, *window, layouts);

#ifdef __ANDROID__
//#if 0 // TODO: Re-enable!
    {
        auto instance = *display->instance;

        vkCreateDebugReportCallbackEXTptr = (PFN_vkCreateDebugReportCallbackEXT)
            vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT");
        vkDestroyDebugReportCallbackEXTptr = (PFN_vkDestroyDebugReportCallbackEXT)
            vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT");
    }

    vk::DebugReportCallbackCreateInfoEXT callback_info { ~vk::DebugReportFlagsEXT{}, DebugReportCallback };
    auto cb = display->instance->createDebugReportCallbackEXTUnique(callback_info);
#endif

    std::thread emuthread;
    std::exception_ptr emuthread_exception = nullptr;

    std::unique_ptr<EmuSession> session;

    try {
        for (auto key_index : ranges::views::indexes(keydb.aes_slots)) {
            auto& aes_slot = keydb.aes_slots[key_index];
            for (auto key_type : { 'X', 'Y', 'N' }) {
                auto& key = key_type == 'X' ? aes_slot.x
                            : key_type == 'Y' ? aes_slot.y
                            : aes_slot.n;
                if (key.has_value()) {
                    frontend_logger->info("Parsed key{} for slot {:#4x}", key_type, key_index);
                }
            }
        }
        for (auto key_index : ranges::views::indexes(keydb.common_y)) {
            auto& key = keydb.common_y[key_index];
            if (key.has_value()) {
                frontend_logger->info("Parsed common{}", key_index);
            }
        }

    auto gamecard = LoadGameCard(*frontend_logger, settings);
    session = std::make_unique<EmuSession>(*log_manager, settings, *display, *display, keydb, std::move(gamecard));

    emuthread = std::thread {
        [&emuthread_exception, &session]() {
            try {
                session->Run();
            } catch (...) {
                emuthread_exception = std::current_exception();
            }
        }
    };

    bool background = false;

    auto& input = session->input;
    auto& circle_pad = session->circle_pad;

//    std::array<bool, 3> active_images { };
//    std::array<bool, 3> active_images { true, false, true };
    std::array<bool, 3> active_images { true, true, true };

    // Application loop
    for (;;) {
        // Process event queue
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            case SDL_WINDOWEVENT:
                switch (event.window.event) {
                case SDL_WINDOWEVENT_EXPOSED:
                    frontend_logger->error("SDL_WINDOWEVENT_EXPOSED");
//                    SDL_RenderClear(renderer.get());
//                    SDL_RenderPresent(renderer.get());
                    break;

                case SDL_WINDOWEVENT_CLOSE:
                    frontend_logger->error("SDL_WINDOWEVENT_CLOSE");
                    goto quit_application_loop;

                case SDL_WINDOWEVENT_RESIZED:
                    frontend_logger->error("SDL_WINDOWEVENT_RESIZED");
                    break;

                default:
                    break;
                }
                break;

            case SDL_QUIT:
                frontend_logger->error("SDL_QUIT");
                goto quit_application_loop;

            case SDL_APP_WILLENTERBACKGROUND:
                frontend_logger->error("About to enter background, destroying swapchain");
                display->ResetSwapchainResources();
                background = true;
                break;

            case SDL_APP_DIDENTERFOREGROUND:
                frontend_logger->error("Entered foreground, recreating swapchain");
                display->CreateSwapchain();
                background = false;
                break;

            case SDL_MOUSEBUTTONDOWN:
            {
                if (event.button.button != SDL_BUTTON_LEFT) {
                    break;
                }

                float x = (event.button.x - layouts[2].x) / static_cast<float>(layouts[2].width);
                float y = (event.button.y - layouts[2].y) / static_cast<float>(layouts[2].height);
                if (x >= 0.f && x <= 1.f && y >= 0.f && y <= 1.f) {
                    input.SetTouch(x, y);
                }

                break;
            }

            // NOTE: SDL provides a separate event type for touch events, but
            //       (at least on Android) it dispatches both touch events and
            //       mouse events upon touch input. Hence, we only need to
            //       handle mouse events
            case SDL_MOUSEMOTION:
            {
                if ((event.motion.state & SDL_BUTTON_LMASK) == 0) {
                    break;
                }

                float x = (event.button.x - layouts[2].x) / static_cast<float>(layouts[2].width);
                float y = (event.button.y - layouts[2].y) / static_cast<float>(layouts[2].height);
                if (x >= 0.f && x <= 1.f && y >= 0.f && y <= 1.f) {
                    input.SetTouch(x, y);
                } else {
                    // NOTE: If the user keeps holding the mouse button when
                    //       moving the mouse outside the emulated screen area,
                    //       they presumably do not intend to stop the touch
                    //       gesture, yet
                }

                break;
            }

            case SDL_MOUSEBUTTONUP:
                if (event.button.button != SDL_BUTTON_LEFT) {
                    break;
                }

                input.EndTouch();
                break;

            // TODO: Need a way for the emulator to acknowledge key presses!
            //       Otherwise, we might process a KEYDOWN event followed by
            //       KEYUP before the emulator core provides the next content
            //       frame
            case SDL_KEYDOWN:
            case SDL_KEYUP:
            {
                // Ignore repeats
                if (event.key.repeat) {
                    // TODO: Does this actually occur in practice?
                    break;
                }

                bool pressed = (event.type == SDL_KEYDOWN);

                // TODO: For the default key mappings, use scancodes (physical key positions in QWERTY layout) and map them to key codes (the key actually pressed as seen by the user)
                switch (event.key.keysym.sym) {
                case SDLK_LEFT:
                    input.SetPressedY(pressed);
                    break;

                case SDLK_UP:
                    input.SetPressedX(pressed);
                    break;

                case SDLK_DOWN:
                    input.SetPressedB(pressed);
                    break;

                case SDLK_RIGHT:
                    input.SetPressedA(pressed);
                    break;

                case SDLK_BACKSPACE:
                    input.SetPressedHome(pressed);
                    break;

                case SDLK_q:
                case SDLK_VOLUMEUP:
                    input.SetPressedL(pressed);
                    break;

                case SDLK_e:
                case SDLK_VOLUMEDOWN:
                    input.SetPressedR(pressed);
                    break;

                case SDLK_RETURN:
                    input.SetPressedStart(pressed);
                    break;

                case SDLK_a:
                    circle_pad.first -= 1.0f * (pressed ? 1.0 : -1.0);
                    input.SetCirclePad(circle_pad.first, circle_pad.second);
                    break;

                case SDLK_d:
                    circle_pad.first += 1.0f * (pressed ? 1.0 : -1.0);
                    input.SetCirclePad(circle_pad.first, circle_pad.second);
                    break;

                case SDLK_w:
                    circle_pad.second += 1.0f * (pressed ? 1.0 : -1.0);
                    input.SetCirclePad(circle_pad.first, circle_pad.second);
                    break;

                case SDLK_s:
                    circle_pad.second -= 1.0f * (pressed ? 1.0 : -1.0);
                    input.SetCirclePad(circle_pad.first, circle_pad.second);
                    break;

                case SDLK_j:
                    input.SetPressedDigiLeft(pressed);
                    break;

                case SDLK_l:
                    input.SetPressedDigiRight(pressed);
                    break;

                case SDLK_i:
                    input.SetPressedDigiUp(pressed);
                    break;

                case SDLK_k:
                    input.SetPressedDigiDown(pressed);
                    break;

                case SDLK_7:
                    if (settings.get<Settings::RendererTag>() == Settings::Renderer::Vulkan) {
                        settings.set<Settings::RendererTag>(Settings::Renderer::Software);
                    } else {
                        settings.set<Settings::RendererTag>(Settings::Renderer::Vulkan);
                    }
                    break;

                case SDLK_AC_BACK:
                    if (event.type == SDL_KEYDOWN) {
                        goto quit_application_loop;
                    }
                    break;
                }
                break;
            }

            case CustomEvents::CirclePadPosition:
            {
                float x, y;
                memcpy(&x, &event.user.data1, sizeof(x));
                memcpy(&y, reinterpret_cast<char*>(&event.user.data1) + sizeof(x), sizeof(y));
                input.SetCirclePad(x, y);
                break;
            }

            default:
                break;
            }
        }

        if (background) {
            continue;
        }

        // Process next frame from the emulation core, if any (and do so three times for each screen id... TODO: Find a nicer way of doing this)
        display->BeginFrame();

        // Repeat until at least one image is active

static int loll = 0;
loll++;

// TODO: Proper time keeping
display->SeekTo(loll * 3);

// TODO: If recording via renderdoc, wait for all active images to arrive
//std::this_thread::sleep_for(std::chrono::milliseconds { 20 });
//std::this_thread::sleep_for(std::chrono::milliseconds { 30 });
        for (auto stream_id : { EmuDisplay::DataStreamId::TopScreenLeftEye, EmuDisplay::DataStreamId::TopScreenRightEye, EmuDisplay::DataStreamId::BottomScreen }) {
            // TODO: If capturing with renderdoc, sync presentation to rendering
//            while (stream_id == EmuDisplay::DataStreamId::TopScreenLeftEye && display->ready_frames[Meta::to_underlying(stream_id)].empty()) {
//                display->SeekTo(loll * 3);
//                std::this_thread::yield();
//            }

            // TODO: Add EmuDisplay interface
            if (display->ready_frames[Meta::to_underlying(stream_id)].empty()) {
                continue;
            }
            auto& frame = display->GetCurrent(stream_id);
//            fprintf(stderr, "Processing frame %lu\n", frame.timestamp);
            display->ProcessDataSource(frame, stream_id);
        }

#if 0 // TODO: Port entire logic over
        auto begin_frame_time = std::chrono::steady_clock::now();
        for (std::array<bool, 3> has_new_image { }; true;) {
#ifdef RENDERDOC_WORKAROUND
            if (!has_new_image[0] && !has_new_image[2]) {
                dumb_lock = true;
            }
#endif

            if (!has_new_image[0]) {
                has_new_image[0] = display->initial_stage->TryToDisplayNextDataSource(EmuDisplay::DataStreamId::TopScreenLeftEye);
            }
            if (!has_new_image[1]) {
                has_new_image[1] = display->initial_stage->TryToDisplayNextDataSource(EmuDisplay::DataStreamId::TopScreenRightEye);
            }
            if (!has_new_image[2]) {
                has_new_image[2] = display->initial_stage->TryToDisplayNextDataSource(EmuDisplay::DataStreamId::BottomScreen);
            }

            // If any image has been submitted, wait until at least one top screen and one bottom screen image have arrived. Otherwise, we'd artificially limit ourselves to 30 fps (on a 60 Hz display) by pulling the two images in separate frames since since the images don't arrive simultaneously

            if (emuthread_exception) {
                break;
            }

            // Infer screen activity information from MMIO instead
            using namespace std::chrono_literals;
            // Check if any new images have become active
            if (!ranges::equal(has_new_image, active_images, std::less_equal{})) {
                for (unsigned i = 0; i < std::size(active_images); ++i) {
                    active_images[i] |= has_new_image[i];
                }
                if (ranges::find(active_images, false) != std::end(active_images)) {
                    // Wait a bit in case others become active too so that we
                    // avoid displaying the images in separate host frames.
                    std::this_thread::sleep_for(3ms);
                    continue;
                } else {
                    break;
                }
            }

            // Wait until *any* image is active
            if (ranges::find(active_images, true) == ranges::end(active_images)) {
                std::this_thread::yield();
                continue;
            }

            // Wait until all images have been submitted that have been found to be active
            if (has_new_image[0] == active_images[0] && has_new_image[1] == active_images[1] && has_new_image[2] == active_images[2]) {
                break;
            }

            // Move frames to "inactive" state if no new image is received for more than one host frame interval
            if (std::chrono::steady_clock::now() - begin_frame_time > 17ms) {
                for (unsigned i = 0; i < std::size(active_images); ++i) {
                    active_images[i] &= has_new_image[i];
                }
                break;
            }

            std::this_thread::yield();
        }
#endif

        {
            std::unique_lock lock(g_vulkan_queue_mutex);
            display->EndFrame();
        }
#ifdef RENDERDOC_WORKAROUND
        dumb_lock = true;
#endif

        auto metrics = session->profiler.Freeze();
        auto print_metrics = [&frontend_logger](std::string indent, Profiler::FrozenMetrics& frozen, auto& cont) -> void {
            frontend_logger->info("{}{}: {} ms", indent, frozen.activity.GetName(), frozen.metric.total.count() / 1000.f);
            indent += "  ";
            auto misc_activity = std::accumulate(frozen.sub_metrics.begin(), frozen.sub_metrics.end(), frozen.metric.total,
                                                 [](auto& dur, auto& frozen) { return dur - frozen.metric.total; });
            if (misc_activity.count() < 0) {
                // TODO
//                throw std::runtime_error("Negative misc activity?");
            }
            // Don't print misc activity if there are any sub activities to begin with, or if the relative difference is negligible
            if (!frozen.sub_metrics.empty() && misc_activity >= frozen.metric.total * 0.03 ) {
                frontend_logger->info("{}Misc: {} ms", indent, misc_activity.count() / 1000.f);
            }
            for (auto& sub_activity : frozen.sub_metrics) {
                cont(indent, sub_activity, cont);
            }
        };
//        frontend_logger->info("PROFILE:");
//        print_metrics("", metrics, print_metrics);

        // TODO: Have the emu thread post an SDL event about this instead of checking every frame
        if (emuthread_exception) {
            std::rethrow_exception(emuthread_exception);
        }
    }
    } catch (std::exception& err) {
        // TODO: On Android, the "back" button doesn't do anything while this is displayed
        frontend_logger->error(err.what());
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Emulation error", err.what(), nullptr);
        goto quit_application_loop;
    } catch (...) {
        auto message = "Unhandled exception!\n";
        frontend_logger->error(message);
        // TODO: On Android, the "back" button doesn't do anything while this is displayed
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Emulation error", "Unknown exception", nullptr);
        goto quit_application_loop;
    }
quit_application_loop:
    frontend_logger->info("Initiating emulator shutdown...");

    if (session) {
        display->exit_requested = true;
        if (session->setup->os) {
            session->setup->os->RequestStop();
        }

        // Wait for emulation thread (OS) to shut down.
        // This may be in one of these blocking states:
        // * OS scheduler stuck in an infinite idle loop
        //   * An internal flag set by RequestStop() is checked here
        // * Attempting to push frame data when no queue slot is available
        //   * Display::exit_requested is checked here
        // * CPU emulation stuck in a loop (e.g. due to the emulated app waiting on external events using a spinlock)
        //   * This is handled indirectly by regular preemption
        frontend_logger->info("Waiting for emulation thread...");
        emuthread.join();

        // Wait for the host GPU to catch up now that no more work will be pushed
        frontend_logger->info("Waiting for host GPU to be idle...");
        display->graphics_queue.waitIdle();
        display->present_queue.waitIdle();
        display->device->waitIdle();

        frontend_logger->info("Shutting down DSP JIT...");

        session.reset();
    }

    frontend_logger->info("Clearing display resources...");
    display->ClearResources();

    frontend_logger->info("Clearing remaining resources...");
    }
    frontend_logger->info("... done. Have a nice day!");

    // NOTE: This function isn't actually the true main(), since SDL overrides
    //       it. Hence we must explicitly return 0 here (whereas usually 0 is
    //       implicitly returned from main)
    return 0;
}