#pragma once

#include <string>
#include <vector>
#include <functional>
#include <unordered_map>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <Windows.h>

#include "impl/QuestModule.h"
#include "eft_api.h"
#include "game_state.h"
#include "../il2cpp/Il2CppObjectInstance.h"
#include "../il2cpp/exports.h"
#include "../il2cpp/il2utils.h"
#include "../il2cpp/unity.h"

namespace quest_esp
{
    enum class poi_type : char
    {
        visit,
        find,
        place,
    };

    struct poi_entry
    {
        unity::vector3 pos{};
        std::string quest_name;
        std::string description;
        poi_type type = poi_type::visit;
        float distance = 0.f;
        Il2CppObject* condition = nullptr;
    };

    inline constexpr int quest_status_started = 2;
    inline constexpr int quest_status_available_for_finish = 3;

    struct trigger_cache_state
    {
        std::vector<std::pair<std::string, Il2CppObject*>> experience_triggers;
        std::vector<std::pair<std::string, Il2CppObject*>> place_triggers;
        std::unordered_map<std::string, Il2CppObject*> experience_by_id;
        std::unordered_map<std::string, Il2CppObject*> place_by_id;
        ULONGLONG last_refresh_ms = 0;
        Il2CppObject* last_world = nullptr;
    };

    enum class collect_phase : char
    {
        idle,
        building,
        scanning_loot,
        ready,
    };

    struct find_rule
    {
        std::string quest_name;
        std::string description;
        Il2CppObject* condition = nullptr;
    };

    struct incremental_collect_state
    {
        collect_phase phase = collect_phase::idle;
        std::vector<poi_entry> result;
        std::vector<find_rule> find_rules;
        Il2CppArray* loot_array = nullptr;
        size_t loot_cursor = 0;
        unity::vector3 my_pos{};
    };

    inline trigger_cache_state& triggers()
    {
        static trigger_cache_state state;
        return state;
    }

    inline incremental_collect_state& incremental()
    {
        static incremental_collect_state state;
        return state;
    }

    inline void reset_incremental()
    {
        auto& state = incremental();
        state.phase = collect_phase::idle;
        state.result.clear();
        state.find_rules.clear();
        state.loot_array = nullptr;
        state.loot_cursor = 0;
    }

