#pragma once
#include <windows.h>
#include <Psapi.h>
#include <cstdint>
#include "../util/scan.h"
#include "../util/logger.h"
#include "../util/Config.h"

namespace {
    uintptr_t get_export_addr(HMODULE module, const char* name) {
        return reinterpret_cast<uintptr_t>(GetProcAddress(module, name));
    }

    PVOID get_offset(Configuration& cfg, HMODULE module, const char* name)
    {
        logger::info(std::string("Getting offset from cfg for ") + name);
        std::string value = cfg.get(name);

        if (value.empty())
            return nullptr;

        try {
            uintptr_t offset = std::stoull(value, nullptr, 0);

            return reinterpret_cast<PVOID>(
                reinterpret_cast<uintptr_t>(module) + offset
            );
        } catch (const std::invalid_argument&) {
            logger::warn("Invalid offset value (" + value + ") for " + name);
        }
        catch (const std::out_of_range&) {
            logger::warn("Offset out of range (" + value + ") for " + name);
        }

        return nullptr;
    }
}

namespace il2cpp_offsets
{

    static inline HMODULE module_base;
    static inline DWORD module_size;

    // direct pattern
#define PATTERN(pattern)  reinterpret_cast<PVOID>(scan::find_pattern(pattern, reinterpret_cast<char*>(module_base), module_size))

    // xref pattern for short call (call ?? ?? ?? ??)
#define PATTERN_CALL(pattern) reinterpret_cast<PVOID>(scan::find_pattern_xref(pattern, reinterpret_cast<char*>(module_base), module_size, 1, 5))

    // load from exports
#define IMPORT(name) reinterpret_cast<PVOID>(scan::import(name))

    // worse case scenario, static offset from the exported address to skip the protection
#define IMPORT_OFFSET(name, offset) reinterpret_cast<PVOID>(scan::import(name) + (offset))

#define EXPORT_OF(name) reinterpret_cast<PVOID>(get_export_addr(module_base, name))

    static inline PVOID il2cpp_class_from_name;
    static inline PVOID il2cpp_class_get_nested_types;
    static inline PVOID il2cpp_class_get_field_from_name;
    static inline PVOID il2cpp_class_get_method_from_name;
    static inline PVOID il2cpp_domain_get;
    static inline PVOID il2cpp_domain_get_assemblies;
    static inline PVOID il2cpp_field_static_get_value;
    static inline PVOID il2cpp_field_static_set_value;
    static inline PVOID il2cpp_field_get_value_object;
    static inline PVOID il2cpp_string_new;
    static inline PVOID il2cpp_type_get_object;

    inline PVOID resolve_pattern(const std::string& pattern) {
        if (pattern.starts_with("E8"))
            return PATTERN_CALL(pattern);
        return PATTERN(pattern);
    }

    inline PVOID resolve_pattern_with_retry(const std::string& pattern, int retries = 3, int retry_delay_ms = 150) {
        for (int i = 0; i < retries; i++) {
            PVOID result;

            result = resolve_pattern(pattern);

            if (result)
                return result;

            logger::warn(
                "Pattern resolve failed, retry " + std::to_string(i + 1) + "/" + std::to_string(retries)
            );

            Sleep(retry_delay_ms);
        }
        return nullptr;
    }

