#include "adapters/il2cpp/Il2CppExports.h"

#include <Windows.h>
#include <Psapi.h>

#include <array>
#include <cstdint>
#include <cstdio>
#include <string>

#include "foundation/diagnostics/Logger.h"
#include "foundation/memory/PatternScan.h"

namespace
{
    ta::adapters::il2cpp::exports::Il2CppApiTable g_api;

    [[nodiscard]] void* export_address(const HMODULE module, const char* name) noexcept
    {
        return reinterpret_cast<void*>(GetProcAddress(module, name));
    }

    [[nodiscard]] void* pattern_address(
        const HMODULE module,
        const DWORD module_size,
        const char* pattern) noexcept
    {
        return reinterpret_cast<void*>(ta::foundation::memory::scan::find_pattern(
            pattern,
            reinterpret_cast<char*>(module),
            module_size));
    }
}

namespace ta::adapters::il2cpp::exports
{
    bool Il2CppApiTable::complete() const noexcept
    {
        return il2cpp_class_from_name &&
            il2cpp_class_get_nested_types &&
            il2cpp_class_get_field_from_name &&
            il2cpp_class_get_method_from_name &&
            il2cpp_domain_get &&
            il2cpp_domain_get_assemblies &&
            il2cpp_field_static_get_value &&
            il2cpp_string_new &&
            il2cpp_type_get_object;
    }

    const Il2CppApiTable& api() noexcept
    {
        return g_api;
    }

    bool init()
    {
        constexpr bool kVerboseIl2CppOffsets = false;

        ta::foundation::logger::info("IL2CPP", "api initialization started");

        const HMODULE module = GetModuleHandleA("GameAssembly.dll");
        if (!module)
        {
            ta::foundation::logger::error("IL2CPP", "GameAssembly.dll unavailable");
            return false;
        }

        MODULEINFO info{};
        if (!GetModuleInformation(GetCurrentProcess(), module, &info, sizeof(info)) || info.SizeOfImage == 0)
        {
            ta::foundation::logger::error("IL2CPP", "GameAssembly.dll module information unavailable");
            return false;
        }

        Il2CppApiTable resolved{};
        resolved.il2cpp_class_from_name = pattern_address(module, info.SizeOfImage,
            "4C 89 44 24 ? 48 89 54 24 ? 53 55 56 57 41 54 41 55 41 56 41 57 48 81 EC 88 00 00 00");
        resolved.il2cpp_class_get_nested_types = pattern_address(module, info.SizeOfImage,
            "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B FA 48 8B D9 48 85 D2 0F 84 ? ? ? ? 48 83 79 ? ? 0F 85");
        resolved.il2cpp_class_get_field_from_name = pattern_address(module, info.SizeOfImage,
            "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B EA 48 8B D9 48 85 C9");
        resolved.il2cpp_class_get_method_from_name = pattern_address(module, info.SizeOfImage,
            "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 41 56 41 57 48 83 EC 20 41 8B E9 41 8B F8");
        resolved.il2cpp_domain_get = pattern_address(module, info.SizeOfImage,
            "48 83 EC 28 48 8B 05 ? ? ? ? 48 85 C0 75 ? 33 D2");
        resolved.il2cpp_field_static_get_value = pattern_address(module, info.SizeOfImage,
            "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B 41 ? 49 8B D8");
        resolved.il2cpp_type_get_object = pattern_address(module, info.SizeOfImage,
            "48 89 4C 24 ? 53 48 83 EC 20 48 8B D9");
        resolved.il2cpp_domain_get_assemblies = export_address(module, "il2cpp_domain_get_assemblies");
        resolved.il2cpp_string_new = export_address(module, "il2cpp_string_new");

        struct Entry
        {
            const char* name;
            void* address;
        };
        const std::array entries{
            Entry{"il2cpp_class_from_name", resolved.il2cpp_class_from_name},
            Entry{"il2cpp_class_get_nested_types", resolved.il2cpp_class_get_nested_types},
            Entry{"il2cpp_class_get_field_from_name", resolved.il2cpp_class_get_field_from_name},
            Entry{"il2cpp_class_get_method_from_name", resolved.il2cpp_class_get_method_from_name},
            Entry{"il2cpp_domain_get", resolved.il2cpp_domain_get},
            Entry{"il2cpp_domain_get_assemblies", resolved.il2cpp_domain_get_assemblies},
            Entry{"il2cpp_field_static_get_value", resolved.il2cpp_field_static_get_value},
            Entry{"il2cpp_string_new", resolved.il2cpp_string_new},
            Entry{"il2cpp_type_get_object", resolved.il2cpp_type_get_object},
        };

        const auto base = reinterpret_cast<std::uintptr_t>(module);
        for (const Entry& entry : entries)
        {
            if (!entry.address)
            {
                ta::foundation::logger::warn(
                    "IL2CPP", std::string("required entry missing | name=") + entry.name);
                continue;
            }

            if (kVerboseIl2CppOffsets)
            {
                const auto offset = reinterpret_cast<std::uintptr_t>(entry.address) - base;
                const auto* bytes = reinterpret_cast<const std::uint8_t*>(entry.address);
                char buffer[128]{};
                std::snprintf(buffer, sizeof(buffer),
                    "%s  +0x%llX  %02X %02X %02X %02X %02X %02X %02X %02X",
                    entry.name,
                    static_cast<unsigned long long>(offset),
                    bytes[0], bytes[1], bytes[2], bytes[3],
                    bytes[4], bytes[5], bytes[6], bytes[7]);
                ta::foundation::logger::info("IL2CPP", buffer);
            }
        }

        if (!resolved.complete())
        {
            ta::foundation::logger::error(
                "IL2CPP", "api initialization failed | reason=required entries missing");
            return false;
        }

        g_api = resolved;
        ta::foundation::logger::trace("IL2CPP", "api initialized");
        return true;
    }
}