    inline std::string mongo_id_key(const mongo_id& id)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "%u:%llx",
            id.time_stamp, static_cast<unsigned long long>(id.counter));
        return buf;
    }

    inline bool class_name_contains(Il2CppObject* obj, const char* needle)
    {
        return obj && obj->klass && obj->klass->name && strstr(obj->klass->name, needle) != nullptr;
    }

    inline bool class_inherits_from(Il2CppObject* obj, const char* base_name)
    {
        if (!obj || !obj->klass || !base_name || !base_name[0]) return false;

        for (const Il2CppClass* klass = obj->klass; klass; klass = klass->parent)
        {
            if (klass->name && strcmp(klass->name, base_name) == 0)
                return true;
        }

        return false;
    }

    inline bool is_place_condition_type(Il2CppObject* condition)
    {
        if (!condition) return false;

        if (class_name_contains(condition, "ConditionZone")
            || class_name_contains(condition, "ConditionLeaveItemAtLocation")
            || class_name_contains(condition, "ConditionPlaceBeacon"))
            return true;

        return class_inherits_from(condition, "ConditionZone");
    }

    inline bool is_multiple_targets_condition(Il2CppObject* condition)
    {
        if (!condition) return false;

        if (class_name_contains(condition, "ConditionFindItem")
            || is_place_condition_type(condition))
            return true;

        return class_inherits_from(condition, "ConditionMultipleTargets");
    }

    inline void register_place_trigger_id(
        std::vector<std::pair<std::string, Il2CppObject*>>& triggers,
        std::unordered_map<std::string, Il2CppObject*>& by_id,
        const std::string& id,
        Il2CppObject* trigger)
    {
        if (!trigger || id.empty()) return;

        triggers.emplace_back(id, trigger);
        by_id.emplace(id, trigger);
    }

    inline Il2CppObject* get_condition_collection_object(Il2CppObject* counter_creator)
    {
        if (!counter_creator || !counter_creator->klass) return nullptr;

        Il2CppObjectInstance creator(counter_creator);

        const auto get_conditions = creator
            .get_method<Il2CppObject*(*)(Il2CppObject*)>("get_Conditions", 0);
        if (get_conditions)
        {
            Il2CppObject* via_method = get_conditions(counter_creator);
            if (via_method) return via_method;
        }

        Il2CppObject* backing = creator.get_field<Il2CppObject*>("<Conditions>k__BackingField");
        if (backing) return backing;

        Il2CppObject* template_obj = creator.get_field<Il2CppObject*>("_templateConditions");
        if (!template_obj) return nullptr;

        return Il2CppObjectInstance(template_obj).get_field<Il2CppObject*>("Conditions");
    }

    inline mongo_id get_condition_id(Il2CppObject* condition)
    {
        if (!condition) return {};
        Il2CppObjectInstance inst(condition);
        return inst.get_field<mongo_id>("<id>k__BackingField");
    }

    inline Il2CppObject* find_progress_checker(Il2CppObject* quest, Il2CppObject* condition)
    {
        if (!quest || !condition) return nullptr;

        Il2CppObjectInstance quest_inst(quest);
        const auto get_checkers = quest_inst
            .get_method<Il2CppObject*(*)(Il2CppObject*)>("get_ProgressCheckers", 0);
        if (!get_checkers) return nullptr;

        Il2CppObject* checkers_obj = get_checkers(quest);
        if (!checkers_obj) return nullptr;

        auto* checkers = reinterpret_cast<unity::dict<Il2CppObject*, Il2CppObject*>*>(checkers_obj);
        if (!checkers || !checkers->m_p_entries) return nullptr;

        Il2CppObject* checker = checkers->get_value_by_key(condition);
        if (checker) return checker;

        const std::string target_key = mongo_id_key(get_condition_id(condition));
        if (target_key.empty()) return nullptr;

        for (int i = 0; i < checkers->m_i_count; ++i)
        {
            Il2CppObject* key = checkers->get_key_by_index(i);
            if (!key) continue;
            if (mongo_id_key(get_condition_id(key)) == target_key)
                return checkers->get_value_by_index(i);
        }

        return nullptr;
    }

    inline bool is_progress_checker_completed(Il2CppObject* checker)
    {
        if (!checker) return false;

        Il2CppObjectInstance inst(checker);
        const auto is_completed = inst.get_method<bool(*)(Il2CppObject*)>("IsCompleted", 0);
        if (is_completed && is_completed(checker))
            return true;

        const auto test = inst.get_method<bool(*)(Il2CppObject*)>("Test", 0);
        return test && test(checker);
    }

    inline bool is_condition_in_completed_collection(Il2CppObject* completed_collection,
                                                       Il2CppObject* condition)
    {
        if (!completed_collection || !condition || !completed_collection->klass) return false;

        const MethodInfo* contains_method =
            il2utils::resolve_method(completed_collection->klass, "Contains", 1);
        if (!contains_method || !contains_method->methodPointer) return false;

        return reinterpret_cast<bool(*)(Il2CppObject*, Il2CppObject*, const MethodInfo*)>(
            contains_method->methodPointer)(completed_collection, condition, contains_method);
    }

    inline bool is_condition_in_status_hashset(Il2CppObject* completed_set,
                                                const mongo_id& condition_id)
    {
        if (!completed_set || !completed_set->klass) return false;

        const MethodInfo* contains_method =
            il2utils::resolve_method(completed_set->klass, "Contains", 1);
        if (!contains_method || !contains_method->methodPointer) return false;

        mongo_id id_copy = condition_id;
        return reinterpret_cast<bool(*)(Il2CppObject*, mongo_id*, const MethodInfo*)>(
            contains_method->methodPointer)(completed_set, &id_copy, contains_method);
    }

    inline bool is_condition_completed(Il2CppObject* quest,
                                       Il2CppObject* condition,
                                       Il2CppObject* status_completed_set)
    {
        if (!condition) return false;

        if (quest)
        {
            Il2CppObject* checker = find_progress_checker(quest, condition);
            if (checker && is_progress_checker_completed(checker))
                return true;

            Il2CppObjectInstance quest_inst(quest);
            const auto get_completed = quest_inst
                .get_method<Il2CppObject*(*)(Il2CppObject*)>("get_CompletedConditions", 0);
            if (get_completed)
            {
                Il2CppObject* completed_collection = get_completed(quest);
                if (completed_collection
                    && is_condition_in_completed_collection(completed_collection, condition))
                    return true;
            }
        }

        if (status_completed_set)
            return is_condition_in_status_hashset(status_completed_set, get_condition_id(condition));

        return false;
    }

    inline std::unordered_map<std::string, std::string>& persistent_desc_cache()
    {
        static std::unordered_map<std::string, std::string> cache;
        return cache;
    }

    inline void clear_caches()
    {
        reset_incremental();
        persistent_desc_cache().clear();

        auto& cache = triggers();
        cache.last_world = nullptr;
        cache.last_refresh_ms = 0;
        cache.experience_triggers.clear();
        cache.place_triggers.clear();
        cache.experience_by_id.clear();
        cache.place_by_id.clear();
    }

    inline size_t get_runtime_cache_count()
    {
        const auto& cache = triggers();
        const auto& state = incremental();
        return cache.experience_triggers.size()
            + cache.place_triggers.size()
            + state.result.size()
            + state.find_rules.size();
    }

    inline size_t get_runtime_cache_bytes()
    {
        const auto& cache = triggers();
        const auto& state = incremental();
        size_t bytes = (cache.experience_triggers.size() + cache.place_triggers.size()) * 128;
        for (const auto& poi : state.result)
            bytes += poi.quest_name.size() + poi.description.size() + 96;
        bytes += state.find_rules.size() * 96;
        bytes += persistent_desc_cache().size() * 64;
        return bytes;
    }

    inline std::string read_il2cpp_string_result(Il2CppString* str)
    {
        if (!str || str->length <= 0) return {};
        return eft_api::to_display_utf8(il2utils::conv_wstring(str));
    }

    inline std::string trim_text(std::string text)
    {
        while (!text.empty() && (text.front() == ' ' || text.front() == '\t'))
            text.erase(text.begin());
        while (!text.empty() && (text.back() == ' ' || text.back() == '\t'))
            text.pop_back();
        return text;
    }

    inline bool is_template_id_char(const char c)
    {
        return std::isxdigit(static_cast<unsigned char>(c)) != 0;
    }

    inline bool is_pure_template_id(const std::string& text)
    {
        if (text.size() != 24) return false;
        for (const char c : text)
            if (!is_template_id_char(c)) return false;
        return true;
    }

    inline std::string normalize_template_id(std::string id)
    {
        for (char& c : id)
            c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        return id;
    }

    inline std::string try_localize_template_id_short_name(const std::string& template_id)
    {
        if (!is_pure_template_id(template_id)) return {};

        static const Il2CppClass* loc_class = il2utils::resolve_class(
            il2utils::resolve_image("Assembly-CSharp.dll"), "EFT", "LocalizationExtensions");
        static const MethodInfo* localized_short = loc_class
            ? il2utils::resolve_method(loc_class, "LocalizedShortName", 1)
            : nullptr;
        if (!localized_short || !localized_short->methodPointer) return {};

        Il2CppString* id_str = il2cpp::il2cpp_string_new(template_id.c_str());
        if (!id_str) return {};

        mongo_id mongo_template{};
        mongo_template.string_id = id_str;

        Il2CppString* name_str = reinterpret_cast<Il2CppString*(*)(mongo_id, const MethodInfo*)>(
            localized_short->methodPointer)(mongo_template, localized_short);
        if (!name_str) return {};

        const std::string name = read_il2cpp_string_result(name_str);
        if (name.empty() || name == template_id) return {};
        return name;
    }

    inline std::string try_resolve_template_name_runtime(const std::string& template_id)
    {
        if (!is_pure_template_id(template_id) || !game_state::item_factory) return {};

        Il2CppObjectInstance factory(game_state::item_factory);
        const auto get_preset = factory.get_method<Il2CppObject*(*)(Il2CppObject*, Il2CppString*)>(
            "GetPresetItem", 1);
        if (!get_preset) return {};

        Il2CppString* id_str = il2cpp::il2cpp_string_new(template_id.c_str());
        if (!id_str) return {};

        Il2CppObject* item = get_preset(game_state::item_factory, id_str);
        if (!item) return {};

        const std::string name = eft_api::try_get_item_localized_display_name(item);
        if (name.empty() || name == "Item" || name == template_id) return {};
        return name;
    }

    inline std::string resolve_template_display_name(const std::string& template_id)
    {
        const std::string normalized = normalize_template_id(template_id);

        if (std::string name = try_resolve_template_name_runtime(normalized); !name.empty())
            return name;

        if (std::string name = try_localize_template_id_short_name(normalized); !name.empty())
            return name;

        return {};
    }

    inline bool looks_like_locale_key(const std::string& text)
    {
        if (text.empty()) return false;
        if (text.front() == '/') return true;
        if (text.find("UI/") != std::string::npos) return true;

        if (text.find('_') == std::string::npos || text.find(' ') != std::string::npos)
            return false;

        for (const char c : text)
        {
            if (static_cast<unsigned char>(c) > 0x7F)
                return false;
        }

        return text.size() > 8;
    }

    inline std::string localize_description_text(const std::string& text)
    {
        if (text.empty()) return text;

        std::string localized = text;

        if (const std::string parsed = eft_api::try_parse_localization(localized);
            eft_api::is_plausible_localized_result(localized, parsed))
            localized = parsed;

        if (looks_like_locale_key(localized))
        {
            if (const std::string by_key = eft_api::try_localize_game_key(localized);
                eft_api::is_plausible_localized_result(localized, by_key))
                localized = by_key;
        }

        return localized;
    }

    inline std::string substitute_template_ids(const std::string& text)
    {
        if (text.size() < 24) return text;

        std::string out;
        out.reserve(text.size());

        for (size_t i = 0; i < text.size();)
        {
            if (i + 24 <= text.size())
            {
                bool hex = true;
                for (size_t j = 0; j < 24; ++j)
                {
                    if (!is_template_id_char(text[i + j]))
                    {
                        hex = false;
                        break;
                    }
                }

                if (hex)
                {
                    const std::string id = text.substr(i, 24);
                    const std::string name = resolve_template_display_name(id);
                    if (!name.empty() && name != normalize_template_id(id))
                    {
                        out += name;
                        i += 24;
                        continue;
                    }
                }
            }

            out += text[i++];
        }

        return out;
    }

    // rollback: старый путь без локализации (GenerateFormattedDescription + сырой FormattedDescription)
    // inline void ensure_condition_formatted_description(Il2CppObject* condition)
    // {
    //     if (!condition || !condition->klass) return;
    //
    //     const MethodInfo* generate = il2utils::resolve_method(
    //         condition->klass, "GenerateFormattedDescription", 0);
    //     if (!generate || !generate->methodPointer) return;
    //
    //     reinterpret_cast<void(*)(Il2CppObject*, const MethodInfo*)>(
    //         generate->methodPointer)(condition, generate);
    // }
    //
    // inline std::string get_raw_condition_description(Il2CppObject* condition)
    // {
    //     if (!condition || !condition->klass) return {};
    //
    //     ensure_condition_formatted_description(condition);
    //
    //     const MethodInfo* get_desc = il2utils::resolve_method(condition->klass, "get_FormattedDescription", 0);
    //     if (!get_desc || !get_desc->methodPointer) return {};
    //
    //     Il2CppString* desc = reinterpret_cast<Il2CppString*(*)(Il2CppObject*, const MethodInfo*)>(
    //         get_desc->methodPointer)(condition, get_desc);
    //     if (!desc) return {};
    //
    //     return trim_text(read_il2cpp_string_result(desc));
    // }

    inline std::string get_condition_display_description(Il2CppObject* condition)
    {
        if (!eft_api::is_il2cpp_object_likely_valid(condition) || !condition->klass) return {};

        const MethodInfo* get_desc = il2utils::resolve_method(condition->klass, "get_FormattedDescription", 0);
        if (!get_desc || !get_desc->methodPointer) return {};

        Il2CppString* desc = reinterpret_cast<Il2CppString*(*)(Il2CppObject*, const MethodInfo*)>(
            get_desc->methodPointer)(condition, get_desc);
        if (!desc) return {};

        std::string text = trim_text(read_il2cpp_string_result(desc));
        if (text.empty()) return {};

        text = localize_description_text(text);
        text = substitute_template_ids(text);

        if (is_pure_template_id(text))
        {
            const std::string resolved = resolve_template_display_name(text);
            if (!resolved.empty()) text = resolved;
        }

        return trim_text(text);
    }

    inline std::string get_cached_raw_description(
        Il2CppObject* condition,
        std::unordered_map<std::string, std::string>& cache)
    {
        if (!condition) return {};

        const std::string key = mongo_id_key(get_condition_id(condition));
        const auto found = cache.find(key);
        if (found != cache.end()) return found->second;

        auto& persistent = persistent_desc_cache();
        const auto persistent_found = persistent.find(key);
        if (persistent_found != persistent.end())
        {
            cache.emplace(key, persistent_found->second);
            return persistent_found->second;
        }

        const std::string description = get_condition_display_description(condition);
        persistent.emplace(key, description);
        cache.emplace(key, description);
        return description;
    }

    inline std::string get_condition_target_string(Il2CppObject* condition)
    {
        if (!condition || !condition->klass) return {};

        const FieldInfo* target_field = il2utils::resolve_field(condition->klass, "target");
        if (!target_field) return {};

        const uintptr_t addr = reinterpret_cast<uintptr_t>(condition) + target_field->offset;
        if (is_multiple_targets_condition(condition))
        {
            Il2CppArray* arr = *reinterpret_cast<Il2CppArray**>(addr);
            if (!arr || arr->max_length == 0) return {};
            Il2CppString* first = reinterpret_cast<Il2CppString**>(&arr->data)[0];
            return first ? eft_api::to_display_utf8(il2utils::conv_wstring(first)) : std::string{};
        }

        Il2CppString* str = *reinterpret_cast<Il2CppString**>(addr);
        return str ? eft_api::to_display_utf8(il2utils::conv_wstring(str)) : std::string{};
    }

    inline bool target_array_contains_template(Il2CppObject* condition, const std::string& template_id)
    {
        if (!condition || !condition->klass || template_id.empty()) return false;

        const FieldInfo* target_field = il2utils::resolve_field(condition->klass, "target");
        if (!target_field) return false;

        Il2CppArray* arr = *reinterpret_cast<Il2CppArray**>(
            reinterpret_cast<uintptr_t>(condition) + target_field->offset);
        if (!arr) return false;

        for (size_t i = 0; i < arr->max_length; ++i)
        {
            Il2CppString* tid = reinterpret_cast<Il2CppString**>(&arr->data)[i];
            if (!tid) continue;
            const std::string target = il2utils::conv_string(tid);
            if (target.empty()) continue;
            if (target.find(template_id) != std::string::npos || template_id.find(target) != std::string::npos)
                return true;
        }
        return false;
    }

    inline std::string get_zone_id(Il2CppObject* condition)
    {
        if (!condition || !condition->klass) return {};

        static const char* zone_fields[] = { "zoneId", "zoneID", "ZoneId" };
        for (const char* field_name : zone_fields)
        {
            const FieldInfo* zone_field = il2utils::resolve_field(condition->klass, field_name);
            if (!zone_field) continue;

            Il2CppString* zone = *reinterpret_cast<Il2CppString**>(
                reinterpret_cast<uintptr_t>(condition) + zone_field->offset);
            if (!zone) continue;

            const std::string value = il2utils::conv_string(zone);
            if (!value.empty()) return value;
        }

        return {};
    }

    inline unity::vector3 get_object_position(Il2CppObject* component)
    {
        if (!component) return {};
        Il2CppObject* go = unity::component_get_game_object(component);
        if (!go) return {};
        Il2CppObject* tr = unity::gameobject_get_transform(go);
        if (!tr) return {};
        return unity::transform_get_pos(tr);
    }

    inline size_t get_bindable_list_count(Il2CppObject* list_obj)
    {
        if (!list_obj) return 0;

        Il2CppObjectInstance list_inst(list_obj);
        const auto get_count = list_inst.get_method<int(*)(Il2CppObject*)>("get_Count", 0);
        if (get_count)
        {
            const int count = get_count(list_obj);
            if (count > 0) return static_cast<size_t>(count);
        }

        unity::list* clist = list_inst.get_field<unity::list*>("List");
        if (!clist || !clist->m_p_list_array) return 0;
        return clist->m_p_list_array->max_length;
    }

    inline void foreach_condition_in_collection(Il2CppObject* collection,
                                                const std::function<void(Il2CppObject*)>& fn)
    {
        if (!collection || !fn) return;

        Il2CppObjectInstance coll(collection);
        unity::list* clist = coll.get_field<unity::list*>("List");
        if (!clist || !clist->m_p_list_array) return;

        Il2CppArray* arr = clist->m_p_list_array;
        const size_t count = get_bindable_list_count(collection);
        const size_t limit = count > 0 ? count : arr->max_length;
        for (size_t i = 0; i < limit; ++i)
        {
            Il2CppObject* cond = reinterpret_cast<Il2CppObject**>(&arr->data)[i];
            if (!cond) continue;
            fn(cond);
        }
    }

    inline void foreach_quest_condition_collection(
        Il2CppObject* quest,
        const std::function<void(Il2CppObject* collection, int status_key)>& fn)
    {
        if (!quest || !quest->klass || !fn) return;

        const MethodInfo* get_conditions =
            il2utils::resolve_method(quest->klass, "get_Conditions", 0);
        if (!get_conditions || !get_conditions->methodPointer) return;

        Il2CppObject* conditions_obj =
            reinterpret_cast<Il2CppObject*(*)(Il2CppObject*)>(get_conditions->methodPointer)(quest);
        if (!conditions_obj) return;

        auto* conditions_dict = reinterpret_cast<unity::dict<int, Il2CppObject*>*>(conditions_obj);
        if (!conditions_dict || !conditions_dict->m_p_entries) return;

        for (int i = 0; i < conditions_dict->m_i_count; ++i)
        {
            Il2CppObject* collection = conditions_dict->get_value_by_index(i);
            if (!collection) continue;
            fn(collection, conditions_dict->get_key_by_index(i));
        }
    }

    inline unity::vector3 get_place_trigger_position(Il2CppObject* trigger)
    {
        unity::vector3 pos = get_object_position(trigger);
        if (pos.x != 0.f || pos.y != 0.f || pos.z != 0.f)
            return pos;

        if (!trigger) return pos;

        Il2CppObjectInstance inst(trigger);
        const auto get_dummy = inst.get_method<Il2CppObject*(*)(Il2CppObject*)>("get_BeaconDummy", 0);
        if (!get_dummy) return pos;

        Il2CppObject* dummy_go = get_dummy(trigger);
        if (!dummy_go) return pos;

        Il2CppObject* tr = unity::gameobject_get_transform(dummy_go);
        if (!tr) return pos;

        return unity::transform_get_pos(tr);
    }

    inline void refresh_triggers(Il2CppObject* game_world)
    {
        auto& cache = triggers();
        const ULONGLONG now = GetTickCount64();
        constexpr ULONGLONG refresh_interval_ms = 60000;

        if (game_world == cache.last_world && now - cache.last_refresh_ms < refresh_interval_ms)
            return;

        cache.last_world = game_world;
        cache.last_refresh_ms = now;
        cache.experience_triggers.clear();
        cache.place_triggers.clear();
        cache.experience_by_id.clear();
        cache.place_by_id.clear();

        const Il2CppImage* image = il2utils::resolve_image("Assembly-CSharp.dll");
        if (!image) return;

        Il2CppObject* exp_type = il2utils::get_system_type(image, "EFT.Interactive", "ExperienceTrigger");
        if (exp_type)
        {
            size_t count = 0;
            Il2CppObject** objects = unity::find_objects_of_type(exp_type, &count);
            cache.experience_triggers.reserve(count);
            for (size_t i = 0; i < count; ++i)
            {
                if (!objects[i]) continue;
                Il2CppObjectInstance inst(objects[i]);
                const auto get_id = inst.get_method<Il2CppString*(*)(Il2CppObject*)>("get_Id", 0);
                if (!get_id) continue;
                Il2CppString* id_str = get_id(objects[i]);
                if (!id_str) continue;
                const std::string id = il2utils::conv_string(id_str);
                cache.experience_triggers.emplace_back(id, objects[i]);
                cache.experience_by_id.emplace(id, objects[i]);
            }
        }

        Il2CppObject* place_type = il2utils::get_system_type(image, "EFT.Interactive", "PlaceItemTrigger");
        if (place_type)
        {
            size_t count = 0;
            Il2CppObject** objects = unity::find_objects_of_type(place_type, &count);
            cache.place_triggers.reserve(count);
            for (size_t i = 0; i < count; ++i)
            {
                if (!objects[i]) continue;
                Il2CppObjectInstance inst(objects[i]);
                const auto get_id = inst.get_method<Il2CppString*(*)(Il2CppObject*)>("get_Id", 0);
                if (get_id)
                {
                    Il2CppString* id_str = get_id(objects[i]);
                    if (id_str)
                        register_place_trigger_id(
                            cache.place_triggers, cache.place_by_id,
                            il2utils::conv_string(id_str), objects[i]);
                }

                const auto get_trigger_id = inst
                    .get_method<Il2CppString*(*)(Il2CppObject*)>("get_TriggerId", 0);
                if (get_trigger_id)
                {
                    Il2CppString* trigger_id_str = get_trigger_id(objects[i]);
                    if (trigger_id_str)
                    {
                        const std::string trigger_id = il2utils::conv_string(trigger_id_str);
                        if (cache.place_by_id.find(trigger_id) == cache.place_by_id.end())
                            register_place_trigger_id(
                                cache.place_triggers, cache.place_by_id,
                                trigger_id, objects[i]);
                    }
                }
            }
        }
    }

    inline Il2CppObject* find_experience_trigger(const std::string& trigger_id)
    {
        const auto found = triggers().experience_by_id.find(trigger_id);
        return found != triggers().experience_by_id.end() ? found->second : nullptr;
    }

    inline Il2CppObject* find_place_trigger(const std::string& zone_id)
    {
        const auto found = triggers().place_by_id.find(zone_id);
        return found != triggers().place_by_id.end() ? found->second : nullptr;
    }

    inline void add_poi(std::vector<poi_entry>& out, const unity::vector3& pos,
                        const std::string& quest_name, const std::string& description,
                        const poi_type type, const unity::vector3& my_pos,
                        Il2CppObject* condition = nullptr)
    {
        if (pos.x == 0.f && pos.y == 0.f && pos.z == 0.f) return;
        out.push_back({ pos, quest_name, description, type, my_pos.distance(pos), condition });
    }

    inline void process_visit_conditions(Il2CppObject* quest, Il2CppObject* condition,
                                         Il2CppObject* completed_set,
                                         const std::string& quest_name, std::vector<poi_entry>& out,
                                         const unity::vector3& my_pos,
                                         std::unordered_map<std::string, std::string>& desc_cache)
    {
        if (!class_name_contains(condition, "ConditionVisitPlace")) return;

        if (is_condition_completed(quest, condition, completed_set)) return;

        const std::string trigger_id = get_condition_target_string(condition);
        if (trigger_id.empty()) return;

        Il2CppObject* trigger = find_experience_trigger(trigger_id);
        if (!trigger) return;

        add_poi(out, get_object_position(trigger), quest_name,
            get_cached_raw_description(condition, desc_cache),
            poi_type::visit, my_pos, condition);
    }

    inline void register_find_condition(Il2CppObject* quest, Il2CppObject* condition,
                                        Il2CppObject* completed_set,
                                        const std::string& quest_name,
                                        std::vector<find_rule>& rules,
                                        std::unordered_map<std::string, std::string>& desc_cache)
    {
        if (!class_name_contains(condition, "ConditionFindItem")) return;

        if (is_condition_completed(quest, condition, completed_set)) return;

        rules.push_back({
            quest_name,
            get_cached_raw_description(condition, desc_cache),
            condition
        });
    }

    inline void process_place_condition(Il2CppObject* quest, Il2CppObject* condition,
                                        Il2CppObject* completed_set,
                                        const std::string& quest_name,
                                        std::vector<poi_entry>& out, const unity::vector3& my_pos,
                                        std::unordered_map<std::string, std::string>& desc_cache)
    {
        if (!is_place_condition_type(condition)) return;

        if (is_condition_completed(quest, condition, completed_set)) return;

        const std::string zone_id = get_zone_id(condition);
        if (zone_id.empty()) return;

        Il2CppObject* trigger = find_place_trigger(zone_id);
        if (!trigger) return;

        add_poi(out, get_place_trigger_position(trigger), quest_name,
            get_cached_raw_description(condition, desc_cache),
            poi_type::place, my_pos, condition);
    }

    inline void process_condition_for_cycle(Il2CppObject* quest, Il2CppObject* condition,
                                            Il2CppObject* completed_set,
                                            const std::string& quest_name,
                                            std::vector<poi_entry>& out,
                                            std::vector<find_rule>& find_rules,
                                            const unity::vector3& my_pos,
                                            std::unordered_map<std::string, std::string>& desc_cache)
    {
        if (!condition) return;

        if (class_name_contains(condition, "ConditionCounterCreator"))
        {
            Il2CppObject* nested = get_condition_collection_object(condition);
            foreach_condition_in_collection(nested, [&](Il2CppObject* sub)
            {
                process_condition_for_cycle(
                    quest, sub, completed_set, quest_name, out, find_rules, my_pos, desc_cache);
            });
            return;
        }

        process_visit_conditions(quest, condition, completed_set, quest_name, out, my_pos, desc_cache);
        register_find_condition(quest, condition, completed_set, quest_name, find_rules, desc_cache);
        process_place_condition(quest, condition, completed_set, quest_name, out, my_pos, desc_cache);
    }

    inline void scan_loot_chunk(
        incremental_collect_state& state,
        const size_t max_items)
    {
        if (!state.loot_array || state.find_rules.empty()) return;

        const size_t end = (std::min)(state.loot_cursor + max_items, state.loot_array->max_length);
        for (size_t i = state.loot_cursor; i < end; ++i)
        {
            Il2CppObject* loot_obj = reinterpret_cast<Il2CppObject**>(&state.loot_array->data)[i];
            if (!loot_obj) continue;

            Il2CppObject* item_obj = Il2CppObjectInstance(loot_obj).get_field<Il2CppObject*>("_item");
            if (!item_obj || !eft_api::try_get_item_is_quest(item_obj)) continue;

            Il2CppObject* template_obj = eft_api::try_get_item_template(item_obj);
            if (!template_obj) continue;

            const std::string template_id = eft_api::try_get_template_string_id(template_obj);
            if (template_id.empty()) continue;

            Il2CppObject* go = unity::component_get_game_object(loot_obj);
            if (!go) continue;
            Il2CppObject* tr = unity::gameobject_get_transform(go);
            if (!tr) continue;

            const unity::vector3 pos = unity::transform_get_pos(tr);
            for (const find_rule& rule : state.find_rules)
            {
                if (!rule.condition) continue;
                if (!target_array_contains_template(rule.condition, template_id)) continue;
                add_poi(state.result, pos, rule.quest_name, rule.description,
                    poi_type::find, state.my_pos, rule.condition);
            }
        }

        state.loot_cursor = end;
    }

    inline void start_collect_cycle(Il2CppObject* game_world, Il2CppObject* main_player,
                                    const unity::vector3& my_pos)
    {
        reset_incremental();
        auto& state = incremental();
        state.phase = collect_phase::building;
        state.my_pos = my_pos;
        state.result.clear();
        state.find_rules.clear();

        if (!game_world || !main_player)
        {
            state.phase = collect_phase::idle;
            return;
        }

        if (triggers().place_triggers.empty())
            triggers().last_refresh_ms = 0;
        refresh_triggers(game_world);

        std::unordered_map<std::string, std::string> desc_cache;

        Il2CppObjectInstance main_player_inst(main_player);
        const auto get_quest_controller = main_player_inst
            .get_method<Il2CppObject*(*)(Il2CppObject*)>("get_QuestController", 0);
        if (!get_quest_controller)
        {
            state.phase = collect_phase::idle;
            return;
        }

        Il2CppObject* quest_controller = get_quest_controller(main_player);
        if (!quest_controller)
        {
            state.phase = collect_phase::idle;
            return;
        }

        Il2CppObjectInstance qc(quest_controller);
        const auto get_quests = qc.get_method<Il2CppObject*(*)(Il2CppObject*)>("get_Quests", 0);
        if (!get_quests)
        {
            state.phase = collect_phase::idle;
            return;
        }

        Il2CppObject* quest_book = get_quests(quest_controller);
        if (!quest_book)
        {
            state.phase = collect_phase::idle;
            return;
        }

        unity::list* quests_data = Il2CppObjectInstance(quest_book).get_field<unity::list*>("_questsData");
        if (!quests_data || !quests_data->m_p_list_array)
        {
            state.phase = collect_phase::idle;
            return;
        }

        Il2CppArray* quest_array = quests_data->m_p_list_array;
        for (size_t qi = 0; qi < quest_array->max_length; ++qi)
        {
            Il2CppObject* quest_data_ptr = reinterpret_cast<Il2CppObject**>(&quest_array->data)[qi];
            if (!quest_data_ptr) continue;

            Il2CppObject* quest_ptr = Il2CppObjectInstance(quest_data_ptr).get_field<Il2CppObject*>("Quest");
            if (!quest_ptr) continue;

            Il2CppObjectInstance quest(quest_ptr);
            Il2CppObject* status_obj = quest.get_field<Il2CppObject*>("_statusData");
            if (!status_obj) continue;

            const int quest_status = Il2CppObjectInstance(status_obj).get_field<int>("Status");
            if (quest_status != quest_status_started
                && quest_status != quest_status_available_for_finish)
                continue;

            Il2CppString* quest_name_str = quest.get_method<Il2CppString*(*)(Il2CppObject*)>(
                "get_Name", 0)(quest_ptr);
            const std::string quest_name = quest_name_str
                ? eft_api::to_display_utf8(il2utils::conv_wstring(quest_name_str))
                : "Quest";

            Il2CppObject* completed_set = Il2CppObjectInstance(status_obj)
                .get_field<Il2CppObject*>("CompletedConditions");

            foreach_quest_condition_collection(quest_ptr, [&](Il2CppObject* collection, int status_key)
            {
                if (status_key != quest_status_started
                    && status_key != quest_status_available_for_finish)
                    return;

                foreach_condition_in_collection(collection, [&](Il2CppObject* condition)
                {
                    process_condition_for_cycle(quest_ptr, condition, completed_set, quest_name,
                        state.result, state.find_rules, my_pos, desc_cache);
                });
            });

        }

        if (!state.find_rules.empty())
        {
            Il2CppObject* loot_hydra_obj = eft_api::try_get_field_object(game_world, "LootItems");
            if (loot_hydra_obj)
            {
                Il2CppObjectInstance loot_hydra(loot_hydra_obj);
                unity::list* loot_list = loot_hydra.get_field<unity::list*>("_iteration");
                if (loot_list && loot_list->m_p_list_array)
                    state.loot_array = loot_list->m_p_list_array;
            }
        }

        if (state.find_rules.empty() || !state.loot_array || state.loot_array->max_length == 0)
            state.phase = collect_phase::ready;
        else
        {
            state.phase = collect_phase::scanning_loot;
            state.loot_cursor = 0;
        }
    }

    inline bool advance_collect(const size_t max_loot_per_tick)
    {
        auto& state = incremental();
        if (state.phase != collect_phase::scanning_loot) return false;

        scan_loot_chunk(state, max_loot_per_tick);
        if (state.loot_cursor >= state.loot_array->max_length)
            state.phase = collect_phase::ready;

        return state.phase == collect_phase::ready;
    }

    inline bool is_collect_ready()
    {
        return incremental().phase == collect_phase::ready;
    }

    inline std::vector<poi_entry> take_collect_result()
    {
        auto& state = incremental();
        std::vector<poi_entry> result = std::move(state.result);
        reset_incremental();
        return result;
    }

    inline bool is_collect_idle()
    {
        return incremental().phase == collect_phase::idle;
    }

    inline bool is_collect_scanning_loot()
    {
        return incremental().phase == collect_phase::scanning_loot;
    }
}