    inline void init()
    {
        logger::info("Loading Il2CPP API");

        module_base = GetModuleHandleA("GameAssembly.dll");
        MODULEINFO info = {};
        GetModuleInformation(GetCurrentProcess(), module_base, &info, sizeof(MODULEINFO));
        module_size = info.SizeOfImage;

        Configuration cfg;
        if (cfg.load(globals::DLL_OFFSET_FILE)) {
            il2cpp_class_from_name = get_offset(cfg, module_base, "il2cpp_class_from_name");
            il2cpp_class_get_nested_types = get_offset(cfg, module_base, "il2cpp_class_get_nested_types");
            il2cpp_class_get_field_from_name = get_offset(cfg, module_base, "il2cpp_class_get_field_from_name");
            il2cpp_class_get_method_from_name = get_offset(cfg, module_base, "il2cpp_class_get_method_from_name");
            il2cpp_domain_get = get_offset(cfg, module_base, "il2cpp_domain_get");
            il2cpp_field_static_get_value = get_offset(cfg, module_base, "il2cpp_field_static_get_value");
            il2cpp_field_static_set_value = get_offset(cfg, module_base, "il2cpp_field_static_set_value");
            il2cpp_type_get_object = get_offset(cfg, module_base, "il2cpp_type_get_object");

            il2cpp_domain_get_assemblies = get_offset(cfg, module_base, "il2cpp_domain_get_assemblies");
            il2cpp_string_new = get_offset(cfg, module_base, "il2cpp_string_new");
            il2cpp_field_get_value_object = get_offset(cfg, module_base, "il2cpp_field_get_value_object");
        }

        if (il2cpp_class_from_name != nullptr && il2cpp_class_get_nested_types != nullptr && il2cpp_class_get_field_from_name != nullptr
            && il2cpp_class_get_method_from_name != nullptr && il2cpp_domain_get != nullptr && il2cpp_field_static_get_value != nullptr
            && il2cpp_field_static_set_value != nullptr && il2cpp_type_get_object != nullptr && il2cpp_domain_get_assemblies != nullptr
            && il2cpp_string_new != nullptr && il2cpp_field_get_value_object != nullptr) {
            logger::info("Il2CPP API Loaded via offset file");
            return;
        }

        std::string class_from_name_pattern = "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";
        std::string class_get_nested_types_pattern = "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";
        std::string class_get_field_from_name_pattern = "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";
        std::string class_get_method_from_name_pattern = "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";
        std::string domain_get_pattern = "48 83 EC 28 48 8B 05 ? ? ? ? 48 85 C0 75 ? 33 D2";
        std::string field_static_get_value_pattern = "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B 41 ? 49 8B D8";
        std::string field_static_set_value_pattern = "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B E9 48 8B FA";
        std::string type_get_object_pattern = "48 89 4C 24 ? 53 48 83 EC 20 48 8B D9";

        if (cfg.load(globals::DLL_PATTERN_FILE)) {
            std::string pattern = cfg.get("il2cpp_class_from_name");
            if (!pattern.empty()) {
                class_from_name_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_class_get_nested_types");
            if (!pattern.empty()) {
                class_get_nested_types_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_class_get_method_from_name");
            if (!pattern.empty()) {
                class_get_method_from_name_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_domain_get");
            if (!pattern.empty()) {
                domain_get_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_field_static_get_value");
            if (!pattern.empty()) {
                field_static_get_value_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_field_static_set_value");
            if (!pattern.empty()) {
                field_static_set_value_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_type_get_object");
            if (!pattern.empty()) {
                type_get_object_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_type_get_object");
            if (!pattern.empty()) {
                type_get_object_pattern = pattern;
            }
            pattern = cfg.get("il2cpp_type_get_object");
            if (!pattern.empty()) {
                type_get_object_pattern = pattern;
            }
        }

        if (il2cpp_class_from_name == nullptr)
            il2cpp_class_from_name = resolve_pattern_with_retry(class_from_name_pattern);
        if (il2cpp_class_get_nested_types == nullptr)
            il2cpp_class_get_nested_types = resolve_pattern_with_retry(class_get_nested_types_pattern);
        if (il2cpp_class_get_field_from_name == nullptr)
            il2cpp_class_get_field_from_name = resolve_pattern_with_retry(class_get_field_from_name_pattern);
        if (il2cpp_class_get_method_from_name == nullptr)
            il2cpp_class_get_method_from_name = resolve_pattern_with_retry(class_get_method_from_name_pattern);
        if (il2cpp_domain_get == nullptr)
            il2cpp_domain_get = resolve_pattern_with_retry(domain_get_pattern);
        if (il2cpp_field_static_get_value == nullptr)
            il2cpp_field_static_get_value = resolve_pattern_with_retry(field_static_get_value_pattern);
        if (il2cpp_field_static_set_value == nullptr)
            il2cpp_field_static_set_value = resolve_pattern_with_retry(field_static_set_value_pattern);
        if (il2cpp_type_get_object == nullptr)
            il2cpp_type_get_object = resolve_pattern_with_retry(type_get_object_pattern);

        if (il2cpp_domain_get_assemblies == nullptr)
            il2cpp_domain_get_assemblies = EXPORT_OF("il2cpp_domain_get_assemblies");
        if (il2cpp_string_new == nullptr)
            il2cpp_string_new = EXPORT_OF("il2cpp_string_new");
        if (il2cpp_field_get_value_object == nullptr)
            il2cpp_field_get_value_object = EXPORT_OF("il2cpp_field_get_value_object");

        const uintptr_t base = reinterpret_cast<uintptr_t>(module_base);
        struct { const char* name; PVOID ptr; } entries[] = {
            { "il2cpp_class_from_name",          il2cpp_class_from_name },
            { "il2cpp_class_get_nested_types",   il2cpp_class_get_nested_types },
            { "il2cpp_class_get_field_from_name",il2cpp_class_get_field_from_name },
            { "il2cpp_class_get_method_from_name",il2cpp_class_get_method_from_name },
            { "il2cpp_domain_get",               il2cpp_domain_get },
            { "il2cpp_domain_get_assemblies",    il2cpp_domain_get_assemblies },
            { "il2cpp_field_static_get_value",   il2cpp_field_static_get_value },
            { "il2cpp_field_static_set_value",   il2cpp_field_static_set_value },
            { "il2cpp_field_get_value_object",  il2cpp_field_get_value_object },
            { "il2cpp_string_new",               il2cpp_string_new },
            { "il2cpp_type_get_object",          il2cpp_type_get_object },
        };
        for (const auto& e : entries)
        {
            if (!e.ptr)
            {
                logger::warn(std::string("PATTERN FAILED (null): ") + e.name);
                continue;
            }
            const uintptr_t offset = reinterpret_cast<uintptr_t>(e.ptr) - base;
            const auto* b = reinterpret_cast<const uint8_t*>(e.ptr);
            char buf[128];
            std::snprintf(buf, sizeof(buf),
                "%s  +0x%llX  %02X %02X %02X %02X %02X %02X %02X %02X",
                e.name, static_cast<unsigned long long>(offset),
                b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]);
            logger::info(buf);
        }

        logger::info("Il2CPP API Loaded");
    }
}
